[C++] 항목 51: new 및 delete를 작성할 때 따라야 할 기존의 관례를 잘 알아두자

게시:     수정

카테고리:

태그:

이 글은 아래의 책을 자세히 정리한 후, 정리한 글을 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 함수가 생겼다
  • 무한 루프가 들어있으며, 빠져나오는 유일한 조건은:
    1. 메모리 할당 성공
    2. 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)와 같이 할당해야 한다

원시 메모리 덩어리를 할당할 수밖에 없는 이유:

  1. 배열 메모리 내 생성되지 않은 객체에 대해 알 수 없다
    • size_t만 받았을 뿐이라, 어떤 객체인지 알 수 없기 때문이다
  2. 객체 하나가 얼마나 큰지 확정할 수 없다
    • 파생 클래스 객체의 배열을 할당할 때, 기본 클래스의 operator new[] 함수가 호출될 수 있다
    • 파생 클래스 객체는 대체적으로 기본 클래스 객체보다 크기가 크다
    • 그러므로, Base::operator new[]에서 할당한 배열 메모리에 들어가는 객체의 개수를
      (필요한 바이트 수/sizeof(Base))로 계산할 수 없다
  3. 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 함수:

  • 비멤버 버전
    • 널 포인터가 들어왔을 때, 아무 일도 하지 않아야 한다
  • 클래스 버전
    • 클래스 전용 버전은 예정 크기보다 더 큰 블록을 처리해야 한다

Cpp 카테고리 내 다른 글 보러가기

댓글남기기