[2026] C++ 가상 소멸자 | 메모리 누수 상속 클래스 소멸자 에러 해결
이 글의 핵심
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 소멸자
실무에서 마주한 현실
개발을 배울 때는 모든 게 깔끔하고 이론적입니다. 하지만 실무는 다릅니다. 레거시 코드와 씨름하고, 급한 일정에 쫓기고, 예상치 못한 버그와 마주합니다. 이 글에서 다루는 내용도 처음엔 이론으로 배웠지만, 실제 프로젝트에 적용하면서 “아, 이래서 이렇게 설계하는구나” 하고 깨달은 것들입니다. 특히 기억에 남는 건 첫 프로젝트에서 겪은 시행착오입니다. 책에서 배운 대로 했는데 왜 안 되는지 몰라 며칠을 헤맸죠. 결국 선배 개발자의 코드 리뷰를 통해 문제를 발견했고, 그 과정에서 많은 걸 배웠습니다. 이 글에서는 이론뿐 아니라 실전에서 마주칠 수 있는 함정들과 해결 방법을 함께 다루겠습니다. 일상 비유로 이해하기: 메모리를 아파트 건물로 생각해보세요. 스택은 엘리베이터 같아서 빠르지만 공간이 제한적입니다. 힙은 창고처럼 넓지만 물건을 찾는 데 시간이 걸립니다. 포인터는 “3층 302호”처럼 주소를 가리키는 메모지라고 보면 됩니다.
목차
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: 동적 할당 메모리
다음은 cpp를 활용한 상세한 구현 코드입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며, 반복문으로 데이터를 처리합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// ❌ 메모리 누수
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: 파일 핸들
다음은 cpp를 활용한 상세한 구현 코드입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// ❌ 파일 핸들 누수
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. 순수 가상 소멸자
순수 가상 소멸자
순수 가상 소멸자는 클래스를 추상 클래스로 만들지만, 반드시 정의를 제공해야 합니다. 다음은 cpp를 활용한 상세한 구현 코드입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// 순수 가상 소멸자
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 소멸자는 베이스 클래스 포인터로 삭제를 방지합니다. 다음은 cpp를 활용한 상세한 구현 코드입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// 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. 성능 오버헤드
메모리 오버헤드
아래 코드는 cpp를 사용한 구현 예제입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
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)
호출 오버헤드
아래 코드는 cpp를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
// 비가상: 직접 호출
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에서 특수 멤버 함수를 배워보세요.