[2026] C++ 메모리 관리 | new/delete/RAII 완벽 정리
이 글의 핵심
C++ 메모리 관리의 스택 vs 힙, 동적 할당 (new/delete), 실전 예시를 실전 코드와 함께 설명합니다. 실무에서 자주 사용되는 패턴과 주의사항을 다룹니다.
스택 vs 힙
스택 메모리
아래 코드는 cpp를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
// 실행 예제
void func() {
int x = 10; // 스택에 할당
int arr[100]; // 스택에 할당
} // 자동으로 해제
특징:
- 빠름
- 크기 제한 있음 (보통 1-8MB)
- 자동 관리
힙 메모리
아래 코드는 cpp를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
void func() {
int* ptr = new int(10); // 힙에 할당
// ....사용 ...
delete ptr; // 수동 해제 필요
}
특징:
- 느림
- 크기 제한 거의 없음
- 수동 관리 필요
동적 할당 (new/delete)
단일 객체
아래 코드는 cpp를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
// 할당
int* ptr = new int; // 초기화 안됨
int* ptr2 = new int(10); // 10으로 초기화
int* ptr3 = new int{10}; // C++11
// 해제
delete ptr;
delete ptr2;
delete ptr3;
배열
아래 코드는 cpp를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
// 할당
int* arr = new int[100];
// 사용
arr[0] = 1;
arr[99] = 100;
// 해제
delete[] arr; // []를 꼭 붙여야 함!
클래스 객체
다음은 cpp를 활용한 상세한 구현 코드입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
class Person {
public:
string name;
Person(string n) : name(n) {
cout << name << " 생성" << endl;
}
~Person() {
cout << name << " 소멸" << endl;
}
};
int main() {
Person* p = new Person("Alice");
p->name = "Bob";
delete p; // 소멸자 호출됨
}
RAII (Resource Acquisition Is Initialization)
기본 개념
다음은 cpp를 활용한 상세한 구현 코드입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며, 비동기 처리를 통해 효율적으로 작업을 수행합니다, 에러 처리를 통해 안정성을 확보합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
class FileHandler {
private:
FILE* file;
public:
FileHandler(const char* filename) {
file = fopen(filename, "w");
if (!file) {
throw runtime_error("파일 열기 실패");
}
cout << "파일 열림" << endl;
}
~FileHandler() {
if (file) {
fclose(file);
cout << "파일 닫힘" << endl;
}
}
void write(const char* data) {
fprintf(file, "%s\n", data);
}
};
int main() {
try {
FileHandler fh("output.txt");
fh.write("Hello");
// 예외 발생해도 소멸자가 호출되어 파일 닫힘
} catch (exception& e) {
cerr << e.what() << endl;
}
}
실전 예시
예시 1: 메모리 누수 방지
다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
#include <iostream>
#include <memory>
using namespace std;
// ❌ 메모리 누수 위험
void badExample() {
int* data = new int[1000];
if (someCondition) {
return; // delete 안됨! 누수!
}
delete[] data;
}
// ✅ RAII로 안전하게
void goodExample() {
unique_ptr<int[]> data = make_unique<int[]>(1000);
if (someCondition) {
return; // 자동으로 해제됨!
}
// 자동 해제
}
int main() {
goodExample();
return 0;
}
설명: 스마트 포인터를 사용하면 예외나 early return 시에도 메모리가 안전하게 해제됩니다.
예시 2: 커스텀 메모리 풀
다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 클래스를 정의하여 데이터와 기능을 캡슐화하며, 반복문으로 데이터를 처리합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
#include <iostream>
#include <vector>
using namespace std;
template <typename T>
class MemoryPool {
private:
vector<T*> pool;
size_t nextIndex;
public:
MemoryPool(size_t size) : nextIndex(0) {
pool.reserve(size);
for (size_t i = 0; i < size; i++) {
pool.push_back(new T());
}
cout << size << "개 객체 미리 할당" << endl;
}
~MemoryPool() {
for (T* obj : pool) {
delete obj;
}
cout << "메모리 풀 해제" << endl;
}
T* acquire() {
if (nextIndex < pool.size()) {
return pool[nextIndex++];
}
return nullptr;
}
void reset() {
nextIndex = 0;
}
};
int main() {
MemoryPool<int> pool(100);
int* p1 = pool.acquire();
int* p2 = pool.acquire();
*p1 = 10;
*p2 = 20;
pool.reset(); // 재사용 가능
return 0;
}
설명: 메모리 풀을 사용하면 반복적인 할당/해제 비용을 줄일 수 있습니다.
예시 3: placement new
다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 클래스를 정의하여 데이터와 기능을 캡슐화하며. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
#include <iostream>
#include <new>
using namespace std;
class Object {
public:
int value;
Object(int v) : value(v) {
cout << "Object(" << value << ") 생성" << endl;
}
~Object() {
cout << "Object(" << value << ") 소멸" << endl;
}
};
int main() {
// 메모리 미리 할당
char buffer[sizeof(Object) * 3];
// placement new로 객체 생성
Object* obj1 = new (&buffer[0]) Object(1);
Object* obj2 = new (&buffer[sizeof(Object)]) Object(2);
cout << obj1->value << ", " << obj2->value << endl;
// 명시적 소멸자 호출
obj1->~Object();
obj2->~Object();
// buffer는 자동 해제 (스택)
return 0;
}
설명: placement new는 이미 할당된 메모리에 객체를 생성할 때 사용합니다.
자주 발생하는 문제
문제 1: double delete
증상: 프로그램 크래시 원인: 같은 포인터를 두 번 delete 해결법: 아래 코드는 cpp를 사용한 구현 예제입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// ❌ double delete
int* ptr = new int(10);
delete ptr;
delete ptr; // 크래시!
// ✅ delete 후 nullptr
int* ptr = new int(10);
delete ptr;
ptr = nullptr;
delete ptr; // 안전 (nullptr delete는 무시됨)
문제 2: delete vs delete[]
증상: 메모리 누수 또는 크래시 원인: 배열을 delete로 해제 해결법: 아래 코드는 cpp를 사용한 구현 예제입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// ❌ 잘못된 해제
int* arr = new int[10];
delete arr; // 잘못됨! 첫 요소만 해제
// ✅ 올바른 해제
int* arr = new int[10];
delete[] arr; // 모든 요소 해제
// ✅ 단일 객체
int* ptr = new int(10);
delete ptr; // OK
문제 3: 댕글링 포인터
증상: 이미 해제된 메모리 접근 원인: delete 후 포인터 사용 해결법: 다음은 cpp를 활용한 상세한 구현 코드입니다. 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// ❌ 댕글링 포인터
int* ptr = new int(10);
delete ptr;
cout << *ptr << endl; // 위험! 이미 해제됨
// ✅ nullptr 체크
int* ptr = new int(10);
delete ptr;
ptr = nullptr;
if (ptr) {
cout << *ptr << endl;
} else {
cout << "포인터가 nullptr" << endl;
}
FAQ
Q1: 언제 스택을 쓰고 언제 힙을 쓰나요?
A:
- 스택: 크기가 작고 수명이 짧은 데이터
- 힙: 크기가 크거나 수명이 긴 데이터, 동적 크기
Q2: new가 실패하면?
A: bad_alloc 예외가 발생합니다. 아래 코드는 cpp를 사용한 구현 예제입니다. 비동기 처리를 통해 효율적으로 작업을 수행합니다, 에러 처리를 통해 안정성을 확보합니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
try {
int* huge = new int[1000000000000];
} catch (bad_alloc& e) {
cout << "메모리 할당 실패: " << e.what() << endl;
}
Q3: malloc vs new 차이는?
A:
- malloc: C 스타일, 생성자 호출 안함
- new: C++ 스타일, 생성자 호출, 타입 안전
Q4: RAII는 왜 중요한가요?
A: 예외 안전성을 보장하고 리소스 누수를 방지합니다. 모던 C++의 핵심 개념입니다.
Q5: 메모리 누수를 찾으려면?
A:
- Valgrind (Linux)
- Visual Studio 메모리 프로파일러
- AddressSanitizer (컴파일러 옵션)
Q6: 스마트 포인터를 항상 써야 하나요?
A: 네, 가능하면 항상 스마트 포인터를 사용하세요. raw 포인터는 저수준 작업이나 레거시 코드에서만 사용합니다.
같이 보면 좋은 글 (내부 링크)
이 주제와 연결되는 다른 글입니다.
- C++ 메모리 누수 | 서버 다운시킨 실제 사례와 Valgrind로 찾는 5가지 패턴
- C++ 스택 vs 힙 | 재귀에서 프로그램이 죽는 이유와 스택 오버플로우 사례
- C++ 스택 vs 힙 완벽 가이드 | 재귀 크래시, 메모리 레이아웃, RAII·스마트 포인터 실전 패턴