C++ Inheritance & Polymorphism: virtual, Interfaces, and Patterns

C++ Inheritance & Polymorphism: virtual, Interfaces, and Patterns

이 글의 핵심

Hands-on guide to C++ inheritance and runtime polymorphism: access levels, abstract interfaces, common pitfalls, and practical patterns.

What are inheritance and polymorphism?

Inheritance reuses attributes and methods from an existing class. Polymorphism lets you treat different types through the same interface.

Why they matter:

  • Reuse: shared behavior in a base class
  • Extensibility: add new types easily
  • Flexibility: choose behavior at runtime
  • Abstraction: separate interface from implementation
// Without inheritance: duplication
class Dog {
    string name;
public:
    void eat() { cout << name << " eats\n"; }
    void bark() { cout << "woof\n"; }
};

class Cat {
    string name;
public:
    void eat() { cout << name << " eats\n"; }  // duplicated
    void meow() { cout << "meow\n"; }
};

// With inheritance: reuse
class Animal {
protected:
    string name;
public:
    Animal(string n) : name(n) {}
    void eat() { cout << name << " eats\n"; }
};

class Dog : public Animal {
public:
    Dog(string n) : Animal(n) {}
    void bark() { cout << "woof\n"; }
};

Inheritance structure:

flowchart TD
    Animal["Animal (base)"]
    Dog["Dog (derived)"]
    Cat["Cat (derived)"]
    
    Animal --> Dog
    Animal --> Cat
    
    Animal -.-> |eat| A1["eat()"]
    Dog -.-> |bark| D1["bark()"]
    Cat -.-> |meow| C1["meow()"]

Basic inheritance

class Animal {
protected:
    string name;
    
public:
    Animal(string n) : name(n) {}
    
    void eat() {
        cout << name << " is eating" << endl;
    }
};

class Dog : public Animal {
public:
    Dog(string n) : Animal(n) {}
    
    void bark() {
        cout << name << " barks: woof!" << endl;
    }
};

int main() {
    Dog dog("Rex");
    dog.eat();   // inherited
    dog.bark();  // Dog-only
}

Access specifiers:

Inheritancepublic memberprotected memberprivate member
publicpublicprotectedinaccessible
protectedprotectedprotectedinaccessible
privateprivateprivateinaccessible
class Base {
public:
    int pub;
protected:
    int prot;
private:
    int priv;
};

class Derived : public Base {
    void func() {
        pub = 1;   // OK
        prot = 2;  // OK
        // priv = 3;  // error
    }
};

virtual functions and polymorphism

class Animal {
public:
    virtual void speak() {
        cout << "animal sound" << endl;
    }
};

class Dog : public Animal {
public:
    void speak() override {
        cout << "woof!" << endl;
    }
};

class Cat : public Animal {
public:
    void speak() override {
        cout << "meow!" << endl;
    }
};

int main() {
    Animal* animals[3];
    animals[0] = new Animal();
    animals[1] = new Dog();
    animals[2] = new Cat();
    
    for (int i = 0; i < 3; i++) {
        animals[i]->speak();
    }
    
    for (int i = 0; i < 3; i++) {
        delete animals[i];
    }
}

Abstract classes (pure virtual)

class Shape {
public:
    virtual double area() = 0;
    virtual double perimeter() = 0;
    virtual ~Shape() {}
};

class Circle : public Shape {
private:
    double radius;
    
public:
    Circle(double r) : radius(r) {}
    
    double area() override {
        return 3.14159 * radius * radius;
    }
    
    double perimeter() override {
        return 2 * 3.14159 * radius;
    }
};

class Rectangle : public Shape {
private:
    double width, height;
    
public:
    Rectangle(double w, double h) : width(w), height(h) {}
    
    double area() override {
        return width * height;
    }
    
    double perimeter() override {
        return 2 * (width + height);
    }
};

int main() {
    Shape* shapes[2];
    shapes[0] = new Circle(5.0);
    shapes[1] = new Rectangle(4.0, 6.0);
    
    for (int i = 0; i < 2; i++) {
        cout << "area: " << shapes[i]->area() << endl;
        delete shapes[i];
    }
}

Practical examples

Example 1: Game characters

#include <iostream>
#include <vector>
#include <string>
using namespace std;

class Character {
protected:
    string name;
    int hp;
    int attackPower;
    
public:
    Character(string n, int h, int ap) 
        : name(n), hp(h), attackPower(ap) {}
    
    virtual ~Character() {}
    
    virtual void attack(Character* target) {
        cout << name << " attacks!" << endl;
        target->takeDamage(attackPower);
    }
    
    virtual void takeDamage(int damage) {
        hp -= damage;
        cout << name << " took " << damage << " damage (HP: " << hp << ")" << endl;
    }
    
    virtual void useSkill() = 0;
    
    bool isAlive() { return hp > 0; }
    string getName() { return name; }
};

class Warrior : public Character {
public:
    Warrior(string n) : Character(n, 150, 30) {}
    
    void useSkill() override {
        cout << name << " uses Strike!" << endl;
        attackPower += 20;
    }
};

class Mage : public Character {
public:
    Mage(string n) : Character(n, 80, 50) {}
    
    void useSkill() override {
        cout << name << " casts Fireball!" << endl;
        attackPower += 30;
    }
};

class Healer : public Character {
public:
    Healer(string n) : Character(n, 100, 15) {}
    
    void useSkill() override {
        cout << name << " heals!" << endl;
        hp += 50;
        cout << "HP restored (now " << hp << ")" << endl;
    }
};

int main() {
    vector<Character*> party;
    party.push_back(new Warrior("Warrior"));
    party.push_back(new Mage("Mage"));
    party.push_back(new Healer("Healer"));
    
    for (auto& character : party) {
        character->useSkill();
    }
    
    for (auto& character : party) {
        delete character;
    }
    
    return 0;
}

Polymorphism lets one container hold different character types behind a common Character* interface.

Example 2: Payment system

#include <iostream>
#include <string>
using namespace std;

class PaymentMethod {
public:
    virtual bool pay(double amount) = 0;
    virtual string getMethodName() = 0;
    virtual ~PaymentMethod() {}
};

class CreditCard : public PaymentMethod {
private:
    string cardNumber;
    
public:
    CreditCard(string num) : cardNumber(num) {}
    
    bool pay(double amount) override {
        cout << "Credit card payment: " << amount << " KRW" << endl;
        cout << "Card: " << cardNumber << endl;
        return true;
    }
    
    string getMethodName() override {
        return "Credit card";
    }
};

class BankTransfer : public PaymentMethod {
private:
    string accountNumber;
    
public:
    BankTransfer(string acc) : accountNumber(acc) {}
    
    bool pay(double amount) override {
        cout << "Bank transfer: " << amount << " KRW" << endl;
        cout << "Account: " << accountNumber << endl;
        return true;
    }
    
    string getMethodName() override {
        return "Bank transfer";
    }
};

class PaymentProcessor {
public:
    void processPayment(PaymentMethod* method, double amount) {
        cout << "\n=== Payment ===" << endl;
        cout << "Method: " << method->getMethodName() << endl;
        
        if (method->pay(amount)) {
            cout << "Success!" << endl;
        } else {
            cout << "Failed!" << endl;
        }
    }
};

int main() {
    PaymentProcessor processor;
    
    CreditCard card("1234-5678-9012-3456");
    processor.processPayment(&card, 50000);
    
    BankTransfer transfer("123-456-789012");
    processor.processPayment(&transfer, 30000);
    
    return 0;
}

Example 3: Document formats

#include <iostream>
#include <string>
using namespace std;

class Document {
protected:
    string content;
    
public:
    Document(string c) : content(c) {}
    virtual ~Document() {}
    
    virtual void save(const string& filename) = 0;
    virtual string getFormat() = 0;
};

class PDFDocument : public Document {
public:
    PDFDocument(string c) : Document(c) {}
    
    void save(const string& filename) override {
        cout << "Save as PDF: " << filename << ".pdf" << endl;
        cout << "Content: " << content << endl;
    }
    
    string getFormat() override {
        return "PDF";
    }
};

class WordDocument : public Document {
public:
    WordDocument(string c) : Document(c) {}
    
    void save(const string& filename) override {
        cout << "Save as Word: " << filename << ".docx" << endl;
        cout << "Content: " << content << endl;
    }
    
    string getFormat() override {
        return "Word";
    }
};

class DocumentConverter {
public:
    void convert(Document* doc, const string& filename) {
        cout << "\n=== Convert ===" << endl;
        cout << "Format: " << doc->getFormat() << endl;
        doc->save(filename);
    }
};

int main() {
    DocumentConverter converter;
    
    PDFDocument pdf("PDF body");
    converter.convert(&pdf, "report");
    
    WordDocument word("Word body");
    converter.convert(&word, "letter");
    
    return 0;
}

Common problems

Problem 1: Missing virtual destructor

Symptom: derived destructor never runs (leak).
Cause: base destructor is not virtual.
Fix:

class Base {
public:
    virtual ~Base() { cout << "~Base" << endl; }
};

Problem 2: Missing override

Symptom: you meant to override but created a new function.
Cause: signature mismatch.
Fix: use override and match the base signature.

Problem 3: Slicing

Symptom: derived state is lost.
Cause: copy by value.
Fix: use pointers or references to base.

Patterns

Template method

class DataProcessor {
public:
    void process() {
        loadData();
        validateData();
        transformData();
        saveData();
    }
    
    virtual ~DataProcessor() = default;
    
protected:
    virtual void loadData() = 0;
    virtual void validateData() {}
    virtual void transformData() = 0;
    virtual void saveData() = 0;
};

class CSVProcessor : public DataProcessor {
protected:
    void loadData() override { std::cout << "load CSV\n"; }
    void transformData() override { std::cout << "transform CSV\n"; }
    void saveData() override { std::cout << "save CSV\n"; }
};

CSVProcessor processor;
processor.process();

Interface segregation

class IReadable {
public:
    virtual std::string read() = 0;
    virtual ~IReadable() = default;
};

class IWritable {
public:
    virtual void write(const std::string& data) = 0;
    virtual ~IWritable() = default;
};

class File : public IReadable, public IWritable {
public:
    std::string read() override { return "file content"; }
    void write(const std::string& data) override {
        std::cout << "write: " << data << '\n';
    }
};

Factory method

class Product {
public:
    virtual void use() = 0;
    virtual ~Product() = default;
};

class ConcreteProductA : public Product {
public:
    void use() override { std::cout << "Product A\n"; }
};

class ConcreteProductB : public Product {
public:
    void use() override { std::cout << "Product B\n"; }
};

class Creator {
public:
    virtual std::unique_ptr<Product> createProduct() = 0;
    virtual ~Creator() = default;
    
    void operation() {
        auto product = createProduct();
        product->use();
    }
};

class CreatorA : public Creator {
public:
    std::unique_ptr<Product> createProduct() override {
        return std::make_unique<ConcreteProductA>();
    }
};

FAQ

Q1: Are virtual functions slow?

A: Small overhead (vtable indirection); flexibility usually wins.

Q2: Make everything virtual?

A: No—only functions that need overriding. Destructors in polymorphic bases should be virtual.

Q3: Multiple inheritance?

A: Prefer sparingly; multiple interface inheritance is common; watch for the diamond problem.

Q4: override vs final?

A: override checks you really override; final stops further overrides.

Q5: Abstract class vs interface?

A: C++ has no interface keyword—use classes with only pure virtual functions as interfaces.

Q6: Inheritance vs composition?

A: is-a vs has-a; composition is often more flexible.

Q7: Why virtual destructor?

A: So deleting through Base* runs Derived::~Derived().

Q8: Resources?

A: Effective C++, C++ Primer, cppreference — derived class.

One-line summary: Inheritance shares implementation; polymorphism dispatches to the right override through a common interface.


  • Virtual functions
  • Polymorphism with composition and variant
  • override and final

Practical tips

Debugging

  • Enable warnings

Performance

  • Profile first

Code review

  • Team conventions

Practical checklist

Before coding

  • Right tool?
  • Maintainable?
  • Meets performance goals?

While coding

  • Warnings clean?
  • Edge cases?
  • Error handling?

At review

  • Clear intent?
  • Tests?
  • Documentation?

Keywords

C++, inheritance, polymorphism, virtual, OOP


  • Virtual destructor
  • Virtual functions
  • Slicing
  • Composition & variant
  • Classes and objects