Learning
레슨 6 / 8·20분

스마트 포인터와 메모리 관리

C++의 메모리 관리 문제

C 스타일의 new/delete 수동 메모리 관리는 메모리 누수, 이중 해제, 댕글링 포인터 등 다양한 버그의 원인이 됩니다. C++11부터 도입된 스마트 포인터는 RAII(Resource Acquisition Is Initialization) 패턴으로 이 문제를 해결합니다.

unique_ptr — 독점 소유권

cpp
#include <iostream>
#include <memory>
using namespace std;

class Player {
public:
    string name;
    int hp;

    Player(const string& n, int h) : name(n), hp(h) {
        cout << name << " 생성" << endl;
    }
    ~Player() {
        cout << name << " 소멸" << endl;
    }
    void info() {
        cout << name << " (HP: " << hp << ")" << endl;
    }
};

int main() {
    // unique_ptr: 하나의 포인터만 객체를 소유
    auto player1 = make_unique<Player>("전사", 100);
    player1->info();  // 전사 (HP: 100)

    // 소유권 이전 (move)
    unique_ptr<Player> player2 = move(player1);
    // player1은 이제 nullptr
    if (!player1) {
        cout << "player1은 비어있음" << endl;
    }
    player2->info();  // 전사 (HP: 100)

    // 배열도 가능
    auto scores = make_unique<int[]>(5);
    for (int i = 0; i < 5; i++) {
        scores[i] = (i + 1) * 10;
    }

    // 스코프 끝나면 자동 해제!
    return 0;
}

shared_ptr과 weak_ptr

cpp
#include <iostream>
#include <memory>
using namespace std;

class Node {
public:
    string value;
    shared_ptr<Node> next;
    weak_ptr<Node> parent;  // 순환 참조 방지

    Node(const string& v) : value(v) {
        cout << value << " 생성" << endl;
    }
    ~Node() {
        cout << value << " 소멸" << endl;
    }
};

int main() {
    // shared_ptr: 여러 포인터가 객체를 공유
    auto node1 = make_shared<Node>("A");
    cout << "참조 카운트: " << node1.use_count() << endl;  // 1

    {
        auto node2 = node1;  // 공유
        cout << "참조 카운트: " << node1.use_count() << endl;  // 2
    }  // node2 소멸, 카운트 감소

    cout << "참조 카운트: " << node1.use_count() << endl;  // 1

    // weak_ptr: 소유권 없이 참조
    weak_ptr<Node> weakRef = node1;
    if (auto locked = weakRef.lock()) {
        cout << "유효: " << locked->value << endl;
    }

    return 0;
}

RAII와 이동 의미론

cpp
#include <iostream>
#include <vector>
#include <string>
using namespace std;

class Buffer {
    int* data;
    size_t size;
public:
    // 생성자
    Buffer(size_t n) : size(n), data(new int[n]) {
        cout << "할당: " << n << " ints" << endl;
    }

    // 소멸자 (RAII: 자동 정리)
    ~Buffer() {
        delete[] data;
        cout << "해제" << endl;
    }

    // 이동 생성자 (복사 대신 소유권 이전)
    Buffer(Buffer&& other) noexcept
        : data(other.data), size(other.size) {
        other.data = nullptr;
        other.size = 0;
        cout << "이동 완료" << endl;
    }

    // 복사 금지
    Buffer(const Buffer&) = delete;
    Buffer& operator=(const Buffer&) = delete;

    size_t getSize() const { return size; }
};

int main() {
    Buffer buf1(100);
    Buffer buf2 = move(buf1);  // 이동 (복사 아님)

    cout << "buf2 크기: " << buf2.getSize() << endl;

    // vector에서의 이동
    vector<string> v;
    string s = "Hello, World!";
    v.push_back(move(s));  // 이동 (복사 비용 없음)
    // s는 이제 빈 문자열

    return 0;
}
  • unique_ptr — 독점 소유, 복사 불가, 이동만 가능
  • shared_ptr — 공유 소유, 참조 카운팅으로 자동 해제
  • weak_ptr — 소유권 없는 참조, 순환 참조 방지
  • make_unique() / make_shared() — 안전한 생성
  • move() — 소유권 이전, 불필요한 복사 제거
  • RAII — 리소스 획득 = 초기화, 소멸자에서 자동 정리
💡

Modern C++에서는 new/delete를 직접 사용하지 마세요. 항상 make_unique 또는 make_shared를 사용하세요. 순환 참조가 예상되면 한쪽을 weak_ptr로 선언합니다.