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:
| Inheritance | public member | protected member | private member |
|---|---|---|---|
public | public | protected | inaccessible |
protected | protected | protected | inaccessible |
private | private | private | inaccessible |
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.
Related posts (internal links)
- 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
Related posts
- Virtual destructor
- Virtual functions
- Slicing
- Composition & variant
- Classes and objects