[문과 코린이의 IT 기록장] C,C++ - 가상(virtual)의 원리와 다중상속 1 : 맴버함수와 가상함수의 동작원리 (객체 안에 정말로 맴버함수가 존재하는가?, 가상함수의 동작원리와 가상함수 테이블)
[문과 코린이의 IT 기록장] C,C++ - 가상(virtual)의 원리와 다중상속 1 : 맴버함수와 가상함수의 동작원리
(객체 안에 정말로 맴버함수가 존재하는가?, 가상함수의 동작원리와 가상함수 테이블)
1. 객체 안에 정말로 맴버함수가 존재하는가?
- 지금까지는 객체 내에 맴버함수가 존재한다고 설명했었다. 그러나 실제로는 객체 외부에 존재한다.
ex ) C언어(구조체 변수와 전역함수)를 이용해, C++(클래스와 객체)를 흉내내보기
ex ) 이 예제를 C언어 스타일로 만들어보자.
#include <iostream>
using namespace std;
// 클래스 Data를 흉내낸 영역
typedef struct Data{ // 함수 포인터 변수가 구조체의 맴버로 등장했다.
int data;
void (*ShowData)(Data*); // ShowData() 함수의 주소값 저장을 위한, 함수 포인터 변수
void (*Add)(Data*, int); // Add() 함수의 주소값 저장을 위한, 함수 포인터 변수
}Data;
void ShowData(Data * THIS){ cout<<"Data : "<<THIS->data<<endl; }
void Add(Data * THIS, int num){ THIS->data += num; }
// 적절히 변경된 main 함수
int main(){
// obj1과 obj2를 객체라고 할 때, 이 두 객체는 ShowData함수와 Add함수를 공유하는 셈이 된다.
Data obj1 = {15,ShowData,Add};
Data obj2 = {7,ShowData,Add};
// 맴버함수 Add()를 호출하는 것처럼 구성한 C코드
obj1.Add(&obj1, 17);
obj2.Add(&obj2, 9);
// 맴버함수 ShowData()를 호출하는 것처럼 구성한 C코드
obj1.ShowData(&obj1);
obj2.ShowData(&obj2);
return 0;
}
- 위의 내용을 실행하면, 다음과 같은 코드가 된다.
- 이 코드에서 알아야 하는 핵심은, 두 개의 구조체 변수(객체)가 함수를 공유하고 있다는 사실이다.
- 실제로 C++에서도 객체와 맴버함수는 이러한 관계를 가진다. 즉, 객체가 생성되면 맴버변수는 객체 내부에 존재하지만, 맴버함수는 메모리의 한 공간에 별도로 위치하고선, 이 함수가 정의된 클래스의 모든 객체가 이를 공유하는 형태를 가진다.
2. 가상함수의 동작원리와 가상함수 테이블
1) 가상함수의 동작원리와 가상함수 테이블
ex )
- 한 개 이상의 가상함수를 포함하는 클래스에 대해서는 컴파일러가 '가상함수 테이블(V-table)'이라는 것을 만든다.
* 가상함수 테이블 : 실제 호출되어야 할 함수의 위치정보를 담고 있는 테이블
#include <iostream>
using namespace std;
class AAA{
private:
int num1;
public:
virtual void Func1() { cout<<"Func1"<<endl; }
virtual void Func2() { cout<<"Func2"<<endl; }
};
[ AAA 클래스의 가상함수 테이블 ]
key | value |
void AAA :: Func1() | 0x 1024번지 |
void AAA :: Func2() | 0x 2048 번지 |
* key : 호출하고자 하는 함수를 구분지어주는 구분자의 역할
* value : 구분자에 해당하는 함수의 주소정보를 알려주는 역할
** 즉, AAA객체의 Func1() 함수를 호출해야 할 경우, 첫 번째 행의 정보를 참조하여 0x 1024번지에 등록되어 있는 Func1 함수를 호출하게 되는 것
class BBB : public AAA{
private:
int num2;
public:
virtual void Func1() { cout<<"BBB::Func1"<<endl; }
void Func3() { cout<<"Func3"<<endl; }
};
[ BBB 클래스의 가상함수 테이블 ]
key | value |
void BBB :: Func1() // 오버라이딩 |
0x 3072번지 |
void AAA :: Func2() | 0x 2048번지 |
void BBB :: Func3() | 0x 4096번지 |
- AAA 클래스의 오버라이딩 된 가상함수 Func1에 대한 정보가 존재하지 않는다.
* 즉, 오버라이딩 된 가상함수의 주소정보는 유도 클래스의 가상함수 테이블에 포함되지 않는다는 것을 알 수 있다.
int main(){
AAA * aptr = new AAA();
aptr->Func1();
BBB * bptr = new BBB();
bptr->Func1();
return 0;
}
2) 가상함수 테이블이 참조되는 방식
- 위의 코드가 실행되면, main함수가 실행되기 전, 아래의 형태로 가상함수 테이블이 메모리 공간에 할당된다.
* 가상함수 테이블은 맴버함수의 호출에 사용되는 일종의 데이터이기 때문에, 객체의 생성과 상관없이 메모리 공간에 할당된다.
- 이후, main 함수가 호출되어 객체가 생성되고 나면, 아래의 구조와 참조관계를 구성한다
- 이와 같이 AAA 객체에는 AAA클래스의 가상함수 테이블의 주소 값이 저장되고, BBB객체에는 BBB클래스의 가상함수 테이블의 주소 값이 저장된다. 이는 우리가 실제로 참조하는 것이 아니라, 내부적으로 필요에 의해 참조되는 주소값이다.
cf ) 가상함수 테이블에 의한 속도의 저하 - 위에서 설명했듯이, 클래스에 가상함수가 포함되면 가상함수 테이블이 생성되고, 이 테이블을 참조하여 호출될 함수가 결정된다. 이 과정에서 실행속도가 감소된다. - 그러나 그 속도의 차이가 매우 적고, 또 이러한 단점에도 불구하고 가상함수는 많은 장점을 제공하기 때문에, 유용하게 활용되고 있다. |
* 유의사항 - 아직 공부하고 있는 문과생 코린이가, 정리해서 남겨놓은 정리 및 필기노트입니다. - 정확하지 않거나, 틀린 점이 있을 수 있으니, 유의해서 봐주시면 감사하겠습니다. - 혹시 잘못된 점을 발견하셨다면, 댓글로 친절하게 남겨주시면 감사하겠습니다 :) |