다형성에 관하여 2 - 가상 함수 테이블 -
요약
가상 함수란 동적 타입을 따르는 함수
정적 결합(Static Binging, 이른 결합, Early Binding)
- 컴파일 하는 시점(정확히는 링크 시점)에 호출할 함수에 대해 함수의 번지를 찾아 결정하는 결합 방법.
동적 결합(Dynamic Binding, 늦은 결합, late Binding)
- 런타임(실행 중)에 호출할 함수를 결정하는 결합 방법
가상 함수 테이블(vtable)
- 가상 함수의 번지 목록을 가지는 일종의 함수 포인터 배열
- vtable을 컴파일 타임에 미리 작성해두고, 실행중에는 객체의 vtable을 찾은 후, vtable에서 다시 호출할 함수의 번지를 찾는다.
다형성
- 같은 코드가 경우에 따라 다른 동작을 할 수 있는 능력. 가상함수를 통해 다형성을 구현
들어가기
지난 포스트에서 업캐스팅, 다운캐스팅, 정적타입, 동적타입 개념을 확인하고,
부모 클래스 객체 pUnit이 monster를 가리켰음에도 unit의 함수를 호출하고, 자식 클래스 객체 pMonster가 unit을 가리켰음에도 monster의 함수를 호출했다.
나는 현재 가리키고 있는 타입(동적 타입)의 함수를 호출하고 싶다.
이때 가상함수를 사용하자.
가상 함수
virtual 키워드: 부모 클래스의 함수에 붙이는 키워드로, 파생 클래스에서 재정의를 예약한다.
override 키워드: 파생 클래스의 함수에 붙이는 키워드로, 재정의함을 명시한다.
예시
#include <iostream>
#include <vector>
using namespace std;
class Unit
{
protected:
string name;
int hp;
int atk;
public:
Unit() = default;
Unit(string _name, int _hp, int _atk) :
name(_name), hp(_hp), atk(_atk) {}
virtual ~Unit() {}
public:
virtual void PrintInfo()
{
cout << "name: " << name << '\n';
cout << "hp: " << hp << '\n';
cout << "atk: " << atk << '\n';
}
virtual void HitFrom(int damage) // 피격 프로세스 추가. 지금은 몬스터 클래스에서 재정의하지 않음
{
hp -= damage;
if (hp <= 0) hp = 0;
}
};
class Monster : public Unit
{
private:
int grade;
public:
Monster() = default;
Monster(string _name, int _hp, int _atk, int _monsterGrade) :
Unit(_name, _hp, _atk), grade(_monsterGrade) {}
~Monster() override {}
public:
void PrintInfo() override
{
Unit::PrintInfo();
cout << "grade: " << grade << '\n';
}
};
enum eMonsterGrade { normal, chief, boss };
int main()
{
Unit unit("유닛", 10, 1);
Monster monster("몬스터", 50, 5, normal);
Unit* pUnit;
Monster* pMonster;
pUnit = &unit; // 당연히 가능
pMonster = &monster; // 당연히 가능
pUnit = &monster; // 가능. 업캐스팅
pUnit->PrintInfo(); // 가능
cout << '\n';
// pMonster = &unit; // 에러
pMonster = (Monster*)&unit; // 가능. 다운캐스팅
pMonster->PrintInfo(); // 가능
return 0;
}
name: 몬스터
hp: 50
atk: 5
grade: 0
name: 유닛
hp: 10
atk: 1
이제 정상적으로 pUnit이 monster를 가리키자, monster의 함수를 불러오고
pMonster가 unit을 가리키자, unit의 함수를 호출하고 있다.
즉, 가상 함수란 동적 타입을 따르는 함수다.
추가로, 자식 클래스의 함수에도 virtual 키워드를 붙일 수 있으며, 나는 붙이는 편을 선호한다.
가상 함수로 선언하는 경우
- 클래스로부터 자식 클래스가 파생될 가능성이 조금이라도 있을 때
- 파생 클래스에서 함수의 동작을 재정의할 가능성이 조금이라도 있을 때
- 모든 파생 클래스가 부모가 정의한 기능만 사용하면 가상 함수로 선언될 필요 없음
- 부모 클래스 타입의 포인터로부터 호출할 가능성이 조금이라도 있을 때
- 가상함수는 포인터로부터 호출될 때만 동작한다.
- 그렇다면 포인터가 아닌 객체로부터 호출되기만 한다면 파생클래스에서 재정의한 함수라도 비가상일 수 있다.
- 다만 객체 포인터를 사용할 가능성은 항상 있으므로 위 가정은 일반적으로 위험하다.
정적 결합 vs 동적 결합
정적 결합(Static Binging, 이른 결합, Early Binding)
- 컴파일 하는 시점(정확히는 링크 시점)에 호출할 함수에 대해 함수의 번지를 찾아 결정하는 결합 방법.
- 컴파일러는 특정 함수가 어떤 주소에 있는지 알고 있으며, 함수 호출문을 이 함수의 주소로 점프하는 코드로 번역한다.
동적 결합(Dynamic Binding, 늦은 결합, late Binding)
- 런타임(실행 중)에 호출할 함수를 결정하는 결합 방법
- 멤버함수를 포인터(or 레퍼런스)로 호출할 때만 동작한다.
- 가상 함수는 포인터가 가리키는 객체의 타입에 따라 호출될 실제 함수가 달라지기 때문에 컴파일 시점에는 함수의 번지를 찾을 수 없다.
- 런타임 중 객체의 타입을 판별해서 이 타입에 맞는 함수를 선택한다.
- 가상함수 == 동적 결합 함수
vtable(virtual function table)
- 가상 함수 테이블
- 가상 함수의 번지 목록을 가지는 일종의 함수 포인터 배열
- 컴파일러는 가상 함수를 1개 이상 가진 클래스에 대해 vtable이라는 가상 함수 목록을 작성한다.
- 이 테이블에는 클래스에 소속된 가상 함수들의 실제 번지들이 선언된 순서대로 기록된다.
- 이 클래스 타입의 객체가 생성될 때 각 객체에 vtable을 가리키는 숨겨진 멤버 vptr을 추가한다.
- p -> vptr -> vtable[n] 을 호출하는 식
놓칠 수 있는 점
- vtable 자체를 런타임 중에 만드는 것이 아니라,
- vtable을 컴파일 타임에 미리 작성해두고, 실행중에는 객체의 vtable을 찾은 후, vtable에서 다시 호출할 함수의 번지를 찾는다.
- 재정의되지 않은 함수는?
- HitFrom 처럼 재정의되지 않은 가상 함수는 상위 클래스의 vtable을 찾는다.
특징
- 가상 함수는 번지를 가져야 하므로 아무리 코드가 짧아도 인라인이 될 수는 없다.
- 동적 결합은 정적 결합보다 호출 속도가 느리다.
- 클래스별로 vtable이라는 여분의 메모리를 더 소모한다.
- vptr을 위해 비트 수준에 따라 2 or 4 or 8 바이트 더 커진다.
결론: 다형성
같은 코드가 경우에 따라 다른 동작을 할 수 있는 능력, 이것이 다형성이다.
가상함수를 통해 다형성을 구현한 것
참고
※ 틀리거나 개선할 부분이 있다면 알려주세요.
댓글남기기