Skip to content

Polymorphism Example

This example demonstrates how to apply polymorphism in C++ by creating a set of video game-style character classes. Although it continues the gaming theme from our previous examples, no prior code is required—everything you need is included here.

You’ll see how virtual functions, inheritance, and other C++ features allow different classes to respond to the same function calls in uniquely tailored ways, all while sharing a common interface.

We’ll be creating a base Enemy class along with two other specialized Enemy types: Bomb and Cactuar. Below are all the files to create:

  • DirectoryCSCE240
    • DirectoryPolymorphism Example
      • bomb.cc
      • bomb.h
      • cactuar.cc
      • cactuar.h
      • enemy.cc
      • enemy.h
      • makefile

Since we know all the names of our files, we can write all the scripts needed to compile and link our program in our makefile:

makefile
enemy.o : enemy.cc enemy.h
g++ -Wall -std=c++17 -c enemy.cc
bomb.o : bomb.cc bomb.h enemy.h
g++ -Wall -std=c++17 -c bomb.cc
cactuar.o : cactuar.cc cactuar.h enemy.h
g++ -Wall -std=c++17 -c cactuar.cc
driver.o : driver.cc bomb.h cactuar.h enemy.h
g++ -Wall -std=c++17 -c driver.cc
driver : driver.o bomb.o cactuar.o enemy.o
g++ -Wall -std=c++17 $^
./a.out
clean :
rm *.o a.out

The Enemy class will be the base class that all types of enemies will inherit from. It will have a pure virtual function in Attack, making it an abstract class. Even though the class doesn’t have special memory management, it will also contain a virtual destructor so that the correct destructor is called if someone were ever to create a pointer of our class.

Go to enemy.h and define the class. A difference between the other example tutorials is that we are defining this class under a namespace. Your course assignments will probably start to implement namespaces so this will serve as extra practice.

Another difference between other example tutorials is the use of a virtual destructor. The virtual destructor will be implemented as an empty block since we don’t need any special operations:

enemy.h
// Copyright 2024 CSCE240
#ifndef ENEMY_H_
#define ENEMY_H_
#include <ostream>
#include <string>
using std::ostream;
using std::string;
namespace csce240 {
class Enemy {
public:
// Constructor - name, hp, level, atk power
explicit Enemy(string = "none", int = 50, int = 1, int = 1);
// Destructor
virtual ~Enemy() {}
// Friend
friend ostream& operator<<(ostream&, const Enemy&);
// Accessors
string GetName() const { return name_; }
int GetHp() const { return hp_; }
int GetLevel() const { return level_; }
int GetAtkPower() const { return atk_power_; }
// Mutators
void SetHp(int);
void SetLevel(int);
void SetAtkPower(int);
// Other Function
virtual void Attack() = 0;
protected:
// Child Classes will have constant names
void SetName(string);
private:
string name_;
int hp_;
int level_;
int atk_power_;
};
} // namespace csce240
#endif

Now we’ll move to enemy.cc to write the implementation for the class. We’ll first import what we need, and declare a namespace block:

enemy.cc
// Copyright 2024 CSCE240
#include "enemy.h"
#include <ostream>
#include <string>
using std::ostream;
using std::string;
namespace csce240 {
} // namespace csce240

Next we’ll write the implementation for our mutator functions so that we can utilize them in the constructor. You may have noticed from the header file that the SetName function is defined under the protected accessor. For this program, we don’t want to change the names of our specialized enemies, so this would be one way of treating the name_ member like a constant, as objects would not have access to SetName.

enemy.cc
// Mutators
void Enemy::SetName(string n) {
if (n != "") {
name_ = n;
} else {
name_ = "none";
}
}
void Enemy::SetHp(int h) {
if (h > 1) {
hp_ = h;
} else {
hp_ = 1;
}
}
void Enemy::SetLevel(int l) {
if (l > 1 && l < 100) {
level_ = l;
} else {
level_ = 1;
}
}
void Enemy::SetAtkPower(int a) {
if (a > 1 && a < 9000) {
atk_power_ = a;
} else {
atk_power_ = 1;
}
}

Now we can use the mutators for the class constructor. It does not require any complicated operations, so we’ll just use each mutator to set its data members:

enemy.cc
// Constructor - name, hp, level, atk power
Enemy::Enemy(string n, int h, int l, int a) {
SetName(n);
SetHp(h);
SetLevel(l);
SetAtkPower(a);
}

Finally, we need to tell cout how to handle the class. We print most of its stats, leaving its atk_power_ hidden:

enemy.cc
// Friend
ostream& operator<<(ostream& whereto, const Enemy& e) {
// Leave atk power hidden
whereto << e.GetName() << "\nHp: " << e.GetHp()
<< "\nLevel: " << e.GetLevel();
return whereto;
}

As a reminder, the Attack function is a pure virtual function so no implementation is written in this class. However, implementation is required in any child class.

The next class to create is the Bomb class. It will publicly inherit the Enemy class, and override the Attack function.

Go to bomb.h, import what you need, use the same namespace as the Enemy class, and define it:

bomb.h
// Copyright 2024 CSCE240
#ifndef BOMB_H_
#define BOMB_H_
#include "enemy.h"
namespace csce240 {
class Bomb : public Enemy {
public:
// Construct - hp, level, atk power
explicit Bomb(int = 50, int = 1, int = 1);
// Override
void Attack() override;
};
} // namespace csce240
#endif

Now move to bomb.cc to write the implementation of the Bomb class. The following is what you need to import:

bomb.cc
// Copyright 2024 CSCE240
#include "bomb.h"
#include <iostream>
#include "enemy.h"
using std::cout;
using std::endl;
namespace csce240 {
} // namespace csce240

The constructor is a tiny bit different than the Enemy class in that there is not a string in its signature. Instead the Enemy constructor is used in an initializer list to give an object a name_. This is how we can treat the name_ data member as a constant since it cannot be changed:

bomb.cc
// Constructor - hp, level, atk power
Bomb::Bomb(int h, int l, int a) : Enemy("Bomb", h, l, a) {}

The last function to implement is Attack. We simply print a statement and use arithmetic to generate how much damage the Bomb does:

bomb.cc
// Override
void Bomb::Attack() {
cout << "Inferno Crash does " << (GetAtkPower() + GetLevel()) << " damage."
<< endl;
}

The last class to create is the Cactuar class. Its similar to the Bomb class we just created except it uses Cactuar specific data.

Move to cactuar.h, import what you need, and define the class inside the same namespace we’ve been using. Notice that the class redefines the SetAtkPower function. This is because we want to give this class a constant atk_power_ and this is one way to handle such a case:

cactuar.h
// Copyright 2024 CSCE240
#ifndef CACTUAR_H_
#define CACTUAR_H_
#include "enemy.h"
namespace csce240 {
class Cactuar : public Enemy {
public:
// Constructor - hp, level
explicit Cactuar(int = 50, int = 1);
// Redifine Mutator
void SetAtkPower(int);
// Override
void Attack() override;
};
} // namespace csce240
#endif

Now we go to cactuar.cc to finalize the class. As always we first import what we need:

cactuar.cc
// Copyright 2024 CSCE240
#include "cactuar.h"
#include <iostream>
#include "enemy.h"
using std::cout;
using std::endl;
namespace csce240 {
} // namespace csce240

The class constructor differs from the Bomb class in that it only allows the hp_ and level_ to be passed. Another difference is that we use an initializer list to create a Bomb object with an atk_power_. For the Cactuar we’ll make its atk_power_ a constant 1000:

cactuar.cc
// Constructor - hp, level
Cactuar::Cactuar(int h, int l) : Enemy("Cactuar", h, l, 1000) {}

The Cactuar class is special in that it always has the same atk_power_. To not allow an object to change the atk_power_, we simply change how the SetAtkPower mutator works. We just print a statement stating that the atk_power_ cannot be changed and we end the function:

cactuar.cc
// Redefine Mutator
void Cactuar::SetAtkPower(int) {
cout << "Cactuars cannot change their attack power." << endl;
}

We finish the Cactuar class by writing the implementation for the Attack function. Its similar to that of the one we wrote in the Bomb class except we print a different statement and we only use the constant atk_power_ to show how much damage the object does:

cactuar.cc
// Override
void Cactuar::Attack() {
cout << "1000 Needles does " << GetAtkPower() << " damage." << endl;
}

The final step of the project is to test our classes. We move to driver.cc and import what we need which includes the class headers. We also use the classes under the namespace we’ve been using:

driver.cc
// Copyright 2024 CSCE240
#include <iostream>
#include "bomb.h"
#include "cactuar.h"
#include "enemy.h"
using std::cout;
using std::endl;
using csce240::Bomb;
using csce240::Cactuar;

In the main function, we create an object of each specialized class and test its member functions:

driver.cc
int main() {
cout << endl;
Bomb b(100, 10, 10);
cout << b << endl;
b.Attack();
cout << endl;
Cactuar c(100, 20);
cout << c << endl;
c.Attack();
cout << endl;
c.SetAtkPower(2000);
c.Attack();
return 0;
}