함수 포인터란?
말 그대로 함수를 가리키는 포인터이다.
코드를 통해 사용방법을 설명하겠다.
void PrintNumber(int n)
{
std::cout << n << std::endl;
}
auto f1 = PrintNumber;
f1(); // 가능
auto f2 = PrintNumber();
f2(); // 불가능
void(*f3)(int);
f3 = PrintNumber;
f3(); // 가능
void(*f4)(int) = PrintNumber;
f4(); // 가능
typedef void(*PrintNumberFunc)(int);
PrintNumberFunc f5 = PrintNumber;
f5(); // 가능
Vtable이란?
Virtual function table의 줄임말로, 쉽게 설명하면 함수 포인터들이 담긴 배열이다.
Vtable은 virtual 키워드가 정상적으로 작동할 수 있게 해준다.
코드에서 명시적으로 표시해주지는 않지만, 각 클래스의 생성자가 실행될 때
Vtable값을 가리키는 포인터 변수가 같이 생성된다고 생각하면 편하다.
이 때 중요한 점은, 만약 다른 클래스를 상속받은 자식클래스라면
상속 관계의 생성자 호출 및 처리 순서에 따라 Vtable의 값도 변한다는 점이다.
예시와 함께 설명해보겠다.
Fruit 클래스와, 이 클래스를 상속받은 Apple 클래스가 있다고 가정하자.
또 Fruit는 virtual void Hello() { std::cout << "Fruit hello"; } 라는 함수를 가지고 있으며
Apple은 이 함수를 오바라이딩한 virtual void Hello() { std::cout << "Apple hello"; }라는 함수를 가지고 있다고 하자.
Apple 클래스의 인스턴스를 동적으로 생성하고, 생성된 개체를 Fruit 타입 포인터 apple로 저장했다고 가정하자.
Fruit* apple = new Apple;
이때 apple->Hello()를 실행하면 virtual 키워드의 성질에 의해 "Apple hello"가 출력된다.
그렇다면 어떻게 virtual 키워드는 다른 타입 포인터임에도 오버라이딩한 메소드를 찾아 사용할까?
그 원리는 Vtable에서 찾을 수 있다.
앞서 제시한 예시대로 코드를 작성해 실행하면 다음 순서로 진행된다.
Apple() 호출
Fruit() 호출
Fruit() 실행
Apple() 실행
이때 중요한 점은 생성자가 실행될 때마다 그 클래스의 Vtable을 값도 새로 초기화된다는 것이다.
virtual 함수를 가지고 있는 클래스 내부에는 __vfptr이라는 이름의 vtable을 가리키는 포인터 변수가 저장되어있는데,
이 포인터 변수는 클래스 생성자가 실행될 때 그 클래스 내부의 함수들을 저장하는 Vtable을 가리키도록 초기화된다.
즉, 위 과정에서는 사실 다음과 같은 일들이 벌어지는 셈이다.
Apple() 호출
Fruit() 호출
Fruit() 실행
--> Vtable 값에는 Fruit::Hello()를 가리키는 함수 포인터가 저장된다. 그리고 *apple->__vfptr은 이 Vtable을 가리킨다.
Apple() 실행
--> Vtable 값에는 Apple::Hello()를 가리키는 함수 포인터가 저장된다. 그리고 *apple->__vfptr은 기존에 가리키던 Vtable 대신 새로 생긴 Vtable을 가리킨다.
이러한 과정을 거친 후 apple->Hello()를 실행하면 Vtable에 저장된 데이터를 통해 우리가 실행해야 하는 함수가 Apple::Hello()라는 것을 알 수 있고, 덕분에 virtual 키워드가 정상적으로 작동하게 되는 것이다.
'lang > c++' 카테고리의 다른 글
std::set의 iteration (0) | 2021.03.30 |
---|---|
동적 바인딩 혹은 늦은 바인딩이란(Dynamic binding) (0) | 2021.01.01 |
STL Set의 메모리 구조 (0) | 2020.12.22 |
STL Vector에 클래스를 저장할 때 발생하는 일들 (0) | 2020.12.22 |
STL Vector의 메모리 구조 (0) | 2020.12.22 |