Notice
Recent Posts
Recent Comments
Link
«   2026/05   »
1 2
3 4 5 6 7 8 9
10 11 12 13 14 15 16
17 18 19 20 21 22 23
24 25 26 27 28 29 30
31
Archives
Today
Total
관리 메뉴

로또

51. new 및 delete를 작성할 때 따라야 할 기존의 관례를 잘 알아 두자 본문

책/Effective C++

51. new 및 delete를 작성할 때 따라야 할 기존의 관례를 잘 알아 두자

아롱로또 2024. 1. 3. 18:37

다루는 내용

operator new와 delete를 작성할 때 반드시 지켜야 하는 점을 알아보자. 또한 operator new와 delete에 대한 클래스 버전을 만들 때 주의해야 할 점을 다룬다.

operator new의 요구사항

  1. 제대로 된 반환값을 가져야 한다.
  2. 가용 메모리가 부족할 경우에는 new 처리자 함수를 호출해야 한다.
  3. 0바이트 메모리 요청에 대한 대비책을 갖춰두어야 한다.
  4. 기본 형태의 new가 가려지지 않아야 한다.

이러한 요구 사항을 의사 코드로 나타내면 다음과 같다.

void* operator new(std::size_t size)
{
    using namespace std;
    
    if(size == 0) size = 1;
    while(true){
        // size byte 할당 성공
        if(할당 성공) return 할당된 메모리에 대한 포인터;

        // 할당 실패
        // 현재의 new 처리자 함수 확인
        // set_new_handler는 기존의 handler 함수를 반환한다.
        new_handler global_handler = set_new_handler(0);
        set_new_handler(global_handler);

        if(global_handler) (*global_handler)();
        else throw std::bad_alloc();
    }
}

operator new는 메모리 할당이 실패할 때마다 new 처리자 함수를 호출하는 식으로 메모리 할당을 2회 이상 시도한다. operator new가 예외를 던지는 경우는 오직 new 처리자 함수에 대한 포인터가 null일 때 뿐이다.

 

앞서 다룬 "49. new 처리자의 동작 원리를 제대로 이해하자."에서 new 처리자가 해야 할 일을 다루었다. 가용 메모리를 늘리든가, 다른 new 처리자를 설치하든가, new 처리자를 제거하든가, bad_alloc 계열의 예외를 던지든가, 함수 복귀를 포기하고 도중 중단을 시켜야 한다. 이 중 하나를 하지 않는다면 operator new 함수는 무한 루프에 빠져버림을 위 코드를 통해 알 수 있다.

 

클래스 전용의 operator new

operator new 멤버 함수는 파생 클래스에게 상속이 되는 함수이므로 주의할 점이 있다.

특정 클래스 전용의 operator new를 만든다고 가정해보자.

class Base{
public:
    static void* operator new(std::size_t size);
    ...
};

class Derived: public Base{...}; // operator new가 존재하지 않음.

Derived* p = new Derived; // Base::operator new 호출

 Base 클래스 전용의 operator new에서는 이런 상황에 대해 적절한 조치를 취해주어야 한다.

그 예로, 틀린 메모리 크기가 들어왔을 때, 표준 operator new를 호출하도록 만들어주는 것이다.

void* Base::operator new(std::size_t size)
{
    if(size != sizeof(Base))
        return ::operator new(size);
    ...
}

이 때, 앞서 언급했던 0바이트 점검 코드가 사라진 것처럼 보일 수 있다.

하지만 C++에서는 모든 독립 구조 객체는 반드시 크기가 0이 넘어야 한다는 사항이 존재한다. 때문에 sizeof(Base)는 절대 0이 될 수 없으며, size가 0이면 기본 버전의 operator new가 호출되어 적절한 처리를 해줄 것이다.

 

클래스 전용의 operator new [ ]

operator new[]를 직접 구현할 수도 있으나, 다음 이야기를 잊지 말자.

 

operator new[] 안에서 해 줄 일은 단순히 원시 메모리의 덩어리를 할당하는 것밖에 없다. 이 시점에서는 배열 메모리에 아직 생기지도 않은 클래스 객체에 대해 아무것도 할 수 없기 때문이다.

 

앞서 언급한 상속으로 인해 파생 클래스 객체의 배열을 할당하는 데 기본 클래스의 operator new[] 함수가 호출될 수 있고 일반적으로 파생 클래스 객체는 기본 클래스 객체보다 크다. 때문에 Base::operator new[] 에서 배열에 들어가는 객체 하나의 크기가 sizeof(Base)라고 가정할 수 없다.

또한, operator new[]에 전달되는 size_t 타입의 인자는 객체들을 담기에 딱 맞는 메모리 양보다 더 많게 설정될 수 있다. 동적으로 할당된 배열 원소의 개수를 담기 위한 공간을 추가적으로 필요로 하기 때문이다.

 

operator delete의 요구사항

C++은 널 포인터에 대한 delete 적용이 항상 안전하도록 보장한다. 이 사실만 잊지 말자. 우리는 operator delete를 작성할 때 이 사실만 기억하면 된다.

아래는 비멤버 버전 delete의 간단한 예시 코드이다.

void operator delete(void* raw_memory)
{
    if(raw_memory == 0) return;
    ...
    // raw_memory가 가리키는 메모리 해제
}

클래스 전용의 operator delete

클래스 전용 delete는 비멤버 delete와 달리 삭제될 메모리의 크기를 점검하는 코드를 넣어주어야 한다.

클래스 전용 버전의 operator new가 틀린 크기의 메모리 요청을 ::operator new로 넘기도록 구현되었다고 가정하면, 클래스 전용 버전의 operator delete 역시 틀린 크기로 할당된 메모리의 삭제 요청을 ::operator delete로 넘기도록 구현할 수 있을 것이다.

class Base{
public:
    static void* operator new(std::size_t size);
    static void operator delete(void* raw_memory, std::size_t size);
};

void Base::operator delete(void* raw_memory, std::size_t size)
{
    if(raw_memory == 0) return;
    if(size != sizeof(Base)){
        ::operator delete(raw_memory);
        return;
    }
    // raw_memory 해제
    ...
}

 

또한, 기본 클래스에서 소멸자를 가상으로 선언하는 것을 잊으면 operator delete 함수가 똑바로 동작하지 않을 수 있다.

가상 소멸자가 없는 기본 클래스로부터 파생된 클래스의 객체를 삭제하려고 할 경우에는, operator delete로 C++가 넘기는 size_t 값이 엉터리일 수 있기 때문이다.

 

이것만은 잊지 말자!

  • 관례적으로, operator new 함수는 메모리 할당을 반복해서 시도하는 무한 루프를 가져야 하고, 메모리 할당 요구를 만족시킬 수 없을 때 new 처리자를 호출해야 하며, 0바이트에 대한 대책도 있어야 합니다. 클래스 전용 버전은 자신이 할당하기로 예정된 크기보다 더 큰(틀린) 메모리 블록에 대한 요구도 처리해야 합니다.
  • operator delete 함수는 널 포인터가 들어왔을 때 아무 일도 하지 않아야 합니다. 클래스 전용 버전의 경우에는 예정 크기보다 더 큰 블록을 처리해야 합니다.