핵심 정리
cpp
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 r) : r_{r} { }
double area() const override { return 3.14159 * r_ * r_; }
std::string name() const override { return "Circle"; }
private:
double r_;
};
std::unique_ptr<Shape> s = std::make_unique<Circle>(5.0);
std::cout << s->area(); // Circle::area() 호출 — 다형성문법
vtable — 가상 함수 디스패치의 원리
virtual 함수가 있는 클래스는 **vtable(가상 함수 테이블)**을 가집니다. 각 객체는 자신의 클래스 vtable을 가리키는 포인터를 숨겨서 갖고 있어, 런타임에 실제 타입의 함수를 찾아 호출합니다.
text
Shape 객체: [vptr → Shape vtable] [area → Shape::area (pure)]
Circle 객체: [vptr → Circle vtable] [area → Circle::area]
[name → Circle::name]cpp
Shape* ptr = new Circle{3.0};
ptr->area(); // ptr이 Circle 인스턴스 → Circle vtable → Circle::area 호출virtual 없으면 정적 바인딩
virtual이 없으면 호출할 함수가 컴파일 타임에 포인터의 타입으로 결정됩니다.
cpp
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 상속은 외부에서 기반 클래스 인터페이스를 숨깁니다.
cpp
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 기반 클래스 — 다이아몬드 문제 해결
cpp
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 함수가 있는지 확인합니다. 오타나 시그니처 불일치를 컴파일 오류로 잡아줍니다.
cpp
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이 아니면 파생 클래스 소멸자가 호출되지 않아 자원이 누수됩니다.
cpp
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