C++ 메모리 관리 기초 — 스택·힙·RAII
개요 — C++ 메모리 영역 비교
Section titled “개요 — C++ 메모리 영역 비교”C++에서 메모리는 크게 세 영역으로 나뉩니다. 어떤 영역을 사용하느냐에 따라 객체의 수명, 성능, 관리 방식이 완전히 달라집니다.
| 영역 | 할당 시점 | 해제 시점 | 크기 제한 | 관리 주체 |
|---|---|---|---|---|
| 스택(Stack) | 변수 선언 시 | 스코프 종료 시 자동 | 수 MB (OS 제한) | 컴파일러 |
| 힙(Heap) | new 호출 시 | delete 호출 시 | 수 GB (가용 메모리) | 개발자 |
| 정적/전역(Static) | 프로그램 시작 시 | 프로그램 종료 시 | 제한적 | 컴파일러 |
1. 스택 메모리
Section titled “1. 스택 메모리”1.1 스택의 동작 원리
Section titled “1.1 스택의 동작 원리”스택은 함수 호출 시 자동으로 할당되고, 함수가 리턴되는 순간 자동으로 해제됩니다. 개발자가 별도로 메모리를 관리할 필요가 없습니다.
#include <iostream>
void StackExample(){ int x = 10; // 스택에 4바이트 할당 double y = 3.14; // 스택에 8바이트 할당 char name[64] = {}; // 스택에 64바이트 할당
std::cout << x << ", " << y << "\n"; // 함수 종료 시 x, y, name 모두 자동 해제}
int main(){ StackExample(); // 호출 후 스택 자동 정리 return 0;}1.2 스택의 장단점
Section titled “1.2 스택의 장단점”장점
- 할당/해제가 매우 빠름 (스택 포인터 이동만으로 완료)
- 메모리 누수 없음 (자동 해제)
- 캐시 친화적 (연속 메모리)
단점
- 크기가 컴파일 타임에 결정되어야 함
- 스택 크기 초과 시 스택 오버플로우 발생
- 함수 리턴 후 데이터 참조 불가 (Dangling Pointer 위험)
// 위험한 패턴: 스택 주소를 리턴int* DangerousFunction(){ int localVar = 42; return &localVar; // 경고: 함수 종료 후 이 주소는 무효}
int main(){ int* ptr = DangerousFunction(); // ptr 역참조는 Undefined Behavior (크래시 가능성) std::cout << *ptr << "\n"; // 절대 하지 말 것 return 0;}2. 힙 메모리
Section titled “2. 힙 메모리”2.1 new / delete 기본 사용법
Section titled “2.1 new / delete 기본 사용법”힙은 런타임에 크기를 결정할 수 있고, 함수 범위를 넘어서 데이터를 유지할 수 있습니다. 대신 개발자가 직접 해제해야 합니다.
#include <iostream>
int main(){ // 단일 객체 할당 int* p = new int(42); std::cout << *p << "\n"; // 42 delete p; // 반드시 해제 p = nullptr; // 해제 후 nullptr 대입 (안전 습관)
// 배열 할당 int n = 10; int* arr = new int[n]; // 런타임 크기 결정 가능 for (int i = 0; i < n; ++i) { arr[i] = i * i; } delete[] arr; // 배열은 반드시 delete[] 사용 arr = nullptr;
return 0;}2.2 메모리 누수 원인
Section titled “2.2 메모리 누수 원인”new로 할당한 메모리를 delete하지 않으면 메모리 누수(Memory Leak)가 발생합니다. 프로그램이 종료될 때까지 해당 메모리를 다른 용도로 사용할 수 없습니다.
#include <stdexcept>
// 메모리 누수 예시 1: delete 누락void LeakExample1(){ int* p = new int(100); // delete p; 를 빠뜨림 → 메모리 누수}
// 메모리 누수 예시 2: 예외 발생 시 누수void LeakExample2(){ int* p = new int(100); // 아래 코드에서 예외가 발생하면 delete에 도달하지 못함 SomeFunctionThatMayThrow(); delete p; // 예외 발생 시 이 줄은 실행되지 않음}
// 메모리 누수 예시 3: 배열을 delete로 해제void LeakExample3(){ int* arr = new int[10]; delete arr; // 잘못됨: delete[] arr 이어야 함 // Undefined Behavior}3. RAII — 리소스 안전 관리 원칙
Section titled “3. RAII — 리소스 안전 관리 원칙”3.1 RAII란 무엇인가
Section titled “3.1 RAII란 무엇인가”RAII(Resource Acquisition Is Initialization)는 C++의 핵심 설계 원칙입니다.
리소스(메모리, 파일 핸들, 락 등)의 획득은 객체 초기화 시점에, 해제는 소멸자에서 자동으로 수행한다.
스택 객체는 스코프 종료 시 소멸자가 자동 호출된다는 C++ 보장을 활용합니다. 예외가 발생하더라도 스택 언와인딩(Stack Unwinding) 과정에서 소멸자가 반드시 실행됩니다.
// RAII 없는 방식 (위험)void BadResourceManagement(){ FILE* file = fopen("data.txt", "r");
if (SomeCondition()) { return; // 여기서 리턴하면 fclose 누락 }
if (ErrorCondition()) { throw std::runtime_error("Error"); // 예외 시 fclose 누락 }
fclose(file); // 모든 경로에서 도달한다는 보장 없음}3.2 RAII 래퍼 클래스 구현
Section titled “3.2 RAII 래퍼 클래스 구현”소멸자에서 리소스를 해제하도록 클래스를 설계하면, 스코프 종료·예외·조기 리턴 모든 상황에서 안전하게 해제됩니다.
#include <iostream>#include <stdexcept>#include <cstdio>
// 파일 핸들을 위한 RAII 래퍼class FileHandle{public: explicit FileHandle(const char* path, const char* mode) : m_File(fopen(path, mode)) { if (!m_File) { throw std::runtime_error("Failed to open file"); } std::cout << "FileHandle: opened\n"; }
// 소멸자에서 자동 해제 ~FileHandle() { if (m_File) { fclose(m_File); std::cout << "FileHandle: closed\n"; } }
// 복사 금지 (리소스 이중 해제 방지) FileHandle(const FileHandle&) = delete; FileHandle& operator=(const FileHandle&) = delete;
// 이동은 허용 FileHandle(FileHandle&& other) noexcept : m_File(other.m_File) { other.m_File = nullptr; }
FILE* Get() const { return m_File; }
private: FILE* m_File;};
void GoodResourceManagement(){ FileHandle file("data.txt", "r"); // 생성자에서 획득
if (SomeCondition()) { return; // 소멸자가 자동 호출 → fclose 보장 }
// 예외 발생 시에도 소멸자 호출 → fclose 보장 ProcessFile(file.Get()); // 스코프 종료 시 자동으로 fclose 호출}3.3 힙 메모리를 위한 RAII 래퍼
Section titled “3.3 힙 메모리를 위한 RAII 래퍼”#include <iostream>#include <utility>
// 단일 객체 소유권을 관리하는 RAII 클래스 (std::unique_ptr의 단순 버전)template<typename T>class ScopedPtr{public: explicit ScopedPtr(T* ptr = nullptr) : m_Ptr(ptr) {}
~ScopedPtr() { delete m_Ptr; }
// 복사 금지 ScopedPtr(const ScopedPtr&) = delete; ScopedPtr& operator=(const ScopedPtr&) = delete;
// 이동 허용 ScopedPtr(ScopedPtr&& other) noexcept : m_Ptr(other.m_Ptr) { other.m_Ptr = nullptr; }
ScopedPtr& operator=(ScopedPtr&& other) noexcept { if (this != &other) { delete m_Ptr; m_Ptr = other.m_Ptr; other.m_Ptr = nullptr; } return *this; }
T* operator->() const { return m_Ptr; } T& operator*() const { return *m_Ptr; } T* Get() const { return m_Ptr; } explicit operator bool() const { return m_Ptr != nullptr; }
private: T* m_Ptr;};
struct Player{ std::string Name; int Health = 100;
Player(const std::string& name) : Name(name) { std::cout << "Player created: " << Name << "\n"; } ~Player() { std::cout << "Player destroyed: " << Name << "\n"; }};
int main(){ { ScopedPtr<Player> player(new Player("Alice")); std::cout << player->Name << " HP: " << player->Health << "\n"; // 스코프 종료 시 ~ScopedPtr() 호출 → delete player } // 출력: "Player destroyed: Alice" return 0;}4. 소멸자와 리소스 해제 패턴
Section titled “4. 소멸자와 리소스 해제 패턴”4.1 소멸자 작성 규칙
Section titled “4.1 소멸자 작성 규칙”class ResourceManager{public: ResourceManager() : m_Buffer(new char[1024]) , m_File(fopen("log.txt", "w")) { }
// 소멸자는 virtual로 선언 (상속 시 안전한 삭제를 위해) virtual ~ResourceManager() { // 역순으로 해제 (생성 역순) if (m_File) { fclose(m_File); m_File = nullptr; }
delete[] m_Buffer; m_Buffer = nullptr; }
// 복사와 대입도 올바르게 처리 (Rule of Three/Five) ResourceManager(const ResourceManager&) = delete; ResourceManager& operator=(const ResourceManager&) = delete;
private: char* m_Buffer; FILE* m_File;};4.2 Rule of Five
Section titled “4.2 Rule of Five”소멸자를 정의했다면 복사 생성자, 복사 대입, 이동 생성자, 이동 대입도 명시적으로 처리해야 합니다.
class Buffer{public: // 1. 생성자 explicit Buffer(size_t size) : m_Size(size) , m_Data(new int[size]) { }
// 2. 소멸자 ~Buffer() { delete[] m_Data; }
// 3. 복사 생성자 (깊은 복사) Buffer(const Buffer& other) : m_Size(other.m_Size) , m_Data(new int[other.m_Size]) { std::copy(other.m_Data, other.m_Data + other.m_Size, m_Data); }
// 4. 복사 대입 연산자 Buffer& operator=(const Buffer& other) { if (this == &other) return *this; delete[] m_Data; m_Size = other.m_Size; m_Data = new int[m_Size]; std::copy(other.m_Data, other.m_Data + m_Size, m_Data); return *this; }
// 5. 이동 생성자 Buffer(Buffer&& other) noexcept : m_Size(other.m_Size) , m_Data(other.m_Data) { other.m_Size = 0; other.m_Data = nullptr; }
// 6. 이동 대입 연산자 Buffer& operator=(Buffer&& other) noexcept { if (this == &other) return *this; delete[] m_Data; m_Size = other.m_Size; m_Data = other.m_Data; other.m_Size = 0; other.m_Data = nullptr; return *this; }
size_t Size() const { return m_Size; } int* Data() { return m_Data; }
private: size_t m_Size; int* m_Data;};5. 실전 패턴 — RAII로 리소스 묶음 관리
Section titled “5. 실전 패턴 — RAII로 리소스 묶음 관리”#include <iostream>#include <mutex>#include <fstream>
// 뮤텍스 락을 RAII로 관리하는 예시 (std::lock_guard의 단순 버전)class LockGuard{public: explicit LockGuard(std::mutex& m) : m_Mutex(m) { m_Mutex.lock(); }
~LockGuard() { m_Mutex.unlock(); }
LockGuard(const LockGuard&) = delete; LockGuard& operator=(const LockGuard&) = delete;
private: std::mutex& m_Mutex;};
// 여러 리소스를 묶어서 RAII로 관리하는 게임 세션 클래스class GameSession{public: GameSession(const std::string& playerName, const std::string& logPath) : m_PlayerName(playerName) , m_LogFile(logPath) , m_Score(0) { if (!m_LogFile.is_open()) { throw std::runtime_error("Cannot open log file"); } m_LogFile << "Session started for: " << m_PlayerName << "\n"; std::cout << "GameSession created\n"; }
~GameSession() { m_LogFile << "Session ended. Final score: " << m_Score << "\n"; // m_LogFile (std::ofstream)은 자동으로 close() 호출 std::cout << "GameSession destroyed\n"; }
void AddScore(int points) { LockGuard guard(m_ScoreMutex); // 스코프 종료 시 자동 해제 m_Score += points; m_LogFile << "Score added: " << points << ", total: " << m_Score << "\n"; }
GameSession(const GameSession&) = delete; GameSession& operator=(const GameSession&) = delete;
private: std::string m_PlayerName; std::ofstream m_LogFile; int m_Score; std::mutex m_ScoreMutex;};
int main(){ try { GameSession session("Alice", "game_log.txt"); session.AddScore(100); session.AddScore(250); // 스코프 종료 시 ~GameSession() 자동 호출 → 파일 닫기, 최종 점수 기록 } catch (const std::exception& e) { std::cerr << "Error: " << e.what() << "\n"; }
return 0;}| 개념 | 핵심 요약 |
|---|---|
| 스택 | 자동 할당/해제, 빠름, 크기 제한 있음, 스코프 종료 시 소멸 |
| 힙 | 수동 new/delete, 유연한 크기, 수명 직접 제어, 누수 위험 |
| RAII | 소멸자에서 해제 보장 → 예외·조기 리턴에도 안전 |
| Rule of Five | 소멸자 정의 시 복사/이동 생성자·대입 연산자도 명시 처리 |
- 스마트 포인터:
std::unique_ptr,std::shared_ptr로 RAII를 직접 구현하지 않고 활용 - 이동 시맨틱: 불필요한 복사를 피해 힙 객체를 효율적으로 전달
- STL 컨테이너:
std::vector등 내부적으로 RAII를 적용한 표준 라이브러리