객체 포인터의 참조관계

1
2
3
4
5
6
7
class Person
{
};

class Student : public Person
{
};

Person형 포인터는 Person 객체뿐만이 아니라, Person을 상속하는 유도 클래스의 객체도 가르킬수 있음.
즉, C++에선, A형 포인터 변수는 A 객체 또는 A를 직접 혹은 간접적으로 상속하는 모든 객체를 가르킬수 있음.

하지만 이 관계에서, 함수 오버라이딩이 있다면 원래의 형이 아닌, 현재 포인터의 형태의 함수를 호출하게 된다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Base
{
public:
void BaseFunc() { cout << "Base Function" << endl; }
};

class Derived : public Base
{
public:
void DerivedFunc() { cout << "Derived Function" << endl; }
}

int main()
{
Base *bptr = new Base(); // 컴파일 OK
bptr->DerivedFunc(); // 컴파일 에러
}

C++ 컴파일러는, 포인터 연산의 가능성 여부를 판단할 때, 포인터의 자료형을 기준으로 판단하지, 실제 가르키는 객체의 자료형을 기준으로 판단하지 않는다.

1
2
3
4
5
int main()
{
Base *bptr = new Derived();
Derived *dptr = bptr; // 컴파일 에러 -> 다운 캐스팅
}

다운캐스팅에서 에러가 발생하는 원인은, 컴파일러가 bptr이 실제로 가리키는 객체가 Derived 객체라는 사실을 기억하지 않는다.
Derived 포인터의 변수에 Base 포인터 변수을 넣을려고 하기 때문에, 불가능.

1
2
3
4
5
int main()
{
Derived * dptr = new Dervied();
Base *bptr = dptr; // UpCast OK
}

dptr은 Derived 클래스의 포인터 변수이기 때문에, 이 객체는 Baes 클래스를 직접 혹은 간접적으로 상속하는 객체이다. Base형 포인터 변수로도 참조가 가능하다.

가상함수의 등장

포인터 변수 자료형에 따라 호출되는 함수가 달라지는 것은 문제가 있다.
가상함수를 통해 상속과 다형성을 이룰수 있는데, 상속을 통해 연관된 클래스에 대해 공통적인 규약을 정의할수 있게 된다.

가상소멸자의 사용이유

앞서 말했다시피, 포인터의 원래의 형태 함수를 호출하는데, 가상소멸자를 쓰지않으면, 포인터의 형태의 소멸자가 호출되게 되고,
만약 포인터가 가르키는 값이 자식 클래스의 객체라면, 자식 클래스만 사용하는 메모리 만큼 누수가 생긴다.

가상함수의 원리

가상함수가 포함된 클래스가 있을때, 컴파일러가 소스코드 파일을 컴파일하면서 알아낸 정보를 바탕으로 PE파일의 섹션에 기록한다.
vtable은 instance 별로 생성되는것이 아니라, 클래스 별로 생기게 된다.
해당 가상함수 테이블은 객체의 생성과 상관없이 메모리 공간에 할당된다. 이 테이블이 맴버함수 호출에 사용되는 일종의 데이터

PE파일 : 우리가 쓰고 있는 윈도우즈 환경의 실행 파일 포맷을 PE라고 하며, “Portable”의 단어 뜻 그대로 의식성이 있으며 플랫폼에 독립적입니다.

가상함수테이블