Skip to content

Composition Example

We’re going to walk through an example of composition in C++ by creating a Character class—much like one you might find in a video game. This Character will contain a Weapon and a collection of Potion objects, both of which are separate classes that will be used within the Character class.

If you followed along with the Basic Example tutorial, some of the code here may look familiar. Rather than modifying your existing files, however, it’s best to create a new folder for this tutorial, ensuring you can experiment without affecting your previous work.

Here are all the files that will be used in the project:

  • DirectoryCSCE240
    • DirectoryComposition Example
      • character.cc
      • character.h
      • driver.cc
      • makefile
      • potion.cc
      • potion.h
      • weapon.cc
      • weapon.h

Since we know all the names of the files, we can go ahead and create a makefile to compile and link our program:

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

The first class we’ll create is the Weapon class. It will contain a name and a type which will be of type string, and an attack power which will be represented by an integer.

We’ll first move to weapon.h to define the Weapon class. If you look at the mutator functions, you will see that we are returning a reference to a Weapon instead of the usual void. This is to show you an example of chaining functions and is a theme you will find throughout the rest of the project:

weapon.h
// Copyright 2024 CSCE240
#ifndef WEAPON_H_
#define WEAPON_H_
#include <ostream>
#include <string>
using std::ostream;
using std::string;
class Weapon {
public:
// Constructor - name, type, power
explicit Weapon(string = "wooden sword", string = "sword", int = 1);
// Friends
friend ostream& operator<<(ostream&, const Weapon&);
// Accessors
string GetName() const { return name_; }
string GetType() const { return type_; }
int GetPower() const { return power_; }
// Mutators
Weapon& SetName(string);
Weapon& SetType(string);
Weapon& SetPower(int);
private:
string name_;
string type_;
int power_;
};
#endif

Now we’ll go to weapon.cc to write the implementation for the member functions. The first code we write are the imports, and what we’ll be using:

weapon.cc
// Copyright 2024 CSCE240
#include "weapon.h"
#include <iostream>
using std::cout;
using std::ostream;

Next lets write the implementation for the mutator functions. This is so we can use them in our constructor:

weapon.cc
// Mutators
Weapon& Weapon::SetName(string n) {
if (n != "") {
name_ = n;
} else {
name_ = "wooden sword";
}
return *this;
}
Weapon& Weapon::SetType(string t) {
if (t == "dagger" || t == "rod" || t == "sword") {
type_ = t;
} else {
type_ = "sword";
}
return *this;
}
Weapon& Weapon::SetPower(int p) {
if (p > 0) {
power_ = p;
} else {
power_ = 1;
}
return *this;
}

Now we just have to write the implementation for the constructor, and our friend, operator<<:

weapon.cc
// Constructor
Weapon::Weapon(string n, string t, int p) { SetName(n).SetType(t).SetPower(p); }
// Friend
ostream& operator<<(ostream& whereto, const Weapon& w) {
whereto << w.name_ << " +" << w.power_ << " " << w.type_;
return whereto;
}

The second small class to create is the Potion class. It will have a name, which will be either “potion” or “super potion”, and a healing power, either 20 or 40. Our Character will have an array of these to use to heal when they need to.

We’ll start by defining the class in potion.h. A slight difference in this class is that it has a mutator function as private instead of public. This is because the power of the Potion is tied to its name, and we don’t want to just be able to change the power to any integer other than 20 and 40:

potion.h
// Copyright 2024 CSCE240
#ifndef POTION_H_
#define POTION_H_
#include <ostream>
#include <string>
using std::ostream;
using std::string;
class Potion {
public:
// Constructor
explicit Potion(string = "potion");
// Friend
friend ostream& operator<<(ostream&, const Potion&);
// Accessor
string GetName() const { return name_; }
int GetPower() const { return power_; }
// Mutator
Potion& SetName(string);
private:
string name_;
int power_;
void SetPower(string);
};
#endif

Now we move to potion.cc to write the implementation for our class. Like every other class, we first import, and use what we need:

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

Since the power of the Potion is tied to its name, every time there’s a name change, then there’s a chance that we have to change the power of the Potion. This is the only place where we’ll call the SetPower function:

potion.cc
// Mutators
Potion& Potion::SetName(string n) {
if (n == "potion" || n == "super potion") {
name_ = n;
} else {
name_ = "potion";
}
SetPower(name_);
return *this;
}
void Potion::SetPower(string n) {
if (n == "potion") {
power_ = 20;
} else {
power_ = 40;
}
}

The final bit of implementation is the constructor and the friend functions. The constructor only calls the SetName function since it also initializes the power of our Potion and the friend function looks similar to the one in the Weapon class:

potion.cc
// Constructor
Potion::Potion(string n) { SetName(n); }
// Friend
ostream& operator<<(ostream& whereto, const Potion& p) {
whereto << p.name_ << " +" << p.power_;
return whereto;
}

The final class we’ll create is the Character class. It will utilize the previous two classes we created. We’ll give it a name, health points, a Weapon, and an array of Potion objects.

The imports for this class are different from the other classes because it needs to import the header files of other classes to create objects of them:

character.h
// Copyright 2024 CSCE240
#ifndef CHARACTER_H_
#define CHARACTER_H_
#include <ostream>
#include <string>
#include "potion.h"
#include "weapon.h"
using std::ostream;
using std::string;

Another difference is that since we have a pointer as a data member, we must customize a copy constructor and a destructor, and we must also overload the assignment operator. Since the destructor is a very short function, lets include the implementation in the header file:

character.h
class Character {
public:
// Constructor
explicit Character(string = "none", int = 50, const Weapon& = Weapon());
// Copy Constructor
Character(const Character&);
// Destructor
~Character() {
if (bag_ != nullptr) delete[] bag_;
}
// Friend / Operator Overload
friend ostream& operator<<(ostream&, const Character&);
Character& operator=(const Character&);
// Accessors
string GetName() const { return name_; }
int GetHp() const { return hp_; }
Weapon GetWeapon() const { return weapon_; }
// Mutators
Character& SetName(string);
Character& SetHp(int);
Character& SetWeapon(Weapon);
// Utilities
void AddToBag(Potion);
void PrintBag() const;
void UsePotion(string = "potion");
private:
string name_;
int hp_;
Weapon weapon_;
int bag_capacity_;
Potion* bag_;
int current_bag_size;
int IndexOf(string = "potion") const;
};
#endif

Now its time to write the implementation for the final class. We’ll first import and use what we need:

character.cc
// Copyright 2024 CSCE 240
#include "character.h"
#include <iostream>
#include "potion.h"
#include "weapon.h"
using std::cout;
using std::endl;
using std::ostream;

Like the previous two classes we’ll write the mutator functions first so that we can use them in the constructor:

character.cc
// Mutators
Character& Character::SetName(string n) {
if (n != "") {
name_ = n;
} else {
name_ = "none";
}
return *this;
}
Character& Character::SetHp(int h) {
if (h > 0 && h <= 100) {
hp_ = h;
} else {
hp_ = 50;
}
return *this;
}
Character& Character::SetWeapon(Weapon w) {
weapon_ = w;
return *this;
}

We’ll then use some of those mutators in our constructor. This constructor shows how you can use an initializer list in your constructor. Be warned however that if you have many items in that list, the auto-styling (if you’re using it) makes the code harder to read:

character.cc
// Constructor
Character::Character(string n, int h, const Weapon& w) : weapon_(w) {
SetName(n).SetHp(h);
bag_capacity_ = 5;
bag_ = new Potion[bag_capacity_];
current_bag_size = 0;
}

The customized copy constructor is our second requirement when having a pointer as a data member. We’re creating a new Character object by passing another Character object as the argument. We simply assign the new Character’s data members to that of the argued one. For our pointer, we have to create a copy of each element since we don’t want to point to the same memory address:

character.cc
// Copy Constructor
Character::Character(const Character& c) {
name_ = c.name_;
hp_ = c.hp_;
weapon_ = c.weapon_;
bag_capacity_ = c.bag_capacity_;
current_bag_size = c.current_bag_size;
if (c.bag_ == nullptr) {
bag_ = nullptr;
} else {
bag_ = new Potion[bag_capacity_];
for (int i = 0; i < current_bag_size; ++i) {
bag_[i] = c.bag_[i];
}
}
}

Just like the previous two classes, lets write a way for cout to handle our Character objects. The chosen format is:

// Name Hp
// Weapon
// Bag

To achieve this structure, we’re first going to need a way to print the contents of our bag. The PrintBag function first checks to see if there is anything in the bag, then runs a for loop and prints each Potion to the terminal. Since we’ve already written a way for cout to handle a Potion object, we can simply refer to the index of bag_:

character.cc
void Character::PrintBag() const {
if (current_bag_size == 0) {
cout << "Your Bag is Empty" << endl;
} else {
for (int i = 0; i < current_bag_size; ++i) {
cout << bag_[i] << " ";
}
cout << endl;
}
}

We can now use the PrintBag function to write the implementation for our friend function. Just like Potion, we’ve already written a way for cout to handle a Weapon object:

character.cc
// Friend
ostream& operator<<(ostream& whereto, const Character& c) {
whereto << c.name_ << " " << c.hp_ << endl;
whereto << c.weapon_ << endl;
c.PrintBag();
return whereto;
}

The final requirement when having a pointer as a data member is that we have to overload the assignment operator (=). The code will look very similar to that of the copy constructor with a difference being that since we’re using it on an object that has already been created, we must release the previously allocated memory by deleting the old array:

character.cc
// Operator Overload
Character& Character::operator=(const Character& c) {
name_ = c.name_;
hp_ = c.hp_;
weapon_ = c.weapon_;
bag_capacity_ = c.bag_capacity_;
current_bag_size = c.current_bag_size;
// Release memory
if (bag_ != nullptr) {
delete[] bag_;
}
if (c.bag_ == nullptr) {
bag_ = nullptr;
} else {
bag_ = new Potion[bag_capacity_];
for (int i = 0; i < current_bag_size; ++i) {
bag_[i] = c.bag_[i];
}
}
return *this;
}

The final functions we have to write all deal with inventory management. We want to be able to add a Potion, and use a Potion along with printing the contents of our inventory (which we’ve already written).

Adding a Potion simply requires us checking to see if the inventory is full, assigning it to an index if it isn’t, and finally updating the current count:

character.cc
// Bag Functions
void Character::AddToBag(Potion p) {
if (current_bag_size == bag_capacity_) {
cout << "Your Bag is Full" << endl;
return;
}
bag_[current_bag_size] = p;
++current_bag_size;
}

When attempting to use a Potion, we run into a couple of obstacles. We have to check if the Character has a Potion, if the Character is below full health (100 from the mutator function), and if we pass all those checks, we have to re-adjust the array to sort of remove the object.

We’ll first create a search function that returns the index of the found object, or -1 if the object is not found:

character.cc
int Character::IndexOf(string s) const {
for (int i = 0; i < current_bag_size; ++i) {
if (s == bag_[i].GetName()) return i;
}
// Not Found
return -1;
}

Next we use the IndexOf function to adjust the array. How we adjust the array is not always by overwriting an element, but sometimes by just ignoring it. That tricky part is handled at the end of the function:

character.cc
void Character::UsePotion(string s) {
if (current_bag_size == 0) {
cout << "Your Bag is Empty" << endl;
return;
}
if (hp_ == 100) {
cout << "You are already at Full Health" << endl;
return;
}
int index = IndexOf(s);
// Potion Not Found
if (index == -1) {
cout << s << " Not Found" << endl;
return;
}
// Use Potion
hp_ += bag_[index].GetPower();
if (hp_ > 100) hp_ = 100;
// Adjust Bag
for (int i = index; i < current_bag_size - 1; ++i) {
bag_[i] = bag_[i + 1];
}
--current_bag_size;
}

Here is a breakdown of the UsePotion function in action:

  • Assume the Character has one Potion in their bag
  • bag_ = { "potion" } and the current_bag_size = 1
  • The UsePotion function is called and the IndexOf function returns 0
  • The for loop does not execute since 0 is not less than current_bag_size - 1 = 0
  • The current_bag_size is decremented to 0 but the Potion is not removed from the array
  • If the UsePotion function is called again, the first if statement will just log "Your Bag is Empty" and return
  • If the AddToBag function is called, the Potion at the 0 index will be overwritten
  • Assume the Character has three Potions in their bag
  • bag_ = { "potion", "super potion", "potion" } and the current_bag_size = 3
  • The UsePotion("super potion") function is called and the IndexOf function returns 1
  • The for loop starts at 1 and will iterate if it is less than current_bag_size - 1 = 2, so only once in this case
  • The Potion at index 1 is overwritten to equal the Potion at index 2
  • The for loop ends and the current_bag_size is decremented to 2
  • Although the current_bag_size is 2, bag_ = { "potion", "potion", "potion" }
  • Although the Potion at index 2 is not removed from the array, it can never be reached since the current_bag_size is 2
  • AddToBag overwrites the Potion at index 2, and UsePotion can never reach it

We can finally go to driver.cc to test and finalize our project. When completing assignments, you’ll probably want to test each class before moving on to the next, but for demonstration purposes, we’re going to test them together.

We should first test the first classes we built, Potion and Weapon. This is the only time the full driver will be shown, the tests that follow will only show what to add to the main function:

driver.cc
// Copyright 2024 CSCE240
#include <iostream>
#include "character.h"
#include "potion.h"
#include "weapon.h"
using std::cout;
using std::endl;
int main() {
Weapon w("Twin Blades", "dagger", 50);
Potion p("potion");
Potion s("super potion");
cout << w << endl;
cout << p << endl;
cout << s << endl;
return 0;
}

Comment out or remove the previous cout statements. Lets create a custom Character by passing a name, health points, and the Weapon object we previously created. By printing the Character object we’ll be also testing the friend function and the PrintBag function:

driver.cc
// Constructor
Character a("Zidane", 80, w);
cout << a << endl;

Right under the previous code, we’ll test adding potions and using them:

driver.cc
// Bag Functions
a.AddToBag(p);
a.AddToBag(s);
a.PrintBag();
cout << endl;
a.UsePotion("super potion");
cout << a;

Comment out or delete the previous bag functions. Lets now test the copy constructor to see if our pointers are different from each other:

driver.cc
// Copy Constructor
Character b(a);
b.AddToBag(p);
cout << a << endl;
cout << b;

The last test we’ll do in this tutorial is the assignment operator. Just like the copy constructor, we’re checking to see that both pointers are different from each other. We’re just changing one line from the previous code:

driver.cc
// Assignment Operator
Character b = a;
b.AddToBag(p);
cout << a << endl;
cout << b;