오늘도 밤이야

[C++] 가상 상속(Virtual Inheritance)의 메모리 할당 구조 및 순서 탐구 본문

언어/C++

[C++] 가상 상속(Virtual Inheritance)의 메모리 할당 구조 및 순서 탐구

hyeonski 2021. 5. 4. 13:22

C++ 클래스 구조의 특징인 다중 상속에서 Dreadful Diamond(죽음의 다이아몬드) 구조 문제를 해결하기 위해 가상 상속을 사용하게 되었다. 클래스 상속 구조는 다음과 같이 구성했다.

 

조부모 A로부터 부모 B와 C가 가상 상속을 받고, D는 B와 C를 상속받는다

문제는 D의 복사 생성자를 사용할 때 일어났는데, 받아온 ref 객체를 복사하지 못하고 A의 기본 생성자를 호출하는 문제가 발생했다. D에서 B와 C의 복사 생성자를 호출하고, B와 C에서 A의 복사 생성자를 호출함에도 A의 기본 생성자가 호출되었다. 작성했던 코드는 다음과 같다.

#include <iostream>
using namespace std;

class A
{
private:
	int a;

public:
	A();
	A(const A&);
	virtual ~A();
};

A::A() { cout << "A default" << endl; }

A::A(const A& a) { cout << "A copy" << endl; }

A::~A() { cout << "A destruct" << endl; }

class B : virtual public A
{
private:
	int b;

public:
	B();
	B(const B&);
	virtual ~B();
};

B::B() : A() { cout << "B default" << endl; }

B::B(const B& b) : A(b) { cout << "B copy" << endl; }

B::~B() { cout << "B destruct" << endl; }

class C : virtual public A
{
private:
	int c;

public:
	C();
	C(const C&);
	virtual ~C();
};

C::C() : A() { cout << "C default" << endl; }

C::C(const C& c) : A(c) { cout << "C copy" << endl; }

C::~C() { cout << "C destruct" << endl; }

class D : public B, public C
{
private:
	int d;

public:
	D();
	D(const D&);
	virtual ~D();
};

D::D() : B(), C() { cout << "D default" << endl; }

D::D(const D& d) : B(d), C(d) { cout << "D copy" << endl; }

D::~D() { cout << "D destruct" << endl; }

int main()
{
	D ref;
	std::cout << std::endl;
	D copy(ref);
	std::cout << std::endl;
}

 

 B와 C에서 호출하려고 했던 A의 복사 생성자가 호출되지 않았다.

B나 C 둘 중 하나에서 알아서 A의 복사 생성자를 호출해줄 것이라는 생각을 했으나, A는 기본 생성자로 호출되었다. 기반 클래스 A의 메모리가 기본 생성자로 초기화되었기 때문에 의도한 대로 복사되지 않는다...

 

가상 상속 구조에서 중간 클래스는 부모의 생성자를 호출하지 않는다.

B와 C에서 A의 생성자를 호출해주는 코드를 작성했으나, 실제 컴파일이 되면 생성자를 호출하는 코드는 사라진다. 대신 D의 복사 생성자에서 A의 기본 생성자를 호출하게 된다. 

기본적으로 가상 상속은 만들고자 하는 후손 클래스에서 가상 상속 해주는 모든 원시 클래스를 생성한다. 이는 가상 상속이 필요한 이유와 메모리 구조를 보면 이해할 수 있다.

 

가상 상속은 다중 상속 구조에서 같은 메모리(조부모)가 두 개 이상 생성되는 문제를 막기 위해 사용된다. 상속 구조에 따라 부모를 만드는 것이 아닌, 가상 상속될 부모를 미리 파악하여 한 번씩만 할당하도록 해준다. 가상 상속되는 부모가 얼마나 생성될 지 알 수 없기 때문에 스택처럼 쌓이는 것이 아닌 기본 메모리의 밑, 즉 낮은 주소에 위에서 아래로 적재되며, 가상 상속 받은 자식은 vptr을 통해 밑에 있는 부모를 참조할 수 있다. 

 

가상 상속 되는 조부모를 미리 파악하고, 최하위 클래스의 밑에 적재하기 위해서는 일반 상속되는 중간 클래스가 쌓이기 전에 할당하고 생성자를 호출해야 한다. 즉 중간 클래스의 생성자 호출 이전에 조부모에 대한 할당 처리를 끝내야한다. 그래서 최하위 클래스에서 가상 상속되는 모든 조부모들의 생성자를 호출하게 된다. 또한 이미 생성된 조부모이기에 중간 부모 클래스에서는 조부모의 생성자를 호출하지 않는다.

 

어셈블리 코드를 통해 직접 확인할 수 있다. 컴파일 시 D의 생성자에서 A의 생성자를 호출하도록 하며, B와 C의 생성자에서는 A의 생성자를 호출하는 명령어를 찾을 수 없다.

 

D 복사 생성자의 어셈블리 코드. A의 기본 생성자를 직접 호출한다

 

D에서 호출된 B의 복사 생성자 어셈블리 코드. A의 복사 생성자를 호출하는 라인은 없다. C의 복사 생성자에도 마찬가지.

결국, 다이아몬드 가상 상속 구조에서 원하는대로 복사 생성자를 호출하기 위해서는 자손 클래스 D에서 명시적으로 원시 클래스 A의 복사 생성자를 호출해주어야 한다. 다른 생성자도 마찬가지다.

 

2회 이상 가상 상속 시 메모리 구조

메모리 할당 순서와 구조를 파악해보기 위해 복잡한 상속 구조를 만들어봤다.

위와 같은 상황에서 생성자 호출 순서와 메모리 할당 구조를 보도록 하자.

먼저 I를 호출할 때 생성자 호출 순서는 다음과 같다,

 

A - C - D - E - B - G - F - H - I 의 순서대로 생성자 호출이 일어난다.

명시된 상속 순서를 따라 가상 상속되는 부모 클래스를 파악 후 생성자가 호출된다. 여기서 특이한 점은 메모리 주소이다. 내부 멤버로 가지고 있는 int 변수의 주소를 출력하여 클래스 메모리 할당 구조를 확인해봤을 때 다음과 같다.

 

I의 밑에 가상 상속된 원시 클래스들이 할당되는데, 그 위치가 특이하다. A, C는 생성자 호출 순서대로 쭉 쌓였지만, E와 D는 예상과 달리 뒤바뀌어 있다. 분명히 D의 생성자가 먼저 호출되었음에도 E의 주소가 더 낮다.... 

Comments