C++의 new/delete vs. Raw pointer vs. Smart pointer

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로 대체 가능합니다.

댓글 남기기