가상 상속(virtual inheritance)
가상 상속(virtual inheritance) 이란??
다중 상속
C++은 객체지향 언어가 아니다.
다만 객체지향형 설계가 가능하도록 그 기능들을 지원해줄 뿐이다.
때문에 완전 객체지향 언어에서는 불가능한 다중 상속이C++에서는 가능하다.
다중 상속의 장점과 단점
장점은 상속을 다양하게 할 수 있다는 것이다.
하지만 단점은 매우 심각하다.
이는 메모리 낭비, 성능 저하, 구조의 복잡도 상승(다이아몬드 구조)의 가능성을 제공한다.
그림의 상속 구조 설명
- B클래스와 C클래스는 각각 A클래스를 상속받았다.
- D클래스는 B클래스와 C클래스를 상속받았다.
코드
#include <iostream>
class A {
public:
A() { printf("A 생성자\n"); }
~A() { printf("A 소멸자\n"); }
int A_num;
};
class B : public A {
public:
B() { printf("B 생성자\n"); }
~B() { printf("B 소멸자\n"); }
private:
int B_num;
};
class C : public A {
public:
C() { printf("C 생성자\n"); }
~C() { printf("C 소멸자\n"); }
private:
int C_num;
};
class D : public B, public C{
public:
D() { printf("D 생성자\n"); }
~D() { printf("D 소멸자\n"); }
private:
int D_num;
};
int main(void) {
D d;
printf("%d\n", sizeof(d));
return 0;
}
생성자 호출만 보면 A생성자가 2번 호출된 걸 확인할 수 있다.
이는 일반적인 상속관계에서의 생성자, 소멸자 호출 때문이다.
현재 D에 할당되는 메모리 구조를 대략적으로 보자면 다음과 같다.
쓸데없는 생성자, 소멸자 호출이 2번 이뤄졌고, int A_num 변수도 2번 중복되었다.
또한 A 클래스의 int A_num에 일반적인 접근을 시도하면 코드상 error를 발생시킨다.
따라서 B::A_num 혹은 C::A_num 형식으로 접근해야 한다.
이런 문제들을 해결하기 위해 C++에서는 virtual 상속을 제공한다.
virtual 상속을 사용한 경우
virtual 상속을 하면 A가 중복되지 않는다.
코드
#include <iostream>
class A {
public:
A() { printf("A 생성자\n"); }
~A() { printf("A 소멸자\n"); }
int A_num;
};
//virtual 상속
class B : virtual public A {
public:
B() { printf("B 생성자\n"); }
~B() { printf("B 소멸자\n"); }
private:
int B_num;
};
//virtual 상속
class C : virtual public A {
public:
C() { printf("C 생성자\n"); }
~C() { printf("C 소멸자\n"); }
private:
int C_num;
};
class D : public B, public C{
public:
D() { printf("D 생성자\n");
B::A_num = 0;
}
~D() { printf("D 소멸자\n");
}
private:
int D_num;
};
int main(void) {
D d;
printf("%d\n", sizeof(d));
return 0;
}
불필요하게 class A의 생성/소멸자가 2번씩 호출되던 것이 한 번으로 줄어든 것을 볼 수 있다.
하지만 데이터의 크기는 증가했다.
그 이유는 virtual 키워드를 사용하여 가상 상속을 받은 객체 내부에 virtual base pointer라는 변수가 생겨났기 때문이다.
class A의 int A_num이 있던 자리를 vbptr가 대신하고 있는 것을 볼 수 있다.
이 vbptr(virtual base table pointer)는 int A_num의 위치(offset)를 가리킨다.
또한 int A_num은 메모리상 가장 뒤로 이동한 것을 알 수 있다.
class B와 class C에서 상속받은 A클래스의 int A_num이 메모리상 가장 뒤로 이동한 이유는 중복을 막기 위해서이고,
위치를 맨 아래에 고정시킴으로 써 offset정보를 계산할 수 있기 때문이다.
B의 vbptr부분을 보면 시작 offset은 0이고 virtual base table offset은 20입니다.
즉, "int A_num은 vbptr로부터 20바이트 뒤에 있다."라고 해석하면 된다.
그렇다면 C의 vbptr의 virtual base table offset이 12라는 건
"int A_num은 vbptr로부터 12바이트 뒤에 있다"라고 해석할 수 있겠다.
시작 offset의 값은 D의 메모리에 적재되는 순간 해당 메모리를 기준으로 계산된다.
offset값은 클래스의 내용에 따라 -(음수)가 될 수도 있고 0이 될 수도 있다.
복잡하게 offset을 나눈 이유는 몇 개의 상속자가 올지 모르기 때문이다.
예측을 할 수가 없으니 계산을 통해 위치를 알아내려고 만든 것이다.
이렇게 돼버리면 위에서 설명했던 단점들이 드러나게 된다.
메모리 공간이 커질 수 있으며, offset을 이용한 자료를 찾는 과정 자체가 성능에 영향을 줄 수 있기 때문이다.
정리하자면....
1. 다중 상속에서 다이아몬드 구조를 띄게 될 경우 데이터의 중복과 불필요한 생성자 호출을 막기 위해
virtual inheritance(가상 상속)을 사용한다.
2. 가상 상속 시 vbptr이라는 offset을 가리키는 포인터가 생성되며, virtual로 상속된 클래스는
메모리 구조에서 제일 아래로 가게 된다.
3. 시작 offset은 0이 될 수도 있고 -(음수)가 될 수도 있다.
4. 이러한 다중 상속으로 인한 가상 상속은 기존 데이터 크기보다 더 커질 수 있으며, 성능 저하를 야기할 수 있다.