Menu

C++ 동적 메모리: new와 delete 완벽 정리

new로 런타임에 메모리를 할당하고 delete로 해제하는 방법, 그리고 힙을 직접 관리할 때 따라오는 누수, 댕글링 포인터, 이중 해제를 피하는 방법.

이 페이지에는 실행 가능한 에디터가 있습니다 - 편집하고 실행하면 결과를 바로 볼 수 있습니다.

왜 런타임에 메모리를 요청하는가

지금까지 만든 모든 변수는 스택에 존재했습니다. 크기가 컴파일 타임에 정해지고, 스코프가 끝나면 자동으로 소멸됩니다. 이는 빠르고 안전하지만, 프로그램이 실행되기 전까지 얼마나 많은 메모리가 필요한지 알 수 없는 경우는 다룰 수 없습니다. 예를 들어 사용자 입력으로 크기가 정해지는 버퍼, 자신을 만든 함수보다 오래 살아남아야 하는 구조체, 또는 모양을 미리 알 수 없는 그래프 같은 경우입니다.

이런 경우를 위해 C++은 new(프리 스토어라고도 함)에서 할당하고 delete로 되돌려 줄 수 있게 해 줍니다. new 표현식은 블록을 예약하고, 생성자를 실행하며, 그것을 가리키는 포인터를 반환합니다. 이는 이전 페이지에서 본 포인터 위에 곧바로 쌓아 올린 것입니다.

변수 p 자체는 스택에 존재합니다. 그저 포인터일 뿐입니다. 그것이 가리키는 int는 힙에 존재하며, 스코프가 아무리 오가더라도 여러분이 delete할 때까지 살아 있습니다.

스택과 힙

이 구분은 new가 존재하는 이유 그 자체이므로 구체적으로 짚어볼 가치가 있습니다.

void demo() {
    int a = 10;            // 스택에 있음 - demo()가 반환되면 사라짐
    int* b = new int(10);  // 'b'는 스택에, 그것이 가리키는 int는 힙에
}                          // 'a'는 소멸됨; 힙의 int는 누수됨 - 절대 delete되지 않음

핵심 차이점:

  • 스택 - 자동 수명, 매우 빠름, 크기 제한(보통 몇 MB), 스코프를 벗어나면 자동으로 해제됨.
  • - 수동 수명, 약간 느림, 큼, 그리고 delete를 호출할 때 해제됨.

이는 책임을 대가로 유연성을 얻는 거래입니다. 힙 메모리는 여러분이 원하는 만큼 정확히 살아 있지만, 그것을 해제하는 것을 기억해야 하는 사람이 바로 여러분이 됩니다.

new[]로 배열 할당하기

길이가 런타임에 정해지는 블록이 필요할 때는 배열 형식 new T[n]을 사용합니다. 이는 첫 번째 요소를 가리키는 포인터를 반환하며, 짝이 되는 delete[]로 해제합니다.

규칙은 엄격하고 틀리기 쉽습니다. new로 얻은 메모리는 delete로, new[]로 얻은 메모리는 delete[]로 해제합니다. 이를 섞는 것(new[]로 할당한 것에 delete arr를 쓰는 것)은 여러분의 컴퓨터에서 동작하는 것처럼 보이더라도 정의되지 않은 동작입니다.

세 가지 고전적인 버그

수동 메모리 관리에는 대부분의 힙 버그를 차지하는 소수의 실수가 있습니다. 세 가지를 모두 알아볼 수 있도록 익혀 두세요.

1. 메모리 누수 - delete를 한 번도 호출하지 않음. 블록이 영원히 예약된 채로 남습니다. 한 번이면 무해하지만, 반복문 안에서는 치명적입니다.

void leaky() {
    int* p = new int(5);
    // ... delete 없음 ...
}   // p는 사라짐; 힙의 int는 이제 도달할 수도 없고 해제되지도 않음

2. 댕글링 포인터 - 메모리를 해제한 뒤에 사용함. 포인터는 여전히 옛 주소를 들고 있지만, 그 메모리는 더 이상 여러분의 것이 아닙니다.

3. 이중 해제 - 같은 블록을 두 번 delete함. 이는 힙의 내부 관리 정보를 손상시키고 대개 충돌을 일으킵니다.

int* p = new int(1);
delete p;
delete p;   // 이중 해제 - 정의되지 않은 동작, 흔히 충돌

포인터를 삭제한 뒤 nullptr로 설정하면 댕글링 사용과 이중 해제를 모두 무력화합니다. nullptr을 역참조하면 즉시 충돌하고(디버깅이 쉬움), delete nullptr은 명시적으로 안전한, 아무것도 하지 않는 연산이기 때문입니다.

현실적인 할당-사용-해제 주기

이 모든 것을 종합하면, 올바른 수동 관리의 형태는 다음과 같습니다. 할당하고, 사용하고, 정확히 한 번 해제하고, 그 후에는 포인터를 건드리지 않는 것입니다.

클래스 타입의 경우 delete u가 두 가지 일을 한다는 점에 주목하세요. 먼저 객체의 소멸자를 실행하고, 그다음 원시 메모리를 해제합니다. 이 순서는 객체가 자신만의 리소스를 소유하게 되는 순간 중요해집니다.

미묘한 함정: newdelete 사이에서 예외가 던져지면 delete는 결코 실행되지 않고 누수가 발생합니다. 이를 처리하려고 모든 할당을 try/catch로 감싸는 것은 번거롭고 실수하기 쉬운데, 바로 이것이 다음 페이지가 해결하는 문제입니다.

다음: 스마트 포인터

이제 메모리를 직접 다루는 데 드는 모든 비용을 보았습니다. 모든 new는 나중에 delete하겠다는 약속이며, 단 한 번의 빠뜨림, 중복, 또는 너무 이른 해제도 정의되지 않은 동작입니다. 현대 C++은 이 약속을 손으로 하는 일이 거의 없습니다. 다음 페이지에서는 스마트 포인터를 소개합니다. std::unique_ptrstd::shared_ptr는 힙 할당을 소유하고 스코프를 벗어날 때 여러분 대신 자동으로 delete를 호출하는 객체로, 세 가지 고전적인 버그를 컴파일러와 RAII가 여러분을 대신해 처리해 주는 것으로 바꿔 줍니다.

자주 묻는 질문

C++에서 new와 delete의 차이는 무엇인가요?

new는 런타임에 힙에 메모리를 할당하고 그 메모리를 가리키는 포인터를 반환합니다. deletenew로 할당한 메모리를 해제합니다. 모든 new는 정확히 하나의 delete와 짝을 이뤄야 하며, 그렇지 않으면 메모리가 누수됩니다. 배열의 경우 new[]delete[]를 함께 사용합니다.

C++에서 delete 호출을 잊으면 어떻게 되나요?

메모리 누수가 발생합니다. 더 이상 아무것도 그 블록을 가리키지 않더라도, 힙 블록은 프로그램이 실행되는 동안 내내 예약된 상태로 남습니다. 한 번의 누수는 대개 무해하지만, 반복문 안이나 오래 실행되는 서비스에서의 누수는 쌓이다가 결국 메모리가 부족해져 프로그램이 충돌합니다.

현대 C++에서 new와 delete를 직접 써야 하나요?

거의 쓰지 않습니다. 메모리를 자동으로 해제해 주는 std::vector 같은 컨테이너나 스마트 포인터(std::unique_ptr, std::shared_ptr)를 선호하세요. 스마트 포인터가 내부에서 이를 감싸기 때문에 날것의 new/delete를 이해해 둘 가치는 있지만, 일상적인 코드에서는 누수와 댕글링 포인터의 원인이 됩니다.

Coddy programming languages illustration

Coddy로 코딩 배우기

시작하기