기본 콘텐츠로 건너뛰기

생성자, 소멸자에서 가상함수 불가

부모 클래스 생성자 또는 소멸자에서 가상함수를 호출하면 부모 클래스에 있는 가상함수를 호출한다. 이유는 간단하다. 자식클래스부분을 아직 생성하지 못했거나 이미 소멸했기 때문이다. 예를 들어보자.
#include <iostream>
using namespace std;

class CParent
{
public:
        CParent()
        {
                cout << __PRETTY_FUNCTION__ << endl;
                call_virtual_function();
        }

        virtual ~CParent()
        {
                cout << __PRETTY_FUNCTION__ << endl;
                call_virtual_function();
        }

        void call_virtual_function(void) const
        {
                cout << __PRETTY_FUNCTION__ << endl;
                virtual_function();
        }

        virtual void virtual_function(void) const
        {
                cout << __PRETTY_FUNCTION__ << endl;
        }
};

class CChild : public CParent
{
public:
        CChild()
        {
                cout << __PRETTY_FUNCTION__ << endl;
        }

        virtual ~CChild()
        {
                cout << __PRETTY_FUNCTION__ << endl;
        }

        virtual void virtual_function(void) const
        {
                cout << __PRETTY_FUNCTION__ << endl;
        }
};

int
main(int argc,char* argv[])
{
        cout << "Case1" << endl;
        CParent* p = new CChild;
        delete p;

        cout << "Case2" << endl;
        CChild* c = new CChild;
        delete c;

        return 0;
}
귀찮아서 gcc 확장인 __PRETTY_FUNCTION__을 사용했다. VC 같은데선 돌아가지 않는 소스다. 아름답게 컴파일을 하고 실행하면 다음과 같은 결과를 얻을 수 있다.
Case1
CParent::CParent()
void CParent::call_virtual_function() const
virtual void CParent::virtual_function() const
CChild::CChild()
virtual CChild::~CChild()
virtual CParent::~CParent()
void CParent::call_virtual_function() const
virtual void CParent::virtual_function() const
Case2
CParent::CParent()
void CParent::call_virtual_function() const
virtual void CParent::virtual_function() const
CChild::CChild()
virtual CChild::~CChild()
virtual CParent::~CParent()
void CParent::call_virtual_function() const
virtual void CParent::virtual_function() const
호출순서는 아래와 같다.
CParent() → CParent::call_virtual_function() → CParent::virtual_function() → CChild() → 객체 생성 완료!

~CChild() → ~CParent() → CParent::call_virtual_function() → CParent::virtual_function() → 객체 해체 완료!
아주 당연한 이치다. CParent가 call_virtual_function()을 통해 virtual_function()을 호출하려고 내부 virtual table을 뒤적였을 것이다. 그러나 virtual table에는 아직 CChild가 없기 때문에(CChild 생성자가 그 역을 한다) 당연히 CParent::virtual_function()을 호출했을 것이다.
반대도 마찬가지이다. CChild 소멸자가 virtual table에서 CChild를 삭제하였기 때문에 CParent::virtual_function()을 호출하는 것이다.

virtual table과 생성 관계를 증명하기 위해 한 가지 실험을 더 해보기로 하자.
#include <iostream>
using namespace std;

class CGrandParent
{
public:
        CGrandParent()
        {
                cout << __PRETTY_FUNCTION__ << endl;
        }

        virtual ~CGrandParent()
        {
                cout << __PRETTY_FUNCTION__ << endl;
        }

        void call_virtual_function(void) const
        {
                cout << __PRETTY_FUNCTION__ << endl;
                virtual_function();
        }

        virtual void virtual_function(void) const
        {
                cout << __PRETTY_FUNCTION__ << endl;
        }
};

class CParent : public CGrandParent
{
public:
        CParent()
        {
                cout << __PRETTY_FUNCTION__ << endl;
                call_virtual_function();
        }

        virtual ~CParent()
        {
                cout << __PRETTY_FUNCTION__ << endl;
                call_virtual_function();
        }

        virtual void virtual_function(void) const
        {
                cout << __PRETTY_FUNCTION__ << endl;
        }
};

class CChild : public CParent
{
public:
        CChild()
        {
                cout << __PRETTY_FUNCTION__ << endl;
        }

        virtual ~CChild()
        {
                cout << __PRETTY_FUNCTION__ << endl;
        }

        virtual void virtual_function(void) const
        {
                cout << __PRETTY_FUNCTION__ << endl;
        }
};

int
main(int argc,char* argv[])
{
        cout << "Case3" << endl;
        CGrandParent* g = new CChild;
        delete g;

        return 0;
}
좀 헷갈리지? CParent 기능 몇개를 CGrandParent로 옮기고 CParent가 CGrandParent를 상속 받았을 뿐이다. (이 예제에서 굳이 CChild는 필요 없는데...) 역시나 CParent()가 call_virtual_function()을 호출하도록 했다.

실행결과는 아래와 같다.
Case3
CGrandParent::CGrandParent()
CParent::CParent()
void CGrandParent::call_virtual_function() const
virtual void CParent::virtual_function() const
CChild::CChild()
virtual CChild::~CChild()
virtual CParent::~CParent()
void CGrandParent::call_virtual_function() const
virtual void CParent::virtual_function() const
virtual CGrandParent::~CGrandParent()
CParent()가 CGrandParent::call_virtual_function()을 호출하기 전에 CParent를 virtual table에 등록했으며, 따라서 CGrandParent::call_virtual_function()이 virtual_function()을 찾을 때 CParent::virtual_function()을 찾는다. 따라서 CParent::virtual_function()을 호출한다. 그러나 CChild는 virtual_table에 등록하지 않았기 때문에 CChild::virtual_function()을 호출하진 않는다. 반대도 마찬가지이다. 더 쓰면 손꾸락만 아프다.

이 문제 핵심은 clear(), destroy() 메서드 구현이다. 역시나 예를 들어보자.
#include <iostream>
using namespace std;

class CParent
{
public:
        CParent() : p(NULL)
        {
        }

        virtual ~CParent()
        {
                destory();
        }

protected:
        virtual void destory(void)
        {
                cout << __PRETTY_FUNCTION__ << endl;
                if ( p )
                {
                        delete p;
                        p = NULL;
                }
        }

protected:
        int* p;
};

class CChild : public CParent
{
public:
        CChild() : CParent(), p2(NULL)
        {
        }

        virtual ~CChild()
        {
                // do nothing
        }

protected:
        virtual void destory(void)
        {
                cout << __PRETTY_FUNCTION__ << endl;
                if ( p2 )
                {
                        delete p2;
                        p2 = NULL;
                }
                CParent::destory();
        }

protected:
        int* p2;
};

int
main(int,char**)
{
        cout << "Case4" << endl;
        CParent* p = new CChild;
        delete p;
        return 0;
}
컴파일해서 실행한 결과는 아래와 같다.
Case4
virtual void CParent::destory()
만약 CChild::p2를 할당한 객체라면 메모리가 좔좔 샐 것이다. 이걸 해결할만한 방법이... 갑자기 생각 안 나므로 나중에 포스팅 해야겠다.

댓글

이 블로그의 인기 게시물

Winget 해시 무시하기

가끔씩 Winget 에서 패키지를 다운로드 했을 때, "설치 관리자 해시가 일치하지 않습니다." 오류가 뜰 때가 있다. 보안 이슈가 있지만, 그냥 무시하고 싶을 때, 아래 순서로 무시해준다. 관리자 권한 winget settings --enable InstallerHashOverride 설치 winget install --ignore-security-hash --id NirSoft.NirCmd