C++
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Object-Oriented C++
-
Section 4: Memory Management
-
Section 5: Templates and Generic Programming
-
Section 6: The Standard Template Library
-
Section 7: Modern C++ Features
-
Section 8: Error Handling
-
Section 9: Multithreading
-
Section 10: Operator Overloading and Type Conversion
-
Section 11: Advanced Topics
-
Section 12: Tooling and Build Systems
-
Section 13: Design Patterns in C++
-
Section 14: Interfacing with C and Systems Programming
-
Section 15: Networking and IPC Basics
-
Section 16: Graphics and Game Programming Basics
-
Section 17: The Boost Libraries
-
Section 18: Data Structures and Algorithms in C++
-
Section 19: Practical Projects
-
Section 20: More Concurrency Patterns
-
Section 21: More OOP and Design Practice
-
Section 22: File I/O and Streams
-
Section 23: More Standard Library
-
Section 24: Practice Exercises
-
Section 25: Interview and Algorithm Practice
-
Section 26: Compiler and Language Internals
-
Section 27: GUI and Application Frameworks Overview
-
Section 28: Testing and Quality Practices
-
Section 29: Numerics Library
-
Section 30: Concepts Library (C++20)
-
Section 31: Ranges Library (C++20) In Depth
-
Section 32: More Utility Library
31: Polymorphism and Virtual Functions
I've seen this exact moment happen dozens of times with the developers I've mentored: they build a beautiful class hierarchy, use a base class pointer to manage a list of different objects, and then stare at their screen in complete confusion when the program ignores their derived class methods. They assume that because the object is a Circle, the program will just "know" to call the Circle::draw() method.
Thinking the Pointer Type is the Object Type
Here is the trap. You create a base class and a derived class, and you try to use polymorphism. It looks like this:
class Shape {
public:
void draw() { std::cout << "Drawing a generic shape\n"; }
};
class Circle : public Shape {
public:
void draw() { std::cout << "Drawing a circle\n"; }
};
// Somewhere in your code:
Shape* myShape = new Circle();
myShape->draw(); // You expect "Drawing a circle", but you get "Drawing a generic shape"
Why did this happen? Because by default, C++ uses static binding. The compiler looks at the type of the pointer (which is Shape*) and decides right then and there which function to call. It doesn't care that the memory actually holds a Circle; it only cares that the pointer is a Shape. This is a performance choice by the C++ designers—virtual lookups have a tiny cost, and C++ refuses to pay that cost unless you explicitly ask for it.
Using Virtual Functions to Enable Dynamic Dispatch
To fix this, we use the virtual keyword. When you mark a function as virtual in the base class, you're telling the compiler: "Don't decide which function to call at compile time. Instead, look at the actual object in memory at runtime and find the most specific version of this function."
class Shape {
public:
virtual void draw() { std::cout << "Drawing a generic shape\n"; }
virtual ~Shape() {} // I'll explain this in a second
};
class Circle : public Shape {
public:
void draw() override { std::cout << "Drawing a circle\n"; }
};
// Now:
Shape* myShape = new Circle();
myShape->draw(); // Output: "Drawing a circle"
Notice the override keyword in the Circle class. While not strictly required by the compiler to make polymorphism work, it's a lifesaver. If you accidentally typo the function name as draww(), the compiler will scream at you because it can't find a virtual function in the base class to override. Without that keyword, the compiler would just think you're adding a brand new method to Circle, and you'd be right back to square one with the wrong method being called.
The Non-Negotiable Rule of Virtual Destructors
Here is a piece of advice that will save you from some truly nightmare-inducing memory leaks: if your class has even one virtual function, you must make your destructor virtual.
Think about it. If you call delete myShape and the destructor isn't virtual, C++ only calls the Shape destructor. If your Circle class had allocated its own memory (like a std::vector of points), that memory would never be freed. By making the destructor virtual, you ensure that the cleanup process starts at the most derived class and bubbles back up to the base.
Essentially, polymorphism turns your objects from "static structures" into "intelligent entities" that know their own identity regardless of how you're pointing at them.
📋 Practical Task
Refactoring the RPG Combat Damage System
You are working on a combat engine where different enemy types react differently to damage. However, the current implementation is broken; every enemy is behaving like a generic Enemy regardless of their actual type.
Your Task:
Modify the provided code to ensure that when takeDamage() is called via an Enemy* pointer, the specific logic for Orc or Goblin is executed. You must also ensure that memory is cleaned up correctly to avoid leaks.
#include <iostream>
#include <vector>
class Enemy {
public:
// FIX ME: Make this function allow polymorphism
void takeDamage(int amount) {
std::cout << "Enemy takes " < amount < " damage.\n";
}
// FIX ME: Ensure derived classes are cleaned up properly
~Enemy() {
std::cout << "Enemy destroyed\n";
}
};
class Orc : public Enemy {
public:
// FIX ME: Use the proper keyword to indicate this overrides the base
void takeDamage(int amount) {
std::cout << "Orc shrugs off some damage! Takes " < (amount - 2) < " damage.\n";
}
~Orc() {
std::cout << "Orc resources cleaned up\n";
}
};
class Goblin : public Enemy {
public:
// FIX ME: Use the proper keyword to indicate this overrides the base
void takeDamage(int amount) {
std::cout << "Goblin screams! Takes " < (amount * 2) < " damage.\n";
}
~Goblin() {
std::cout << "Goblin resources cleaned up\n";
}
};
int main() {
std::vector<Enemy*> enemies;
enemies.push_back(new Orc());
enemies.push_back(new Goblin());
for (Enemy* e : enemies) {
e->takeDamage(10);
}
for (Enemy* e : enemies) {
delete e;
}
return 0;
}
There are no comments for now.