C++/C++

가상 상속(virtual inheritance)

Elan 2021. 8. 28. 21:28

가상 상속(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;
}

 

< 코드 실행 결과 - Console >

 

생성자 호출만 보면 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;
}

<코드 실행 결과 - Console >

 

불필요하게 class A의 생성/소멸자가 2번씩 호출되던 것이 한 번으로 줄어든 것을 볼 수 있다.

하지만 데이터의 크기는 증가했다.

그 이유는 virtual 키워드를 사용하여 가상 상속을 받은 객체 내부에 virtual base pointer라는 변수가 생겨났기 때문이다.

< 가상 상속 시 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. 이러한 다중 상속으로 인한 가상 상속은 기존 데이터 크기보다 더 커질 수 있으며, 성능 저하를 야기할 수 있다.