[C++] 항목 52: 위치지정 new를 작성한다면 위치지정 delete도 같이 준비하자
카테고리: Cpp
태그: Cpp
이 글은 아래의 책을 자세히 정리한 후, 정리한 글을 GPT에게 요약을 요청하여 작성되었습니다.
이펙티브 C++ 제3판, 스콧 마이어스 저자, 곽용재 번역
📦 8. new와 delete를 내 맘대로
👉🏻 항목 52: 위치지정 new를 작성한다면 위치지정 delete도 같이 준비하자
🔍 new 연산의 동작 과정
Widget* pw = new Widget;
이 코드는 두 개의 함수를 순서대로 호출한다:
- 메모리 할당을 위해
operator new호출 Widget 기본 생성자호출
예외 발생 시 문제:
- 만약 2번째에서 예외가 발생하면:
- 메모리는 할당이 되어있지만,
pw에 메모리 위치를 가리키는 포인터가 대입되어 있지 않다 - 사용자 코드에서 해당 메모리를 해제할 방법은 없다
- 이러한 경우, C++ 런타임 시스템이 메모리 할당을 해제해준다
- 메모리는 할당이 되어있지만,
⚙️ 런타임 시스템 처리 과정
처리 방법:
- 1번째에서 호출한
operator new함수와 짝이 되는operator delete를 호출해야 한다 - 런타임 시스템은
operator delete함수들 중, 어떤 것을 호출해야 하는지 알아야 한다- 문제는
new/delete함수가 짝이 맞지 않는 경우 발생한다
- 문제는
📌 기본형 operator new/delete 함수
// 기본형 operator new
void* operator new(size_t) throw(bad_alloc);
// 기본형 operator delete들
// 전역 유효범위
void operator delete(void* rawMemory) throw();
// 클래스 유효범위
void operator delete(void* rawMemory, size_t size) throw();
기본형이란?
- 전형적인 매개변수를 갖는 표준 형태의 함수를 뜻한다
- 기본형
operator new는 기본형operator delete와 짝을 맞추기 때문에 문제가 없다
🎯 비기본형 operator new/delete 함수
class Widget {
public:
...
// 비기본형 operator new
static void* operator new(
size_t size,
ostream& logStream
) throw(bad_alloc);
// 기본형 operator delete
// 클래스 유효범위를 가진다.
static void operator delete(
void* pMemory,
size_t size
) throw();
...
};
비기본형이란?
- 다른 매개변수를 추가로 갖는 비표준 형태의 함수를 뜻한다
📍 위치지정(placement) new
정의:
operator new함수는 매개변수를 추가로 받는 형태로 선언할 수 있다- 이를 위치지정(placement) new라고 부른다
void* operator new(size_t, void* pMemory) throw();
대표적인 위치지정 new 함수:
- 어떤 객체를 생성시킬 메모리 위치를 나타내는 포인터를 매개변수로 받는다
- 유용성을 인정받아 C++ 표준 라이브러리에
<new>로 들어가 있다vector도 원소 객체를 생성할 때위치지정 new를 사용한다
⚠️ 문제 발생 상황
class Widget {
public:
...
static void* operator new(
size_t size,
ostream& logStream
) throw(bad_alloc);
static void operator delete(
void* pMemory,
size_t size
) throw();
...
};
int main() {
// operator new를 호출할 때,
// cerr을 ostream 인자로 넘긴다.
// Widget 생성자에서 예외 발생 시,
// 메모리가 누출된다.
Widget* pw = new (std::cerr) Widget;
}
문제 분석:
- 메모리 할당은 성공했지만,
Widget 생성자에서 예외가 발생하였다고 가정하자
동작 과정:
- C++ 런타임 시스템이 처리해야 한다
operator new의 동작 방법을 알아낼 방법이 없다- 할당 자체를 되돌릴 수 없다
- 호출된
operator new와 시그니처가 같은operator delete를 찾고,
찾았다면 그 함수를 호출해야 한다- 하지만 존재하지 않는다
- C++ 런타임 시스템은 아무것도 하지 않는다
- 어떠한
operator delete도 호출하지 않게 된다
- 어떠한
해결 방법:
void operator delete(void*, ostream&) throw();
- 동작하기 위해선 동일 시그니처를 가진
delete가 있어야 한다- 매개변수를 추가로 받아들인다는 점에서
위치지정 delete라고 부른다
- 매개변수를 추가로 받아들인다는 점에서
✅ 수정된 코드
class Widget {
public:
...
static void* operator new(
size_t size,
ostream& logStream
) throw(bad_alloc);
static void operator delete(
void* pMemory
) throw();
static void operator delete(
void* pMemory,
ostream& logStream
) throw();
...
};
int main() {
Widget* pw = new (std::cerr) Widget;
delete pw; // 기본형 operator delete 호출
}
핵심 사항:
- 짝이 맞는
위치지정 new/delete가 존재하여 메모리 누출을 막을 수 있다 - 만약 예외 없이
delete pw까지 도달하면 기본형operator delete를 호출한다- 위치지정 버전을 호출하지 않는다!
- 위치지정 버전을 호출하는 경우는 생성자에서 예외가 발생할 경우뿐이다
- 즉, 기본(표준) 형태의
operator delete를 함께 마련해두어야 한다
🚫 이름 가려짐 문제
클래스 내부에서의 가려짐:
class Base {
public:
...
// 이 new가 표준 형태의 전역 new를 가린다.
static void* operator new(
size_t size,
ostream& logStream
) throw(bad_alloc);
...
}
int main() {
// ❌ 에러: 표준 형태의 전역 operator new가 가려짐.
Base* pb = new Base;
// ✅ Base의 위치지정 new를 호출한다.
Base* pb = new (std::cerr) Base;
}
원칙:
- 바깥 유효범위에 있는 어떤 함수의 이름과, 클래스 멤버 함수의 이름이 같으면
- 바깥 유효범위에 있는 함수가 가려진다
상속에서의 가려짐:
class Derived: public Base {
public:
...
static void* operator new(size_t size)
throw(bad_alloc);
...
};
int main() {
// ✅ Derived의 operator new를 호출한다.
Derived* pd = new Derived;
// ❌ 에러: Base의 위치지정 new가 가려짐.
Derived* pd = new (std::clog) Derived;
}
- 이는 상속에서도 그대로 이어진다
📋 표준 전역 operator new의 형태들
// 기본형 new
void* operator new(size_t) throw(bad_alloc);
// 위치지정 new
void* operator new(size_t, void*) throw();
// 예외불가 new (항목 49 참조)
void* operator new(size_t, const nothrow_t&) throw();
주의사항:
- 어떤 형태이든
operator new가 클래스 안에 선언되기만 하면- 위의 표준 형태들이 모두 가려진다
- 클래스 내에서 할당/해제 함수들이 표준 형태로 동작하길 원하면
- 클래스 전용 버전이 전역 버전을 호출하도록 하자
- 이것이 귀찮다면 상속을 사용하면 된다
🔧 상속과 using을 통한 해결
class StandardNewDeleteForms {
public:
// 기본형 new/delete
static void* operator new(size_t size)
throw(bad_alloc)
{ return ::operator new(size); }
static void operator delete(void* pMemory)
throw()
{ ::operator delete(pMemory); }
// 위치지정 new/delete
static void* operator new(size_t size, void* ptr)
throw()
{ return ::operator new(size, ptr); }
static void operator delete(void* pMemory, void* ptr)
throw()
{ ::operator delete(pMemory, ptr); }
// 예외불가 new/delete
static void* operator new(size_t size, const nothrow_t& nt)
throw()
{ return ::operator new(size, nt); }
static void operator delete(void* pMemory, const nothrow_t&)
throw()
{ ::operator delete(pMemory); }
};
// 표준 형태를 물려받는다.
class Widget: public StandardNewDeleteForms {
public:
// 표준 형태를 using 선언한다.
using StandardNewDeleteForms::operator new;
using StandardNewDeleteForms::operator delete;
// 사용자 정의 위치지정 new를 추가한다.
static void* operator new(size_t size, ostream& logStream)
throw(bad_alloc);
// 위와 짝이 되는 위치지정 delete를 추가한다.
static void operator delete(void* pMemory, ostream& logStream)
throw();
...
};
해결 방법:
상속과using을 사용하여 표준 형태를 끌고 온다- 이후 원하는
new/delete를 만들면 된다
- 이후 원하는
🧐 정리
operator new함수의 위치지정(placement) 버전을 만들 때는 해당 함수와 짝을 이루는 위치지정 버전의operator delete함수도 꼭 만들자- 그렇지 않으면 찾기도 힘든 메모리 누출 현상을 경험하게 된다
new/delete위치지정 버전을 선언할 때는 의도하지 않았지만 표준 버전이 가려지는 일이 없도록 주의하자
댓글남기기