본문 바로가기
프로그래밍/C\C++

[Effective C++ 정리 #16] new와 delete 사용 시 유의 점 - 괄호 하나로 무너지는 코드의 안정성

by 허구의 2025. 6. 26.
728x90

이 글은 『Effective C++』를 읽고 개인적으로 공부한 내용을 정리한 기록입니다.

저는 컴퓨터공학을 전공하지 않았으며, 프로그래밍을 공부하는 과정에서의 이해와 생각을 정리하기 위해 글을 작성하고 있습니다.

따라서 내용 중 일부에 오류나 부정확한 설명이 있을 수 있으며, 피드백은 언제든지 환영합니다. 확인 후 수정하도록 하겠습니다.

전문적인 해설이 아닌 개인적 시선에서의 정리임을 참고하고 읽어주시면 감사하겠습니다.

[Effective C++ 정리 #16] new와 delete: 반드시 같은 형태를 맞춰 사용해야 합니다

C++에서 newdelete는 자원을 동적으로 관리할 수 있도록 해주는 강력한 도구입니다. 하지만 그만큼 주의해서 사용해야 합니다. 특히 **new와 delete를 사용할 때 항상 동일한 형태를 사용해야 한다**는 이 아이템의 규칙은 매우 중요한 핵심입니다.

 

이 간단해 보이는 규칙을 어기면 프로그램은 정의되지 않은 동작에 빠집니다. 심각한 메모리 누수, 프로그램 비정상 종료, 데이터 손상 등이 일어날 수 있습니다.


문제 상황:

다음 코드를 살펴봅니다:

std::string* stringArray = new std::string[100];

// ... 중간 코드 생략 ...

delete stringArray;

 

표면적으로는 문제가 없어 보입니다. new로 동적 메모리를 할당했고, delete로 해제했으니 정상처럼 보입니다.

하지만 이는 심각한 오류입니다. 그 이유는 바로 new에서 배열을 생성했는데 delete에서는 배열을 해제하지 않았기 때문입니다.

 

이 코드가 올바르려면 이렇게 작성해야 합니다:

delete[] stringArray;

 

 

따라서. new[]로 할당한 메모리는 반드시 delete[]로 해제해야 하며, new로 할당한 메모리는 반드시 delete로 해제해야 합니다.


왜 이렇게 규칙이 까다로운가?

newdelete는 단순한 메모리 할당과 해제를 넘어서 두 가지 작업을 수행합니다:

  • new: 메모리 할당 + 생성자 호출
  • delete: 소멸자 호출 + 메모리 해제

이때 얼마나 많은 객체의 소멸자를 호출할 것인가?가 delete의 핵심 고민입니다. 배열을 할당할 때는 몇 개의 객체가 생성됐는지를 delete가 알아야 합니다.

 

많은 컴파일러들은 new[]를 호출할 때 내부적으로 **배열 크기를 어딘가에 저장**합니다. delete[]는 이 정보를 찾아와서 그만큼 반복적으로 소멸자를 호출합니다.

 

하지만 delete는 단일 객체라고 가정하기 때문에 이런 추가 정보가 없다고 생각하고 단 하나의 소멸자만 호출하고 바로 메모리를 해제합니다. 배열의 나머지 객체들은 소멸되지 않고 메모리 누수가 발생합니다.


반대로 delete[]를 단일 객체에 사용하면?

이번엔 반대 상황을 살펴봅니다:

std::string* stringPtr = new std::string;

delete[] stringPtr;  // 잘못된 사용

 

이 경우 delete[]는 메모리 어딘가에서 배열 크기를 추출하려 합니다. 그 정보는 존재하지 않기 때문에 엉뚱한 값을 읽게 되고, 이후 그 수만큼 소멸자를 호출합니다. 이미 생성되지도 않은 객체들의 소멸자를 호출하는 셈이 됩니다. 결과는 치명적인 **정의도지 않은 동작**입니다.

 

이 때문에 C++에서는 new와 delete를 사용할 때 **항상 짝을 맞춰야 한다**는 규칙이 존재합니다.


클래스 설계에서 new/delete 짝맞춤의 위험

문제가 더 심각해지는 곳은 바로 **클래스 내부에서 동적 메모리를 관리할 때**입니다. 예를 들어 다음과 같은 클래스가 있다고 가정합시다:

class Sample {
public:
    Sample(int n) {
        pArray = new std::string[n];
    }
    ~Sample() {
        delete pArray;  // 잘못된 코드
    }
private:
    std::string* pArray;
};

 

생성자에서 new[]로 배열을 생성했으면서 소멸자에서는 delete를 사용하고 있습니다. 이런 실수가 발생하기 쉬운 이유는:

  • 배열인지 아닌지 구분이 헷갈리기 쉬움
  • 구현자가 pArray의 초기화 방식을 정확히 인지하지 못할 수 있음
  • 생성자가 여러 개 있는 경우 new와 new[]를 혼용할 위험이 있음

정확히 작성하면 다음과 같아야 합니다:

~Sample() {
    delete[] pArray;
}

 

생성자 모든 경로에서 new[]만 사용했다는 전제가 확실할 때만 가능합니다.


typedef로 감춰진 배열은 특히 위험

C++에서는 typedef를 통해 배열 타입을 선언할 수 있습니다:

typedef std::string AddressLines[4];

 

이제 다음과 같은 코드가 작성될 수 있습니다:

AddressLines* pal = new AddressLines;

delete pal;    // 잘못된 코드
delete[] pal;  // 올바른 코드

 

이렇게 typedef가 사용되면 pal의 타입은 std::string*처럼 보이지만 사실은 내부적으로 배열을 가리키고 있기 때문에 delete[]가 필요합니다.

 

따라서 **배열에 typedef를 사용하는 것은 혼란을 야기하므로 피하는 것이 좋습니다**. 표준 라이브러리의 vector<string> 같은 컨테이너를 사용하는 것이 훨씬 안전합니다.


스마트 포인터로 이 모든 고민을 피할 수 있습니다!

C++ 표준 라이브러리에서 제공하는 std::vectorstd::unique_ptr를 사용하면 new와 delete를 직접 다루는 일이 거의 사라집니다.

 

예를 들어 std::vector는 내부에서 자동으로 메모리를 관리하므로 배열 크기를 신경 쓸 필요가 없습니다.

std::vector<std::string> addressLines(4);

 

unique_ptrunique_ptr<Type[]>를 이용하면 delete[]를 자동으로 호출합니다:

std::unique_ptr<std::string[]> pArray(new std::string[100]);

 

이런 방법을 사용하면 new/delete의 짝맞춤 문제에서 거의 완전히 벗어날 수 있습니다.


핵심 요약

  • new[]는 반드시 delete[]로, new는 반드시 delete로 대응해야 한다.

감사합니다.

728x90