← 문제로

1. 가상 소멸자(virtual destructor)와 다형적 삭제

난이도 중
내 리뷰 · C++
해설 · C++

해설 — 가상 소멸자(virtual destructor)와 다형적 삭제

난이도: 중

답변 프레임워크: 요약 → 핵심 개념 → 단계별 내부 동작 → 흔한 오해·함정 → 면접 포인트

요약

Base 의 소멸자가 virtual 이 아니므로, delete p(p 의 정적 타입 Base*)는 정적 타입 기준으로 ~Base() 만 호출한다. ~Derived() 와 멤버 Resource res_ 의 소멸자가 호출되지 않아 "Resource released" 가 출력되지 않는다 — 이는 표준상 정의되지 않은 동작(UB) 이며 실무에선 리소스 누수(소켓/메모리/핸들)로 나타난다. Base 의 소멸자를 virtual 로 만들면 delete동적 타입을 따라 ~Derived() → 멤버들 → ~Base() 순으로 완전히 소멸한다.

출력 예측

  • 현재 코드(비가상 소멸자):
    Base()
    Resource acquired
    Derived()
    ~Base()
    
    ~Derived()~Resource()("Resource released")가 누락(UB).
  • 가상 소멸자로 고친 경우:
    Base()
    Resource acquired
    Derived()
    ~Derived()
    Resource released
    ~Base()
    

핵심 개념

  • 정적 타입 vs 동적 타입: p 의 정적 타입은 Base*, 가리키는 객체의 동적 타입은 Derived.
  • delete p 의 디스패치: 소멸자가 비가상이면 컴파일러는 정적 타입으로 호출할 소멸자를 결정한다(비다형적). 가상이면 vtable 을 통해 동적 타입의 소멸자로 디스패치한다(다형적).
  • 소멸 순서: 파생 소멸자 본문 → 파생 멤버(역순) → 기반 소멸자. 비가상이면 이 체인이 ~Base() 에서 시작·끝나 파생 부분이 통째로 빠진다.

단계별 내부 동작

  1. new Derived(): Base() → 멤버 Resource()Derived() 순으로 생성(기반→멤버→파생).
  2. delete p (비가상): 컴파일러가 Base::~Base정적 바인딩으로 호출. Derived 의 소멸자와 res_ 소멸자는 실행되지 않음. 그 뒤 operator delete(p) 로 메모리 해제는 되지만, Derived 서브오브젝트가 소멸되지 않은 채 해제됨 → 표준상 UB.
  3. 가상으로 바꾸면: Base 객체에 vptr 이 생기고, delete p 는 vtable 의 소멸자 슬롯을 조회해 Derived::~Derived 를 호출. 이후 멤버 ~Resource(), 기반 ~Base() 가 순서대로 실행. (컴파일러는 보통 "완전 소멸자/삭제 소멸자" 형태로 vtable 에 넣는다.)

흔한 오해·함정

  • "메모리는 해제됐으니 괜찮다" ✗ — operator delete 로 바이트는 회수돼도, 파생/멤버 소멸자가 안 돌면 그들이 들고 있던 소켓/파일/락/힙 버퍼가 누수된다. 게임서버에서 세션 객체가 이렇게 새면 FD/메모리 고갈로 이어진다.
  • 모든 클래스에 virtual 소멸자를 달면 된다? ✗ — vptr 로 객체 크기 8바이트 증가, 다형적이지 않은 작은 값 타입(POD 류)·표준 레이아웃이 깨질 수 있다. 규칙: 기반 클래스로서 다형적으로 delete 될 수 있으면 virtual 소멸자(또는 protected 비가상 소멸자로 기반 포인터 delete 자체를 금지).
  • unique_ptr<Base> 면 안전? ✗ — unique_ptr<Base> 의 기본 deleter 도 delete (Base*) 라 동일 문제. (shared_ptr<Base> 는 생성 시점의 삭제자를 타입소거로 저장해 예외적으로 파생 소멸자를 부른다 — 단 make_shared<Derived>() 또는 shared_ptr<Base>(new Derived) 처럼 생성 시 동적 타입을 알 때만.)

면접 포인트 (예상 질문)

  1. 비가상 소멸자 기반 포인터 delete 가 왜 UB 이고, 실무에서 어떤 증상(누수)로 나타나나?
  2. virtual 을 붙이면 객체 레이아웃이 어떻게 바뀌나(vptr/vtable)? 그 비용은?
  3. unique_ptr<Base>shared_ptr<Base> 가 이 상황에서 다르게 동작하는 이유는? 기반 클래스를 다형 삭제 못 하게 막는 대안(protected 비가상 소멸자)은?