본문 바로가기

C++/윤성우의 열혈 프로그래밍 C++

윤성우의 열혈 C++ chpt 13.

가상 소멸자(Virtual Destructor)

class First
{
    ...
public:
    virtual ~First() { ... }
};
class Second: public First
{
    ...
public:
    virtual ~Second() { ... }
};
class Third: public Second
{
    ...
public:
    virtual ~Third() { ... }
};
int main(void)
{
    First *ptr = new Third();
    delete ptr;
    ...
}

위 코드에서 클래스의 소멸자에 virtual 이 명시된 것을 볼 수 있다.

만약 virtual 명시가 없을 경우, 생성자와는 다르게 소멸자는 타입으로 설정된 Fisrt 의 소멸자만 호출된다.

그렇게되면 객체의 전체 레이아웃은 해제되나, 자식 클래스 영역에 할당된 자원들은 해제되지 않아leak 문제가 발생한다.

이전 챕터에서 다웠던 가상 함수랑 유사하면서 조금 다른 메커니즘으로 소멸자를 호출하는데

가상 함수는 virtual 을 사용함으로써 생성된 객체의 클래스를 기준으로 가장 마지막 오버라이딩된 함수를 호출한다면

https://code-jh.tistory.com/102 

 

가상 소멸자 상속 과정에서 거친 모든 클래스의 소멸자를 호출하여 각각의 해당하는 파트의 자원을 해제시켜준다


참조자의 참조 가능성

C++ 에서 AAA 형 참조자는 AAA 객체 또는 AAA 를 직적 혹은 간접적으로 상속하는 모든 객체를 참조할 수 있다.

class First
{
public:
    void FirstFunc() { cout << "FirstFunc()" << endl; }
    virtual void SimpleFunc() ( cout << "First's SimpleFunc()" << endl; }
}
class Second: public First
{
public:
    void SecondFunc() { cout << "SecondFunc()" << endl; }
    virtual void SimpleFunc() ( cout << "Second's SimpleFunc()" << endl; }
};
class Third: public Second
{
public:
    void ThirdFunc() ( cout << "ThirdFunc()" << endl; }
    virtual void SimpleFunc() ( cout << "Third's SimpleFunc()" << endl; }
};
int main(void)
{
    Third obj;
    obj.FirstFunc();
    obj.SecondFunc();
    obj.ThirdFunc();
    obj.SimpleRunc();

    Second& rsef = obj;

    sref.FirstFunc();
    sref.SecondFunc();
    srdf.ThirdFunc();
    sref.SimpleFunc();

    First& fref = obj;

    fref.FirstFunc();
    fref.SimpleFunc();

    return ;
}
----------------------------------------------------------------
// 결과
FirstFunc()
SecondFunc()
ThirdFunc()
Third's SimpleFunc()
FirstFunc()
SecondFunc()
Thrid's SimpleFunc()
FirstFunc()
Third's SimpleFunc()

[ 가상의 원리와 다중상속]

멤버함수와 가상함수의 동작 원리

 

객체 안에 정말로 멤버함수가 존재하는가?

// 클래스 Data를 흉내 낸 영역
typedef struct Data
{
    int data;
    void (*ShowData)(Data*);
    void (*Add)(Data*, int);
} Data;

void ShowData(Data* THIS) { cout << "Data: " << THIS->data << endl; }
void Add(Data *THIS, int num) { THIS->data += num; }

// 적절히 변경된 main 함수 
int main(void)
{
    Data obj1 = {15, ShowData, Add};
    Data obj2 = {7, ShowData, Add};

    obj1.Add(&obj1, 17);
    obj2.Add(&obj2, 9);
    obj1.ShowData(&obj1);
    obj2.ShowData(&obj2);

    return 0;
}

구조체가 같은 함수 포인터를 가지고 있을지라도, 인자로 던져주는 THIS 포인터를 구조체 각각의 주소를 전달함으로써,

각자에게 맞는 결과를 취할 수 있도록 유도되는 메커니즘이

C++ 에서 객체의 멤버 함수인스턴스에서 활용되는 메서드의 기본 메커니즘이다.

 

가상함수의 동작원리와 가상함수 테이블

class AAA
{
private:
    int num1;
public:
    virtual void Func1() { cout << "Func1" << endl; }
    virtual void Func2() { cout << "Func2" << endl; }
};
class BBB: public AAA
{
private:
	int num2;
public:
	virtual void Func1() { cout << "BBB::Func1" << endl; }
    void Func3() { cout << "Func3" << endl; }
};
int main(void)
{
	AAA *aptr = new AAA();
    aptr->Func1();
    
    BBB *bprt = new BBB();
    bptr->Func1();
    
    return 0;
}
---------------------------------------------------------
// 결과
Func1
BBB::Func1

 

하나 이상의 가상함수가 멤버로 포함되면 위와 같은 형태의 V-Table 이 생성되고 매 함수 호출시마다 이를 참조하게 된다.

 

BBB 클래스에서 오버라이딩 된 Func1 로 인해서 BBB 클래스의 가상테이블에기존의 AAA::Func1은 사라지고

재정의된 BBB::Func1 이 남아있는 것을 주의깊게 볼 필요가 있다.

 

업캐스팅된 객체에서 하위 클래스에 해당하는 요소를 직접적으로 활용할 수 없는게 기본 상태이지만,

상위에서 부터 virtual 로 오버라이딩된 함수일 경우엔 하위 클래스의 함수를 호출할 수 있게 된다.

 

(업캐스팅된 객체의 클래스 구조의 상위에서 virtual 없이 명시된 함수

하위에서 virtual 을 명시한 후 해당 함수를 호출할 경우엔

이를 오버라이딩 된 것으로 간주하지 않고, 별도의 함수로 취급하게 된다.

상위에선 해당 함수가 virtual 이 아니기 때문에 v-table 을 확인하지 않고, 바로 상위의 함수를 호출한다는 것이다.

격이 맞는 객체로(클래스 == 객체) 생성됐을 경우엔 virtual 취급이 가능하지만 크게 의미가 있는가 싶은 대목이다)

각 클래스는 virtual 로 오버라이딩 된 함수가 가지는 주소는 클래스 별로 생성된 v-table 의 주소이다.


다중 상속(Multiple Inheritance) 에 대한 이해

다중 상속의 설명에 앞서

  • 많은 사람들이 다중상속은 득보다 실이 더 많은 문법이라고들 한다.
  • 일반적인 경우 다중상속은 다양한 문제를 동반한다
  • 가급적 사용하지 않아야 하는 의견에 동의율이 많다
  • 그러나 예외적으로 매우 제한적인 사용까지 부정할 필요는 없다고 본다.
class BaseOne
{
public:
    void SimpleFuncOne() { cout << "BaseOne" << endl; }
};
class BaseTwo
{
public:
    void SimpleFuncTwo() { cout << "BaseTwo" << endl; }
};
class MultiDerived: public BaseOne, protected BaseTwo
{
public:
    void ComolexFunc()
    {
        SimpleFuncOne();
        SimpleFuncTwo();
    }
};
int main(void)
{
    MultiDerived mdr;
    mdr.ComplexFunc();

    return 0;
}
---------------------------------------------
// 결과
BaseOne
BaseTwo

다중 상속은 말 그대로 둘 이상의 클래스를 상속하는 형태이고,

이로 인해서 유도 클래스의 객체모든 기초 클래스의 멤버를 포함하게 된다.

 

다중상속에서 메서드를 통일할 경우에 다중상속의 모호성을 확인 할 수 있다.

class BaseOne
{
public:
    void SimpleFunc() { cout << "BaseOne" << endl; }
};
class BaseTwo
{
public:
    void SimpleFunc() { cout << "BaseTwo" << endl; }
};
class MultiDerivde: public BaseOne, protected BaseTwo
{
public:
    void ComplexFunc()
    {
        BaseOne::SimpleFunc();	// 호출 대상 구분
        BaseTwo::SimpleFunc();	// 호출 대상 구분
    }
};

이렇게 선 뜻 구분할 수 없기 때문에 호출의 대상을 구분해서 명시해야 하는 불편함이 동반된다.

 

이보다 더한 모호한 상황을 확인해보자면

class Base
{
public:
    Base() { cout << "Base Constructor" << endl; }
    void SimpleFunc() { cout << "BaseOne" << endl; }
};
class MiddleDerivedOne: virtual public Base
{
public:
    MiddelDerivedOne(): Base()
    {
        cout << "MiddelDErivedOne Constructor" << endl;
    }
    void MiddleFuncOne()
    {
        SimpleFunc();
        cout << "MiddelDerivedOne" << endl;
    }
};
class MiddleDerivedTwo: virtual public Base
{
public:
    MiddleDerivedTwo(): Base()
    {
        cout << "MiddelDerivedTwo Constructor" << endl;
    }
    void MiddleFuncTow()
    {
        SimpleFunc();
        cout << "MiddleDerivedTwo" << endl;
    }
};
class LastDerived: public MiddleDerivedOne, public MiddleDerivedTwo
{
public:
    LastDerived(): MiddleDerivedOne(), MiddleDerivedTwo()
    {
        cout << "LastDerived Constructor" << endl;
    }
    void ComplexFunc()
    {
        MiddleFuncOne();
        MiddleFuncTwo();
        SimpleFunc();
    }
};

 

LastDerived 는 Base 를 상속받은 MiddleDerivedOne 과 MiddleDerivedTwo 로 인해 2개의 Base 를 갖게 된다.

(이는 다이아몬드 상속이라고도 부른다)

virtual 상속이 없을 경우의 다중 상속의 결과

 

이와 같인 동일한 Base 의 멤버가 중복적인 것은 좋은 프로그램을 작성하는 방법과는 거리가 있는 형태이다.

 

다중 상속을 받더라도, 기초 클래스의 멤버 중복을 제한하고자 사용되는 게 virtual 상속이다.

 

virtual 상속은 상속 계층에서 같은 기초 클래스를 여러 경로를 통해 상속 받을 때, 

해당 기초 클래스의 인스턴스를 단 하나만 공유하도록 명시하는 상속 방식이다.

virtual 상속의 실질적인 효과는 파생 클래스의 공통 조상이 중복 상속되는 상황에서만 드러나기 때문에

조부모 기준으로 중복 인스턴스가 제거된다.