C++ 가상 함수 심화 가이드 | vtable·vptr, 가상 상속, 추상 클래스, 가상 소멸자
이 글의 핵심
가상 함수의 동적 디스패치, vtable·vptr 레이아웃, 가상 상속·다이아몬드 문제, 순수 가상·추상 클래스, 가상 소멸자 필요 조건, 프로덕션 다형성 패턴까지 한 번에 정리합니다.
가상 함수가 왜 필요할까? (실생활 비유)
커피숍 예시로 이해하기: 여러분이 커피숍 사장이라고 상상해보세요. “음료”라는 카테고리 안에 아메리카노, 라떼, 프라푸치노 등 다양한 종류가 있습니다. 손님이 주문할 때 “음료 하나 주세요”라고만 하면, 실제로는 그 손님이 주문한 구체적인 음료를 만들어야 합니다.
프로그래밍에서도 마찬가지입니다. “동물”이라는 기본 개념이 있으며, 개·고양이·새처럼 구체적인 동물들이 있습니다. speak() (울음소리 내기)라는 행동은 모든 동물이 할 수 있지만, 실제로 내는 소리는 동물마다 다릅니다. 이때 virtual 키워드를 쓰면, “동물” 포인터로 가리켜도 실제 동물의 울음소리가 나오게 할 수 있습니다.
가상 함수란?
가상 함수(virtual function)는 실행 시점(런타임)에 실제 객체의 타입을 보고 어떤 함수를 호출할지 결정하는 멤버 함수입니다. 쉽게 말해, “겉으로는 동물인데, 실제로는 개”인 경우, 개의 speak()를 호출해주는 것입니다.
첫 번째 예제: 동물 울음소리
// 기본 "동물" 클래스 - 모든 동물의 공통 특성
class Animal {
public:
// virtual 키워드: "이 함수는 나중에 다시 정의될 수 있어요"
virtual void speak() {
cout << "Animal sound" << endl; // 기본 동물 소리
}
};
// 개 클래스 - Animal을 상속받아 확장
class Dog : public Animal {
public:
// override 키워드: "부모의 speak()를 다시 정의합니다"
void speak() override {
cout << "Woof!" << endl; // 개는 멍멍!
}
};
// 고양이 클래스 - Animal을 상속받아 확장
class Cat : public Animal {
public:
void speak() override {
cout << "Meow!" << endl; // 고양이는 야옹!
}
};
int main() {
// 중요! Animal* 타입이지만 실제로는 Dog 객체를 가리킴
Animal* animal1 = new Dog();
// Animal* 타입이지만 실제로는 Cat 객체를 가리킴
Animal* animal2 = new Cat();
// virtual 덕분에 실제 객체(Dog, Cat)의 함수가 호출됨!
animal1->speak(); // 출력: Woof! (Dog의 speak 호출)
animal2->speak(); // 출력: Meow! (Cat의 speak 호출)
// 메모리 정리
delete animal1;
delete animal2;
}
핵심 포인트: Animal* 타입의 포인터지만, virtual 덕분에 실제 객체가 개인지 고양이인지를 런타임에 확인하고 올바른 speak()를 호출합니다.
virtual vs non-virtual: 차이가 뭘까?
핵심 차이: virtual이 없으면 포인터 타입만 보고 함수를 결정하며, virtual이 있으면 실제 객체의 타입을 보고 함수를 결정합니다.
비교 예제
class Base {
public:
// virtual이 없는 일반 함수
void nonVirtual() {
cout << "Base::nonVirtual" << endl;
}
// virtual이 있는 가상 함수
virtual void virtualFunc() {
cout << "Base::virtualFunc" << endl;
}
};
class Derived : public Base {
public:
// 같은 이름의 함수를 만들었지만 virtual이 없음
void nonVirtual() {
cout << "Derived::nonVirtual" << endl;
}
// override로 부모의 가상 함수를 재정의
void virtualFunc() override {
cout << "Derived::virtualFunc" << endl;
}
};
int main() {
// Base* 타입 포인터지만 실제로는 Derived 객체
Base* ptr = new Derived();
// ❌ virtual이 없어서 Base 버전이 호출됨 (정적 바인딩)
ptr->nonVirtual(); // 출력: Base::nonVirtual
// ✅ virtual 덕분에 Derived 버전이 호출됨 (동적 바인딩)
ptr->virtualFunc(); // 출력: Derived::virtualFunc
delete ptr;
}
용어 설명
- 정적 바인딩: 컴파일 시점에 “포인터 타입”만 보고 결정 (빠름)
- 동적 바인딩: 실행 시점에 “실제 객체 타입”을 확인하고 결정 (유연함)
초보자 팁: virtual 없이는 “겉모습”만 보며, virtual이 있으면 “실제 정체”를 보고 판단한다고 기억하세요!
순수 가상 함수: “구현은 너희가 해!”
순수 가상 함수(pure virtual function)는 구현을 강제하는 함수입니다. = 0을 붙이면 “이 함수는 반드시 자식 클래스에서 구현해야 해요”라는 의미입니다.
왜 필요할까?
예를 들어 “도형”이라는 개념은 추상적입니다. “도형의 넓이”는 삼각형이냐 사각형이냐에 따라 계산 방법이 완전히 다릅니다. 따라서 Shape 클래스에서는 “넓이를 계산하는 함수가 있어야 한다”는 규칙만 정하며, 실제 계산은 각 도형 클래스가 알아서 하도록 강제하는 것입니다.
// 추상 클래스: 직접 만들 수 없는 "개념"
class Shape {
public:
// = 0이 붙으면 "순수 가상 함수" - 구현 강제!
virtual double area() const = 0;
virtual void draw() const = 0;
// 가상 소멸자 (나중에 설명)
virtual ~Shape() = default;
};
class Circle : public Shape {
private:
double radius;
public:
Circle(double r) : radius(r) {}
double area() const override {
return 3.14159 * radius * radius;
}
void draw() const override {
cout << "Drawing Circle" << endl;
}
};
class Rectangle : public Shape {
private:
double width, height;
public:
Rectangle(double w, double h) : width(w), height(h) {}
double area() const override {
return width * height;
}
void draw() const override {
cout << "Drawing Rectangle" << endl;
}
};
int main() {
// ❌ 에러! Shape는 추상 클래스라 직접 만들 수 없음
// Shape shape;
// ✅ 하지만 Shape* 포인터는 OK! (실제로는 구체적인 도형 가리킴)
vector<unique_ptr<Shape>> shapes;
// 반지름 5인 원 추가
shapes.push_back(make_unique<Circle>(5.0));
// 가로 4, 세로 6인 직사각형 추가
shapes.push_back(make_unique<Rectangle>(4.0, 6.0));
// 모든 도형을 순회하면서
for (const auto& shape : shapes) {
shape->draw(); // 각 도형의 그리기 방법 호출
cout << "Area: " << shape->area() << endl; // 각 도형의 넓이 계산 호출
}
// 출력 예시:
// Drawing Circle
// Area: 78.5398
// Drawing Rectangle
// Area: 24
}
핵심 개념
- 추상 클래스: 순수 가상 함수가 하나라도 있으면 직접 객체를 만들 수 없음
- 구체 클래스: 모든 순수 가상 함수를 구현한 클래스는 객체를 만들 수 있음
- 인터페이스 역할: “이런 기능이 있어야 해”라는 규약만 정의
순수 가상 함수와 추상 클래스 심화
virtual void f() = 0 형태의 순수 가상 함수가 하나라도 있으면 해당 클래스는 추상 클래스(abstract class)가 되어 직접 인스턴스화할 수 없습니다. 컴파일러는 모든 순수 가상 함수를 구체 클래스(concrete class)에서 구현했는지 검사합니다. 순수 가상 함수에도 본문을 함께 정의할 수 있는데(예: 인터페이스 기본 동작을 헤더와 분리할 때), 이 경우에도 추상성은 유지되며 파생 클래스에서 override로 호출하거나 재정의할 수 있습니다.
인터페이스 역할을 강조하려면 public 순수 가상만 두고 데이터 멤버를 최소화하는 편이 일반적입니다. 반면 부분 추상으로 공통 멤버나 비가상 멤버 함수를 기저에 두며, 변형이 필요한 지점만 순수 가상으로 남기는 설계도 흔합니다. 추상 기저는 std::unique_ptr<Interface>와 함께 쓰일 때 소유권 경계와 수명을 명확히 하기 좋습니다. 다만 추상 클래스라도 생성자는 존재할 수 있으며, 파생 클래스 생성자에서 기저 생성자를 호출하는 흐름은 그대로입니다.
실전 예시
예시 1: 파일 시스템
class FileSystemNode {
protected:
string name;
public:
FileSystemNode(const string& n) : name(n) {}
virtual ~FileSystemNode() = default;
virtual void print(int indent = 0) const = 0;
virtual size_t getSize() const = 0;
};
class File : public FileSystemNode {
private:
size_t size;
public:
File(const string& n, size_t s) : FileSystemNode(n), size(s) {}
void print(int indent = 0) const override {
cout << string(indent, ' ') << "- " << name
<< " (" << size << " bytes)" << endl;
}
size_t getSize() const override {
return size;
}
};
class Directory : public FileSystemNode {
private:
vector<unique_ptr<FileSystemNode>> children;
public:
Directory(const string& n) : FileSystemNode(n) {}
void add(unique_ptr<FileSystemNode> node) {
children.push_back(move(node));
}
void print(int indent = 0) const override {
cout << string(indent, ' ') << "+ " << name << "/" << endl;
for (const auto& child : children) {
child->print(indent + 2);
}
}
size_t getSize() const override {
size_t total = 0;
for (const auto& child : children) {
total += child->getSize();
}
return total;
}
};
int main() {
auto root = make_unique<Directory>("root");
auto docs = make_unique<Directory>("docs");
docs->add(make_unique<File>("readme.txt", 1024));
docs->add(make_unique<File>("guide.pdf", 5120));
root->add(move(docs));
root->add(make_unique<File>("main.cpp", 2048));
root->print();
cout << "Total size: " << root->getSize() << " bytes" << endl;
}
예시 2: 전략 패턴
class PaymentStrategy {
public:
virtual ~PaymentStrategy() = default;
virtual void pay(double amount) = 0;
};
class CreditCardPayment : public PaymentStrategy {
private:
string cardNumber;
public:
CreditCardPayment(const string& card) : cardNumber(card) {}
void pay(double amount) override {
cout << "카드 " << cardNumber << "로 "
<< amount << "원 결제" << endl;
}
};
class PayPalPayment : public PaymentStrategy {
private:
string email;
public:
PayPalPayment(const string& e) : email(e) {}
void pay(double amount) override {
cout << "PayPal " << email << "로 "
<< amount << "원 결제" << endl;
}
};
class ShoppingCart {
private:
unique_ptr<PaymentStrategy> paymentStrategy;
double total = 0;
public:
void setPaymentStrategy(unique_ptr<PaymentStrategy> strategy) {
paymentStrategy = move(strategy);
}
void addItem(double price) {
total += price;
}
void checkout() {
if (paymentStrategy) {
paymentStrategy->pay(total);
total = 0;
}
}
};
int main() {
ShoppingCart cart;
cart.addItem(10000);
cart.addItem(20000);
cart.setPaymentStrategy(make_unique<CreditCardPayment>("1234-5678"));
cart.checkout();
cart.addItem(15000);
cart.setPaymentStrategy(make_unique<PayPalPayment>("user@example.com"));
cart.checkout();
}
예시 3: 로거
class Logger {
public:
virtual ~Logger() = default;
virtual void log(const string& message) = 0;
};
class ConsoleLogger : public Logger {
public:
void log(const string& message) override {
cout << "[Console] " << message << endl;
}
};
class FileLogger : public Logger {
private:
string filename;
public:
FileLogger(const string& file) : filename(file) {}
void log(const string& message) override {
ofstream ofs(filename, ios::app);
ofs << "[File] " << message << endl;
}
};
class Application {
private:
unique_ptr<Logger> logger;
public:
void setLogger(unique_ptr<Logger> l) {
logger = move(l);
}
void run() {
if (logger) {
logger->log("Application started");
// ...
logger->log("Application finished");
}
}
};
int main() {
Application app;
app.setLogger(make_unique<ConsoleLogger>());
app.run();
app.setLogger(make_unique<FileLogger>("app.log"));
app.run();
}
vtable과 vptr: 가상 함수의 비밀 무기
“어떻게 런타임에 올바른 함수를 찾을까?”
C++는 vtable(가상 함수 테이블)과 vptr(가상 함수 테이블 포인터)이라는 메커니즘을 사용합니다.
쉬운 비유: 전화번호부
- vtable: 각 클래스마다 가지고 있는 “함수 주소 목록” (전화번호부)
- vptr: 객체 안에 숨어있는 “전화번호부를 가리키는 포인터”
객체 생성 시:
Dog 객체 → vptr → Dog용 vtable [speak=Dog::speak 주소]
Cat 객체 → vptr → Cat용 vtable [speak=Cat::speak 주소]
함수 호출 시:
animal->speak()
→ animal의 vptr 확인
→ vtable에서 speak 찾기
→ 해당 주소로 점프!
내부 동작 3단계
Animal* animal = new Dog();
animal->speak();
// 컴파일러가 하는 일 (개념적으로):
// 1단계: animal 객체의 vptr을 찾는다
// 2단계: vptr이 가리키는 vtable을 찾는다
// 3단계: vtable에서 speak() 함수 주소를 찾아 호출한다
메모리 추가 비용
가상 함수를 쓰면:
- 객체마다 vptr 포인터 하나 추가 (보통 8바이트)
- 함수 호출 시 간접 참조 한 번 더 (미세하게 느림)
초보자 팁: 대부분의 경우 이 비용은 무시할 만합니다. “성능 때문에 virtual을 안 써야 하나?” 고민하기보다는, 먼저 코드를 깔끔하게 만드는 게 중요합니다!
다중 상속에서는 기저 클래스마다 별도의 vptr·vtable이 필요할 수 있으며, 두 번째 이후 기저로 포인터를 바꿀 때 this 포인터 조정이 일어납니다. 이때 썽크(thunk) 코드가 끼어 올바른 this로 가상 호출을 이어주기도 합니다. 따라서 다중 상속·가상 상속이 겹칠수록 레이아웃과 호출 비용이 커지며, 성능·단순성 측면에서 단일 상속 + 컴포지션이 우선 검토 대상이 됩니다.
// 개념적 주석: 실제 레이아웃은 ABI/컴파일러에 의존
class Base {
public:
virtual void func1() {}
virtual void func2() {}
virtual ~Base() = default;
};
// Base 인스턴스 ≈ [ vptr ] + [ 비트 필드·데이터 멤버 ... ]
// vptr → vtable[0]=&Base::func1, [1]=&Base::func2, [2]=destructor pair 등
가상 상속과 다이아몬드 문제
다중 상속에서 동일한 비가상 기저 클래스가 서로 다른 경로로 두 번 이상 들어오면, 파생 객체 안에 동일 기저의 서브객체가 복수 생깁니다. 이를 다이아몬드 구조라고 부릅니다. 예를 들어 A를 B, C가 각각 상속하고 D가 B와 C를 동시에 상속하면, D 안에 A가 두 벌 생길 수 있어 A의 멤버에 접근할 때 모호함이 생기거나, 논리적으로 하나여야 할 기저 상태가 두 개로 갈라집니다.
이를 막으려면 B, C가 A를 virtual 상속하도록 하면 됩니다. 그러면 D에는 A 서브객체가 하나만 존재합니다. 대신 구현체는 가상 베이스 오프셋을 해석하기 위한 vbptr 등 추가 메타데이터를 둘 수 있어, 객체 크기·접근 비용이 커집니다. 따라서 실무에서는 다이아몬드를 설계로 피할 수 있는지(인터페이스 분리, 컴포지션)를 먼저 검토하는 경우가 많습니다.
// 개념 예시: 가상 상속으로 기저 A 단일화
struct A {
int value = 1;
virtual ~A() = default;
virtual void f() {}
};
struct B : virtual A { /* ... */ };
struct C : virtual A { /* ... */ };
struct D : B, C {
// A는 D 안에서 한 번만 존재
};
가상 소멸자: 메모리 누수를 막는 핵심!
문제 상황
class Base {
public:
~Base() { } // ❌ virtual이 없음!
};
class Derived : public Base {
private:
int* bigData; // 큰 데이터를 가리키는 포인터
public:
Derived() {
bigData = new int[10000]; // 메모리 할당
}
~Derived() {
delete[] bigData; // 메모리 해제
}
};
int main() {
Base* ptr = new Derived(); // Base 포인터지만 실제로는 Derived
delete ptr; // 💥 문제 발생!
// Base 소멸자만 호출됨
// Derived 소멸자가 호출 안 됨
// bigData 메모리 해제 안 됨 = 메모리 누수!
}
해결책: 가상 소멸자 사용
class Base {
public:
virtual ~Base() { // ✅ virtual 추가!
cout << "~Base()" << endl;
}
};
class Derived : public Base {
private:
int* data;
public:
Derived() : data(new int[100]) {}
~Derived() {
cout << "~Derived()" << endl;
delete[] data; // 이제 정상적으로 호출됨!
}
};
int main() {
Base* ptr = new Derived();
delete ptr;
// 출력:
// ~Derived() ← 먼저 파생 클래스 소멸자
// ~Base() ← 그 다음 기저 클래스 소멸자
}
초보자가 자주 하는 실수: “내 클래스는 상속받을 일이 없을 것 같아서 virtual 안 붙였어요” → 나중에 누군가 상속받으면 버그 발생!
황금 규칙: 다른 클래스가 상속받을 수 있는 클래스라면 무조건 가상 소멸자를 만드세요!
가상 소멸자가 호출되면 파생 → 기저 순으로 소멸 규칙이 적용되며, 가장 파생 클래스의 소멸자 본문이 먼저 실행된 뒤 기저것으로 진행됩니다. 가상 소멸자가 “항상” 필요한 것은 아닙니다. 기저 클래스를 다형적으로 삭제하지 않고, 문서/설계상 기저 포인터로 파생을 잡지 않는다면 비가상 소멸자를 유지할 수도 있습니다. 혹은 다형 인터페이스이지만 delete를 막고 싶다면 protected ~Base(); 같은 패턴으로 잘못된 스택/힙 삭제를 제한하기도 합니다.
class Base {
public:
virtual ~Base() {
cout << "~Base()" << endl;
}
};
class Derived : public Base {
private:
int* data;
public:
Derived() : data(new int[100]) {}
~Derived() {
cout << "~Derived()" << endl;
delete[] data;
}
};
int main() {
Base* ptr = new Derived();
delete ptr; // ~Derived() 후 ~Base() — 가상 소멸자 덕분에 안전
}
프로덕션 다형성 패턴
인터페이스 + 팩토리 + 스마트 포인터 조합은 서비스 코드에서 가장 흔한 형태입니다. 구현 타입은 컴파일 의존을 줄이기 위해 .cpp에 두며, 헤더에는 추상 기저와 std::unique_ptr<Interface> create...() 정도만 노출합니다. NVI(Non-Virtual Interface) 패턴은 public 비가상 멤버가 템플릿 메서드처럼 전후처리를 고정하며, private/protected 가상 함수로만 확장점을 열어 불변식(invariant)을 지키기 쉽게 합니다.
테스트에서는 동일 인터페이스에 목(mock) 구현을 꽂아 I/O·네트워크를 대체합니다. 성능이 중요한 경로에서는 final로 더 이상 오버라이드되지 않음을 알리거나, 컴파일 타임 다형성(CRTP 등)으로 가상 호출을 없애는 방안을 비교합니다. 핫패스는 프로파일로 확인한 뒤에만 최적화하는 것이 안전합니다. 광범위한 virtual 남발보다 경계가 명확한 소수 인터페이스가 유지보수에 유리한 경우가 많습니다.
// NVI 스케치: 공개 비가상 인터페이스 + protected 가상 구현 훅
class Engine {
public:
void run() { // 고정된 전후처리
on_before();
do_work(); // 파생에서만 오버라이드
on_after();
}
virtual ~Engine() = default;
protected:
virtual void do_work() = 0;
virtual void on_before() {}
virtual void on_after() {}
};
초보자가 자주 하는 실수 TOP 3
실수 1: 가상 소멸자를 안 만듦
// ❌ 가상 소멸자 없음
class Base {
public:
~Base() {
cout << "~Base()" << endl;
}
};
class Derived : public Base {
private:
int* data;
public:
Derived() : data(new int[100]) {}
~Derived() {
cout << "~Derived()" << endl;
delete[] data; // 호출 안 됨!
}
};
Base* ptr = new Derived();
delete ptr; // 메모리 누수
// ✅ 가상 소멸자
class Base {
public:
virtual ~Base() {
cout << "~Base()" << endl;
}
};
실수 2: override를 안 연결하여서 오타 발견 못 함
class Base {
public:
virtual void calculate() {}
};
// ❌ 오타가 있는데 컴파일은 됨 (새 함수로 인식)
class Derived : public Base {
public:
void calcuate() {} // 오타! calculate가 아니라 calcuate
// 부모 함수를 재정의한 게 아니라 새 함수를 만든 것!
};
int main() {
Base* ptr = new Derived();
ptr->calculate(); // Base의 calculate가 호출됨 (의도와 다름!)
}
해결책: 항상 override 붙이기!
class Derived : public Base {
public:
void calcuate() override {}
// ✅ 컴파일 에러 발생!
// "부모에 calcuate라는 함수가 없어요!"
};
초보자 팁: override는 귀찮아도 무조건 붙이세요. 오타를 컴파일 단계에서 잡아줍니다!
실수 3: 객체 슬라이싱 (Object Slicing)
가장 헷갈리는 버그!
class Base {
public:
virtual void print() {
cout << "I'm Base" << endl;
}
};
class Derived : public Base {
private:
int extraData = 42; // Derived만의 추가 데이터
public:
void print() override {
cout << "I'm Derived with " << extraData << endl;
}
};
// ❌ 슬라이싱 발생!
Derived d;
Base b = d; // Derived를 Base에 "복사"
// 이때 Derived의 extraData는 잘려나감!
b.print(); // 출력: I'm Base (Derived 부분이 사라짐)
// ✅ 올바른 방법: 포인터나 참조 사용
Base* ptr = &d;
ptr->print(); // 출력: I'm Derived with 42
왜 이런 일이?: Base b = d;는 복사입니다. Base 크기만큼만 복사되므로 Derived의 추가 부분은 잘려나갑니다.
초보자 팁: 다형성을 쓸 땐 항상 포인터(*)나 참조(&)를 사용하세요!
초보자 FAQ (자주 묻는 질문)
Q1: 가상 함수는 언제 써야 하나요?
A: 다음 상황에서 사용하세요:
- “종류가 여러 개”인 경우: 동물(개, 고양이, 새…), 도형(원, 사각형, 삼각형…)
- “같은 인터페이스, 다른 구현”: 모두
draw()가 있지만 그리는 방법은 다름 - 런타임에 타입이 결정: 사용자 입력에 따라 어떤 객체를 쓸지 결정
쉬운 판단 기준: “이 함수를 자식 클래스마다 다르게 구현해야 하나?” → Yes면 virtual!
Q2: virtual을 쓰면 얼마나 느려지나요?
A: 걱정하지 마세요!
- 추가 비용: 함수 호출 시 포인터 한 번 더 따라가기 (나노초 단위)
- 대부분의 프로그램에서는 전혀 체감 안 됨
- 정말 느리다면 문제는 virtual이 아니라 알고리즘일 확률 99%
초보자 조언: 성능 걱정보다 코드를 이해하기 쉽게 만드는 게 먼저입니다!
Q3: override를 꼭 써야 하나요?
A: 필수는 아니지만 강력 추천!
// override 없이도 작동함 (C++11 이전 방식)
class Dog : public Animal {
void speak() { } // 작동은 함
};
// override 있으면 실수를 막아줌 (C++11 이후 권장)
class Dog : public Animal {
void speak() override { } // 오타가 있으면 컴파일 에러!
};
결론: 귀찮아도 override 붙이세요. 나중에 디버깅 시간을 매우 절약합니다!
Q4: 가상 소멸자는 언제 필요한가요?
A: 포인터로 삭제할 가능성이 1%라도 있으면 무조건 필요!
// 이런 코드가 있을 수 있다면
Base* ptr = new Derived();
delete ptr; // ← 이게 있으면 가상 소멸자 필수!
// 항상 스택에서만 쓴다면 가상 소멸자 불필요
void func() {
Derived d;
// ...
} // 자동 소멸
Q5: 순수 가상 함수(= 0)는 언제 써요?
A: “이 함수는 반드시 구현해!”라고 강제하고 싶을 때
class Shape {
virtual double area() = 0; // "넓이 계산은 필수야!"
};
// Circle에서 area()를 구현 안 하면 컴파일 에러!
사용 예: 인터페이스를 정의할 때 (Java의 interface와 비슷)
Q6: 더 공부하려면 뭘 봐야 하나요?
A:
- 책: “Effective C++” (Scott Meyers)
- 사이트: cppreference.com - C++ 레퍼런스
- 책: “C++ Primer” (초보자용 두꺼운 교과서)
초보자를 위한 체크리스트
가상 함수를 쓸 때 이것만 기억하세요:
- 상속받을 수 있는 클래스라면 가상 소멸자 만들기
- 재정의하는 함수에는
override붙이기 - 다형성 쓸 때는 포인터(
*)나 참조(&) 사용하기 - 순수 가상 함수(
= 0)로 구현 강제하기 - 성능 걱정은 프로파일러로 확인한 후에 하기
같이 보면 좋은 글 (내부 링크)
이 주제와 연결되는 다른 글입니다.
관련 글
이 글에서 다루는 키워드 (관련 검색어)
C++, virtual, polymorphism, 다형성, OOP, vtable, 가상상속 등으로 검색하시면 이 글이 도움이 됩니다.