[C++] 항목 51: new 및 delete를 작성할 때 따라야 할 기존의 관례를 잘 알아두자
카테고리: Cpp
태그: Cpp
이 글은 아래의 책을 자세히 정리한 후, 정리한 글을 GPT에게 요약을 요청하여 작성되었습니다.
이펙티브 C++ 제3판, 스콧 마이어스 저자, 곽용재 번역
📦 8. new와 delete를 내 맘대로
👉🏻 항목 51: new 및 delete를 작성할 때 따라야 할 기존의 관례를 잘 알아두자
🎯 operator new 구현 시 필수 요구사항
operator new를 구현할 때 반드시 지켜야 할 네 가지 핵심 사항:
- ✅ 반환 값이 제대로 되어있을 것
- ✅ 가용 메모리가 부족할 경우, new 처리자 함수 호출할 것 (항목 49)
- ✅ 크기가 없는(0바이트) 메모리에 대한 대비책
- ✅ 기본 new가 가려지지 않도록 할 것
📋 반환 값 처리 규칙
메모리 할당 성공 시:
- 해당 메모리에 대한 포인터 반환
메모리 할당 실패 시:
bad_alloc타입 예외 던짐
주의사항:
operator new는 메모리 할당이 실패할 때마다,
new 처리자 함수를 호출하는 식으로 메모리 할당을 2회 이상 시도한다operator new가 예외를 던지는 경우는,
new 처리자 함수에 대한 포인터가 널일 경우뿐이다- 0바이트를 요구했을 때도
operator new는 적법한 포인터를 반환해야 한다
💻 비멤버 버전 operator new 함수 의사코드
void* operator new(size_t size) throw(bad_alloc) {
using namespace std;
if(size == 0) {
size = 1;
}
while(true) {
"size 바이트 할당 시도"
if("할당 성공")
return ("할당된 메모리에 대한 포인터");
// 할당이 실패했을 경우:
// 현재 new 처리자 함수가 무엇인지 알아낸다.
new_handler globalHandler = set_new_handler(0);
set_new_handler(globalHandler);
// 현재 new 처리자가 있다면, new 처리자 호출
// 없다면, 예외 던지기
if(globalHandler) (*globalHandler)();
else throw bad_alloc();
}
}
코드 설명:
- 현재 new 처리자 함수를 알아내는 방법이 깔끔하지 않다
- C++11 이후부터
std::get_new_handler함수가 생겼다
- C++11 이후부터
- 무한 루프가 들어있으며, 빠져나오는 유일한 조건은:
- 메모리 할당 성공
- new 처리자 함수에서 처리 (항목 49 참조)
- 가용 메모리 늘림
- 다른 new 처리자 설치
- new 처리자 설치 제거
bad_alloc혹은bad_alloc파생 타입 예외 던짐 - 이를 지키지 않으면operator new내부 루프는 끝나지 않는다
🔄 operator new 함수의 상속 특성
중요한 특징:
operator new함수는 상속이 된다- 특정 클래스 전용의 할당자를 만들어 최적화하려 한다
- 특정 클래스란 ‘그’ 클래스 하나를 가리킬 뿐, 파생 클래스들도 뜻하는 것은 아니다
문제 상황:
X라는 클래스를 위한operator new함수가 있다- 해당 함수의 동작은 크기가
sizeof(X)인 객체에 맞추어져 있다 - 파생 클래스 객체의 메모리를 할당할 때, 기본 클래스의
operator new함수가 호출될 수 있다
- 해당 함수의 동작은 크기가
해결 방법:
- “틀린” 메모리 크기가 들어왔을 때, 표준
operator new를 호출하도록 하면 된다
class Base {
public:
static void* operator new(size_t size) throw(bad_alloc);
...
};
void* Base::operator new(size_t size) throw(bad_alloc) {
// "틀린" 크기가 들어오면,
// 표준 operator new 호출
if(size != sizeof(Base))
return ::operator new(size);
// 맞는 크기가 들어오면, 메모리 할당 요구 처리
...
}
class Derived: public Base { ... };
int main() {
Derived* p = new Derived;
}
0바이트 처리:
- 0바이트 점검 코드가 없는 것처럼 보이지만,
if(size != sizeof(Base))에서 처리되고 있다- 독립 구조 크기는 0이 넘어야 한다는 금기사항이 있다
- 그러므로
sizeof(Base)는 0이 될 일이 없다
📦 배열에 대한 메모리 할당
기본 원칙:
operator new[]함수를 구현하면 된다- 그러나 원시 메모리 덩어리를 할당할 수밖에 없다
malloc(size)와 같이 할당해야 한다
원시 메모리 덩어리를 할당할 수밖에 없는 이유:
- 배열 메모리 내 생성되지 않은 객체에 대해 알 수 없다
size_t만 받았을 뿐이라, 어떤 객체인지 알 수 없기 때문이다
- 객체 하나가 얼마나 큰지 확정할 수 없다
- 파생 클래스 객체의 배열을 할당할 때, 기본 클래스의
operator new[]함수가 호출될 수 있다 - 파생 클래스 객체는 대체적으로 기본 클래스 객체보다 크기가 크다
- 그러므로,
Base::operator new[]에서 할당한 배열 메모리에 들어가는 객체의 개수를
(필요한 바이트 수/sizeof(Base))로 계산할 수 없다
- 파생 클래스 객체의 배열을 할당할 때, 기본 클래스의
- operator new[]에 들어가는 size_t 타입의 인자는,
객체들을 담기에 맞는 메모리 양보다 더 많이 설정되어 있을 수도 있다- 동적 할당된 배열에는 배열 원소를 담기 위한 공간이 있다 (항목 16 참조)
🗑️ operator delete 구현
operator new보다 간단하다:
- 따로 메모리 할당할 필요가 없으니, new 처리자도 필요 없다
비멤버 버전 operator delete 함수 의사코드:
void operator delete(void* rawMemory) throw() {
if(rawMemory == 0) return;
"rawMemory가 가리키는 메모리 해제";
}
- null 포인터에 대해 delete를 하는 경우만 처리해주면 된다
클래스 버전 operator delete 함수 의사코드:
class Base {
public:
static void* operator new(size_t size) throw(bad_alloc);
static void operator delete(void* rawMemory, size_t size) throw();
...
};
void* Base::operator delete(void* rawMemory, size_t size) throw(bad_alloc) {
// 널 포인터 점검
if(rawMemory == 0) return;
// "틀린" 크기가 들어오면,
// 표준 operator new 호출
if(size != sizeof(Base)) {
::operator new(size);
return;
}
"rawMemory가 가리키는 메모리 해제";
return;
}
⚠️ 중요한 추가 사항:
- 가상 소멸자가 없는 기본 클래스로부터 파생된 클래스의 객체를 삭제하려는 경우,
operator delete로 C++가 넘기는size_t값이 잘못되었을 수도 있다 - 그러므로 기본 클래스에 가상 소멸자를 두어야 한다!
🧐 정리
operator new 함수:
- 비멤버 버전
- 메모리 할당을 반복해서 시도하는 무한 루프를 가져야 한다
- 메모리 할당 요구를 만족시킬 수 없을 때, new 처리자를 호출해야 한다
- 0바이트에 대한 대책도 있어야 한다
- 클래스 버전
- 자신이 할당하기로 예정된 크기보다 더 큰(틀린) 메모리 블록에 대한 요구도 처리해야 한다
operator delete 함수:
- 비멤버 버전
- 널 포인터가 들어왔을 때, 아무 일도 하지 않아야 한다
- 클래스 버전
- 클래스 전용 버전은 예정 크기보다 더 큰 블록을 처리해야 한다
댓글남기기