본문 바로가기
개발/C++ (98,03,11,14,17,20,23)

Modern C++ : dynamic heap memory allocation (98, 11, 14)

by snowoods 2025. 8. 14.

Modern C++

동적 힙 메모리 할당 (Dynamic Heap Memory Allocation)

 

개요

C++에서 메모리는 크게 스택(Stack)힙(Heap) 두 영역으로 나뉩니다. 동적 메모리 할당은 프로그램 실행 중에(런타임) 필요한 만큼의 메모리를 힙 영역에서 할당받아 사용하는 방식입니다. 컴파일 시점에 크기를 알 수 없거나, 객체의 생명주기를 특정 범위(scope)를 넘어서까지 제어하고 싶을 때 유용합니다.

반면, 함수 내에 선언된 일반 변수들은 스택에 저장되며, 해당 함수가 종료되면 자동으로 메모리에서 해제됩니다. 동적으로 할당된 힙 메모리는 프로그래머가 delete 키워드를 사용하여 명시적으로 해제하기 전까지 계속 유지되므로, 메모리 관리에 각별한 주의가 필요합니다.

 

C++ 버전별 주요 키워드 도입 시기

  • C++98: new, delete, new[], delete[] 연산자가 도입되어 C 스타일의 malloc/free를 대체하는 표준적인 동적 메모리 관리 방법을 제공했습니다.
  • C++11: 메모리 누수와 같은 문제를 방지하기 위해 스마트 포인터(std::unique_ptr, std::shared_ptr)와 nullptr가 도입되었습니다. 이는 RAII(Resource Acquisition Is Initialization) 패턴을 활용하여 메모리를 자동으로 관리합니다.
  • C++14: std::make_unique가 추가되어 std::unique_ptr를 더 안전하고 효율적으로 생성할 수 있게 되었습니다.

 

내용 설명

newdelete

  • new 연산자는 힙 영역에 특정 타입의 객체나 배열을 위한 메모리 공간을 할당하고, 해당 공간을 가리키는 포인터를 반환합니다.
  • delete 연산자는 new를 통해 할당받았던 메모리를 해제합니다.
  • 단일 객체는 new로 할당하고 delete로 해제하며, 배열은 new T[size]로 할당하고 delete[]로 해제해야 합니다. 이 둘을 혼용하면 미정의 동작(Undefined Behavior)이 발생할 수 있습니다.

메모리 누수 (Memory Leak)

new로 할당된 메모리를 delete로 해제하지 않으면, 해당 메모리는 프로그램이 종료될 때까지 계속 힙 공간을 차지하게 됩니다. 이러한 현상을 메모리 누수라고 하며, 프로그램의 성능 저하 및 비정상 종료의 원인이 될 수 있습니다.

댕글링 포인터 (Dangling Pointer)

delete로 메모리를 해제한 후에도 포인터 변수 자체에는 여전히 이전의 메모리 주소가 남아있습니다. 이 포인터를 댕글링 포인터라고 하며, 이를 통해 이미 해제된 메모리에 접근하려고 시도하면 심각한 오류가 발생할 수 있습니다. 이를 방지하기 위해 메모리 해제 후 포인터를 nullptr로 초기화하는 것이 좋은 습관입니다.

 

예제 코드

#include <iostream>
#include <cstdint>
#include <vector> // 스마트 포인터를 사용하지 않을 경우 대안

int main()
{
    // 정적 배열 (스택 할당)
    const auto len = std::size_t{5};
    std::uint32_t my_array[len] = {1, 2, 3, 4, 5};

    // 동적 배열 크기 입력 받기
    auto len2 = std::size_t{};
    std::cout << "배열 크기를 입력하세요: ";
    std::cin >> len2;

    // 동적 메모리 할당 (힙 할당)
    std::uint32_t *heap_arr = new(std::nothrow) std::uint32_t[len2]; // std::nothrow는 할당 실패 시 예외 대신 nullptr 반환

    if (heap_arr != nullptr)
    {
        std::cout << "할당된 메모리 주소: " << heap_arr << '\n';
        // 배열 초기화
        for (std::size_t i = 0; i < len2; i++)
        {
            heap_arr[i] = static_cast<std::uint32_t>(i);
        }

        // 배열 출력
        std::cout << "배열 내용:\n";
        for (std::size_t i = 0; i < len2; i++)
        {
            std::cout << heap_arr[i] << '\n';
        }

        // 메모리 해제
        delete[] heap_arr;
        heap_arr = nullptr; // 댕글링 포인터 방지
    }
    else
    {
        std::cout << "메모리 할당에 실패했습니다." << std::endl;
    }

    // 포인터 유효성 검사
    if (!heap_arr)
    {
        std::cout << "유효하지 않은 포인터입니다!\n";
    }

    return 0;
}

 

실행 결과

배열 크기를 입력하세요: 5
할당된 메모리 주소: 0x1f8d8efb650
배열 내용:
0
1
2
3
4
유효하지 않은 포인터입니다!

 

활용팁

  • 최신 C++에서는 직접적인 new/delete 사용을 지양합니다. 메모리 누수, 댕글링 포인터와 같은 실수를 방지하기 위해 스마트 포인터(std::unique_ptr, std::shared_ptr) 사용이 강력히 권장됩니다.
  • 동적 크기의 배열이 필요할 때는 new T[] 대신 std::vector를 사용하는 것이 훨씬 안전하고 편리합니다. std::vector는 내부적으로 동적 메모리를 사용하지만, 소멸자에서 자동으로 메모리를 해제해주므로 개발자가 직접 관리할 필요가 없습니다.
  • 메모리 할당이 실패할 경우 newstd::bad_alloc 예외를 발생시킵니다. 예외 처리를 원하지 않는다면 new(std::nothrow)를 사용하여 할당 실패 시 nullptr를 반환받도록 할 수 있습니다.