Topic 3: 소멸자와 자원 관리 🗑️
🎯 학습 목표
- 소멸자의 역할과 호출 시점을 이해할 수 있다
- 메모리 누수를 방지하는 방법을 알 수 있다
- RAII 패턴을 이해하고 활용할 수 있다
- 자원 관리의 중요성을 설명할 수 있다
🏠 이사 나가기로 이해하는 소멸자
소멸자는 집에서 이사 나갈 때 하는 일과 같습니다!
이사 나갈 때 꼭 해야 할 일들 📦
- 🔌 전기/가스 끄기: 열려있는 파일 닫기
- 🗑️ 쓰레기 버리기: 동적 메모리 해제
- 🔑 열쇠 반납: 시스템 자원 반환
- 🧹 청소하기: 임시 데이터 정리
class Tenant {
public:
~Tenant() {
turnOffElectricity(); // 전기 끄기
throwAwayTrash(); // 쓰레기 처리
returnKeys(); // 열쇠 반납
cout << "깔끔하게 이사 완료! 👋" << endl;
}
};💭 소멸자란?
**소멸자(Destructor)**는 객체가 소멸될 때 자동으로 호출되는 특별한 함수입니다!
소멸자의 특징 📝
class MyClass {
public:
// 소멸자의 특징
~MyClass() { // 1. 물결표(~) + 클래스명
// 2. 매개변수 없음
// 3. 반환 타입 없음
// 4. 오버로딩 불가 (하나만!)
cout << "객체가 소멸됩니다!" << endl;
}
};🚨 메모리 누수의 위험
메모리 누수 시연
#include <iostream>
using namespace std;
// 문제가 있는 클래스 - 메모리 누수 발생! ❌
class LeakyClass {
private:
int* data;
int size;
public:
LeakyClass(int s) : size(s) {
data = new int[size];
cout << "📦 " << size << "개 int 메모리 할당" << endl;
}
// 소멸자가 없음! - 메모리 누수!
};
// 올바른 클래스 - 메모리 누수 방지! ✅
class SafeClass {
private:
int* data;
int size;
public:
SafeClass(int s) : size(s) {
data = new int[size];
cout << "📦 " << size << "개 int 메모리 할당" << endl;
}
~SafeClass() {
delete[] data; // 메모리 해제!
cout << "🗑️ 메모리 해제 완료" << endl;
}
};
void demonstrateMemoryLeak() {
cout << "=== 메모리 누수 테스트 ===" << endl;
for (int i = 0; i < 3; i++) {
LeakyClass leak(1000); // 메모리 누수!
// 함수 끝나면 data가 가리키던 메모리는 그대로 남음
}
cout << "\n=== 안전한 메모리 관리 ===" << endl;
for (int i = 0; i < 3; i++) {
SafeClass safe(1000); // 자동으로 정리됨!
// 소멸자가 메모리를 해제
}
}📁 실습: 파일 관리 클래스
#include <iostream>
#include <fstream>
#include <string>
using namespace std;
class FileHandler {
private:
string filename;
ofstream* file;
bool isOpen;
int writeCount;
public:
// 생성자 - 파일 열기
FileHandler(const string& fname) : filename(fname), writeCount(0) {
file = new ofstream(filename);
if (file->is_open()) {
isOpen = true;
cout << "📂 파일 열기: " << filename << endl;
*file << "=== 로그 시작 ===" << endl;
} else {
isOpen = false;
cout << "❌ 파일 열기 실패: " << filename << endl;
}
}
// 소멸자 - 자동으로 파일 닫기
~FileHandler() {
if (isOpen && file) {
*file << "=== 로그 종료 ===" << endl;
*file << "총 " << writeCount << "개 항목 기록됨" << endl;
file->close();
cout << "📁 파일 닫기: " << filename << endl;
cout << " ✅ " << writeCount << "개 항목 저장 완료" << endl;
}
delete file;
}
// 파일에 쓰기
void writeLine(const string& text) {
if (isOpen && file) {
*file << "[" << ++writeCount << "] " << text << endl;
cout << "✏️ 기록: " << text << endl;
}
}
// 중요 로그 쓰기
void writeImportant(const string& text) {
if (isOpen && file) {
*file << "[" << ++writeCount << "] ⭐ 중요: " << text << endl;
cout << "⭐ 중요 기록: " << text << endl;
}
}
};
// 자동 자원 관리 테스트
void testFileHandler() {
cout << "=== 파일 핸들러 테스트 ===" << endl;
{ // 스코프 시작
FileHandler log("game_log.txt");
log.writeLine("게임 시작");
log.writeLine("플레이어 로그인");
log.writeImportant("보스 처치!");
log.writeLine("아이템 획득");
// 스코프 끝 - 자동으로 파일 닫힘!
}
cout << "\n파일이 자동으로 닫혔습니다!" << endl;
}🛡️ RAII 패턴 (Resource Acquisition Is Initialization)
RAII는 C++의 핵심 패턴으로, 자원 획득은 초기화라는 의미입니다!
RAII의 원칙 📋
- 생성자에서 자원 획득
- 소멸자에서 자원 해제
- 예외 안전성 보장
#include <iostream>
#include <memory>
using namespace std;
// RAII를 적용한 스마트 배열 클래스
template<typename T>
class SmartArray {
private:
T* data;
size_t size;
public:
// 생성자 - 자원 획득
SmartArray(size_t s) : size(s) {
data = new T[size];
cout << "🔨 SmartArray 생성 (크기: " << size << ")" << endl;
}
// 소멸자 - 자원 해제
~SmartArray() {
delete[] data;
cout << "🗑️ SmartArray 소멸 (메모리 자동 해제)" << endl;
}
// 복사 생성자 (깊은 복사)
SmartArray(const SmartArray& other) : size(other.size) {
data = new T[size];
for (size_t i = 0; i < size; i++) {
data[i] = other.data[i];
}
cout << "📋 SmartArray 복사" << endl;
}
// 인덱스 연산자
T& operator[](size_t index) {
if (index >= size) {
throw out_of_range("인덱스 범위 초과!");
}
return data[index];
}
size_t getSize() const { return size; }
};
void testSmartArray() {
cout << "=== SmartArray 테스트 ===" << endl;
try {
SmartArray<int> arr(5);
// 배열 사용
for (int i = 0; i < 5; i++) {
arr[i] = i * 10;
}
// 출력
cout << "배열 내용: ";
for (int i = 0; i < 5; i++) {
cout << arr[i] << " ";
}
cout << endl;
// 예외 발생 시에도 자동으로 메모리 해제!
// arr[10] = 100; // 예외 발생
} catch (const exception& e) {
cout << "예외 발생: " << e.what() << endl;
}
cout << "함수 종료 - 메모리 자동 정리!" << endl;
}🎮 실전 예제: 게임 리소스 매니저
#include <iostream>
#include <vector>
#include <string>
using namespace std;
// 게임 리소스 타입
enum ResourceType {
TEXTURE,
SOUND,
MODEL
};
// 리소스 클래스
class GameResource {
private:
string name;
ResourceType type;
void* data;
size_t size;
public:
GameResource(const string& n, ResourceType t, size_t s)
: name(n), type(t), size(s) {
data = malloc(size); // 실제로는 파일 로드 등
string typeStr = (type == TEXTURE) ? "텍스처" :
(type == SOUND) ? "사운드" : "3D 모델";
cout << "📦 " << typeStr << " 로드: " << name
<< " (" << size << " bytes)" << endl;
}
~GameResource() {
free(data);
string typeStr = (type == TEXTURE) ? "텍스처" :
(type == SOUND) ? "사운드" : "3D 모델";
cout << "🗑️ " << typeStr << " 해제: " << name << endl;
}
string getName() const { return name; }
size_t getSize() const { return size; }
};
// 리소스 매니저 클래스
class ResourceManager {
private:
vector<GameResource*> resources;
size_t totalMemory;
size_t maxMemory;
public:
ResourceManager(size_t maxMem = 1000000)
: totalMemory(0), maxMemory(maxMem) {
cout << "🎮 리소스 매니저 초기화" << endl;
cout << " 최대 메모리: " << maxMemory << " bytes" << endl;
}
~ResourceManager() {
cout << "\n🔄 리소스 매니저 종료 중..." << endl;
// 모든 리소스 자동 해제
for (auto* resource : resources) {
delete resource;
}
resources.clear();
cout << "✅ 모든 리소스 정리 완료!" << endl;
}
bool loadResource(const string& name, ResourceType type, size_t size) {
if (totalMemory + size > maxMemory) {
cout << "❌ 메모리 부족! (" << name << ")" << endl;
return false;
}
GameResource* resource = new GameResource(name, type, size);
resources.push_back(resource);
totalMemory += size;
cout << " 메모리 사용량: " << totalMemory << "/"
<< maxMemory << " ("
<< (totalMemory * 100 / maxMemory) << "%)" << endl;
return true;
}
void showStatus() const {
cout << "\n=== 리소스 상태 ===" << endl;
cout << "로드된 리소스: " << resources.size() << "개" << endl;
cout << "메모리 사용: " << totalMemory << "/" << maxMemory
<< " bytes" << endl;
if (!resources.empty()) {
cout << "\n리소스 목록:" << endl;
for (const auto* res : resources) {
cout << " • " << res->getName()
<< " (" << res->getSize() << " bytes)" << endl;
}
}
}
};
int main() {
cout << "=== 게임 리소스 관리 시스템 ===" << endl;
{ // 스코프 시작
ResourceManager manager(500000); // 500KB 제한
// 리소스 로드
manager.loadResource("player.png", TEXTURE, 50000);
manager.loadResource("enemy.png", TEXTURE, 45000);
manager.loadResource("background.jpg", TEXTURE, 120000);
manager.loadResource("shoot.wav", SOUND, 30000);
manager.loadResource("bgm.mp3", SOUND, 180000);
manager.loadResource("sword.obj", MODEL, 65000);
// 메모리 초과 테스트
manager.loadResource("huge_texture.png", TEXTURE, 100000);
manager.showStatus();
cout << "\n게임 종료 중..." << endl;
} // 스코프 끝 - 모든 리소스 자동 해제!
cout << "\n✅ 프로그램 종료 - 메모리 누수 없음!" << endl;
return 0;
}📊 소멸자 호출 순서
class Component {
string name;
public:
Component(string n) : name(n) {
cout << " 📦 Component 생성: " << name << endl;
}
~Component() {
cout << " 🗑️ Component 소멸: " << name << endl;
}
};
class GameObject {
string name;
Component comp1;
Component comp2;
public:
GameObject(string n)
: name(n), comp1("그래픽"), comp2("물리") {
cout << "🎮 GameObject 생성: " << name << endl;
}
~GameObject() {
cout << "🎮 GameObject 소멸: " << name << endl;
// 멤버 변수들은 역순으로 소멸됨!
}
};💡 핵심 정리
- 소멸자: 객체 소멸 시 자동 호출되는 정리 함수
- 메모리 누수: 동적 할당된 메모리를 해제하지 않는 문제
- RAII: 생성자에서 획득, 소멸자에서 해제
- 자동 자원 관리: 스코프를 벗어나면 자동으로 정리
- 호출 순서: 생성의 역순으로 소멸
✅ 실습 체크리스트
🚀 다음 시간 예고
다음 시간에는 이동 생성자와 이동 대입에 대해 알아볼 거예요!
- 이동 의미론 (Move Semantics)
- rvalue 참조
- 성능 최적화
“소멸자로 깔끔한 자원 관리를 실현하세요! 🧹”
Last updated on