Quick Reference
기반 타입 포인터나 참조로 여러 구현을 다뤄야 하면 멤버 함수를 virtual로 선언하고 파생 클래스에는 override를 붙입니다. 기반 포인터로 삭제할 가능성이 있는 다형 기반 클래스에는 가상 소멸자가 필요합니다.
#include <iostream>
#include <memory>
#include <string>
#include <utility>
class Shape {
public:
virtual double area() const = 0;
virtual std::string name() const { return "Shape"; }
virtual ~Shape() = default;
};
class Circle : public Shape {
public:
explicit Circle(double radius) : radius_{radius} {}
double area() const override { return 3.14159 * radius_ * radius_; }
std::string name() const override { return "Circle"; }
private:
double radius_;
};
int main() {
std::unique_ptr<Shape> shape = std::make_unique<Circle>(5.0);
std::cout << shape->name() << ": " << shape->area() << '\n';
}문법
동적 디스패치와 구현 방식
virtual 호출은 기반 포인터·참조의 정적 타입이 아니라 실제 객체의 동적 타입에 맞는 오버라이드를 실행합니다. 표준은 이 결과를 정할 뿐 구현 방식을 규정하지 않습니다. 많은 구현이 아래처럼 vtable(가상 함수 테이블)과 vptr을 사용하지만, 이는 이해를 위한 대표 구현입니다.
대표 구현: Shape 객체 [vptr → Shape vtable]
대표 구현: Circle 객체 [vptr → Circle vtable → Circle::area]std::unique_ptr<Shape> ptr = std::make_unique<Circle>(3.0);
ptr->area(); // 실제 Circle 객체의 area()가 호출됨virtual 없으면 정적 바인딩
virtual이 없으면 호출할 함수가 컴파일 타임에 포인터의 타입으로 결정됩니다.
class Animal {
public:
void speak() { std::cout << "...\n"; } // virtual 없음
virtual void breathe() { std::cout << "breathe\n"; }
};
class Dog : public Animal {
public:
void speak() { std::cout << "Woof\n"; }
void breathe() override { std::cout << "pant\n"; }
};
Animal* a = new Dog{};
a->speak(); // ❌ "..." — Animal::speak (정적 바인딩, 포인터 타입 기준)
a->breathe(); // ✅ "pant" — Dog::breathe (동적 바인딩, 실제 타입 기준)상속 접근 지정자 — public / protected / private
class Derived : public Base는 기반 클래스의 접근 지정자를 그대로 유지합니다. protected나 private 상속은 외부에서 기반 클래스 인터페이스를 숨깁니다.
class Base {
public: void pub() {}
protected: void prot() {}
private: void priv() {}
};
class PubDerived : public Base {}; // pub→public, prot→protected
class ProtDerived : protected Base {}; // pub→protected, prot→protected
class PrivDerived : private Base {}; // pub→private, prot→private
PubDerived pd; pd.pub(); // ✅ public 상속 — 외부 접근 가능
ProtDerived rt; rt.pub(); // ❌ protected 상속 — 외부 접근 불가
PrivDerived pv; pv.pub(); // ❌ private 상속 — 외부 접근 불가
// private 상속 = "has-a" 구현에서만 사용
// public 상속 = "is-a" 관계 (LSP 만족)virtual 기반 클래스 — 다이아몬드 문제 해결
class Animal { public: void breathe() {} };
// ❌ 다이아몬드 — Animal이 두 번 포함됨
class Mammal : public Animal {};
class Bird : public Animal {};
class Bat : public Mammal, public Bird {};
Bat bat;
bat.breathe(); // ❌ 모호함 — Mammal::Animal::breathe? Bird::Animal::breathe?
// ✅ virtual 상속 — Animal 하나만 공유
class Mammal2 : virtual public Animal {};
class Bird2 : virtual public Animal {};
class Bat2 : public Mammal2, public Bird2 {};
Bat2 bat2;
bat2.breathe(); // ✅ 하나의 Animal::breatheoverride — 컴파일러에게 오버라이드 확인 요청
override를 붙이면 컴파일러가 기반 클래스에 동일한 시그니처의 virtual 함수가 있는지 확인합니다. 오타나 시그니처 불일치를 컴파일 오류로 잡아줍니다.
class Base {
virtual void f(int x) const { }
};
class Derived : public Base {
void f(int x) override { } // ❌ const 누락 — 컴파일 오류 (override 덕에 감지)
void f(int x) const override { } // ✅
// override 없이 쓰면 새 함수 정의로 조용히 넘어감
};체크포인트
| 상황 | 적합한 선택 |
|---|---|
| 런타임 다형성 필요 | virtual + 파생 클래스에 override |
| 반드시 구현 강제 | 순수 가상 = 0 |
| 오버라이드 실수 방지 | override 키워드 항상 명시 |
| 상속 금지 | final 클래스 또는 함수 |
| 가상 함수 있는 클래스의 소멸자 | virtual ~T() = default 필수 |
| 구현 재사용 (is-a 아닌 경우) | private 상속 (선호: 멤버로 포함) |
| 다이아몬드 상속 중복 방지 | virtual 기반 클래스 |
주의할 점
기반 클래스 소멸자가 virtual이 아니면 파생 클래스 소멸자가 호출되지 않아 자원이 누수됩니다.
class Base {
public:
~Base() { std::cout << "Base 소멸\n"; } // ❌ virtual 없음
};
class Derived : public Base {
std::vector<int> data_; // 동적 자원
public:
~Derived() { std::cout << "Derived 소멸\n"; }
};
Base* p = new Derived{};
delete p; // ❌ Base::~Base만 호출 — Derived::~Derived 호출 안 됨 (누수)
// ✅ 기반 클래스에 virtual 소멸자
class Base2 {
public:
virtual ~Base2() = default; // Derived::~Derived도 자동 호출
};참고 링크
1 sources