Quick Reference
다형 기반 타입을 Base* 또는 unique_ptr<Base>로 삭제할 수 있으면 소멸자를 virtual로 둡니다. 파생 객체를 기반 클래스 값으로 복사하면 파생 부분이 잘리는 slicing이 발생하므로, 다형성은 참조나 포인터로 보존합니다.
#include <iostream>
#include <memory>
#include <string>
class Animal {
public:
virtual ~Animal() = default;
virtual void speak() const = 0;
};
class Dog : public Animal {
public:
void speak() const override { std::cout << "Woof\n"; }
~Dog() override { std::cout << "Dog destroyed\n"; }
};
int main() {
std::unique_ptr<Animal> animal = std::make_unique<Dog>();
animal->speak();
}문법
virtual destructor — 기반 포인터로 delete할 때의 위험
동적 타입이 Derived인 객체를 Base*로 delete할 때 Base 소멸자가 virtual이 아니면 동작은 정의되지 않습니다. 어떤 소멸자가 호출될지나 자원이 정리될지를 가정할 수 없으므로, 기반 포인터 삭제를 허용하는 다형 기반 타입에는 virtual 소멸자가 필요합니다.
class Base {
public:
// ❌ virtual 없음
~Base() { std::cout << "Base dtor\n"; }
};
class Derived : public Base {
int* data_ = new int[100];
public:
~Derived() {
delete[] data_;
std::cout << "Derived dtor\n";
}
};
Base* p = new Derived();
delete p; // ❌ 정의되지 않은 동작: Derived 정리를 기대할 수 없음
// ✅ virtual ~Base() = default; 추가 시
// delete p → Derived 소멸자 → Base 소멸자 순으로 올바르게 호출object slicing — 값 복사 시 파생 부분 소실
파생 객체를 기반 클래스 값으로 복사하면 기반 클래스 크기만큼만 복사되고 파생 부분이 잘려 나갑니다. 이후 virtual dispatch가 의도대로 동작하지 않습니다.
struct Animal {
virtual std::string sound() const { return "..."; }
};
struct Dog : Animal {
std::string sound() const override { return "Woof"; }
};
Dog dog;
Animal a = dog; // ❌ object slicing — Dog 부분 제거됨
a.sound(); // "..." — Dog::sound 아님, Base::sound 호출
// ❌ 벡터에 값으로 저장 — 삽입 시 슬라이싱
std::vector<Animal> animals;
animals.push_back(Dog{}); // ❌ Dog 부분 잘림
// ✅ 포인터/스마트 포인터로 다형성 유지
std::vector<std::unique_ptr<Animal>> animals;
animals.push_back(std::make_unique<Dog>());
animals[0]->sound(); // ✅ "Woof"다형성의 핵심 — 참조/포인터로만 유지
다형성은 값 복사 시 사라집니다. 기반 클래스 참조나 포인터를 통해서만 virtual dispatch가 올바르게 동작합니다.
void makeSoundByReference(Animal& a) { // ✅ 참조 — 슬라이싱 없음
a.sound(); // 동적 타입에 따라 올바른 함수 호출
}
void makeSoundByValue(Animal a) { // ❌ 값 — 슬라이싱 발생
a.sound(); // 항상 Animal::sound 호출
}
Dog d;
makeSoundByReference(d); // "Woof"
makeSoundByValue(d); // "..." (정의된 slicing)체크포인트
| 상황 | 적합한 선택 |
|---|---|
| 기반 포인터로 delete 가능성 있을 때 | virtual ~Base() = default |
| 다형성을 컨테이너에 담을 때 | vector<unique_ptr<Base>> |
| 함수에서 다형성 유지 | Base& 또는 Base* 인자 |
| 복사는 허용하지 않는 추상 기반 클래스 | = delete copy ctor/assignment |
주의할 점
다형 기반 객체를 기반 포인터로 delete할 수 있는데 소멸자가 virtual이 아니면 정의되지 않은 동작입니다. 반대로 기반 포인터 삭제를 막는 타입은 protected non-virtual 소멸자로 의도를 표현할 수 있습니다.
// ❌ virtual 소멸자 없음 + 기반 포인터로 delete: 정의되지 않은 동작
struct BadBase { ~BadBase() = default; };
struct BadDerived : BadBase {
std::string data{"lots of data"};
~BadDerived() { std::cout << "cleaned\n"; }
};
BadBase* bad = new BadDerived();
delete bad; // ❌ 파생 소멸자 호출 여부를 가정할 수 없음
// ✅ virtual 소멸자: 기반 포인터로 삭제 가능
struct GoodBase { virtual ~GoodBase() = default; virtual void speak() const {} };
struct GoodDerived : GoodBase { void speak() const override {} };
std::unique_ptr<GoodBase> good = std::make_unique<GoodDerived>();
// ❌ 함수 인자를 값으로 받으면 slicing 발생
void processByValue(GoodBase b) { b.speak(); } // 정의된 slicing, 항상 GoodBase 버전 호출
// ✅ 참조로 받아야 다형성 유지
void processByReference(const GoodBase& b) { b.speak(); } // ✅ 동적 타입 기준 호출참고 링크
2 sources