이 글은 『Effective C++』를 읽고 개인적으로 공부한 내용을 정리한 기록입니다.
저는 컴퓨터공학을 전공하지 않았으며, 프로그래밍을 공부하는 과정에서의 이해와 생각을 정리하기 위해 글을 작성하고 있습니다.
따라서 내용 중 일부에 오류나 부정확한 설명이 있을 수 있으며, 피드백은 언제든지 환영합니다. 확인 후 수정하도록 하겠습니다.
전문적인 해설이 아닌 개인적 시선에서의 정리임을 참고하고 읽어주시면 감사하겠습니다.
[Effective C++ 정리 #16] new와 delete: 반드시 같은 형태를 맞춰 사용해야 합니다
C++에서 new와 delete는 자원을 동적으로 관리할 수 있도록 해주는 강력한 도구입니다. 하지만 그만큼 주의해서 사용해야 합니다. 특히 **new와 delete를 사용할 때 항상 동일한 형태를 사용해야 한다**는 이 아이템의 규칙은 매우 중요한 핵심입니다.
이 간단해 보이는 규칙을 어기면 프로그램은 정의되지 않은 동작에 빠집니다. 심각한 메모리 누수, 프로그램 비정상 종료, 데이터 손상 등이 일어날 수 있습니다.
문제 상황:
다음 코드를 살펴봅니다:
std::string* stringArray = new std::string[100];
// ... 중간 코드 생략 ...
delete stringArray;
표면적으로는 문제가 없어 보입니다. new로 동적 메모리를 할당했고, delete로 해제했으니 정상처럼 보입니다.
하지만 이는 심각한 오류입니다. 그 이유는 바로 new에서 배열을 생성했는데 delete에서는 배열을 해제하지 않았기 때문입니다.
이 코드가 올바르려면 이렇게 작성해야 합니다:
delete[] stringArray;
따라서. new[]로 할당한 메모리는 반드시 delete[]로 해제해야 하며, new로 할당한 메모리는 반드시 delete로 해제해야 합니다.
왜 이렇게 규칙이 까다로운가?
new와 delete는 단순한 메모리 할당과 해제를 넘어서 두 가지 작업을 수행합니다:
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::vector나 std::unique_ptr를 사용하면 new와 delete를 직접 다루는 일이 거의 사라집니다.
예를 들어 std::vector는 내부에서 자동으로 메모리를 관리하므로 배열 크기를 신경 쓸 필요가 없습니다.
std::vector<std::string> addressLines(4);
unique_ptr도 unique_ptr<Type[]>를 이용하면 delete[]를 자동으로 호출합니다:
std::unique_ptr<std::string[]> pArray(new std::string[100]);
이런 방법을 사용하면 new/delete의 짝맞춤 문제에서 거의 완전히 벗어날 수 있습니다.
핵심 요약
- new[]는 반드시 delete[]로, new는 반드시 delete로 대응해야 한다.
감사합니다.
'프로그래밍 > C\C++' 카테고리의 다른 글
| [Effective C++ 정리 #19] 클래스 설계는 타입 설계로 접근하라 (16) | 2025.07.09 |
|---|---|
| [Effective C++ 정리 #18] 좋은 인터페이스 설계의 핵심 (6) | 2025.07.02 |
| [Effective C++ 정리 #15] RAII의 Raw resource 접근 방법 (2) | 2025.06.25 |
| [Effective C++ 정리 #14] 리소스 관리 클래스 복사 시 유의사항 (6) | 2025.06.24 |
| [Effective C++ 정리 #13] RAII란 무엇인가? RAII로 자원 관리 방법 (2) | 2025.06.23 |