250x250
Notice
Recent Posts
Recent Comments
«   2024/10   »
1 2 3 4 5
6 7 8 9 10 11 12
13 14 15 16 17 18 19
20 21 22 23 24 25 26
27 28 29 30 31
Archives
Today
Total
관리 메뉴

SYDev

C++ Chapter 08-3 : 가상 소멸자와 참조자의 참조 가능성 본문

Programming Lang/C++

C++ Chapter 08-3 : 가상 소멸자와 참조자의 참조 가능성

시데브 2023. 7. 19. 18:56
728x90

 가상함수 말고도 virtual 키워드를 붙여줘야 할 대상이 하나 더 있는데, 그게 바로 소멸자이다.

 

가상 소멸자(Virtual Destructor)

 virtual로 선언된 소멸자를 '가상 소멸자'라 하는데 이것이 필요한 이유와 설명을 위해 예제를 하나 살펴보자.

#include <iostream>
#include <cstring>
using namespace std;

class First
{
private:
    char * strOne;
public:
    First(char * str)
    {
        strOne = new char[strlen(str)+1];
    }
    ~First()
    {
        cout<<"~First()"<<endl;
        delete []strOne;
    }
};

class Second : public First
{
private:
    char * strTwo;
public:
    Second(char * str1, char * str2) : First(str1)
    {
        strTwo = new char[strlen(str2)+1];
    }
    ~Second()
    {
        cout<<"~Second()"<<endl;
        delete []strTwo;
    }
};

int main(void)
{
    First * ptr = new Second("simple", "complex");
    delete ptr; //원래대로는 First, Second의 소멸자가 모두 호출되어야 한다.

    return 0;
}
~First()

 main 함수의 첫 번째 문장에서 First와 Second의 생성자가 모두 호출되었기에, 이후 소멸자를 모두 호출하여 할당된 공간을 해제해야 한다. 하지만 실행결과에서 알 수 있듯이, 객체의 소멸을 First형 포인터로 명령하니 First 클래스의 소멸자만 호출되었다. 이럴 경우에는 메모리의 누수(leak)가 발생하기 때문에 객체의 소멸과정에서 delete 연산자에 사용된 포인터 변수의 자료형에 상관없이 모든 소멸사가 호출되어야 한다. 이런 문제를 해결할 수 있는 것이 바로 가상 소멸자이다.

 

 가상 소멸자의 선언은 소멸자 앞에 virtual 선언만 추가해주면 된다. 가상함수와 마찬가지로 가상소멸자 역시 기초 클래스 하나에 virtual 선언만 마치면, 해당 클래스를 상속하는 모든 유도 클래스의 소멸자에 '가상 소멸자' 선언이 된다. 또한, 가상 소멸자 호출 시에는, 상속의 계층구조상 맨 아래에 존재하는 유도 클래스의 소멸자가 대신 호출되면서 기초 클래스의 소멸자까지 순차적으로 호출된다.

 

참조자의 참조 가능성

 앞서 Chapter 08-1 : 객체 포인터의 참조 관계에서 "C++에서, AAA형 포인터 변수는 AAA객체 or AAA를 직접 혹은 간접적으로 상속하는 모든 객체를 가리킬 수 있다(객체의 주소 값을 저장할 수 있음)." 라고 했는데, 이는 참조자에도 적용할 수 있다.

 

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

 

 이뿐만 아니라 앞서 보였던 포인터를 이용한 함수의 호출에서 클래스의 상속 관계에 따라 호출되는 함수의 규칙도 같고, 가상함수의 개념도 포인터에서와 마찬가지로 참조자에 그대로 적용된다. 

 

 이를 모두 보여주는 예제를 하나 살펴보자.

#include <iostream>
using namespace std;

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.SimpleFunc();

    Second & sref = obj;
    sref.FirstFunc();
    sref.SecondFunc();
    sref.SimpleFunc();

    First & fref = obj;
    fref.FirstFunc();
    fref.SimpleFunc();

    return 0;
}
FirstFunc()
SecondFunc()
ThirdFunc()
Third's SimpleFunc()
FirstFunc()
SecondFunc()
Third's SimpleFunc()
FirstFunc()
Third's SimpleFunc()

 위 예제에서 obj는 Third 객체이므로 가상함수인 SimpleFunc에서는 Third 객체의 멤버함수가 호출되고, virtual 선언이 되지 않은 멤버함수들에서는 참조자의 자료형에 따라서 멤버함수가 호출된다.

 

 따라서 다음 함수가 주어진다면,

void GoodFunction(const First &ref) { . . . . .}

 가상함수가 아닌 이상, 인자로 전달되는 객체의 실제 자료형에 상관없이 해당 함수에서는 First 클래스 내부의 멤버 함수만 호출할 수 있음을 고려하고 함수를 정의해야 한다.

 


출처 : 윤성우, <윤성우의 열혈 C++ 프로그래밍>, 오렌지미디어, 2010.05.12

728x90