1. 이론
1.1. 다형성(Polymorphism)과 동적 바인딩(Dynamic Binding)
C++에서 다형성은 "하나의 인터페이스가 다양한 타입의 객체에 대해 다르게 동작"하는 것을 의미한다. 다형성을 구현하는 핵심 메커니즘이 바로 동적 바인딩이다.
- 정적 바인딩 (Static Binding): 컴파일 시점에 어떤 함수가 호출될지 결정된다. 일반적인 함수 호출이 여기에 해당.
- 동적 바인딩 (Dynamic Binding): 런타임 시점에 객체의 실제 타입에 따라 어떤 함수가 호출될지 결정된다. 가상 함수를 통해 이루어지며, 포인터나 참조를 통해 객체에 접근할 때 동작한다.
동적 바인딩은 가상 테이블(Virtual Table, vtable) 이라는 메커니즘을 통해 구현된다.
- vtable: 가상 함수를 하나 이상 가진 클래스에 대해, 컴파일러는 해당 클래스의 가상 함수들의 주소를 담고 있는 테이블을 생성한다. 이 테이블은 클래스 단위로 하나만 존재.
- vptr (virtual pointer): 가상 함수를 가진 클래스의 객체가 생성될 때, 객체의 메모리 공간 앞부분에 숨겨진 포인터(
vptr
)가 추가된다. 이 포인터는 자기 클래스의vtable
을 가리킨다.
함수 호출 시, 컴파일러는 vptr
을 통해 vtable
에 접근하고, vtable
에서 호출할 함수의 주소를 찾아 실행한다. 이 과정이 런타임에 일어나므로 동적 바인딩이 가능해진다.
1.2. 가상 함수 (Virtual Function)
"이 함수는 파생 클래스에서 재정의(Override)될 수 있습니다."
가상 함수는 virtual
키워드를 사용하여 선언하며, 파생 클래스에서 재정의될 것을 기대하는 함수다. 기반 클래스의 포인터나 참조가 파생 클래스의 객체를 가리킬 때, 가상 함수를 호출하면 실제 객체의 타입에 맞는 재정의된 함수가 호출된다.
- 특징
- 기반 클래스에서 기본 구현(Default Implementation)을 제공할 수 있다.
- 파생 클래스는 필요에 따라 이 함수를 재정의할 수도, 하지 않을 수도 있다. 재정의하지 않으면 기반 클래스의 함수가 호출된다.
- 가상 소멸자 (Virtual Destructor)
- 기반 클래스의 포인터로 파생 클래스 객체를
delete
할 때, 소멸자가virtual
이 아니면 기반 클래스의 소멸자만 호출되어 메모리 누수가 발생한다. - 따라서 다형성을 사용하는 클래스 계층 구조에서는 반드시 기반 클래스의 소멸자를
virtual
로 선언해야 한다.
- 기반 클래스의 포인터로 파생 클래스 객체를
class Base {
public:
virtual void myFunction() { /* 기본 구현 */ }
virtual ~Base() {} // 가상 소멸자
};
1.3. 순수 가상 함수 (Pure Virtual Function)
"이 함수는 파생 클래스에서 반드시 재정의되어야 합니다."
순수 가상 함수는 함수 선언부 끝에 = 0;
을 붙여 선언한다. 순수 가상 함수는 본문(구현)을 가지지 않으며, 이 함수를 하나 이상 포함하는 클래스는 추상 클래스(Abstract Class)가 된다.
- 특징:
- 추상 클래스는 인스턴스화(객체 생성)할 수 없다.
new MyAbstractClass();
와 같은 코드는 컴파일 에러를 발생시킨다. - 오직 파생 클래스를 위한 인터페이스(Interface) 명세 역할을 한다.
- 파생 클래스는 기반 클래스의 모든 순수 가상 함수를 재정의해야만 일반 클래스가 되어 객체를 생성할 수 있다. 하나라도 재정의하지 않으면 해당 파생 클래스 또한 추상 클래스가 된다.
- 추상 클래스는 인스턴스화(객체 생성)할 수 없다.
class AbstractBase {
public:
virtual void mustBeImplemented() = 0; // 순수 가상 함수
virtual ~AbstractBase() {}
};
1.4. 비교 및 사용 사례
구분 | 가상 함수 (Virtual Function) | 순수 가상 함수 (Pure Virtual Function) |
---|---|---|
선언 | virtual void func(); |
virtual void func() = 0; |
기반 클래스 구현 | 구현을 가질 수 있음 (선택) | 구현을 가지지 않음 (인터페이스 역할) |
파생 클래스 재정의 | 선택 사항 | 필수 사항 |
기반 클래스 객체 생성 | 가능 | 불가능 (추상 클래스) |
주요 목적 | 기본 동작을 제공하되, 확장/변경의 여지를 둠 | 파생 클래스가 반드시 구현해야 할 기능의 규격(Interface)을 강제 |
언제 무엇을 사용할까?
- 가상 함수:
- "대부분의 파생 클래스는 이 동작을 그대로 사용하지만, 몇몇 특별한 클래스는 다르게 동작해야 해."
- 예:
GameObject
의onCollision()
함수. 기본적으로는 아무 일도 하지 않지만(virtual void onCollision(){}
),Player
나Item
객체는 특별한 처리를 하도록 재정의할 수 있다.
- 순수 가상 함수:
- "이 클래스 계열의 모든 객체는 반드시 이 기능을 가져야 하지만, 그 내용은 각자 알아서 정해야 해."
- 예:
IGameObject
의update()
나render()
함수. 세상의 모든 게임 오브젝트는 매 프레임update
되고render
되어야 하지만,Player
의update
와Enemy
의update
는 완전히 다르다. 기반 클래스에서update
를 어떻게 구현해야 할지 정의할 수 없으므로, 순수 가상 함수로 만들어 파생 클래스에게 구현을 강제한다.
2. 실습 코드 (게임 예제)
아래는 게임 월드에 존재하는 다양한 오브젝트(Player
, Monster
, Prop
)를 다형적으로 관리하는 예제다.
IGameObject
: 모든 게임 오브젝트가 따라야 할 인터페이스다. (추상 클래스).update()
: 매 프레임 오브젝트의 상태를 갱신한다. (순수 가상 함수)render()
: 매 프레임 오브젝트를 화면에 그린다. (순수 가상 함수)onCollision()
: 다른 오브젝트와 충돌했을 때의 반응이다. (가상 함수)isDestroyed()
/destroy()
: 오브젝트의 파괴 상태를 관리한다.
#include <iostream>
#include <vector>
#include <string>
#include <memory>
// --- 인터페이스 정의 ---
/* Q1. 아래 클래스의 역할과 '순수 가상 소멸자'가 필요한 이유를 설명하세요.
* A. 역할 : 게임 내 모든 오브젝트의 공통 인터페이스 역할. 기본 동작을 정의함.
* 순수 가상 소멸자가 필요한 이유 : 가상 소멸자를 선언하지 않으면 기반 클래스 포인터로
* 파생 클래스 객체를 삭제할 때 파생 클래스의 소멸자가 호출되지 않아 메모리 누수가 발생함.
*/
class IGameObject {
public:
virtual ~IGameObject() = default;
/* Q2. 아래 두 함수가 '순수 가상 함수'인 이유를 설명하세요.
* A. update : 오브젝트 상태를 매 프레임 갱신해야 하지만 어떤 갱신 로직을 쓸지는
* 객체마다 다름. 즉, 추상화가 필요함.
* render : 화면의 그리는 방식은 객체마다 다르므로 파생 클래스에서 재정의가 필요함.
*/
virtual void update() = 0;
virtual void render() const = 0;
/* Q3. 아래 함수가 '순수 가상 함수'가 아닌 '가상 함수'인 이유를 설명하세요.
* A. 충돌은 일어날 수도 있고, 안 일어날 수도 있음. 즉 필요한 객체만 구현하면 됨.
*/
virtual void onCollision(IGameObject* other) {
// 기본적으로는 아무런 충돌 처리도 하지 않음
}
// 오브젝트의 생존 상태를 관리하는 일반 함수들
bool isDestroyed() const { return m_isDestroyed; }
void destroy() { m_isDestroyed = true; }
const std::string& getName() const { return m_name; }
protected:
// 생성자를 protected로 선언하여 외부에서 IGameObject 단독으로 생성하는 것을 막음
IGameObject(std::string name) : m_name(std::move(name)), m_isDestroyed(false) {}
private:
std::string m_name;
bool m_isDestroyed;
};
// --- 구체적인 오브젝트 구현 ---
// Player 클래스
class Player : public IGameObject {
public:
Player(std::string name, int hp) : IGameObject(std::move(name)), m_hp(hp) {
std::cout << "플레이어 '" << getName() << "' 생성 (HP: " << m_hp << ")" << std::endl;
}
// IGameObject의 순수 가상 함수들을 반드시 구현해야 함
void update() override {
// 플레이어는 매 프레임 사용자 입력을 받거나, 자동으로 체력을 회복할 수 있음
m_hp += 1; // 매 프레임 체력 1 회복한다고 가정
std::cout << "플레이어 '" << getName() << "' 업데이트! (현재 HP: " << m_hp << ")" << std::endl;
}
void render() const override {
std::cout << " -> 화면에 플레이어 '" << getName() << "'를 그립니다." << std::endl;
}
// Player는 충돌 시 특별한 반응을 하므로 onCollision을 재정의
void onCollision(IGameObject* other) override {
std::cout << "플레이어 '" << getName() << "'가 '" << other->getName() << "'와 충돌!" << std::endl;
// 만약 몬스터와 충돌했다면 데미지를 입음
m_hp -= 10;
if (m_hp <= 0) {
std::cout << "플레이어 '" << getName() << "'가 파괴되었습니다..." << std::endl;
destroy();
}
}
private:
int m_hp;
};
// Monster 클래스
class Monster : public IGameObject {
public:
Monster(std::string name, int attackPower) : IGameObject(std::move(name)), m_attackPower(attackPower) {
std::cout << "몬스터 '" << getName() << "' 생성 (공격력: " << m_attackPower << ")" << std::endl;
}
void update() override {
// 몬스터는 매 프레임 주변을 배회하거나 플레이어를 추적하는 AI를 실행할 수 있음
std::cout << "몬스터 '" << getName() << "' 업데이트! (플레이어를 찾고 있습니다...)" << std::endl;
}
void render() const override {
std::cout << " -> 화면에 몬스터 '" << getName() << "'를 그립니다." << std::endl;
}
// 몬스터는 충돌 시 별다른 반응이 없으므로 onCollision을 재정의하지 않음
// 따라서 IGameObject의 기본 onCollision이 호출됨
private:
int m_attackPower;
};
// Prop(배경 소품) 클래스
class Prop : public IGameObject {
public:
Prop(std::string name) : IGameObject(std::move(name)) {
std::cout << "배경 소품 '" << getName() << "' 생성" << std::endl;
}
void update() override {
// 배경 소품은 보통 매 프레임 상태가 변하지 않음
}
void render() const override {
std::cout << " -> 화면에 배경 소품 '" << getName() << "'을 그립니다." << std::endl;
}
};
// --- 게임 월드 및 메인 루프 ---
int main() {
// 게임 월드에 존재하는 모든 오브젝트를 관리하는 벡터
// 스마트 포인터를 사용하여 메모리를 안전하게 관리
std::vector<std::unique_ptr<IGameObject>> gameObjects;
// 월드에 오브젝트 추가
gameObjects.emplace_back(std::make_unique<Player>("용사", 100));
gameObjects.emplace_back(std::make_unique<Monster>("고블린", 10));
gameObjects.emplace_back(std::make_unique<Prop>("나무"));
gameObjects.emplace_back(std::make_unique<Monster>("오크", 25));
std::cout << "--- 게임 루프 시작! ---" << std::endl;
// 3 프레임 동안 게임 루프 실행
for (int frame = 1; frame <= 3; ++frame) {
std::cout << "--- 프레임 " << frame << " ---";
// 1. 업데이트: 모든 오브젝트의 상태를 갱신
for (const auto& obj : gameObjects) {
/* Q4. 아래 obj->update() 호출이 어떻게 각기 다른 클래스의 update를 실행시키는지 설명하세요. (동적 바인딩)
* A. obj는 항상 IGameObject* 타입을 가리킴. update는 virtual로 선언. 즉, vtable을 사용해
* 런타임에 실제 객체 타입에 맞는 함수가 호출됨.
*/
obj->update();
}
// 2. 충돌 처리 (예시: 플레이어와 첫 번째 몬스터가 충돌했다고 가정)
if (frame == 2) {
std::cout << "*이벤트 발생: 플레이어와 고블린 충돌!*";
gameObjects[0]->onCollision(gameObjects[1].get()); // Player의 onCollision 호출
gameObjects[1]->onCollision(gameObjects[0].get()); // Monster의 onCollision (기반 클래스 버전) 호출
std::cout << std::endl;
}
// 3. 렌더링: 모든 오브젝트를 화면에 그림
for (const auto& obj : gameObjects) {
obj->render();
}
// 4. 파괴된 오브젝트 제거 (실제 게임에서는 더 복잡한 방식 사용)
// 여기서는 간단히 루프 종료
std::cout << std::endl;
}
std::cout << "--- 게임 루프 종료! ---" << std::endl;
// unique_ptr을 사용했으므로 main 함수가 끝나면 자동으로 메모리가 해제됨
// 이 때 각 객체의 소멸자가 호출되고, IGameObject의 가상 소멸자 덕분에
// Player, Monster, Prop의 소멸자가 모두 안전하게 호출됨
return 0;
}
'C++' 카테고리의 다른 글
구조체 (0) | 2025.09.03 |
---|---|
클래스 (0) | 2025.09.02 |
템플릿 (2) | 2025.09.01 |
malloc/free 와 new/delete (1) | 2025.08.26 |
가상 소멸자 Virtual Destructor (0) | 2025.08.25 |