[C++] 부모 클래스의 소멸자는 virtual이어야 한다 / 정적 바인딩과 동적 바인딩 이해하기

지즈·2025년 8월 23일
0

C++

목록 보기
4/5

https://velog.io/@jizzvibe/C-%EA%B0%80%EC%83%81-%ED%95%A8%EC%88%98%EA%B0%80%EC%83%81-%ED%95%A8%EC%88%98-%ED%85%8C%EC%9D%B4%EB%B8%94%EC%9D%98-%EB%8F%99%EC%9E%91-%EC%9B%90%EB%A6%AC%EB%8B%A4%ED%98%95%EC%84%B1-%ED%95%B5%EC%8B%AC-%EC%9B%90%EB%A6%AC

다형성과 가상 함수 테이블의 동작 원리에 대한 포스팅에서 이어지는 글입니다.



Base* base = new Derived(); // 부모 포인터로 자식 객체의 주소를 받는다
delete base; // 부모 포인터로 해제한다 

C++에서 부모 클래스 포인터를 통해 자식 객체를 삭제할 때, 소멸자가 어떻게 호출되는지는 안전한 메모리 관리의 핵심입니다. 소멸자가 virtual인지 아닌지에 따라 실제 호출되는 함수가 달라지고, 잘못하면 자식 객체가 차지한 자원이 해제되지 않아 메모리 누수로 이어질 수 있습니다. 이번 글에서는 정적 바인딩과 동적 바인딩 관점에서 소멸자의 동작 원리를 살펴보고, 안전하게 객체를 관리하는 방법을 알아보겠습니다.

정적 바인딩 : 소멸자가 virtual이 아닌 경우

class Base {
public:
    ~Base() { }
};

class Derived : public Base {
public:
    ~Derived() { }
};


정적 바인딩이란 컴파일 시점에 호출할 함수가 결정되는 방식을 말합니다. 부모 클래스의 소멸자가 virtual이 아닌 경우, 컴파일러는 부모 소멸자를 기계어 수준에서 바로 연결합니다. 이 경우 부모 포인터를 통해 자식 객체를 삭제하면 자식 소멸자는 호출되지 않고, 부모 소멸자만 실행됩니다. 결과적으로 자식 객체가 차지한 자원은 해제되지 않아 메모리 누수가 발생할 가능성이 존재합니다. 이러한 이유로 C++에서는 다형성을 가진 클래스에서 소멸자를 반드시 virtual로 선언하는 것이 권장됩니다.


동적 바인딩 + virtual 소멸자

class Base {
public:
    virtual ~Base() { }
};

class Derived : public Base {
public:
    ~Derived() { }
};

반대로 동적 바인딩을 사용하면 소멸자 호출이 안전하게 이루어집니다. 부모 클래스에서 소멸자를 virtual로 선언하면, 자식 클래스 객체가 생성될 때 컴파일러는 자식 전용의 vtable을 만듭니다. 이때 부모 vtable의 소멸자 주소를 자식 객체의 소멸자 주소로 덮어쓰는 방식으로 등록됩니다.

참고 자료 : https://en.cppreference.com/w/cpp/language/virtual.html

즉, 부모 소멸자가 virtual로 선언된다면, 자식 소멸자는 자동으로 이를 재정의(override)합니다.


따라서 부모 포인터를 통해 자식 객체를 접근하더라도, 실제 실행 시점에는 자식 객체의 vtable에 접근하여 자식 소멸자가 먼저 호출됩니다.



소멸자의 연쇄 호출

그런데 자식 객체의 소멸자로 v-table 엔트리를 대체했다면 부모 소멸자는 왜 호출되는 것인지 의문이 생깁니다. 소멸자는 연쇄 호출(chain call) 구조로 정의되어 있기 때문입니다. 즉, Derived::~Derived()가 실행되면, 그 내부에서 컴파일러가 자동으로 Base::~Base()를 호출하도록 코드를 생성한다. 이 과정은 자식에서 부모로, 그리고 가장 위의 최상위 부모까지 순차적으로 진행됩니다. 덕분에 부모 포인터로 자식 객체를 삭제해도 안전하게 모든 소멸자가 호출되고, 메모리 및 리소스 누수 없이 객체가 해제할 수 있습니다.

profile
클라이언트 개발자가 되는 그 날까지 킵 고잉

0개의 댓글