사전 지식 : 오버로딩 vs 오버라이딩
C++에서 다형성을 구현하는 대표적인 두 가지 기술입니다.(C#에서도 마찬가지로 사용)
| 구분 | 함수 오버로딩 (Function Overloading) | 함수 오버라이딩 (Function Overriding) |
| 핵심 | 같은 이름, 다른 매개변수 | 같은 이름, 같은 매개변수 (재정의) |
| 관계 | 같은 클래스 또는 같은 네임스페이스 | 부모-자식 상속 관계에서만 성립 |
| 바인딩 | 컴파일 시점에 호출 함수 결정 (정적 바인딩) | 실행 시점에 호출 함수 결정 (동적 바인딩) |
| 목적 | 이름 하나로 다양한 타입 처리 (사용 편의성) | 부모의 기능을 자식에 맞게 수정/확장 |
| 키워드 | 없음 | virtual, override (권장) |
간단히 말해, 오버로딩은 이름만 같은 별개의 함수들을 만드는 기술이고, 오버라이딩은 부모에게 물려받은 함수를 자식이 자신에 맞게 고쳐 쓰는 상속의 핵심 기술입니다.
가상 함수(Virtual Function)와 동적 바인딩
앞서 설명한 함수 오버라이딩이 실행 시점 다형성(동적 다형성)으로 동작하도록 만드는 핵심 스위치가 바로 가상 함수입니다.
가상 함수는 간단히 말해, "실행 시간에 어떤 함수를 호출할지 결정하게 만드는 함수"입니다.
C++에서는 부모 클래스의 포인터로 자식 클래스의 객체를 가리킬 수 있습니다. 이때 어떤 오버라이딩 함수가 호출될지는 '바인딩' 방식에 따라 달라집니다.
- 정적 바인딩 (Static Binding) : virtual이 없는 일반 함수는 포인터의 타입(부모 클래스)을 따라 컴파일 시점에 호출할 함수가 결정됩니다.
- 동적 바인딩 (Dynamic Binding) : virtual 키워드가 붙은 가상 함수는, 포인터의 타입이 아닌 실제로 가리키고 있는 객체의 타입(자식 클래스)을 따라 실행 시점에 호출할 함수가 결정됩니다.
class Parent
{
public:
// 이 함수는 실제 객체 타입에 따라 호출될 버전이 달라질 수 있음을 선언
virtual void print()
{
cout << "This is a Parent." << endl;
}
};
class Child : public Parent
{
public:
// 'override'는 이 함수가 부모의 가상 함수를 재정의했음을 명시 (권장)
void print() override
{
cout << "This is a Child." << endl;
}
};
가상 함수 테이블(VTBL)
그렇다면 동적 바인딩은 어떻게 가능할까요? 컴파일러는 이 기능을 위해 가상 함수 테이블(v-table)과 가상 포인터(v-pointer)를 사용합니다.
- v-table (가상 함수 테이블) : virtual 함수들의 실제 주소를 담고 있는 함수 포인터 배열입니다. 클래스당 하나씩, 프로그램의 읽기 전용 메모리 영역에 생성됩니다.
- v-pointer (가상 포인터) : virtual 함수를 가진 클래스의 객체가 생성될 때, 객체 내부에 숨겨진 포인터입니다. 이 포인터는 자신의 클래스에 맞는 v-table을 가리킵니다.
가상 함수 호출 과정은 다음과 같습니다.
- 객체 생성 : Child 객체가 생성되면, 객체의 메모리 공간에 Child 클래스의 v-table을 가리키는 v-pointer가 포함됩니다.
- 함수 호출 : 부모 클래스 포인터(Parent* ptr = new Child();)로 print() 함수를 호출합니다.
- v-pointer 참조 : ptr이 가리키는 객체(Child 객체)의 v-pointer를 찾아냅니다.
- v-table 참조 : v-pointer를 통해 Child 클래스의 v-table에 접근합니다.
- 실제 함수 실행 : v-table 안에서 print() 함수에 해당하는 포인터를 찾아, Child 클래스가 재정의한 print() 함수를 실행합니다.
게임 캐릭터 스킬 비유
모든 플레이어는 '공격' 스킬을 가지고 있지만, 직업마다 공격 방식이 다릅니다.
- Player (부모 클래스) : attack() 이라는 스킬을 가짐
- Warrior (자식 클래스) : attack() 스킬을 '칼 휘두르기'로 오버라이딩
- Wizard (자식 클래스) : attack() 스킬을 '파이어볼 발사'로 오버라이딩
이때 Player의 attack()을 가상 함수로 만드는 것은, "이 스킬은 어떤 직업인지에 따라 사용법이 달라질 수 있다"고 선언하는 것과 같습니다.
#include <iostream>
#include <vector>
#include <memory>
using namespace std;
class Player
{
public:
// 이 스킬은 실제 캐릭터의 직업에 따라 다르게 나갈 수 있다고 알려줌
virtual void attack()
{
cout << "플레이어가 기본 공격을 합니다." << endl;
}
// 부모 포인터로 자식 객체를 delete 할 때를 대비해 가상 소멸자는 필수!
virtual ~Player() {}
};
class Warrior : public Player
{
public:
// Player의 attack을 재정의했음을 명시
void attack() override
{
cout << "전사가 칼을 휘두릅니다! ⚔️" << endl;
}
};
class Wizard : public Player
{
public:
// Player의 attack을 재정의했음을 명시
void attack() override
{
cout << "마법사가 파이어볼을 발사합니다! 🔥" << endl;
}
};
int main()
{
// 파티원들을 Player 포인터로 관리 (다형성)
vector<unique_ptr<Player>> party;
party.push_back(make_unique<Warrior>()); // 전사를 파티에 추가
party.push_back(make_unique<Wizard>()); // 마법사를 파티에 추가
// 모든 파티원에게 공격 명령
for (const auto& member : party)
{
// "어떤 공격인지 지금 결정하지 말고, member가 진짜 가리키는 녀석을 보고 결정해!"
member->attack();
}
return 0;
}
전사가 칼을 휘두릅니다! ⚔️
마법사가 파이어볼을 발사합니다! 🔥
이것이 가능한 이유는 다음과 같습니다.
- 스킬북(v-table) 제작 : 컴파일러는 Warrior와 Wizard 클래스를 보고, 각자 '직업 스킬북(v-table)'을 만들어 줍니다.
- 전사 스킬북 : [attack -> Warrior::attack 주소]
- 마법사 스킬북 : [attack -> Wizard::attack 주소]
- 캐릭터(객체) 생성 시 스킬북 장착 : Warrior 캐릭터를 만들 때, 캐릭터 정보 안에 "나는 전사 스킬북을 써!" 라는 보이지 않는 꼬리표(v-pointer)를 붙여줍니다. 마법사도 마찬가지입니다.
- 공격 명령 : member->attack() 명령이 내려지면,
- 시스템은 member가 가리키는 캐릭터의 꼬리표(v-pointer)를 확인합니다.
- 꼬리표가 전사 스킬북(v-table)을 가리키면, 그 스킬북에 적힌 Warrior::attack을 사용합니다.
- 꼬리표가 마법사 스킬북을 가리키면, 그 스킬북에 적힌 Wizard::attack을 사용합니다.
이처럼 실행 시점에 캐릭터의 진짜 정체(꼬리표)를 확인하고 그에 맞는 스킬(함수)을 사용하는 것이 바로 동적 바인딩입니다.
정리하자면, C++의 다형성을 구현하기 위해 상속 관계에서 부모 함수를 오버라이딩하며, 이때 virtual 키워드를 사용하여 가상 함수로 만들면 가상 함수 테이블(VTBL)을 통해 동적 바인딩이 일어나 원하는 기능이 호출된다.
'CS > C#_C++' 카테고리의 다른 글
| C# 매개변수 전달 ref의 유무 (0) | 2025.10.19 |
|---|---|
| C# Virtual / Abstract / Interface (0) | 2025.10.13 |
| C++ 캐스팅 연산자 (0) | 2025.10.07 |
| C++ STL(Standard Template Library) (0) | 2025.10.04 |
| C# 클래스(Class)와 구조체(Struct)의 차이점 (0) | 2025.10.03 |