[C++] 가상 소멸자(Virtual destructor)

1 분 소요

가상 소멸자의 필요성

C++에서는 업캐스팅을 고려했을 때 virtual 키워드를 사용한 멤버 함수만 정상적으로 재정의(override) 될 수 있다.

  • virtual 키워드를 사용하지 않은 멤버 함수를 재정의하면 의도와는 다르게 작동하는 경우가 존재한다.
  • 가상 함수에 대한 내용은 여기를 참고하자.
  • Java에서는 컴파일러가 모든 메소드를 가상 메소드로 취급한다.

‘C++에서도 컴파일러가 모든 멤버 함수를 가상 함수로 취급하는 것이 낫지 않았나?’라는 목소리도 있지만, 가상 함수의 vtable 이용에는 오버헤드가 존재한다. 이때의 오버헤드는 가상 함수를 호출하기 위해 포인터를 역참조하여 실행할 멤버 함수를 결정하는 작업을 의미한다.

그래서 C++언어를 만든 사람들은 선택권을 주는 것이 더 낫다고 판단했는데, 만약 재정의가 필요하지 않다면 virtual 키워드를 사용하지 않으면 되고 이 경우에는 가상 함수가 아니기 때문에 vtable 이용에 따른 오버헤드가 없어진다. (요즘은 하드웨어의 발전으로 인하여 오버헤드가 나노초 단위로 측정될 만큼 감소하고 있다고 한다.)

하지만 소멸자(destructor)만큼은 반드시 virtual 키워드를 사용하여 가상 소멸자로 만드는 것에 동의하는 경우가 많다.

가상 소멸자 (Virtual destructor)

가상 소멸자(virtual destructor)는 클래스의 소멸자에 virtual 키워드를 사용한 소멸자를 의미한다.

아래의 코드에서 파생 클래스의 생성자에서는 10000개의 정수 공간을 동적으로 할당하고 소멸자에서 할당된 메모리를 해제해주고 있다. 파생 클래스만 사용하면 문제가 없는 코드이지만 업케스팅(Upcasting)을 하는 경우에는 가상 소멸자가 필요하다.

기반 클래스의 포인터가 동적 할당된 파생 클래스 객체를 가리키고 있을 때 기반 클래스가 가리키는 메모리 영역을 해제하면 파생 클래스의 소멸자는 호출되지 않기 때문이다.

#include <bits/stdc++.h>

class Base {
public:
    Base() {
        std::cout << "Base()\n";
    }
    ~Base() {
        std::cout << "~Base()\n";
    }
};

class Derived : public Base {
private:
    int* numbers;
public:
    Derived() : numbers(new int[10000]) {
        std::cout << "Derived()\n";
    }
    ~Derived() {
        delete[] numbers;
        std::cout << "~Derived()\n";
    }
};

int main() {
    Base* b = new Derived();
    delete b;

    return 0;
}
Base()
Derived()
~Base()

이처럼 ~Derived()는 가상 소멸자로 정의되지 않았기 때문에 b가 실질적으로 가리키고 있는 Derived 객체의 소멸자를 찾지 못한다.

소멸자가 정상적으로 호출될 수 있게 하려면 가상 소멸자로 정의해야 한다.

#include <bits/stdc++.h>

class Base {
public:
    Base() {
        std::cout << "Base()\n";
    }
    virtual ~Base() { // virtual destructor
        std::cout << "~Base()\n";
    }
};

class Derived : public Base {
private:
    int* numbers;
public:
    Derived() : numbers(new int[10000]) {
        std::cout << "Derived()\n";
    }
    virtual ~Derived() { // virtual destructor
        delete[] numbers;
        std::cout << "~Derived()\n";
    }
};

int main() {
    Base* b = new Derived();
    delete b;

    return 0;
}
Base()
Derived()
~Derived()
~Base()