C++ 메모리 관리: new/delete vs Raw Pointer vs Smart Pointer:
C++에서 객체를 생성하고 관리하는 주요 방식들의 차이점을 설명하는 예제 코드입니다.
스택(Stack)과 힙(Heap)이란?
코드에서 언급된 스택(Stack)과 힙(Heap)은 프로그램이 실행될 때 사용하는 메모리 공간의 종류를 뜻합니다.
1. 스택 (Stack) 메모리
- 의미: 책을 쌓아 올리듯 차곡차곡 쌓고, 위에서부터 꺼내는(LIFO) 구조의 메모리 공간입니다.
- 특징:
- 매우 빠름: CPU가 포인터를 단순히 이동시키는 것만으로 할당/해제가 끝납니다.
- 자동 관리: 함수나 블록(
{})에 들어갈 때 공간이 생기고, 나올 때 자동으로 정리됩니다.
- 코드 적용:
MyObject obj;처럼 변수를 선언하면 이곳에 저장됩니다.
2. 힙 (Heap) 메모리
- 의미: 필요할 때마다 덩어리를 떼어내어 사용하는, 자유롭고 넓은 메모리 공용 공간(Free Store)입니다.
- 특징:
- 유연함: 프로그램 실행 중에 필요한 만큼 '동적으로' 빌려 쓸 수 있습니다.
- 수동 관리: 자동으로 사라지지 않습니다. 빌린 사람이 책임을 지고 반납(
delete)해야 합니다.
- 코드 적용:
new MyObject;를 호출하면 힙 영역에 객체가 만들어집니다.
코드 예제 설명
1. 스택(Stack) 할당 (자동 관리)
가장 권장되는 일반적인 방식입니다.
- 방법:
MyObject obj("name");형태로 변수 선언. - 수명: 변수가 스택 메모리의 범위(Scope)를 벗어나면 자동으로 소멸자(
~MyObject)가 호출됩니다. - 장점: 메모리 관리 실수가 없고 빠릅니다.
2. 힙(Heap) 할당 (수동 관리: new / delete)
객체의 수명을 프로그래머가 직접 제어해야 할 때 사용합니다.
- 방법:
new연산자를 사용해 힙 메모리에 공간을 만들고, 포인터로 주소를 받습니다. - 수명: 블록이 끝나도 힙에 데이터가 계속 남아있습니다. 반드시
delete를 호출해줘야만 소멸됩니다. - 주의:
delete를 하지 않으면 계속 자리를 차지하여 메모리 누수(Memory Leak)가 발생합니다.
3. Raw Pointer (단순 참조/Observer)
Raw Pointer(*)가 항상 new와 짝을 이뤄야 하는 것은 아닙니다.
- 방법: 이미 존재하는(스택이나 힙에 있는) 객체의 주소만
&연산자로 가져와서 가리킵니다. - 역할: 객체의 "소유권"이 없이, 단순히 관찰(Access) 만 하는 용도입니다.
- 주의: 소유자가 아니므로 절대
delete를 호출하면 안 됩니다.
4. std::unique_ptr (스마트 포인터: 독점 소유)
모던 C++에서 가장 권장되는 방식입니다. 힙 메모리를 사용하지만, 관리는 스택 변수처럼 자동으로 합니다.
- 방법:
std::make_unique사용.(...) - 특징:
- 소유권 독점: 복사할 수 없습니다. 오직 하나의
unique_ptr만 해당 객체를 가리킵니다. - 자동 해제: 스코프(
{})를 벗어나면 자동으로delete를 호출하여 힙 메모리를 정리합니다.
- 소유권 독점: 복사할 수 없습니다. 오직 하나의
5. std::shared_ptr (스마트 포인터: 공유 소유)
여러 곳에서 하나의 객체를 동시에 사용해야 할 때 씁니다.
- 방법:
std::make_shared사용.(...) - 특징:
- 참조 카운팅: 몇 개의 포인터가 이 객체를 보고 있는지 숫자(Reference Count)를 셉니다.
- 자동 해제: 마지막 포인터가 사라져 카운트가 0이 되는 순간 객체가 파괴됩니다.
실행 방법
# 컴파일 (C++14 이상 필요)
clang++ -std=c++14 -o main main.cpp
# 실행
./main
Source Code (main.cpp)
#include < iostream>
#include < string>
#include < memory> // 스마트 포인터를 위해 필요
// 간단한 클래스를 정의하여 생성자와 소멸자가 호출되는 시점을 확인합니다.
class MyObject {
std::string name;
public:
MyObject(std::string n) : name(n) {
std::cout << "[Constructor] " << name << " 생성됨 (주소: " << this << ")n";
}
~MyObject() {
std::cout << "[Destructor] " << name << " 파괴됨 (주소: " << this << ")n";
}
void sayHello() {
std::cout << ">> " << name << " says: 안녕하세요!n";
}
};
int main() {
// ---------------------------------------------------------
// Case 1: 스택(Stack) 할당 (new/delete 미사용)
// 특징: 변수 선언 시 생성되고, 범위(Scope)를 벗어나면 자동 소멸.
// ---------------------------------------------------------
std::cout << "n=== 1. 스택(Stack) 할당 예제 ===n";
{
std::cout << "(스코프 시작)n";
MyObject stackObj("StackObject"); // 객체 직접 생성
stackObj.sayHello();
std::cout << "(스코프 종료 직전 - 자동으로 파괴됩니다)n";
} // 여기서 stackObj의 소멸자가 자동으로 호출됨
// ---------------------------------------------------------
// Case 2: 힙(Heap) 할당 (new / delete 사용)
// 특징: 개발자가 원할 때 생성하고, 원할 때 파괴함 (수동 관리).
// ---------------------------------------------------------
std::cout << "n=== 2. 힙(Heap) 할당 (new/delete) 예제 ===n";
{
std::cout << "(new 호출)n";
MyObject* heapPtr = new MyObject("HeapObject");
heapPtr->sayHello();
std::cout << "(delete 호출 전 - 아직 살아있음)n";
delete heapPtr;
std::cout << "(delete 호출 후 - 파괴됨)n";
}
// ---------------------------------------------------------
// Case 3: Raw Pointer를 단순 참조(Observer)용으로 사용
// 특징: 소유권 없이 주소만 가리킴. delete 금지.
// ---------------------------------------------------------
std::cout << "n=== 3. Raw Pointer 관찰자(Observer) 예제 ===n";
{
MyObject owner("OriginalOwner");
MyObject* ptr = &owner;
std::cout << "포인터를 통해 호출: ";
ptr->sayHello();
}
// ---------------------------------------------------------
// Case 4: std::unique_ptr (소유권 독점, 자동 해제)
// 특징: 범위를 벗어나면 힙 메모리도 자동으로 해제됨.
// 가장 권장되는 모던 C++ 방식.
// ---------------------------------------------------------
std::cout << "n=== 4. std::unique_ptr 예제 (Smart Pointer) ===n";
{
std::cout << "(unique_ptr 생성)n";
// make_unique는 new MyObject(...)와 비슷하지만 더 안전함
std::unique_ptr uPtr = std::make_unique("UniquePtrObj");
uPtr->sayHello();
// 다른 포인터에 대입/복사 불가능 (소유권 독점)
// std::unique_ptr p2 = uPtr; // 컴파일 에러!
std::cout << "(스코프 종료 - 자동으로 메모리 해제)n";
} // 여기서 uPtr 소멸 -> 내부 포인터 delete 자동 실행
// ---------------------------------------------------------
// Case 5: std::shared_ptr (소유권 공유, 참조 카운팅)
// 특징: 여러 포인터가 하나의 객체를 공유.
// 마지막 포인터가 사라질 때 객체가 파괴됨.
// ---------------------------------------------------------
std::cout << "n=== 5. std::shared_ptr 예제 (Shared Ownership) ===n";
{
std::cout << "(shared_ptr 생성 - RefCount: 1)n";
std::shared_ptr sPtr1 = std::make_shared("SharedObj");
sPtr1->sayHello();
{
std::cout << " (내부 스코프: sPtr2가 공유 -> RefCount 증가)n";
std::shared_ptr sPtr2 = sPtr1; // 복사 가능 (Ref Count 증가)
std::cout << " [Current RefCount: " << sPtr1.use_count() << "]n";
sPtr2->sayHello();
std::cout << " (내부 스코프 종료 -> sPtr2 소멸, RefCount 감소)n";
}
std::cout << "(외부 스코프: sPtr1만 생존)n";
std::cout << "[Current RefCount: " << sPtr1.use_count() << "]n";
std::cout << "(외부 스코프 종료 -> sPtr1 소멸 -> RefCount 0 -> 자동 파괴)n";
}
return 0;
}
---
### SmartSpace KDM 포인터 사용 가이드, 5가지 방식을 언제 사용해야 하는지 정리했습니다.
#### 1순위: 스택(Stack) 할당 (일반 변수)
> *"대부분의 경우 이것을 사용하세요."*
- **언제?**: 객체의 수명이 명확하고, 함수나 블록({}) 안에서만 쓰일 때.
- **이유**: 가장 빠르고, 메모리 누수 위험이 **0%**입니다.
#### 2순위: std::unique_ptr (독점 소유 스마트 포인터)
> *"힙(Heap)이 필요하다면 이것을 기본으로 쓰세요."*
- **언제?**:
1. 객체가 너무 커서 스택에 넣기 힘들 때.
2. 객체를 함수 밖으로 내보내거나(반환), 수명을 길게 가져가야 할 때.
- **이유**: new의 유연함을 가지면서도, 메모리 해제를 **자동**으로 해줍니다. 성능 오버헤드도 거의 없습니다.
#### 3순위: Raw Pointer (Observer)
> *"소유권 없이 잠시 빌려 쓸 때만 쓰세요."*
- **언제?**: 이미 존재하는 객체를 잠시 참조하거나, 함수 인자로 넘겨서 "보기만" 할 때.
- **규칙**: **절대 delete 하지 마세요!** 소유권이 없기 때문입니다.
#### 4순위: std::shared_ptr (공유 소유 스마트 포인터)
> *"정말 공유가 필요한가요? 다시 한 번 생각해보세요."*
- **언제?**: 하나의 객체를 여러 곳에서 동시에 소유해야 하고, 누가 마지막에 남을지 예측할 수 없을 때 (예: 복잡한 자료구조, 스레드 간 공유).
- **단점**: 내부적으로 숫자를 세는(Reference Counting) 비용이 있어 unique_ptr보다 약간 무겁습니다.
#### 5순위: new / delete (수동 힙 할당)
> *"가능하면 피하세요."*
- **언제?**: 아주 오래된 라이브러리를 쓰거나, 매우 낮은 레벨의 메모리 제어가 필요할 때만.
- **이유**: delete를 깜빡하기 쉽고, 예외 발생 시 메모리 누수가 생기기 쉽습니다. 현대 C++(Modern C++)에서는 거의 unique_ptr로 대체 가능합니다.