본문 바로가기

동적 메모리 할당

new와 delete의 객체 수명·저장 공간·소유권 관계를 구분하고, 예외와 별칭까지 고려해 RAII로 안전하게 관리합니다.

C++의 new 표현식은 실행 중에 저장 공간을 얻고 그곳에 객체를 생성할 수 있게 합니다. 이 방식으로 만든 객체는 동적 저장 기간(dynamic storage duration)을 가지며, 흔히 “힙에 만든다”고 표현합니다.

핵심은 주소 그 자체가 아니라 객체의 수명과 저장 공간을 누가 소유하고 언제 정리하는가입니다. 포인터 변수는 주소를 보관할 뿐이며, 포인터에 nullptr를 대입하거나 포인터 변수가 범위를 벗어난다고 해서 가리키던 객체가 자동으로 파괴되지는 않습니다.

현대 C++ 애플리케이션 코드에서는 직접 new와 delete를 짝짓기보다 std::vector, std::string, std::unique_ptr 같은 RAII 타입을 기본으로 사용합니다. 다만 수동 관리의 규칙을 알아야 기존 코드와 저수준 API를 정확히 읽을 수 있습니다.

이 절에서는 앞에서 배운 raw 포인터 규칙에 “소유자”를 추가합니다. 뒤의 auto는 초깃값으로 변수 타입을 정하는 문법이며, make_unique<int>(42)는 int 값 42를 만들고 그것을 정리할 소유 객체를 돌려줍니다. 클래스의 소멸자 문법을 아직 몰라도, 이 소유 객체가 정상적으로 범위를 벗어나거나 예외로 되감길 때 정리한다는 계약을 먼저 따라갈 수 있습니다. try/catch의 문법은 예외 처리에서 이어집니다.


동적 저장 공간이 필요한 경우

실행 중에 원소 수가 결정되거나, 객체의 소유권을 다른 범위로 옮겨야 하거나, 저수준 인터페이스가 직접 할당을 요구할 때 동적 저장 공간이 필요할 수 있습니다.

  • 실행 중 결정되는 연속 원소 묶음은 보통 std::vector<T>로 소유합니다.
  • 문자열은 보통 std::string으로 소유합니다.
  • 단일 객체의 독점 소유권은 보통 std::unique_ptr<T>로 표현합니다.
  • raw 포인터 T*는 소유권을 나타내지 않는 관찰자나 C API 경계에서 제한적으로 사용하는 편이 안전합니다.

동적 저장 기간은 소스 코드의 중괄호 범위와 직접 연결되지 않습니다. 그렇다고 “반드시 delete할 때까지 또는 프로그램 종료까지 같은 상태로 남는다”는 뜻은 아닙니다. 객체 수명과 저장 공간의 상태는 수행한 생성·파괴·해제 연산과 프로그램의 실행 경로에 따라 판단해야 합니다.


new 표현식은 저장 공간을 얻고 객체를 만든다

new 표현식은 필요한 저장 공간을 할당한 뒤 그 공간에서 객체를 초기화하고, 생성된 객체를 가리키는 포인터 값을 반환합니다.

new 표현식의 두 형태
T* one = new T(arguments);   // 단일 T 객체 생성
T* many = new T[count]{};    // T 객체 count개로 이루어진 배열 생성

단일 객체와 배열은 해제 표현식의 짝이 다릅니다.

할당과 해제의 올바른 짝
#include <cstddef>
#include <iostream>

int main() {
    int* value = new int{42};
    std::cout << *value << '\n';
    delete value;          // new int{42}와 짝
    value = nullptr;

    constexpr std::size_t count = 3;
    int* values = new int[count]{10, 20, 30};
    std::cout << values[1] << '\n';
    delete[] values;       // new int[count]와 짝
    values = nullptr;
}

new T에는 delete, new T[n]에는 delete[]를 사용합니다. 어떤 해제 표현식을 쓸지는 포인터 변수의 표면적인 타입만 보고 정하는 것이 아니라, 그 저장 공간을 만든 할당 형태에 맞춰 정합니다.

C++ 동적 할당의 객체 수명과 해제 짝

저장 공간 획득→객체 초기화/수명 시작→접근→맞는 delete로 소멸/해제→별칭 사용 중지의 자원 상태 전이이다. 할당/초기화 실패는 별도 경로로 남긴다.

C++ new 표현식이 저장 공간에 객체를 만들고 포인터를 반환한 뒤, 단일 객체는 delete로 배열은 delete[]로 수명을 끝내고 저장 공간을 해제하는 흐름

NEW-EXPRESSION · OBJECT LIFETIME · MATCHED DELETE

동적 할당은 포인터가 아니라 객체 수명을 관리하는 일이다

new 표현식은 저장 공간을 얻고 그곳에 객체를 초기화한 뒤 포인터 값을 돌려줍니다. 단일 객체와 배열은 서로 다른 형태로 만들었으므로 수명을 끝낼 때도 각각 delete와 delete[]를 정확히 짝지어야 합니다.

핵심 관계단일 new T에는 delete, 배열 new T[n]에는 delete[]를 짝지어 객체 수명과 저장 공간을 정리한다. 기본 new의 할당 실패는 bad_alloc이다. owner만 nullptr로 바꿔도 alias의 주소는 남으므로 해제된 객체에 접근하면 안 된다.new T(args)new T[n]{}T 객체 1개T 객체 n개delete pdelete[] p기본 new: 할당 실패std::bad_allocowner: nullptralias: 주소 유지해제된 객체간접 참조 금지
핵심 관계단일 new T에는 delete, 배열 new T[n]에는 delete[]를 짝지어 객체 수명과 저장 공간을 정리한다. 기본 new의 할당 실패는 bad_alloc이다. owner만 nullptr로 바꿔도 alias의 주소는 남으므로 해제된 객체에 접근하면 안 된다.new T(args)new T[n]{}T 객체 1개T 객체 n개delete pdelete[] p기본 new할당 실패std::bad_allocownernullptralias주소 유지해제된 객체간접 참조 금지

단일 new T에는 delete, 배열 new T[n]에는 delete[]를 짝지어 객체 수명과 저장 공간을 정리한다. 기본 new의 할당 실패는 bad_alloc이다. owner만 nullptr로 바꿔도 alias의 주소는 남으므로 해제된 객체에 접근하면 안 된다.

관계도 해설과 예제 원자료
single object

T* p = new T(args);

동적으로 할당한 저장 공간에서 T 객체 하나를 초기화합니다. 더 이상 필요하지 않을 때 delete p;가 그 객체를 파괴하고 대응하는 저장 공간을 해제합니다.

array object

T* p = new T[n]{};

T 객체 n개로 이루어진 동적 배열을 만듭니다. 배열 형태의 할당에는 대괄호를 보존한 delete[] p;가 필요합니다.

할당 → 초기화 → 사용 → 소멸

성공과 실패를 포함한 한 객체의 생명주기

  1. 저장 공간을 요청한다

    일반적인 new가 필요한 저장 공간을 얻지 못하면 기본 동작으로 std::bad_alloc 계열 예외를 던집니다. 널 포인터 반환은 std::nothrow를 명시한 별도 형태입니다.

  2. 객체를 초기화하고 수명을 시작한다

    저장 공간을 얻은 뒤 생성자 또는 초기화가 수행됩니다. 초기화가 예외로 완료되지 못하면 해당 new 표현식이 얻었던 저장 공간은 대응하는 해제 함수로 정리됩니다.

  3. 포인터로 객체에 접근한다

    반환된 포인터 값으로 살아 있는 객체를 사용합니다. 다른 포인터에 같은 값을 복사할 수 있지만, 포인터 값의 복사가 자동으로 소유권 규칙을 만들어 주지는 않습니다.

  4. 할당 형태와 맞는 표현식으로 정리한다

    delete p; 또는 delete[] p;가 대상 객체의 수명을 끝내고 대응하는 저장 공간을 해제합니다. 잘못된 짝과 이중 해제는 정의되지 않은 동작입니다.

  5. 남은 모든 별칭을 더 이상 사용하지 않는다

    해제 뒤 원래 포인터에 nullptr를 넣어도 다른 포인터 변수는 자동으로 바뀌지 않습니다. 남은 별칭을 간접 참조하거나 다시 해제하면 정의되지 않은 동작입니다.

생성 형태가 정하는 해제 규칙
시작 표현식올바른 종료핵심 결과
new T(args)delete p;T 객체 하나를 파괴하고 대응하는 저장 공간을 해제합니다.
new T[n]{}delete[] p;배열 원소들의 수명을 끝내고 배열 저장 공간을 해제합니다.
널 포인터 값

delete p;

또는 delete[] p;

대상이 없으므로 아무 일도 하지 않습니다.
이미 해제된 주소 또는 잘못된 짝다시 해제하지 않음이중 해제와 new/delete[] 짝 불일치는 정의되지 않은 동작입니다.

new T(args) → delete p;

단일 객체 하나의 수명을 끝내고 저장 공간을 해제합니다.

new T[n]{} → delete[] p;

배열 원소들의 수명을 끝내고 배열 저장 공간을 해제합니다.

널 포인터 해제

대상이 없으므로 아무 일도 하지 않습니다.

해제된 주소·잘못된 짝

다시 해제하지 않습니다. 이중 해제와 짝 불일치는 정의되지 않은 동작입니다.

failure branch

new 성공만 가정하지 않는다

기본 할당 실패는 nullptr 검사로 잡히지 않습니다. 복구 정책이 있는 계층에서 std::bad_alloc을 처리하거나 상위로 전파합니다.

alias remains

owner = nullptr는 별칭을 지우지 않는다

delete owner; 뒤 owner만 널로 바꾸어도 같은 주소를 저장하던 alias는 자동 갱신되지 않습니다. 소유권은 RAII 타입 하나에 모으는 편이 안전합니다.

동적 저장 기간은 중괄호 범위에 자동으로 묶이지 않습니다. 마지막 포인터 값을 잃으면 수동으로 만든 객체를 정상적으로 파괴하고 저장 공간을 해제할 경로도 잃을 수 있으므로, 실제 코드는 컨테이너와 스마트 포인터에 소유권을 맡깁니다.

이 예의 조건

  • 본문과 원 도표에 제시된 사례의 입력·조건을 사용합니다. 설명용 수치나 독립 비교 경로는 각 단계에 표시합니다.
  • 현재 상태·값과 전환 근거의 위치는 모든 단계에서 같습니다. 조건이 맞지 않거나 실행되지 않은 경로를 성공한 동작으로 해석하지 않습니다.
관계도의 보충 설명
원 도표의 제목·노드·관계·조건·주석
범위원문
본문·주석성공과 실패를 포함한 한 객체의 생명 주기
예의 값과 변화
관찰·상황값·보충 정보변화·이유
저장·수명 시작

성공 사례에서 저장 공간을 얻고 객체를 초기화한다.

  • 저장 공간: 획득됨
  • 객체 수명: 초기화 성공 뒤 시작
  • 할당과 객체 초기화는 구분되는 경계다.
유효한 접근

객체가 살아 있는 동안 자원을 사용한다.

  • 객체: 유효
  • 접근: 정상 사용
  • 초기화 실패는 이 성공 실행과 별도 경로다.
소멸·해제

맞는 delete로 수명을 끝내고 저장 공간을 반납한다.

  • 객체 수명: 종료
  • 별칭: 후속 사용 금지
  • 해제 뒤 남은 주소값이 유효한 객체를 뜻하지 않는다.

C++ 동적 할당의 객체 수명과 해제 짝

저장 공간 획득→객체 초기화/수명 시작→접근→맞는 delete로 소멸/해제→별칭 사용 중지의 자원 상태 전이이다. 할당/초기화 실패는 별도 경로로 남긴다.

C++ new 표현식이 저장 공간에 객체를 만들고 포인터를 반환한 뒤, 단일 객체는 delete로 배열은 delete[]로 수명을 끝내고 저장 공간을 해제하는 흐름

NEW-EXPRESSION · OBJECT LIFETIME · MATCHED DELETE

동적 할당은 포인터가 아니라 객체 수명을 관리하는 일이다

new 표현식은 저장 공간을 얻고 그곳에 객체를 초기화한 뒤 포인터 값을 돌려줍니다. 단일 객체와 배열은 서로 다른 형태로 만들었으므로 수명을 끝낼 때도 각각 delete와 delete[]를 정확히 짝지어야 합니다.

핵심 관계단일 new T에는 delete, 배열 new T[n]에는 delete[]를 짝지어 객체 수명과 저장 공간을 정리한다. 기본 new의 할당 실패는 bad_alloc이다. owner만 nullptr로 바꿔도 alias의 주소는 남으므로 해제된 객체에 접근하면 안 된다.new T(args)new T[n]{}T 객체 1개T 객체 n개delete pdelete[] p기본 new: 할당 실패std::bad_allocowner: nullptralias: 주소 유지해제된 객체간접 참조 금지
핵심 관계단일 new T에는 delete, 배열 new T[n]에는 delete[]를 짝지어 객체 수명과 저장 공간을 정리한다. 기본 new의 할당 실패는 bad_alloc이다. owner만 nullptr로 바꿔도 alias의 주소는 남으므로 해제된 객체에 접근하면 안 된다.new T(args)new T[n]{}T 객체 1개T 객체 n개delete pdelete[] p기본 new할당 실패std::bad_allocownernullptralias주소 유지해제된 객체간접 참조 금지

단일 new T에는 delete, 배열 new T[n]에는 delete[]를 짝지어 객체 수명과 저장 공간을 정리한다. 기본 new의 할당 실패는 bad_alloc이다. owner만 nullptr로 바꿔도 alias의 주소는 남으므로 해제된 객체에 접근하면 안 된다.

관계도 해설과 예제 원자료
single object

T* p = new T(args);

동적으로 할당한 저장 공간에서 T 객체 하나를 초기화합니다. 더 이상 필요하지 않을 때 delete p;가 그 객체를 파괴하고 대응하는 저장 공간을 해제합니다.

array object

T* p = new T[n]{};

T 객체 n개로 이루어진 동적 배열을 만듭니다. 배열 형태의 할당에는 대괄호를 보존한 delete[] p;가 필요합니다.

할당 → 초기화 → 사용 → 소멸

성공과 실패를 포함한 한 객체의 생명주기

  1. 저장 공간을 요청한다

    일반적인 new가 필요한 저장 공간을 얻지 못하면 기본 동작으로 std::bad_alloc 계열 예외를 던집니다. 널 포인터 반환은 std::nothrow를 명시한 별도 형태입니다.

  2. 객체를 초기화하고 수명을 시작한다

    저장 공간을 얻은 뒤 생성자 또는 초기화가 수행됩니다. 초기화가 예외로 완료되지 못하면 해당 new 표현식이 얻었던 저장 공간은 대응하는 해제 함수로 정리됩니다.

  3. 포인터로 객체에 접근한다

    반환된 포인터 값으로 살아 있는 객체를 사용합니다. 다른 포인터에 같은 값을 복사할 수 있지만, 포인터 값의 복사가 자동으로 소유권 규칙을 만들어 주지는 않습니다.

  4. 할당 형태와 맞는 표현식으로 정리한다

    delete p; 또는 delete[] p;가 대상 객체의 수명을 끝내고 대응하는 저장 공간을 해제합니다. 잘못된 짝과 이중 해제는 정의되지 않은 동작입니다.

  5. 남은 모든 별칭을 더 이상 사용하지 않는다

    해제 뒤 원래 포인터에 nullptr를 넣어도 다른 포인터 변수는 자동으로 바뀌지 않습니다. 남은 별칭을 간접 참조하거나 다시 해제하면 정의되지 않은 동작입니다.

생성 형태가 정하는 해제 규칙
시작 표현식올바른 종료핵심 결과
new T(args)delete p;T 객체 하나를 파괴하고 대응하는 저장 공간을 해제합니다.
new T[n]{}delete[] p;배열 원소들의 수명을 끝내고 배열 저장 공간을 해제합니다.
널 포인터 값

delete p;

또는 delete[] p;

대상이 없으므로 아무 일도 하지 않습니다.
이미 해제된 주소 또는 잘못된 짝다시 해제하지 않음이중 해제와 new/delete[] 짝 불일치는 정의되지 않은 동작입니다.

new T(args) → delete p;

단일 객체 하나의 수명을 끝내고 저장 공간을 해제합니다.

new T[n]{} → delete[] p;

배열 원소들의 수명을 끝내고 배열 저장 공간을 해제합니다.

널 포인터 해제

대상이 없으므로 아무 일도 하지 않습니다.

해제된 주소·잘못된 짝

다시 해제하지 않습니다. 이중 해제와 짝 불일치는 정의되지 않은 동작입니다.

failure branch

new 성공만 가정하지 않는다

기본 할당 실패는 nullptr 검사로 잡히지 않습니다. 복구 정책이 있는 계층에서 std::bad_alloc을 처리하거나 상위로 전파합니다.

alias remains

owner = nullptr는 별칭을 지우지 않는다

delete owner; 뒤 owner만 널로 바꾸어도 같은 주소를 저장하던 alias는 자동 갱신되지 않습니다. 소유권은 RAII 타입 하나에 모으는 편이 안전합니다.

동적 저장 기간은 중괄호 범위에 자동으로 묶이지 않습니다. 마지막 포인터 값을 잃으면 수동으로 만든 객체를 정상적으로 파괴하고 저장 공간을 해제할 경로도 잃을 수 있으므로, 실제 코드는 컨테이너와 스마트 포인터에 소유권을 맡깁니다.

이 예의 조건

  • 본문과 원 도표에 제시된 사례의 입력·조건을 사용합니다. 설명용 수치나 독립 비교 경로는 각 단계에 표시합니다.
  • 현재 상태·값과 전환 근거의 위치는 모든 단계에서 같습니다. 조건이 맞지 않거나 실행되지 않은 경로를 성공한 동작으로 해석하지 않습니다.

관계도의 보충 설명

원 도표의 제목·노드·관계·조건·주석
범위원문
본문·주석성공과 실패를 포함한 한 객체의 생명 주기
예의 값과 변화
관찰·상황값·보충 정보변화·이유
저장·수명 시작

성공 사례에서 저장 공간을 얻고 객체를 초기화한다.

  • 저장 공간: 획득됨
  • 객체 수명: 초기화 성공 뒤 시작
  • 할당과 객체 초기화는 구분되는 경계다.
유효한 접근

객체가 살아 있는 동안 자원을 사용한다.

  • 객체: 유효
  • 접근: 정상 사용
  • 초기화 실패는 이 성공 실행과 별도 경로다.
소멸·해제

맞는 delete로 수명을 끝내고 저장 공간을 반납한다.

  • 객체 수명: 종료
  • 별칭: 후속 사용 금지
  • 해제 뒤 남은 주소값이 유효한 객체를 뜻하지 않는다.

할당 실패는 기본적으로 예외로 알린다

일반적인 new 표현식은 저장 공간을 얻지 못하면 널 포인터를 반환하는 대신 std::bad_alloc 예외를 던집니다. 배열 길이가 구현 한계를 넘는 경우에는 std::bad_array_new_length가 던져질 수도 있으며, 이 예외는 std::bad_alloc에서 파생됩니다.

따라서 기본 new의 결과를 단순히 nullptr와 비교하는 코드는 할당 실패 처리가 되지 않습니다. 널 반환 동작이 꼭 필요한 특수한 코드만 <new>의 std::nothrow 형태를 명시적으로 선택합니다.

std::bad_alloc 처리 예시
#include <cstddef>
#include <iostream>
#include <memory>
#include <new>

int main() {
    const std::size_t count = 1'000;

    try {
        auto values = std::make_unique<int[]>(count);
        values[0] = 42;
        std::cout << values[0] << '\n';
    } catch (const std::bad_alloc& error) {
        std::cerr << "메모리 할당 실패: " << error.what() << '\n';
        return 1;
    }
}

예외를 잡은 뒤 실제로 복구할 방법이 없다면, 무조건 현장에서 잡기보다 상위 계층에 전파하여 일관된 오류 정책으로 처리하는 편이 낫습니다. 위 예제의 std::unique_ptr는 할당 이후 다른 연산에서 예외가 발생하더라도 정상적인 스택 되감기 과정에서 소유한 배열을 정리합니다.


delete는 객체 수명과 저장 공간을 함께 정리한다

단일 객체에 대한 올바른 delete 표현식은 객체의 소멸자를 실행해 수명을 끝낸 뒤 대응하는 저장 공간을 해제합니다. delete[]는 동적으로 만든 배열의 원소들을 파괴한 뒤 배열 저장 공간을 해제합니다.

int* ptr = nullptr;처럼 널 포인터 값을 저장한 포인터에 delete ptr; 또는 delete[] ptr;를 적용하면 파괴할 대상 객체가 없습니다. 반대로 다음 연산은 정의되지 않은 동작(undefined behavior)입니다.

  • 이미 해제된 대상을 다시 delete하는 이중 해제
  • new와 delete[], 또는 new[]와 delete를 섞는 짝 불일치
  • 해제된 객체를 가리키는 포인터를 간접 참조하거나 다시 해제하는 사용
  • 올바른 new 표현식에서 얻지 않은 주소를 delete하는 사용

정의되지 않은 동작은 반드시 즉시 충돌한다는 뜻이 아닙니다. 우연히 실행되는 것처럼 보여도 결과를 신뢰할 수 없습니다.


nullptr는 한 포인터만 지운다

여러 포인터가 같은 객체를 가리킬 수 있습니다. 한 포인터로 객체를 해제하면 그 객체의 수명은 끝나지만, 다른 포인터 변수의 비트가 자동으로 nullptr로 바뀌지는 않습니다.

별칭과 댕글링 포인터
int* owner = new int{7};
int* alias = owner;       // 같은 객체를 가리키는 별칭

delete owner;             // 객체 수명 종료와 저장 공간 해제
owner = nullptr;          // owner 하나만 널 포인터로 바뀜

// alias는 자동으로 바뀌지 않으며 더 이상 유효한 객체를 가리키지 않는다.
// *alias = 9;            // 정의되지 않은 동작
// delete alias;          // 이중 해제: 정의되지 않은 동작
alias = nullptr;          // 사용하지 않도록 이 변수도 따로 정리

해제 직후 소유 포인터를 nullptr로 바꾸는 습관은 같은 변수의 우발적인 재사용을 줄일 수 있지만, 모든 별칭을 고쳐 주거나 소유권 설계를 대신하지는 않습니다. 가장 중요한 예방책은 소유자를 하나의 RAII 객체로 표현하고, 다른 raw 포인터가 그 소유자보다 오래 살아남지 않게 하는 것입니다.


수동 관리가 실패하는 경로

C++ 수동 메모리 관리의 실패 경로와 RAII 선택

할당 성공 뒤 정상 작업은 3 42, 조기 반환은 skip을 출력하지만 둘 다 values/answer 소유자의 범위 종료 정리로 합류한다.

C++ 수동 동적 할당에서 누수, 댕글링 포인터, 이중 해제, 할당과 해제의 짝 불일치, 예외와 조기 반환이 만드는 위험을 RAII 컨테이너와 스마트 포인터 선택으로 예방하는 점검표

RAW OWNERSHIP · FAILURE PATHS · RAII

수동 해제의 빈틈은 소유권을 타입에 맡겨 없앤다

직접 new한 raw 포인터는 모든 정상 반환과 예외 경로에서 정확히 한 번, 정확한 형태로 해제해야 합니다. 분기마다 delete를 기억하는 대신 컨테이너와 스마트 포인터가 수명을 소유하게 하면 실패 경로도 같은 규칙으로 정리됩니다.

정상 작업과 조기 반환 모두 소유 객체의 정리로 이어진다5노드5간선이다.values와answer소유준비→stopEarly또는빈vector조건.아니요는값복사와3 42출력뒤정리로우회합류하며skip를실행하지않는다.예는skip출력뒤정리로간다.두경로모두정상범위종료소멸계약을따르며실제해제출력을측정한것아니다.할당실패예외는본문별도범위이고이5노드그림은성공할당뒤의두정상반환만그린다.모바일도같다.소유 객체 준비values와answer먼저 반환할까?stopEarly / 빈 vector값 복사·출력3 42조기 반환 출력skip범위 종료각 소유자 정리아니요예두 반환 경로가 정리에서 합류
정상 작업과 조기 반환 모두 소유 객체의 정리로 이어진다5노드5간선이다.values와answer소유준비→stopEarly또는빈vector조건.아니요는값복사와3 42출력뒤정리로우회합류하며skip를실행하지않는다.예는skip출력뒤정리로간다.두경로모두정상범위종료소멸계약을따르며실제해제출력을측정한것아니다.할당실패예외는본문별도범위이고이5노드그림은성공할당뒤의두정상반환만그린다.모바일도같다.소유 객체 준비values와answer먼저 반환할까?stopEarly / 빈 vector값 복사·출력3 42조기 반환 출력skip범위 종료각 소유자 정리아니요예두 반환 경로가 정리에서 합류
manual owner

주소와 해제 책임이 코드 여러 곳으로 흩어진다

raw 포인터 복사만으로는 어느 변수가 소유자인지 알 수 없습니다. 새 반환·예외·소유권 전달이 추가될 때 누수나 중복 해제 경로가 생기기 쉽습니다.

RAII owner

소유 객체의 소멸자가 정리를 한곳에서 수행한다

std::vector, std::string, std::unique_ptr는 자원과 소유권을 하나의 타입에 묶습니다. 정상 범위 종료와 예외 스택 되감기에서 같은 소멸 규칙이 적용됩니다.

수동 동적 할당의 대표 실패와 언어 수준의 결과
!무엇이 잘못되는가구조적인 예방책
누수마지막 소유 경로를 잃거나 조기 반환이 해제를 건너뜁니다. 접근할 수 없는 객체와 저장 공간을 정상적으로 정리할 방법도 잃습니다.연속 원소는 std::vector, 문자열은 std::string, 단일 객체는 std::unique_ptr가 소유합니다.
별칭한 포인터로 해제한 뒤 같은 객체를 가리키던 다른 포인터가 남습니다. 그 별칭의 간접 참조나 재해제는 정의되지 않은 동작입니다.소유자는 하나로 제한하고, raw 관찰자는 소유 객체의 수명보다 오래 보관하지 않습니다.
이중이미 수명이 끝나고 해제된 저장 공간을 다시 delete합니다. 결과는 정의되지 않은 동작입니다.std::unique_ptr로 독점 소유권을 표현하고 이동으로만 전달합니다.
짝new T에 delete[]를 쓰거나 new T[n]에 delete를 쓰면 정의되지 않은 동작입니다.직접 배열 할당 대신 std::vector를 사용하고, 저수준 경계의 할당 형태는 한 추상화 안에 감춥니다.
이탈할당 뒤 수동 해제 전 흐름이 빠져나가면 정리 구문에 도달하지 못합니다.자원을 얻는 즉시 RAII 객체가 소유하게 하여 모든 정상 반환과 예외 스택 되감기에 소멸자를 연결합니다.
leak

마지막 소유 경로를 잃음

std::vector, std::string, std::unique_ptr가 소유하게 합니다.

dangling

해제된 객체를 가리키는 별칭

raw 관찰자는 소유 객체보다 오래 보관하지 않습니다. 간접 참조와 재해제는 정의되지 않은 동작입니다.

double delete

같은 저장 공간을 두 번 해제

std::unique_ptr로 소유자를 하나로 만들고 이동으로 전달합니다.

mismatch

단일·배열 할당과 해제의 짝 불일치

new T는 delete, new T[n]는 delete[]와 짝입니다. 잘못된 짝은 정의되지 않은 동작이므로 직접 배열 할당보다 std::vector를 사용합니다.

early exit

예외나 반환이 정리 코드를 건너뜀

자원을 얻는 즉시 RAII 객체에 맡겨 소멸자가 정리를 담당하게 합니다.

default choice

필요한 데이터 형태에서 소유 타입을 먼저 고른다

  1. 가변 길이 원소 묶음은 std::vector<T>

    크기, 원소 수명, 저장 공간 해제를 한 객체가 함께 관리합니다. 반복과 범위 기반 접근도 직접 배열보다 명확합니다.

  2. 소유 문자열은 std::string

    문자 버퍼의 크기와 종료 조건을 직접 추적하지 않고 값 타입처럼 복사·이동할 수 있습니다.

  3. 단일 객체의 독점 소유권은 std::make_unique<T>

    std::unique_ptr는 복사를 막고 이동으로 소유권 전달을 드러내며, 소멸할 때 정확히 한 번 대상 객체를 정리합니다.

  4. raw T*는 수명이 제한된 관찰자로 사용

    owner.get()으로 얻은 포인터는 소유권을 갖지 않습니다. 소유 RAII 객체가 살아 있는 동안에만 접근하고 저장 기간을 별도로 늘리지 않습니다.

ownership in code

수동 해제 구문 대신 소유 타입을 읽는다

std::vector<int> values(count);
std::string label = "dynamic values";
auto answer =
    std::make_unique<int>(42);

int* view = answer.get();
// answer보다 짧게 사용한다.

조기 반환과 예외 스택 되감기에서는 values, label, answer의 소멸자가 각 자원을 정리합니다.

nullptr is local

한 포인터의 널 대입은 소유권 해결책이 아니다

owner = nullptr는 그 변수의 포인터 값만 바꿉니다. 같은 주소를 저장하던 다른 포인터의 상태와 대상 객체의 수명을 자동으로 관리하지 않습니다.

low-level boundary

직접 new가 남는 곳은 책임을 한 경계에 가둔다

레거시 코드나 특수 API에서 직접 할당이 필요하면 소유자, 할당 형태, 소유권 전달, 모든 종료 경로를 하나의 작은 추상화 안에서 완결합니다.

RAII는 “해제를 잊지 말자”는 주의 문구가 아니라, 정상 범위 종료와 예외 스택 되감기를 소멸자에 연결하는 구조입니다. 비정상적인 프로세스 종료까지 모든 정리가 실행된다고 가정하지는 않습니다.

이 예의 조건

  • 본문과 원 도표에 제시된 사례의 입력·조건을 사용합니다. 설명용 수치나 독립 비교 경로는 각 단계에 표시합니다.
  • 현재 상태·값과 전환 근거의 위치는 모든 단계에서 같습니다. 조건이 맞지 않거나 실행되지 않은 경로를 성공한 동작으로 해석하지 않습니다.
관계도의 보충 설명
원 도표의 제목·노드·관계·조건·주석
범위원문
관계·조건 설명5개 노드와 5개 간선이다. values와 answer 소유 준비→stopEarly 또는 빈 vector 조건으로 이어진다. 아니요 분기는 값 복사와 3 42 출력 뒤 정리로 우회 합류하며 skip 출력을 하지 않는다. 예 분기는 skip 출력 뒤 정리로 간다. 두 경로 모두 정상 범위 종료의 소멸 계약을 따르며 실제 해제 출력을 측정한 것은 아니다. 할당 실패 예외는 본문의 별도 범위이고 이 5개 노드 그림은 성공한 할당 뒤의 두 정상 반환만 그린다. 모바일도 같다.
노드·연결선 라벨values와 answer
예의 값과 변화
관찰·상황값·보충 정보변화·이유
소유 준비

할당 성공 뒤 values와 answer가 자원을 보유한다.

  • 소유자: values / answer
  • 할당: 성공
  • 정상·조기 반환 두 갈래는 동일한 RAII 소유 경계에서 시작한다.
선택된 작업

본문 실행 결과를 갈래별로 비교한다.

  • 정상 출력: 3 42
  • 조기 반환 출력: skip
  • 조기 반환은 정상 작업을 건너뛰지만 지역 범위는 벗어난다.
공통 정리

두 갈래 모두 소유자의 범위 종료로 정리한다.

  • values: 자원 해제
  • answer: 자원 해제
  • RAII 소멸은 정상 끝과 조기 반환 모두에 적용된다.

C++ 수동 메모리 관리의 실패 경로와 RAII 선택

할당 성공 뒤 정상 작업은 3 42, 조기 반환은 skip을 출력하지만 둘 다 values/answer 소유자의 범위 종료 정리로 합류한다.

C++ 수동 동적 할당에서 누수, 댕글링 포인터, 이중 해제, 할당과 해제의 짝 불일치, 예외와 조기 반환이 만드는 위험을 RAII 컨테이너와 스마트 포인터 선택으로 예방하는 점검표

RAW OWNERSHIP · FAILURE PATHS · RAII

수동 해제의 빈틈은 소유권을 타입에 맡겨 없앤다

직접 new한 raw 포인터는 모든 정상 반환과 예외 경로에서 정확히 한 번, 정확한 형태로 해제해야 합니다. 분기마다 delete를 기억하는 대신 컨테이너와 스마트 포인터가 수명을 소유하게 하면 실패 경로도 같은 규칙으로 정리됩니다.

정상 작업과 조기 반환 모두 소유 객체의 정리로 이어진다5노드5간선이다.values와answer소유준비→stopEarly또는빈vector조건.아니요는값복사와3 42출력뒤정리로우회합류하며skip를실행하지않는다.예는skip출력뒤정리로간다.두경로모두정상범위종료소멸계약을따르며실제해제출력을측정한것아니다.할당실패예외는본문별도범위이고이5노드그림은성공할당뒤의두정상반환만그린다.모바일도같다.소유 객체 준비values와answer먼저 반환할까?stopEarly / 빈 vector값 복사·출력3 42조기 반환 출력skip범위 종료각 소유자 정리아니요예두 반환 경로가 정리에서 합류
정상 작업과 조기 반환 모두 소유 객체의 정리로 이어진다5노드5간선이다.values와answer소유준비→stopEarly또는빈vector조건.아니요는값복사와3 42출력뒤정리로우회합류하며skip를실행하지않는다.예는skip출력뒤정리로간다.두경로모두정상범위종료소멸계약을따르며실제해제출력을측정한것아니다.할당실패예외는본문별도범위이고이5노드그림은성공할당뒤의두정상반환만그린다.모바일도같다.소유 객체 준비values와answer먼저 반환할까?stopEarly / 빈 vector값 복사·출력3 42조기 반환 출력skip범위 종료각 소유자 정리아니요예두 반환 경로가 정리에서 합류
manual owner

주소와 해제 책임이 코드 여러 곳으로 흩어진다

raw 포인터 복사만으로는 어느 변수가 소유자인지 알 수 없습니다. 새 반환·예외·소유권 전달이 추가될 때 누수나 중복 해제 경로가 생기기 쉽습니다.

RAII owner

소유 객체의 소멸자가 정리를 한곳에서 수행한다

std::vector, std::string, std::unique_ptr는 자원과 소유권을 하나의 타입에 묶습니다. 정상 범위 종료와 예외 스택 되감기에서 같은 소멸 규칙이 적용됩니다.

수동 동적 할당의 대표 실패와 언어 수준의 결과
!무엇이 잘못되는가구조적인 예방책
누수마지막 소유 경로를 잃거나 조기 반환이 해제를 건너뜁니다. 접근할 수 없는 객체와 저장 공간을 정상적으로 정리할 방법도 잃습니다.연속 원소는 std::vector, 문자열은 std::string, 단일 객체는 std::unique_ptr가 소유합니다.
별칭한 포인터로 해제한 뒤 같은 객체를 가리키던 다른 포인터가 남습니다. 그 별칭의 간접 참조나 재해제는 정의되지 않은 동작입니다.소유자는 하나로 제한하고, raw 관찰자는 소유 객체의 수명보다 오래 보관하지 않습니다.
이중이미 수명이 끝나고 해제된 저장 공간을 다시 delete합니다. 결과는 정의되지 않은 동작입니다.std::unique_ptr로 독점 소유권을 표현하고 이동으로만 전달합니다.
짝new T에 delete[]를 쓰거나 new T[n]에 delete를 쓰면 정의되지 않은 동작입니다.직접 배열 할당 대신 std::vector를 사용하고, 저수준 경계의 할당 형태는 한 추상화 안에 감춥니다.
이탈할당 뒤 수동 해제 전 흐름이 빠져나가면 정리 구문에 도달하지 못합니다.자원을 얻는 즉시 RAII 객체가 소유하게 하여 모든 정상 반환과 예외 스택 되감기에 소멸자를 연결합니다.
leak

마지막 소유 경로를 잃음

std::vector, std::string, std::unique_ptr가 소유하게 합니다.

dangling

해제된 객체를 가리키는 별칭

raw 관찰자는 소유 객체보다 오래 보관하지 않습니다. 간접 참조와 재해제는 정의되지 않은 동작입니다.

double delete

같은 저장 공간을 두 번 해제

std::unique_ptr로 소유자를 하나로 만들고 이동으로 전달합니다.

mismatch

단일·배열 할당과 해제의 짝 불일치

new T는 delete, new T[n]는 delete[]와 짝입니다. 잘못된 짝은 정의되지 않은 동작이므로 직접 배열 할당보다 std::vector를 사용합니다.

early exit

예외나 반환이 정리 코드를 건너뜀

자원을 얻는 즉시 RAII 객체에 맡겨 소멸자가 정리를 담당하게 합니다.

default choice

필요한 데이터 형태에서 소유 타입을 먼저 고른다

  1. 가변 길이 원소 묶음은 std::vector<T>

    크기, 원소 수명, 저장 공간 해제를 한 객체가 함께 관리합니다. 반복과 범위 기반 접근도 직접 배열보다 명확합니다.

  2. 소유 문자열은 std::string

    문자 버퍼의 크기와 종료 조건을 직접 추적하지 않고 값 타입처럼 복사·이동할 수 있습니다.

  3. 단일 객체의 독점 소유권은 std::make_unique<T>

    std::unique_ptr는 복사를 막고 이동으로 소유권 전달을 드러내며, 소멸할 때 정확히 한 번 대상 객체를 정리합니다.

  4. raw T*는 수명이 제한된 관찰자로 사용

    owner.get()으로 얻은 포인터는 소유권을 갖지 않습니다. 소유 RAII 객체가 살아 있는 동안에만 접근하고 저장 기간을 별도로 늘리지 않습니다.

ownership in code

수동 해제 구문 대신 소유 타입을 읽는다

std::vector<int> values(count);
std::string label = "dynamic values";
auto answer =
    std::make_unique<int>(42);

int* view = answer.get();
// answer보다 짧게 사용한다.

조기 반환과 예외 스택 되감기에서는 values, label, answer의 소멸자가 각 자원을 정리합니다.

nullptr is local

한 포인터의 널 대입은 소유권 해결책이 아니다

owner = nullptr는 그 변수의 포인터 값만 바꿉니다. 같은 주소를 저장하던 다른 포인터의 상태와 대상 객체의 수명을 자동으로 관리하지 않습니다.

low-level boundary

직접 new가 남는 곳은 책임을 한 경계에 가둔다

레거시 코드나 특수 API에서 직접 할당이 필요하면 소유자, 할당 형태, 소유권 전달, 모든 종료 경로를 하나의 작은 추상화 안에서 완결합니다.

RAII는 “해제를 잊지 말자”는 주의 문구가 아니라, 정상 범위 종료와 예외 스택 되감기를 소멸자에 연결하는 구조입니다. 비정상적인 프로세스 종료까지 모든 정리가 실행된다고 가정하지는 않습니다.

이 예의 조건

  • 본문과 원 도표에 제시된 사례의 입력·조건을 사용합니다. 설명용 수치나 독립 비교 경로는 각 단계에 표시합니다.
  • 현재 상태·값과 전환 근거의 위치는 모든 단계에서 같습니다. 조건이 맞지 않거나 실행되지 않은 경로를 성공한 동작으로 해석하지 않습니다.

관계도의 보충 설명

원 도표의 제목·노드·관계·조건·주석
범위원문
관계·조건 설명5개 노드와 5개 간선이다. values와 answer 소유 준비→stopEarly 또는 빈 vector 조건으로 이어진다. 아니요 분기는 값 복사와 3 42 출력 뒤 정리로 우회 합류하며 skip 출력을 하지 않는다. 예 분기는 skip 출력 뒤 정리로 간다. 두 경로 모두 정상 범위 종료의 소멸 계약을 따르며 실제 해제 출력을 측정한 것은 아니다. 할당 실패 예외는 본문의 별도 범위이고 이 5개 노드 그림은 성공한 할당 뒤의 두 정상 반환만 그린다. 모바일도 같다.
노드·연결선 라벨values와 answer
예의 값과 변화
관찰·상황값·보충 정보변화·이유
소유 준비

할당 성공 뒤 values와 answer가 자원을 보유한다.

  • 소유자: values / answer
  • 할당: 성공
  • 정상·조기 반환 두 갈래는 동일한 RAII 소유 경계에서 시작한다.
선택된 작업

본문 실행 결과를 갈래별로 비교한다.

  • 정상 출력: 3 42
  • 조기 반환 출력: skip
  • 조기 반환은 정상 작업을 건너뛰지만 지역 범위는 벗어난다.
공통 정리

두 갈래 모두 소유자의 범위 종료로 정리한다.

  • values: 자원 해제
  • answer: 자원 해제
  • RAII 소멸은 정상 끝과 조기 반환 모두에 적용된다.

수동 할당 코드는 성공 경로보다 예외와 조기 반환 경로에서 더 쉽게 깨집니다.

조기 반환 때문에 해제가 빠지는 예
void process(bool stopEarly) {
    int* value = new int{42};

    if (stopEarly) {
        return;           // delete value를 건너뛰어 누수
    }

    delete value;
}

해제 코드를 모든 분기마다 복제하면 새 분기나 예외 경로를 추가할 때 빠뜨리기 쉽습니다. 이 문제를 구조적으로 해결하는 방식이 RAII(Resource Acquisition Is Initialization)입니다.


현대 C++의 기본 선택은 RAII다

RAII 타입은 자원의 소유권을 객체에 묶습니다. 자동 저장 기간을 가진 소유 객체가 정상적으로 범위를 벗어나거나 예외로 스택이 되감기면 소멸자가 실행되어 자원을 정리합니다.

컨테이너와 스마트 포인터로 수명 관리
#include <cstddef>
#include <memory>
#include <string>
#include <vector>

void process(std::size_t count, bool stopEarly) {
    std::vector<int> values(count);          // 실행 중 정한 크기의 배열
    std::string label = "dynamic values";  // 소유 문자열
    auto answer = std::make_unique<int>(42);

    if (stopEarly || values.empty()) {
        return; // 세 객체의 소멸자가 각 자원을 정리
    }

    values.front() = *answer;
}

선택 기준은 다음과 같습니다.

  1. 크기가 실행 중 결정되는 연속 원소는 먼저 std::vector<T>를 고려합니다.
  2. 소유 문자열은 std::string을 사용합니다.
  3. 단일 객체의 독점 소유권은 std::make_unique<T>로 만듭니다.
  4. 정말 배열 형태의 독점 소유권이 필요하면 std::unique_ptr<T[]>를 사용할 수 있지만, 크기와 반복 처리가 필요한 일반적인 경우에는 std::vector<T>가 더 적합합니다.
  5. 직접 new와 delete가 필요한 저수준 경계에서는 소유자, 정확한 할당 형태, 모든 종료 경로를 코드와 문서에 함께 표시합니다.

동적 메모리 관리의 목표는 delete를 잘 기억하는 것이 아니라, 누가 소유하는지 타입으로 드러내어 수동 해제를 잊을 기회를 없애는 것입니다.

실행 경로를 바꾸어 보기

다음 main은 위의 RAII process를 조금 줄여, 원소 개수와 조기 반환을 출력으로 확인하는 별도의 완전한 프로그램입니다. unique_ptr가 소유한 int를 vector에 값으로 복사할 뿐 소유권을 옮기지는 않습니다.

빈 범위와 조기 반환도 같은 소유자에게 맡기기
#include <cstddef>
#include <iostream>
#include <memory>
#include <vector>

void process(std::size_t count, bool stopEarly) {
    std::vector<int> values(count);
    auto answer = std::make_unique<int>(42);
    if (stopEarly || values.empty()) {
        std::cout << "skip\n";
        return;
    }
    values.front() = *answer;
    std::cout << values.size() << " " << values.front() << "\n";
}

int main() {
    process(3, false);
    process(0, false);
    process(3, true);
}

출력은 3 42, skip, skip의 세 줄입니다. count=0은 빈 vector를 만들 수 있지만 front에는 접근할 수 없어서 먼저 반환합니다. 세 호출 모두 정상적으로 범위를 벗어날 때 answer와 values가 각자 소유한 자원을 정리합니다. 출력만으로 모든 해제가 관측되는 것은 아니며, 이 판단은 소유 타입과 실행 경로의 계약에 근거합니다.

해제 뒤 별칭 판단하기

위 owner/alias 예제에서 delete owner 직후에는 살아 있는 int가 남아 있나요? owner=nullptr까지 실행한 뒤 alias를 다시 delete하면 정리가 더 안전해지나요? 이 판단은 문제가 되는 간접 참조나 이중 해제를 실제 실행하지 않고 설명하세요.

소유자와 별칭의 풀이

int의 수명과 저장 공간은 첫 delete로 이미 정리됐습니다. owner의 널 대입은 다른 포인터 변수를 갱신하지 않습니다. alias로 다시 해제하는 것은 이중 해제이며, 그 값을 읽거나 써서 확인하는 것도 허용되지 않습니다. 별칭 자체에 nullptr를 대입하는 것은 그 변수의 상태만 정리합니다. 원 수동 process의 조기 반환은 소유 int를 정리할 경로를 건너뛰지만, RAII process의 반환은 소유 객체의 소멸 경로를 거칩니다.