티스토리 뷰
🔷 가상 소멸자(Virtual Destructor): 포인터 해석 범위의 한계와 객체 소멸의 불완전성
✅ 1. 정의
가상 소멸자(Virtual Destructor)란 C++에서 상속 관계의 클래스 구조에서, 기본 클래스 포인터를 통해 파생 클래스 객체를 삭제할 때, 파생 클래스의 소멸자가 반드시 먼저 호출되도록 보장하는 언어적 장치입니다.
이는 기본 클래스의 소멸자 앞에 virtual
키워드를 명시함으로써, 실제 객체의 타입에 따라 런타임 시점에서 소멸자 호출 경로를 결정하게 하는 구조입니다.
❓ 2. 필요성 – 왜 부모 포인터로 자식 객체를 삭제하면 문제가 생기는가?
다음은 가장 흔한 구조의 C++ 코드입니다.
#include <iostream>
using namespace std;
class Animal {
public:
Animal() {
cout << "Animal created\n";
sharedData = new int[100]; // 약 400바이트
}
~Animal() {
delete[] sharedData;
cout << "Animal destroyed\n";
}
protected:
int* sharedData;
};
class Dog : public Animal {
public:
Dog() {
cout << "Dog created\n";
barkSound = new char[256]; // 약 256바이트
}
~Dog() {
delete[] barkSound;
cout << "Dog destroyed\n";
}
private:
char* barkSound;
};
int main() {
Animal* pet = new Dog();
delete pet;
return 0;
}
🖨 출력 결과
Animal created
Dog created
Animal destroyed
🔍 이 출력 결과가 말해주는 구조적 문제
Dog destroyed가 출력되지 않았다는 것은 ~Dog()
가 호출되지 않았다는 뜻입니다.
즉, 다음 코드가 실행되지 않았다는 의미입니다.
delete[] barkSound;
이로 인해 Dog 객체가 내부적으로 할당한 256바이트는 메모리에 남아 있게 됩니다. 한두 번이라면 문제가 되지 않겠지만, 수천 번 반복되면 실질적인 메모리 누수(memory leak)로 이어집니다.
그렇다면 왜 C++은 자식 소멸자 ~Dog()
를 호출하지 못했을까요?
🔧 3. 작동 원리 – 왜 C++은 자식 소멸자에 도달하지 못하는가?
🧩 3.1 delete pet은 어떻게 동작하는가?
Animal* pet = new Dog();
delete pet;
pet
은 Animal*
타입으로 선언되어 있습니다.
따라서 delete pet;
은 컴파일 타임에 다음과 같이 해석됩니다:
pet->~Animal(); // 고정된 함수 호출
이 함수 호출은 컴파일 타임에 주소 치환 방식으로 고정되며, 런타임 타입은 반영되지 않습니다.
📦 3.2 메모리 배치: Animal 포인터의 해석 범위는 어디까지인가?
Heap 메모리 (Dog 객체 전체)
[0x1000] ── sharedData 포인터 (0x5000) ← Animal 영역
[0x1008] ── barkSound 포인터 (0x6000) ← Dog 영역
pet
은0x1000
을 가리키며, 타입은Animal*
입니다.- 따라서 컴파일러는 0x1000 ~ 0x1007까지만 해석할 수 있습니다.
- 0x1008 이후는 Animal 클래스 정의에 존재하지 않으므로 해석 자체가 불가능합니다.
❌ 구조적으로 ~Dog()
를 호출할 수 없는 이유
C++ 컴파일러는call Animal::~Animal
형태로 호출 경로를 고정합니다.
Dog
의 소멸자 정보는 이 경로에 포함되지 않습니다.
- 컴파일러는 포인터 타입이 Animal*이라는 이유로, 해석 가능한 구조를 Animal까지만 고정합니다.
- 따라서 Dog의 소멸자 함수, 멤버 메모리 모두 접근 불가합니다.
📌 4. 정리: 문제의 본질은 “타입 해석 범위의 한계”
C++은 포인터 타입에 따라 해당 메모리를 해석합니다.
Animal*
타입은 Animal 클래스에 정의된 멤버까지만 접근할 수 있으며, 그 이후의 Dog
정보는 전혀 고려되지 않습니다.
결국 delete pet;
은 Animal 수준에서의 소멸만 수행하며, 그 이후에 위치한 Dog의 소멸자 호출, 리소스 정리 모두 생략됩니다.
🔑 4. 해결 – 객체 내부에 자식까지 도달 가능한 해석 경로를 명시적으로 삽입하는 방법
앞서 설명드린 문제의 핵심은 다음과 같습니다:
Animal*
포인터는 객체의 상단 구조인 Animal 클래스까지만 해석할 수 있으며,
그 아래 메모리 영역에 존재하는 Dog 클래스의 멤버나 소멸자에 도달할 수 있는 해석 경로 자체가 존재하지 않습니다.
이 구조적 한계를 해결하기 위해, C++은 다음과 같은 장치를 사용합니다: 바로 가상 소멸자(virtual destructor)입니다.
🧬 virtual 소멸자가 선언되면 객체 구조에 어떤 변화가 생기는가?
class Animal {
public:
virtual ~Animal() {
delete[] sharedData;
cout << "Animal destroyed\n";
}
};
이렇게 virtual을 붙이면, 컴파일러는 다음 두 가지를 자동으로 삽입합니다:
- 객체 내부에 vptr이라는 포인터가 추가됩니다. 이 포인터는 객체 생성 시점에 vtable의 주소를 가리키도록 초기화됩니다.
- 클래스별로 vtable이라는 별도의 함수 테이블이 생성됩니다. 이 테이블에는 자식 클래스에서 재정의한 함수가 포함됩니다.
📦 메모리 구조: new Dog();
호출 시 생성되는 구조
Heap 영역 (Dog 객체 전체)
[0x1000] ── vptr → 0x9000 ← virtual로 인해 추가됨
[0x1008] ── sharedData = 0x5000 ← Animal 멤버
[0x1010] ── barkSound = 0x6000 ← Dog 멤버
vtable at 0x9000:
[0x9000] = &Dog::~Dog()
[0x9008] = &Animal::~Animal()
🧠 이 구조가 주는 핵심적인 변화
가상 소멸자가 없는 구조에서는, pet
포인터를 통해 접근 가능한 영역은 다음과 같았습니다:
pet = 0x1000
접근 가능한 영역:
[0x1000] → sharedData (Animal 멤버) 까지
↓
[0x1010] → barkSound (Dog 멤버)에는 도달할 방법이 없음
하지만 이제는 구조가 바뀌었습니다:
- 객체 내부 맨 위에 있는 vptr(0x1000)이 vtable 주소(0x9000)를 가리킵니다.
- vtable은 현재 객체가 Dog라는 사실을 기반으로, Dog의 소멸자 주소를 가장 먼저 포함합니다.
- 이제 pet은 더 이상 Animal까지만 해석되는 포인터가 아닙니다. 객체 상단에 삽입된 vptr를 통해 해석 경로가 Dog까지 도달할 수 있도록 연결된 것입니다.
✅ 의미: 명시적으로 "자식 클래스도 있다"는 경로를 부여받은 포인터
- 이전에는 컴파일러와 delete pet 모두 "이건 Animal이다"라는 해석만 가능했습니다.
- 이제는 객체 내부에 "나는 Dog이다"를 간접적으로 알려주는 장치(vptr)가 생겼습니다.
- 이 vptr를 따라가면 → vtable → Dog 소멸자 주소 → 정확한 소멸자 호출이 가능합니다.
- 해석 범위가 Animal → Dog로 확장되었고, 그 범위 확장은
virtual
이라는 키워드가 만든 것입니다.
💡 요약 문장
가상 소멸자는 단순히 “함수를 virtual로 만든다”는 의미가 아닙니다.
그것은 C++ 객체 내부에 “이 객체가 실제로는 자식 클래스일 수 있다”는 구조적 단서를 심는 작업입니다.
이 단서(vptr
)는 정적 타입에 묶여 있는 포인터에게,
런타임에 해석 범위를 확장할 수 있는 길(vtable)을 제공합니다.
그 길이 생기면,delete pet;
같은 정적 표현도 정확하게 Dog 소멸자에 도달할 수 있게 됩니다.
📊 전체 구조 요약
🧾 정리: 가상 소멸자의 유무에 따른 객체 삭제 해석의 구조 비교
📌 핵심 요약
- C++에서
Animal*
포인터는 기본적으로 Animal 클래스까지만 해석 가능한 고정 범위 포인터입니다. - 가상 소멸자를 선언하지 않으면, 해석 경로가 Animal 내부까지만 허용되며,
Dog 내부에 존재하는 소멸자와 자원은 존재하지 않는 것처럼 무시됩니다. virtual
키워드는 단순한 문법이 아니라,
객체 내부에 vptr이라는 길을 삽입하여,
정적 포인터가 해석 범위를 런타임에 확장할 수 있도록 만든 구조적 장치입니다.
'프로그래밍 > 홍정모의 따라배우는 C++' 카테고리의 다른 글
추상 클래스 (Abstract Class)란 무엇인가? (0) | 2025.05.04 |
---|---|
순수 가상 함수 (Pure Virtual Function)란? (0) | 2025.05.04 |
가상함수란 무엇인가? 2. 해결편 (0) | 2025.04.29 |
가상함수란 무엇인가? 1. 문제편 (0) | 2025.04.29 |
바인딩과 동적 바인딩: 비전공자가 헷갈릴 모든 포인트 총정리 (0) | 2025.04.28 |
- Total
- Today
- Yesterday
- 오블완
- 보세사
- 류근관
- 심리학
- 일본어
- 뇌와행동의기초
- 사회심리학
- 일문따
- 회계
- 백준
- 파이썬
- 강화학습
- 티스토리챌린지
- C/C++
- C
- c++
- 정보처리기사
- stl
- 코딩테스트
- Python
- 여인권
- 데이터분석
- 열혈프로그래밍
- K-MOOC
- 조건형성
- 일본어문법무작정따라하기
- 인지부조화
- 윤성우
- 통계학
- 통계
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |