s-nova 님의 블로그
[C++] 다형성, virtual, override 본문
C++에서 다형성$^{\text{Polymorphism}}$을 구현하는 핵심 메커니즘은 바로 가상 함수$^{\text{Virtual Function}}$입니다. 같은 이름의 함수가 객체의 실제 타입에 따라 다르게 동작하는 원리, 그리고 그 내부 구조인 vtable까지 차근차근 살펴보겠습니다.
1. 다형성이란?
다형성$^{\text{Polymorphism}}$이란 같은 인터페이스를 공유하지만, 실제 동작은 객체의 타입에 따라 달라지는 성질입니다.
예를 들어, Animal 클래스를 상속받은
Dog와
Cat이 있을 때,
Speak() 함수 호출은 이름이 같지만 실제 동작은 각자 다르게 실행됩니다.
#include <iostream>
using namespace std;
// 부모 클래스
class Animal
{
public:
virtual void Speak() // 가상 함수
{
cout << "Some generic animal sound" << endl;
}
};
// 자식 클래스: Dog
class Dog : public Animal
{
public:
void Speak() override // override로 재정의
{
cout << "Woof! Woof!" << endl;
}
};
// 자식 클래스: Cat
class Cat : public Animal
{
public:
void Speak() override
{
cout << "Meow~" << endl;
}
};
int main()
{
Animal* a1 = new Dog(); // Animal 포인터지만 Dog 객체 생성
Animal* a2 = new Cat(); // Animal 포인터지만 Cat 객체 생성
a1->Speak(); // 출력: Woof! Woof!
a2->Speak(); // 출력: Meow~
delete a1;
delete a2;
return 0;
}
2. virtual 키워드의 역할
virtual을 붙이면 런타임 다형성$^{\text{Runtime Polymorphism}}$이 활성화됩니다.
부모 클래스의 포인터나 참조를 통해 호출하더라도, 실제 객체 타입에 맞는 함수가 실행됩니다.
| 구분 | virtual 없음 | virtual 있음 |
| 바인딩 시점 | 컴파일 타임 $($정적 바인딩$)$ | 런타임 $($동적 바인딩$)$ |
| 호출되는 함수 | 포인터 타입 기준 | 실제 객체 타입 기준 |
| 다형성 | 불가 | 가능 |
class Character
{
public:
virtual void Attack() // virtual 선언
{
std::cout << "기본 공격!\n";
}
};
class Warrior : public Character
{
public:
void Attack() override // 자식 클래스에서 재정의
{
std::cout << "검으로 공격!\n";
}
};
💡 팁:
virtual은 부모 클래스에서,override는 자식 클래스에서 사용합니다. 둘 다 명시하면 가독성이 높아집니다.
3. 소멸자에 virtual이 필요한 이유
부모 포인터로 자식 객체를 가리키고 delete할 때,
소멸자가 virtual이 아니면 부모 소멸자만 호출됩니다.
자식 객체가 관리하는 자원 $($동적 메모리, 파일 핸들 등$)$이 해제되지 않아 메모리 누수$^{\text{Memory Leak}}$가 발생합니다.
따라서 상속 관계에서 기본 클래스의 소멸자는 반드시 virtual로 선언하는 것이 안전합니다.
상속 관계에서 소멸자가 호출되는 순서는 항상 자식 → 부모 순입니다.
Derived 객체가 해제될 때, Derived의 소멸자가 먼저 실행된 뒤 Base의 소멸자가 실행됩니다.
✅ 올바른 예시 $($virtual 소멸자$)$
#include <iostream>
using namespace std;
class Base
{
public:
virtual ~Base() { cout << "Base 소멸\n"; }
};
class Derived : public Base
{
public:
~Derived() { cout << "Derived 소멸\n"; }
};
int main()
{
Base* obj = new Derived();
delete obj; // 자식 → 부모 순으로 소멸
return 0;
}
Derived 소멸
Base 소멸
❌ 잘못된 예시 $($virtual 없음$)$
class Base
{
public:
~Base() { cout << "Base 소멸\n"; } // virtual 없음
};
class Derived : public Base
{
public:
~Derived() { cout << "Derived 소멸\n"; }
};
// Base* obj = new Derived();
// delete obj; → Base 소멸만 출력! Derived 소멸자 미호출
Base 소멸
Derived 소멸자가 호출되지 않아, Derived가 관리하는 자원이 해제되지 않는 메모리 누수가 발생합니다.
4. C++11의 override 키워드
C++11 이전에는 자식 클래스가 부모의 가상 함수를 재정의할 때 시그니처를 잘못 써도 컴파일 에러가 나지 않았습니다. 실수로 함수 이름이나 매개변수 타입이 달라지면, 재정의가 아닌 새로운 함수가 만들어지는 버그가 발생했습니다.
C++11부터 도입된 override 키워드를 붙이면,
부모 함수와 시그니처가 정확히 일치하지 않을 때 컴파일 에러를 발생시켜 실수를 방지합니다.
class Warrior : public Character
{
public:
void Attack() override // 시그니처가 다르면 컴파일 에러 발생
{
std::cout << "검으로 공격!\n";
}
};
📌 참고: C++11 이상 프로젝트에서는 가상 함수를 재정의할 때 항상
override를 붙이는 습관을 들이는 것이 좋습니다.
5. 순수 가상 함수와 추상 클래스
= 0을 붙이면 순수 가상 함수$^{\text{Pure Virtual Function}}$가 되고,
해당 클래스는 추상 클래스$^{\text{Abstract Class}}$가 됩니다.
추상 클래스는 객체를 직접 생성할 수 없고, 반드시 자식 클래스에서 해당 함수를 구현해야만 합니다.
게임 개발에서는 Update(), Render()처럼 모든 게임 오브젝트가 반드시 구현해야 하는 공통 인터페이스 정의에 자주 활용됩니다.
class Character
{
public:
virtual void Attack() = 0; // 순수 가상 함수 → 추상 클래스
virtual void Update() = 0;
};
// Character c; // 컴파일 에러: 추상 클래스는 인스턴스 생성 불가
class Warrior : public Character
{
public:
void Attack() override // 반드시 구현해야 함
{
std::cout << "검으로 공격!\n";
}
void Update() override
{
// 전사 업데이트 로직
}
};
6. vtable - 런타임 다형성의 핵심 구조
컴파일러는 가상 함수가 포함된 클래스에 대해 vtable$^{\text{Virtual Function Table}}$을 생성합니다. 각 객체는 내부적으로 vptr$^{\text{Virtual Pointer}}$를 가지고 있으며, 이 포인터가 vtable을 가리킵니다. 함수 호출 시 vptr → vtable → 실제 함수 주소 순으로 탐색하여 올바른 함수를 실행합니다.
| 구성 요소 | 위치 | 역할 |
| vtable | 클래스당 하나 $($정적 영역$)$ | 가상 함수 주소 목록 |
| vptr | 객체마다 하나 $($객체 내부$)$ | 자신의 vtable을 가리키는 포인터 |
💡 게임 개발 관점: vtable을 이해하면 가상 함수 호출이 일반 함수 호출보다 약간의 오버헤드 $($포인터 역참조 1회$)$가 있음을 알 수 있습니다. 성능이 극도로 중요한 핫 패스에서는 가상 함수 호출 빈도를 고려하는 설계가 필요합니다.
7. 정리
| 키워드/개념 | 위치 | 역할 |
virtual |
부모 클래스 | 다형성 활성화, 동적 바인딩 |
override |
자식 클래스 | 재정의 실수 방지 $($C++11$)$ |
| virtual 소멸자 | 부모 클래스 | 안전한 메모리 해제 필수 |
| 순수 가상 함수 | 추상 클래스 | 인터페이스 강제, 구현 강요 |
| vtable / vptr | 컴파일러 생성 | 런타임 다형성의 내부 구현 구조 |
'C++' 카테고리의 다른 글
| [C++] 스레드, CPU 캐시 (0) | 2025.09.18 |
|---|---|
| [C++] lvalue, rvalue, Perfect forwarding (0) | 2025.09.14 |
| [C++] 스마트 포인터 (0) | 2025.09.08 |
| [C++] 얕은 복사 vs 깊은 복사 (0) | 2025.08.31 |
| [C++] 매크로, constexpr, inline, template (0) | 2025.07.19 |