Intermediate 13 min

Abstract Classes

Design abstract base classes that define common interfaces and shared implementation

Learn how to design abstract base classes that combine required interfaces (pure virtual functions) with shared implementation (concrete methods).

A Simple Example

#include <iostream>
#include <string>
#include <vector>

class GameEntity {
protected:
    std::string name;
    int x;
    int y;
    int health;
    bool alive;

public:
    GameEntity(std::string name, int x, int y, int hp)
        : name{name}, x{x}, y{y}, health{hp}, alive{true} {
        std::cout << "GameEntity created: " << name << "\n";
    }

    virtual void update() = 0;

    virtual void render() const = 0;

    void takeDamage(int damage) {
        health -= damage;
        if (health <= 0) {
            health = 0;
            alive = false;
            onDeath();
        }
        std::cout << name << " took " << damage << " damage. HP: " << health << "\n";
    }

    void move(int dx, int dy) {
        x += dx;
        y += dy;
        std::cout << name << " moved to (" << x << ", " << y << ")" << "\n";
    }

    bool isAlive() const {
        return alive;
    }

    std::string getName() const {
        return name;
    }

    virtual void onDeath() {
        std::cout << name << " has died!" << "\n";
    }

    virtual ~GameEntity() {
        std::cout << "GameEntity destroyed: " << name << "\n";
    }
};

class Player : public GameEntity {
private:
    int score;
    int lives;

public:
    Player(std::string name, int x, int y)
        : GameEntity{name, x, y, 100}, score{0}, lives{3} {
        std::cout << "Player created with " << lives << " lives" << "\n";
    }

    void update() override {
        if (alive) {
            std::cout << name << " (Player) updating... Score: " << score << "\n";
        }
    }

    void render() const override {
        if (alive) {
            std::cout << "Rendering player '" << name << "' at (" << x << ", " << y << ")" << "\n";
        }
    }

    void collectCoin() {
        score += 10;
        std::cout << name << " collected coin! Score: " << score << "\n";
    }

    void onDeath() override {
        lives--;
        std::cout << name << " died! Lives remaining: " << lives << "\n";
        if (lives > 0) {
            health = 100;
            alive = true;
            std::cout << name << " respawned!" << "\n";
        }
    }

    ~Player() override {
        std::cout << "Player destroyed" << "\n";
    }
};

class Enemy : public GameEntity {
private:
    int attackPower;
    std::string enemyType;

public:
    Enemy(std::string type, int x, int y, int hp, int power)
        : GameEntity{type + "_Enemy", x, y, hp}, attackPower{power}, enemyType{type} {
    }

    void update() override {
        if (alive) {
            std::cout << name << " (Enemy) patrolling..." << "\n";
        }
    }

    void render() const override {
        if (alive) {
            std::cout << "Rendering enemy '" << name << "' at (" << x << ", " << y << ")" << "\n";
        }
    }

    void attack(GameEntity& target) {
        if (alive) {
            std::cout << name << " attacks " << target.getName() << "!" << "\n";
            target.takeDamage(attackPower);
        }
    }

    ~Enemy() override {
        std::cout << "Enemy destroyed" << "\n";
    }
};

int main() {
    // GameEntity* entity = new GameEntity{"Generic", 0, 0, 100};  // ERROR: Abstract class!

    std::vector<GameEntity*> gameWorld;

    Player* player = new Player{"Hero", 0, 0};
    gameWorld.push_back(player);

    gameWorld.push_back(new Enemy{"Goblin", 10, 5, 50, 15});
    gameWorld.push_back(new Enemy{"Orc", 15, 8, 80, 25});

    std::cout << "\n=== Game Update Loop ===" << "\n";
    for (GameEntity* entity : gameWorld) {
        entity->update();
        entity->render();
        std::cout << "\n";
    }

    std::cout << "=== Combat ===" << "\n";
    Enemy* goblin = dynamic_cast<Enemy*>(gameWorld[1]);
    if (goblin) {
        goblin->attack(*player);
        goblin->attack(*player);
    }

    player->collectCoin();

    std::cout << "\n=== Cleanup ===" << "\n";
    for (GameEntity* entity : gameWorld) {
        delete entity;
    }

    return 0;
}

Breaking It Down

Pure Virtual vs Concrete Methods

  • Pure virtual (= 0): Must be implemented by derived classes - defines the interface
  • Concrete methods: Shared implementation that all derived classes can use
  • Example: update() is pure virtual (each entity updates differently), takeDamage() is concrete (all use same logic)
  • Remember: Abstract classes combine both to provide structure AND reusable code

Abstract Class Cannot Be Instantiated

  • Having even one pure virtual function makes a class abstract
  • Cannot create objects: GameEntity e; is a compile error
  • Can create pointers/references: GameEntity* ptr is valid
  • Remember: Abstract classes are blueprints, not concrete things

Protected Members for Derived Access

  • Protected: Accessible by the class and derived classes, not outside
  • Example: health, x, y are protected so Player and Enemy can access them
  • Public would break encapsulation, private would prevent derived access
  • Remember: Protected is the sweet spot for shared state in hierarchies

Virtual Destructor is Critical

  • Abstract classes almost always need virtual destructors
  • Why: When deleting through base pointer, correct destructor must be called
  • Without virtual: Memory leaks and resource cleanup failures
  • Remember: virtual ~ClassName() in abstract base classes

Why This Matters

  • Abstract classes are the architects of your code - they define the blueprint that concrete classes must follow while providing shared functionality.
  • This is how professional frameworks are built. Look at any game engine, GUI library, or plugin system - they all use abstract classes to define what derived classes must implement while providing common utilities.
  • Understanding abstract class design separates junior from senior developers - it's about thinking in terms of contracts, hierarchies, and extensibility.

Critical Insight

Abstract classes let you define the "template method pattern" - the base class defines the algorithm structure, and derived classes fill in specific steps. This is incredibly powerful:

class DataProcessor {
public:
    void process() {  // Template method - not virtual!
        loadData();
        validateData();
        transformData();
        saveData();
    }

protected:
    virtual void loadData() = 0;
    virtual void transformData() = 0;

    void validateData() {
        std::cout << "Validating data...\n";
    }

    void saveData() {
        std::cout << "Saving data...\n";
    }
};
// Base class controls the algorithm flow,
// derived classes customize specific steps!

Best Practices

Always add virtual destructor: Abstract classes need virtual ~ClassName() for proper cleanup.

Balance pure virtual and concrete: Provide shared implementation where it makes sense, require implementation where behavior differs.

Use protected for shared state: Protected members are accessible to derived classes but hidden from outside.

Design for extension: Think about what derived classes need and what should be enforced.

Common Mistakes

Too many pure virtual functions: Making everything pure virtual defeats code reuse.

Forgetting virtual destructor: Abstract classes almost always need virtual destructors.

Not using protected effectively: Making everything private in abstract classes prevents derived classes from accessing shared state.

Trying to instantiate: You cannot create objects of abstract classes directly.

Debug Challenge

This abstract class is missing something critical. Click the highlighted line to fix it:

1 class Shape {
2 public:
3 virtual double area() const = 0;
4 virtual void draw() const = 0;
5 ~Shape() { }
6 };

Quick Quiz

  1. What makes a class abstract?
Having at least one pure virtual function
The `abstract` keyword
Having a virtual destructor
  1. Can abstract classes have concrete (non-virtual) methods?
Yes, abstract classes can mix pure virtual and concrete methods
No, all methods must be pure virtual
Only in C++17
  1. What's the benefit of shared implementation in abstract classes?
Reduces code duplication across derived classes
No benefit, should be avoided
Makes the code slower

Practice Playground

Time to try out what you just learned! Play with the example code below, experiment by making changes and running the code to deepen your understanding.

Lesson Progress

  • Fix This Code
  • Quick Quiz
  • Practice Playground - run once