C++ 가상 소멸자 | "메모리 누수" 상속 클래스 소멸자 에러 해결
이 글의 핵심
C++ 가상 소멸자에 대한 실전 가이드입니다.
들어가며: “파생 클래스를 삭제했는데 메모리 누수가 생겼어요"
"베이스 클래스 포인터로 delete 했더니 소멸자가 안 불려요”
C++에서 베이스 클래스 포인터로 파생 클래스를 삭제할 때, 가상 소멸자가 없으면 파생 클래스의 소멸자가 호출되지 않아 메모리 누수가 발생합니다.
// ❌ 가상 소멸자 없음
class Base {
public:
~Base() { // 비가상 소멸자
std::cout << "~Base\n";
}
};
class Derived : public Base {
int* data_;
public:
Derived() : data_(new int[1000]) {}
~Derived() {
delete[] data_; // 호출 안 됨!
std::cout << "~Derived\n";
}
};
int main() {
Base* ptr = new Derived();
delete ptr; // ❌ ~Derived 호출 안 됨 → 메모리 누수
// 출력: ~Base
}
이 글에서 다루는 것:
- 가상 소멸자가 필요한 이유
- 메모리 누수와 미정의 동작
- 순수 가상 소멸자
- protected 소멸자
목차
1. 가상 소멸자가 필요한 이유
문제: 비가상 소멸자
// ❌ 비가상 소멸자
class Base {
public:
~Base() {
std::cout << "~Base\n";
}
};
class Derived : public Base {
public:
~Derived() {
std::cout << "~Derived\n";
}
};
int main() {
Base* ptr = new Derived();
delete ptr; // ❌ 미정의 동작
// 출력: ~Base (파생 클래스 소멸자 호출 안 됨)
}
해결: 가상 소멸자
// ✅ 가상 소멸자
class Base {
public:
virtual ~Base() {
std::cout << "~Base\n";
}
};
class Derived : public Base {
public:
~Derived() override {
std::cout << "~Derived\n";
}
};
int main() {
Base* ptr = new Derived();
delete ptr; // ✅ 올바른 소멸자 호출
// 출력:
// ~Derived
// ~Base
}
2. 메모리 누수 예시
예시 1: 동적 할당 메모리
// ❌ 메모리 누수
class Base {
public:
~Base() {}
};
class Derived : public Base {
int* data_;
public:
Derived() : data_(new int[1000000]) {
std::cout << "Allocated 4MB\n";
}
~Derived() {
delete[] data_; // 호출 안 됨!
std::cout << "Freed 4MB\n";
}
};
int main() {
for (int i = 0; i < 100; ++i) {
Base* ptr = new Derived();
delete ptr; // ❌ 4MB 누수 × 100 = 400MB 누수
}
}
예시 2: 파일 핸들
// ❌ 파일 핸들 누수
class Base {
public:
~Base() {}
};
class FileLogger : public Base {
std::ofstream file_;
public:
FileLogger(const std::string& path) : file_(path) {}
~FileLogger() {
file_.close(); // 호출 안 됨!
std::cout << "File closed\n";
}
};
int main() {
Base* ptr = new FileLogger("log.txt");
delete ptr; // ❌ 파일 핸들 누수
}
3. 순수 가상 소멸자
순수 가상 소멸자
순수 가상 소멸자는 클래스를 추상 클래스로 만들지만, 반드시 정의를 제공해야 합니다.
// 순수 가상 소멸자
class Base {
public:
virtual ~Base() = 0; // 순수 가상
};
// 정의 필수
Base::~Base() {
std::cout << "~Base\n";
}
class Derived : public Base {
public:
~Derived() override {
std::cout << "~Derived\n";
}
};
int main() {
// Base b; // 컴파일 에러: 추상 클래스
Base* ptr = new Derived();
delete ptr; // OK
}
사용 시기: 다른 순수 가상 함수 없이 추상 클래스를 만들고 싶을 때.
4. protected 소멸자
protected 소멸자
protected 소멸자는 베이스 클래스 포인터로 삭제를 방지합니다.
// protected 소멸자
class Base {
protected:
~Base() { // protected (비가상)
std::cout << "~Base\n";
}
};
class Derived : public Base {
public:
~Derived() {
std::cout << "~Derived\n";
}
};
int main() {
Base* ptr = new Derived();
// delete ptr; // 컴파일 에러: ~Base is protected
Derived* ptr2 = new Derived();
delete ptr2; // OK
}
장점:
- vtable 오버헤드 없음
- 잘못된 삭제 방지
단점:
- 다형성 삭제 불가
5. 성능 오버헤드
메모리 오버헤드
class NonVirtual {
int x;
};
class Virtual {
int x;
virtual ~Virtual() {}
};
std::cout << sizeof(NonVirtual) << '\n'; // 4
std::cout << sizeof(Virtual) << '\n'; // 16 (vtable 포인터 8 + int 4 + 패딩 4)
호출 오버헤드
// 비가상: 직접 호출
delete ptr; // ~Derived() 직접 호출
// 가상: 간접 호출
delete ptr; // vtable을 통한 간접 호출 (약간 느림)
결론: 오버헤드는 미미하며, 안전성이 훨씬 중요합니다.
정리
가상 소멸자 규칙
| 상황 | 소멸자 | 이유 |
|---|---|---|
| 상속 베이스 | virtual | 다형성 삭제 |
| 추상 클래스 | = 0 | 인스턴스화 방지 |
| 삭제 방지 | protected | vtable 없음 |
| 일반 클래스 | 비가상 | 오버헤드 없음 |
핵심 규칙
- 상속 베이스 클래스는 가상 소멸자
- 순수 가상 소멸자는 정의 필수
- protected 소멸자로 삭제 방지
- 일반 클래스는 비가상
체크리스트
- 상속 베이스 클래스에 가상 소멸자가 있는가?
- 순수 가상 소멸자에 정의를 제공했는가?
- 다형성 삭제가 필요한가?
- 성능이 중요한 클래스인가?
같이 보면 좋은 글 (내부 링크)
이 주제와 연결되는 다른 글입니다.
- C++ 상속 | Inheritance 완벽 가이드
- C++ 가상 함수 | virtual function 가이드
- C++ Rule of Five | 특수 멤버 함수
- C++ 다형성 | Polymorphism 가이드
마치며
가상 소멸자는 상속 클래스의 메모리 누수를 방지하는 핵심 기능입니다.
핵심 원칙:
- 상속 베이스 클래스는 가상 소멸자
- 순수 가상 소멸자는 정의 필수
- protected 소멸자로 삭제 방지
베이스 클래스 포인터로 삭제할 가능성이 있다면 반드시 가상 소멸자를 사용하세요.
다음 단계: 가상 소멸자를 이해했다면, C++ Rule of Five에서 특수 멤버 함수를 배워보세요.
관련 글
- C++ 상속과 다형성 |
- C++ 슬라이싱 문제 |