Skip to content

Basic Example

We’re going to walk through the entire process of defining and using a class in C++. In this example, we’ll create a Character class, write its member function implementations, and then test our class in a driver program. You’ll see how these components are organized into separate files:

  1. character.h: Header file containing the class definition.

  2. character.cc: Source file where we implement the member functions.

  3. driver.cc: Driver file to test and demonstrate our class.

  4. makefile: A script that automates compiling and linking the program.

Since we already know the names of our files, lets first create our makefile:

makefile
character.o : character.cc character.h
g++ -Wall -std=c++17 -c character.cc
driver.o : driver.cc character.h
g++ -Wall -std=c++17 -c driver.cc
driver: driver.o character.o
g++ -Wall -std=c++17 driver.o character.o
./a.out
clean :
rm *.o a.out

Next lets define our Character class in character.h. Don’t worry if you don’t understand some of the code, we’ll be going over each section:

character.h
// Copyright 2024 CSCE240
#ifndef CHARACTER_H_
#define CHARACTER_H_
#include <string>
using std::string;
class Character {
public:
// Constructor name, hp, attack, defense
explicit Character(string = "none", int = 0, int = 0, int = 0);
// Copy Constructor
Character(const Character&);
// Destructor
~Character() {
if (bag_ != nullptr) delete[] bag_;
}
// Accessors
string GetName() const { return name_; }
int GetHp() const { return hp_; }
int GetAtk() const { return attack_; }
int GetDef() const { return defense_; }
// Mutators
void SetName(string);
void SetHp(int);
void SetAtk(int);
void SetDef(int);
// Utilities
void AddToBag(string);
void PrintBag() const;
private:
string name_;
int hp_;
int attack_;
int defense_;
int bag_capacity_;
string* bag_;
int current_bag_size;
};
#endif

Under the private accessor, we’ve added a couple of basic stats for our class along with a pointer that acts like an inventory:

character.h
string name_;
int hp_;
int attack_;
int defense_;
int bag_capacity_;
string* bag_;
int current_bag_size;

Our constructor uses default parameters, allowing objects to be created with or without arguments. We’ve also declared it as explicit to prevent unintended implicit conversions—specifically, creating a Character object by assigning it a string (e.g., Character v = "Villain";).

Because one of the constructor’s parameters is a string, the explicit keyword ensures an object is only constructed when we explicitly call the constructor, rather than through an automatic type conversion:

character.h
// Constructor name, hp, attack, defense
explicit Character(string = "none", int = 0, int = 0, int = 0);

The copy constructor is used when creating a new object by passing an existing object of the same class. In this class, we have a dynamic data member (a pointer), so it’s important to create a copy constructor that allocates new memory for the copy. Otherwise, multiple objects would end up sharing the same pointer and point to the same memory—leading to issues like double-deletion and data corruption:

character.h
// Copy Constructor
Character(const Character&);

Because our class contains a pointer data member, a destructor is needed to properly release any dynamically allocated memory. In this example, the destructor checks whether bag_ is pointing to an allocated array. If so, it calls delete[] to free the memory. Since the implementation is brief, we’ve included it directly in the header:

character.h
// Destructor
~Character() {
if (bag_ != nullptr) delete[] bag_;
}

Just like the destructor, our Getter Functions contain very little code so we can write the implementation in the header file:

character.h
// Accessors
string GetName() const { return name_; }
int GetHp() const { return hp_; }
int GetAtk() const { return attack_; }
int GetDef() const { return defense_; }

Our Setter functions require a bit of conditional statements so we’ll usually only define them in the header file:

character.h
// Mutators
void SetName(string);
void SetHp(int);
void SetAtk(int);
void SetDef(int);

Since we have an inventory, we need to write a function to add items. After adding items, we can write a function to print the contents of the bag_:

character.h
// Utilities
void AddToBag(string);
void PrintBag() const;

Lets move to character.cc to write the implementation for our class. Instead of feeding you the entire document, we’ll go over each section.

First we setup the document by importing what we need:

character.cc
// Copyright 2024 CSCE240
#include "character.h"
#include <iostream>
#include <string>
using std::cout;
using std::endl;
using std::string;

We’ll first write our mutators since we can use them in the constructor. They’re all pretty similar in requiring a simple condition:

character.cc
// Mutators
void Character::SetName(string n) {
if (n != "") {
name_ = n;
} else {
name_ = "none";
}
}
void Character::SetHp(int h) {
if (h > 0) {
hp_ = h;
} else {
hp_ = 0;
}
}
void Character::SetAtk(int a) {
if (a > 0) {
attack_ = a;
} else {
attack_ = 0;
}
}
void Character::SetDef(int d) {
if (d > 0) {
defense_ = d;
} else {
defense_ = 0;
}
}

We can now use our mutators in the constructor to initialize some data members with any arguments passed. Any data member dealing with the Character’s inventory will be initialized manually:

character.cc
// Constructor name, hp, attack, defence
Character::Character(string n, int h, int a, int d) {
SetName(n);
SetHp(h);
SetAtk(a);
SetDef(d);
bag_capacity_ = 5;
bag_ = new string[bag_capacity_];
current_bag_size = 0;
}

For the copy constructor, we assign most data members of the new class to the data members of the object’s data members. We then iterate to pass the contents of the inventory from one class to the new class:

character.cc
// Copy Constructor
Character::Character(const Character& c) {
name_ = c.name_;
hp_ = c.hp_;
attack_ = c.attack_;
defense_ = c.defense_;
bag_capacity_ = c.bag_capacity_;
current_bag_size = c.current_bag_size;
if (c.bag_ == nullptr) {
bag_ = nullptr; // Handles cases where bag is NULL
} else {
bag_ = new string[bag_capacity_];
for (int i = 0; i < current_bag_size; ++i) {
bag_[i] = c.bag_[i];
}
}
}

To add an item to our inventory, we first check to see that the passed value is not an empty string and that our inventory is not full. We then assign it to an index and updated the size:

character.cc
void Character::AddToBag(string item) {
if (item == "" || current_bag_size == bag_capacity_) return;
bag_[current_bag_size] = item;
++current_bag_size;
}

To print our inventory we first check if the inventory is empty. Then if it contains items we iterate and print each element:

character.cc
void Character::PrintBag() const {
if (current_bag_size == 0) return;
for (int i = 0; i < current_bag_size; ++i) {
cout << bag_[i] << endl;
}
}

Finally we move to the driver file. Lets first check that our constructor works without any arguments. This will be the only time I’ll show the complete driver:

driver.cc
// Copyright 2024 CSCE240
#include <iostream>
#include "character.h"
using std::cout;
using std::endl;
int main() {
// Check Default Constructor
cout << "Default Constructor Check" << endl;
cout << "-------------------------" << endl;
Character a;
cout << "Name " << (a.GetName() == "none") << endl;
cout << "HP " << (a.GetHp() == 0) << endl;
cout << "Atk " << (a.GetAtk() == 0) << endl;
cout << "Def " << (a.GetDef() == 0) << endl;
cout << endl;
return 0;
}

Next we’ll check to see that our constructor works when passing parameters. I’ll only show the results for the new tests instead of the entire output:

driver.cc
// Check Overloaded Constructor
cout << "Overloaded Constructor Check" << endl;
cout << "----------------------------" << endl;
Character b("Zidane", 100, 40, 20);
cout << "Name " << (b.GetName() == "Zidane") << endl;
cout << "HP " << (b.GetHp() == 100) << endl;
cout << "Atk " << (b.GetAtk() == 40) << endl;
cout << "Def " << (b.GetDef() == 20) << endl;
cout << endl;

Now we’ll test the copy constructor by passing the previously made Character b object. We’ll alter the data in the new Character c to make sure the contents in Character b are not altered:

driver.cc
// Check Copy Constructor
cout << "Copy Constructor Check" << endl;
cout << "----------------------" << endl;
Character c(b);
cout << "Same Name " << (c.GetName() == "Zidane") << endl;
cout << "Same HP " << (c.GetHp() == 100) << endl;
cout << "Same Atk " << (c.GetAtk() == 40) << endl;
cout << "Same Def " << (c.GetDef() == 20) << endl;
c.SetName("Vivi");
c.SetHp(80);
c.SetAtk(30);
c.SetDef(15);
cout << "Name Change " << (b.GetName() != c.GetName()) << endl;
cout << "Hp Change " << (b.GetHp() != c.GetHp()) << endl;
cout << "Atk Change " << (b.GetAtk() != c.GetAtk()) << endl;
cout << "Def Change " << (b.GetDef() != c.GetDef()) << endl;
cout << endl;

Finally we test our inventory system. We’ll add some items, then test that our conditional statement is working by trying to pass an empty string, and by trying to add an item when the inventory is full:

driver.cc
// Bag Check
cout << "Bag Check" << endl;
cout << "---------" << endl;
a.AddToBag("sword");
a.AddToBag("rod");
a.AddToBag("potion");
a.AddToBag("special pendant");
a.AddToBag(""); // should not add
a.AddToBag("running shoes");
a.AddToBag("should not add"); // should not add, reached capacity limit
a.PrintBag();