본문 바로가기
프로그래밍/C++

C++ 가상 상속(virtual inheritance)

by Hwan2 2020. 6. 20.
반응형

1. 가상 상속(virtual inheritance) 이란??

C++에선 다중상속을 지원합니다. JAVA는 다중상속을 막고 있지요.
다중상속은 장점과 단점이 존재합니다.
장점으론 객체지향의 상속성을 좀 더 유연하게 해주는 역할을 하죠.
하지만 단점은 다이아몬드 상속구조를 띌 수 있으며, 이는 메모리 낭비, 성능저하로 이어질 수 있습니다.

그럼 다음 그림과 같은 형태로 상속을 진행해 보겠습니다.

그림

코드

#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 Bpublic 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 형식으로 접근해야 합니다.


이를 해결할 수 있는 방법은 상속 앞에 virtual 키워드를 붙여주는 것입니다.



그림




코드

#include <iostream>

class A {
public:
    A() { printf("A 생성자\n"); }
    ~A() { printf("A 소멸자\n"); }
    int A_num;
};

class B : virtual public A {
public:
    B() { printf("B 생성자\n"); }
    ~B() { printf("B 소멸자\n"); }
private:
    int B_num;
};

class C : virtual public A {
public:
    C() { printf("C 생성자\n"); }
    ~C() { printf("C 소멸자\n"); }
private:
    int C_num;
};

class D : public Bpublic 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;
}


불필요한 소멸자와 생성자가 없어진걸 확인할 수 있습니다.

하지만 데이터의 크기는 증가됐습니다.


왜 그런건지 메모리를 보겠습니다.


보시면 vbptr이 추가된 것을 확인할 수 있습니다.

그리고 vbptr(virtual base table pointer)는 int A_num의 위치를 알고있는 offset데이터를 가르키게 됩니다.

또한 int A_num은 맨 아래로 갔습니다.


상위 클래스의 A클래스의 int A_num이 맨 아래로 간 이유는 중복을 막기 위해서이고, 

위치를 맨 아래에 고정시킴으로 써 offset정보를 계산할 수 있기 때문입니다.


B의 vbptr부분을 보면 시작 offset은 0이고 virtual base table offset은 20입니다.

즉, "0부터 시작하여 20바이트 떨어진 곳에 있다."라고 해석이 됩니다.


시작offset의 값은 D의 메모리에 적재되는 순간 해당 매모리를 기준으로 계산됩니다.

offset값은 클래스의 내용에 따라 -(음수)가 될 수도 있고 0이 될 수도 있습니다. 


복잡하게 offset을 나눈 이유는 몇개의 상속자가 올지 모르기 때문입니다.

예측을 할 수가 없으니 계산을 통해 위치를 알아내려고 만든 것입니다.



이렇게 되버리면 위에서 설명했던 단점들이 들어나게 됩니다.

메모리 공간이 커질 수 있으며, offset을 이용한 자료를 찾는 과정 자체가 성능에 영향을 줄 수 있기 때문입니다.



정리하자면....


1. 다중상속에서 다이아몬드 구조를 띄게 될 경우 데이터의 중복과 불필요한 생성자 호출을 막기위해

virtual inheritance(가상 상속)을 사용한다.


2. 가상 상속시 vbptr이라는 offset을 가르키는 포인터가 생성되며, virtual로 상속된 클래스는

메모리 구조에서 제일 아래로 가게된다.


3. 시작 offset은 0이 될수도 있고 -(음수)가 될 수도 있다.


4. 이러한 다중상속으로 인한 가상 상속은 기존 데이터 크기보다 더 커질 수 있으며, 성능 저하를 야기할 수 있다.




참고 자료 : https://www.kdata.or.kr/info/info_04_view.html?field=&keyword=&type=techreport&page=12&dbnum=185561&mode=detail&type=techreport

반응형

댓글


스킨편집 -> html 편집에서