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.
Step 1. Create Files
Section titled “Step 1. Create Files”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
Step 2. Update Makefile
Section titled “Step 2. Update 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:
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.outStep 3. Enemy Class
Section titled “Step 3. Enemy Class”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.
Define Header
Section titled “Define Header”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:
// 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
#endifDefine Members
Section titled “Define Members”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:
// Copyright 2024 CSCE240
#include "enemy.h"
#include <ostream>#include <string>
using std::ostream;using std::string;
namespace csce240 {
} // namespace csce240Mutators
Section titled “Mutators”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.
// Mutatorsvoid 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; }}Constructor
Section titled “Constructor”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:
// Constructor - name, hp, level, atk powerEnemy::Enemy(string n, int h, int l, int a) { SetName(n); SetHp(h); SetLevel(l); SetAtkPower(a);}Friend
Section titled “Friend”Finally, we need to tell cout how to handle the class. We print most of its stats, leaving its atk_power_ hidden:
// Friendostream& operator<<(ostream& whereto, const Enemy& e) { // Leave atk power hidden whereto << e.GetName() << "\nHp: " << e.GetHp() << "\nLevel: " << e.GetLevel(); return whereto;}Attack
Section titled “Attack”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.
Step 4. Bomb Class
Section titled “Step 4. Bomb Class”The next class to create is the Bomb class. It will publicly inherit the Enemy class,
and override the Attack function.
Define Header
Section titled “Define Header”Go to bomb.h, import what you need, use the same namespace as the Enemy class, and
define it:
// 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
#endifDefine Members
Section titled “Define Members”Now move to bomb.cc to write the implementation of the Bomb class. The following is
what you need to import:
// Copyright 2024 CSCE240
#include "bomb.h"
#include <iostream>
#include "enemy.h"
using std::cout;using std::endl;
namespace csce240 {
} // namespace csce240Constructor
Section titled “Constructor”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:
// Constructor - hp, level, atk powerBomb::Bomb(int h, int l, int a) : Enemy("Bomb", h, l, a) {}Attack
Section titled “Attack”The last function to implement is Attack. We simply print a statement and use arithmetic to generate how much damage
the Bomb does:
// Overridevoid Bomb::Attack() { cout << "Inferno Crash does " << (GetAtkPower() + GetLevel()) << " damage." << endl;}Step 5. Cactuar Class
Section titled “Step 5. Cactuar Class”The last class to create is the Cactuar class. Its similar to the Bomb class we just created except it
uses Cactuar specific data.
Define Header
Section titled “Define Header”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:
// 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
#endifDefine Members
Section titled “Define Members”Now we go to cactuar.cc to finalize the class. As always we first import what we need:
// Copyright 2024 CSCE240
#include "cactuar.h"
#include <iostream>
#include "enemy.h"
using std::cout;using std::endl;
namespace csce240 {
} // namespace csce240Constructor
Section titled “Constructor”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:
// Constructor - hp, levelCactuar::Cactuar(int h, int l) : Enemy("Cactuar", h, l, 1000) {}SetAtkPower
Section titled “SetAtkPower”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:
// Redefine Mutatorvoid Cactuar::SetAtkPower(int) { cout << "Cactuars cannot change their attack power." << endl;}Attack
Section titled “Attack”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:
// Overridevoid Cactuar::Attack() { cout << "1000 Needles does " << GetAtkPower() << " damage." << endl;}Step 6. Test
Section titled “Step 6. Test”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:
// 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:
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;}make driverg++ -Wall -std=c++17 -c driver.ccg++ -Wall -std=c++17 -c bomb.ccg++ -Wall -std=c++17 -c cactuar.ccg++ -Wall -std=c++17 -c enemy.ccg++ -Wall -std=c++17 driver.o bomb.o cactuar.o enemy.o./a.out
BombHp: 100Level: 10Inferno Crash does 20 damage.
CactuarHp: 100Level: 201000 Needles does 1000 damage.
Cactuars cannot change their attack power.1000 Needles does 1000 damage.