C++ Memory Leak | "메모리 누수" 가이드

C++ Memory Leak | "메모리 누수" 가이드

이 글의 핵심

C++ Memory Leak에 대한 실전 가이드입니다. 개념부터 실무 활용까지 예제와 함께 상세히 설명합니다.

Memory Leak이란?

할당한 메모리를 해제하지 않아 발생하는 문제입니다. 예방·구조 측면에서는 스마트 포인터RAII가 기본이고, 소유권 이전은 이동 의미론과 함께 이해하는 것이 좋습니다. 언어 간 관점 비교는 Rust 소유권·Rust 구조체와 대조해 보세요. 탐지·도구는 Valgrind, 누수·ASan 실전을 이어서 읽으면 됩니다.

// ❌ 메모리 누수
void func() {
    int* ptr = new int(10);
    // delete 없음
}  // ptr 소멸, 메모리는 남음

// ✅ 올바른 해제
void func() {
    int* ptr = new int(10);
    delete ptr;
}

발생 원인

// 1. delete 누락
int* ptr = new int(10);
// delete ptr;  // 누락

// 2. 예외 발생
void func() {
    int* ptr = new int(10);
    throw std::runtime_error("에러");  // delete 실행 안됨
    delete ptr;
}

// 3. 조기 반환
int* func(bool flag) {
    int* ptr = new int(10);
    if (flag) {
        return ptr;  // 반환
    }
    delete ptr;
    return nullptr;
}

// 4. 순환 참조
class Node {
    std::shared_ptr<Node> next;
    std::shared_ptr<Node> prev;  // 순환 참조
};

실전 예시

예시 1: 기본 누수

#include <iostream>

class Widget {
public:
    Widget(int size) : size(size) {
        data = new int[size];
        std::cout << "Widget 할당: " << size << std::endl;
    }
    
    ~Widget() {
        delete[] data;
        std::cout << "Widget 해제: " << size << std::endl;
    }
    
private:
    int* data;
    int size;
};

// ❌ 메모리 누수
void leak() {
    Widget* w = new Widget(100);
    // delete w;  // 누락
}

// ✅ 스마트 포인터
void noLeak() {
    auto w = std::make_unique<Widget>(100);
    // 자동 해제
}

int main() {
    leak();
    noLeak();
}

예시 2: 예외 안전성

#include <stdexcept>

// ❌ 예외 시 누수
void processData(bool throwError) {
    int* data = new int[1000];
    
    if (throwError) {
        throw std::runtime_error("에러 발생");
    }
    
    delete[] data;
}

// ✅ RAII 패턴
void processData(bool throwError) {
    auto data = std::make_unique<int[]>(1000);
    
    if (throwError) {
        throw std::runtime_error("에러 발생");
    }
    // 자동 해제
}

int main() {
    try {
        processData(true);
    } catch (const std::exception& e) {
        std::cout << e.what() << std::endl;
    }
}

예시 3: 컨테이너 누수

#include <vector>

class Resource {
public:
    Resource() {
        data = new int[100];
        std::cout << "Resource 할당" << std::endl;
    }
    
    ~Resource() {
        delete[] data;
        std::cout << "Resource 해제" << std::endl;
    }
    
    Resource(const Resource&) = delete;
    Resource& operator=(const Resource&) = delete;
    
private:
    int* data;
};

// ❌ 포인터 컨테이너
void leak() {
    std::vector<Resource*> resources;
    
    for (int i = 0; i < 10; i++) {
        resources.push_back(new Resource());
    }
    
    // delete 누락
}

// ✅ 스마트 포인터 컨테이너
void noLeak() {
    std::vector<std::unique_ptr<Resource>> resources;
    
    for (int i = 0; i < 10; i++) {
        resources.push_back(std::make_unique<Resource>());
    }
    
    // 자동 해제
}

예시 4: 순환 참조

#include <memory>

class Node {
public:
    std::shared_ptr<Node> next;
    std::shared_ptr<Node> prev;
    
    ~Node() {
        std::cout << "Node 소멸" << std::endl;
    }
};

// ❌ 순환 참조 (누수)
void circularReference() {
    auto node1 = std::make_shared<Node>();
    auto node2 = std::make_shared<Node>();
    
    node1->next = node2;
    node2->prev = node1;  // 순환 참조
    
    // 참조 카운트가 0이 안됨
}

// ✅ weak_ptr 사용
class NodeFixed {
public:
    std::shared_ptr<NodeFixed> next;
    std::weak_ptr<NodeFixed> prev;  // weak_ptr
    
    ~NodeFixed() {
        std::cout << "NodeFixed 소멸" << std::endl;
    }
};

void noCircularReference() {
    auto node1 = std::make_shared<NodeFixed>();
    auto node2 = std::make_shared<NodeFixed>();
    
    node1->next = node2;
    node2->prev = node1;  // weak_ptr
    
    // 정상 소멸
}

탐지 방법

// 1. Valgrind (Linux)
// valgrind --leak-check=full ./program

// 2. AddressSanitizer (GCC/Clang)
// g++ -fsanitize=address -g program.cpp

// 3. Visual Studio (Windows)
// _CrtSetDbgFlag(_CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF);

// 4. 커스텀 추적
class MemoryTracker {
public:
    static void* allocate(size_t size) {
        void* ptr = malloc(size);
        allocations[ptr] = size;
        return ptr;
    }
    
    static void deallocate(void* ptr) {
        allocations.erase(ptr);
        free(ptr);
    }
    
    static void report() {
        if (!allocations.empty()) {
            std::cout << "누수: " << allocations.size() << "개" << std::endl;
        }
    }
    
private:
    static std::map<void*, size_t> allocations;
};

자주 발생하는 문제

문제 1: delete vs delete[]

// ❌ 잘못된 delete
int* arr = new int[10];
delete arr;  // delete[] 사용해야 함

// ✅ 올바른 delete
int* arr = new int[10];
delete[] arr;

// ✅ 스마트 포인터
auto arr = std::make_unique<int[]>(10);

문제 2: 이중 delete

// ❌ 이중 delete
int* ptr = new int(10);
delete ptr;
delete ptr;  // 크래시

// ✅ nullptr 설정
int* ptr = new int(10);
delete ptr;
ptr = nullptr;
delete ptr;  // 안전 (nullptr delete는 무시)

문제 3: 소유권 불명확

// ❌ 소유권 불명확
void func(int* ptr) {
    // delete ptr?  // 누가 해제?
}

int* getData() {
    return new int(10);  // 누가 해제?
}

// ✅ 스마트 포인터로 명확화
void func(std::unique_ptr<int> ptr) {
    // 자동 해제
}

std::unique_ptr<int> getData() {
    return std::make_unique<int>(10);
}

문제 4: 컨테이너 clear

// ❌ clear만으로 부족
std::vector<int*> vec;
for (int i = 0; i < 10; i++) {
    vec.push_back(new int(i));
}
vec.clear();  // 포인터만 제거, 메모리는 남음

// ✅ 수동 해제
for (auto ptr : vec) {
    delete ptr;
}
vec.clear();

// ✅ 스마트 포인터
std::vector<std::unique_ptr<int>> vec;
for (int i = 0; i < 10; i++) {
    vec.push_back(std::make_unique<int>(i));
}
vec.clear();  // 자동 해제

방지 방법

// 1. 스마트 포인터 사용
auto ptr = std::make_unique<int>(10);

// 2. RAII 패턴
class FileHandle {
    FILE* file;
public:
    FileHandle(const char* name) {
        file = fopen(name, "r");
    }
    ~FileHandle() {
        if (file) fclose(file);
    }
};

// 3. 컨테이너 사용
std::vector<int> vec(10);  // 자동 관리

// 4. new 피하기
// 스택 할당 또는 스마트 포인터

FAQ

Q1: Memory Leak은 언제?

A:

  • delete 누락
  • 예외 발생
  • 순환 참조

Q2: 탐지 방법은?

A:

  • Valgrind
  • AddressSanitizer
  • Visual Studio 디버거

Q3: 방지 방법은?

A:

  • 스마트 포인터
  • RAII 패턴
  • new 피하기

Q4: 성능 영향은?

A:

  • 메모리 고갈
  • 프로그램 느려짐
  • 크래시 가능

Q5: 스마트 포인터 선택은?

A:

  • unique_ptr: 단독 소유
  • shared_ptr: 공유 소유
  • weak_ptr: 순환 참조 방지

Q6: Memory Leak 학습 리소스는?

A:

  • “Effective C++”
  • “C++ Core Guidelines”
  • Valgrind 문서

같이 보면 좋은 글 (내부 링크)

이 주제와 연결되는 다른 글입니다.

  • C++ Use After Free | “해제 후 사용” 가이드
  • C++ Heap Corruption | “힙 손상” 가이드
  • C++ Valgrind | “메모리 디버깅” 가이드

관련 글

  • C++ 디버깅 완벽 가이드 | GDB·Sanitizer·메모리 누수·멀티스레드 디버깅 실전
  • C++ Heap Corruption |
  • C++ Sanitizers |
  • C++ Use After Free |
  • C++ Valgrind |