기본 콘텐츠로 건너뛰기

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

부모 클래스 생성자 또는 소멸자에서 가상함수를 호출하면 부모 클래스에 있는 가상함수를 호출한다. 이유는 간단하다. 자식클래스부분을 아직 생성하지 못했거나 이미 소멸했기 때문이다. 예를 들어보자.
#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를 할당한 객체라면 메모리가 좔좔 샐 것이다. 이걸 해결할만한 방법이... 갑자기 생각 안 나므로 나중에 포스팅 해야겠다.

댓글

이 블로그의 인기 게시물

Bash Array, Map 정리

Bash에서 Array, Map에 대한 정리. (매번 찾기 귀찮) 찾아보진 않았지만, Bash에서 Array든 Map이든 동일하게 Map(C++에서 Unordered Map)으로 동작하는 것 같다. 왜냐하면, Array의 Index가 연속하지 않아도 동작한다. 그저 Key가 0 이상의 정수인 Map이랑 비슷하게 동작한다. 예) 1, 2, 3, 9, 10 Array # 생성 declare -a empty_array declare -a ar=(haha hoho baba "long string haha hoho") # 접근 echo "ar[0]=${ar[0]}" echo "all as array=${ar[@]}" # 큰따옴표 안에서 각 원소를 따로따로 전달한다. echo "all as one=${ar[*]}" # 큰따옴표 안에서 각 원소를 문자열 하나로 합쳐 전달한다. echo "indexes=${!ar[@]}" echo "indexes=${!ar[*]}" echo "length=${#ar[@]}" echo "length=${#ar[*]}" echo "last=${ar[-1]}" echo "last=${ar[@]: -1}" # 콜론 뒤에 빈 칸이 꼭 필요하다. 옛 방식 # 현재 상황 declare -p ar #(출력) declare -a ar=([0]="haha" [1]="hoho" [2]="baba" [3]="long string haha hoho") ar[100]=hello # 인덱스를 건너 뛰어도 동작한다. declare -p ar #(출력) declare -a ar=([0]="haha" [1]="hoho" [2]="baba" [3]=...

설치한 패키지에서 RPM 추출하기

오래된 패키지를 관리할 저장소가 없어졌고, 기존 패키지로 다른 서버를 세팅해야할 일이 생겼다면 RPM의 리패키지 기능을 이용해보자. $ rpm -e --repackage [PACKAGE_NAME] 위와 같이 리패키지하면, /var/spool/repackage/ 에 생성한 RPM파일이 있다. :-)