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.
Step 1. Create Files
Section titled “Step 1. Create Files”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
Step 2. Makefile
Section titled “Step 2. Makefile”Since we know all the names of the files, we can go ahead and create a makefile to
compile and link our program:
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.outStep 3. Weapon Class
Section titled “Step 3. Weapon Class”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.
Define Header
Section titled “Define Header”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:
// 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_;};
#endifImplement Members
Section titled “Implement Members”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:
// Copyright 2024 CSCE240
#include "weapon.h"
#include <iostream>
using std::cout;using std::ostream;Mutators
Section titled “Mutators”Next lets write the implementation for the mutator functions. This is so we can use them in our constructor:
// MutatorsWeapon& 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;}Constructor & Friend
Section titled “Constructor & Friend”Now we just have to write the implementation for the constructor, and our friend, operator<<:
// ConstructorWeapon::Weapon(string n, string t, int p) { SetName(n).SetType(t).SetPower(p); }
// Friendostream& operator<<(ostream& whereto, const Weapon& w) { whereto << w.name_ << " +" << w.power_ << " " << w.type_; return whereto;}Step 4. Potion Class
Section titled “Step 4. Potion Class”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.
Define Header
Section titled “Define Header”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:
// 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);};
#endifImplement Members
Section titled “Implement Members”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:
// Copyright 2024 CSCE240
#include "potion.h"
#include <iostream>
using std::cout;using std::endl;using std::ostream;Mutators
Section titled “Mutators”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:
// MutatorsPotion& 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; }}Constructor & Friend
Section titled “Constructor & Friend”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:
// ConstructorPotion::Potion(string n) { SetName(n); }
// Friendostream& operator<<(ostream& whereto, const Potion& p) { whereto << p.name_ << " +" << p.power_; return whereto;}Step 5. Character Class
Section titled “Step 5. Character Class”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.
Define Header
Section titled “Define Header”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:
// 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:
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;};
#endifImplement Members
Section titled “Implement Members”Now its time to write the implementation for the final class. We’ll first import and use what we need:
// Copyright 2024 CSCE 240
#include "character.h"
#include <iostream>
#include "potion.h"#include "weapon.h"
using std::cout;using std::endl;using std::ostream;Mutators
Section titled “Mutators”Like the previous two classes we’ll write the mutator functions first so that we can use them in the constructor:
// MutatorsCharacter& 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;}Constructor
Section titled “Constructor”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:
// ConstructorCharacter::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;}Copy Constructor
Section titled “Copy Constructor”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:
// Copy ConstructorCharacter::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]; } }}PrintBag & Friend
Section titled “PrintBag & Friend”Just like the previous two classes, lets write a way for cout to handle our Character objects. The chosen format is:
// Name Hp// Weapon// BagTo 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_:
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:
// Friendostream& operator<<(ostream& whereto, const Character& c) { whereto << c.name_ << " " << c.hp_ << endl; whereto << c.weapon_ << endl; c.PrintBag(); return whereto;}Overload Assignment
Section titled “Overload Assignment”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:
// Operator OverloadCharacter& 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;}Bag Functions
Section titled “Bag Functions”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:
// Bag Functionsvoid 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:
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:
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:
Scenario 1: One Potion
Section titled “Scenario 1: One Potion”- Assume the
Characterhas onePotionin their bag bag_ = { "potion" }and thecurrent_bag_size = 1- The
UsePotionfunction is called and theIndexOffunction returns0 - The
forloop does not execute since0is not less thancurrent_bag_size - 1 = 0 - The
current_bag_sizeis decremented to0but thePotionis not removed from the array - If the
UsePotionfunction is called again, the firstifstatement will just log"Your Bag is Empty"andreturn - If the
AddToBagfunction is called, thePotionat the0index will be overwritten
Scenario 2: Multiple Potions
Section titled “Scenario 2: Multiple Potions”- Assume the
Characterhas threePotionsin their bag bag_ = { "potion", "super potion", "potion" }and thecurrent_bag_size = 3- The
UsePotion("super potion")function is called and theIndexOffunction returns1 - The
forloop starts at1and will iterate if it is less thancurrent_bag_size - 1 = 2, so only once in this case - The
Potionat index1is overwritten to equal thePotionat index2 - The
forloop ends and thecurrent_bag_sizeis decremented to2 - Although the
current_bag_sizeis2,bag_ = { "potion", "potion", "potion" } - Although the
Potionat index2is not removed from the array, it can never be reached since thecurrent_bag_sizeis2 AddToBagoverwrites thePotionat index2, andUsePotioncan never reach it
Step 6. Test Classes
Section titled “Step 6. Test Classes”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.
Potion & Weapon
Section titled “Potion & Weapon”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:
// 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;}make driverg++ -Wall -std=c++17 -c weapon.ccg++ -Wall -std=c++17 -c driver.ccg++ -Wall -std=c++17 -c character.ccg++ -Wall -std=c++17 -c potion.ccg++ -Wall -std=c++17 driver.o character.o potion.o weapon.o./a.outTwin Blades +50 daggerpotion +20super potion +40Character Constructor
Section titled “Character Constructor”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:
// ConstructorCharacter a("Zidane", 80, w);cout << a << endl;make driverg++ -Wall -std=c++17 -c driver.ccg++ -Wall -std=c++17 driver.o character.o potion.o weapon.o./a.outZidane 80Twin Blades +50 daggerYour Bag is EmptyBag Functions
Section titled “Bag Functions”Right under the previous code, we’ll test adding potions and using them:
// Bag Functionsa.AddToBag(p);a.AddToBag(s);a.PrintBag();cout << endl;
a.UsePotion("super potion");cout << a;make driverg++ -Wall -std=c++17 -c driver.ccg++ -Wall -std=c++17 driver.o character.o potion.o weapon.o./a.outZidane 80Twin Blades +50 daggerYour Bag is Empty
potion +20 super potion +40
Zidane 100Twin Blades +50 daggerpotion +20Copy Constructor
Section titled “Copy Constructor”Comment out or delete the previous bag functions. Lets now test the copy constructor to see if our pointers are different from each other:
// Copy ConstructorCharacter b(a);b.AddToBag(p);
cout << a << endl;cout << b;make driverg++ -Wall -std=c++17 -c driver.ccg++ -Wall -std=c++17 driver.o character.o potion.o weapon.o./a.outZidane 80Twin Blades +50 daggerYour Bag is Empty
Zidane 80Twin Blades +50 daggerpotion +20Assignment Operator
Section titled “Assignment Operator”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:
// Assignment OperatorCharacter b = a;b.AddToBag(p);
cout << a << endl;cout << b;make driverg++ -Wall -std=c++17 -c driver.ccg++ -Wall -std=c++17 driver.o character.o potion.o weapon.o./a.outZidane 80Twin Blades +50 daggerYour Bag is Empty
Zidane 80Twin Blades +50 daggerpotion +20