728x90
반응형

new 표현식을 사용했을 때 호출되는 두 가지 함수

Widget * pw = new Widget;

위에서는 함수 두 개가 호출된다.

우선 메모리 할당을 위해 operator new가 호출되고, 그 뒤를 이어 Widget의 기본 생성자가 호출된다.

여기서 첫 번째 함수 호출은 무사히 지나갔는데 두 번째 함수 호출이 진행되다가 예외가 발생했다고 가정해보자.

이렇게 사고가 났을 경우 첫 단계에서 이미 끝난 메모리 할당을 취소해야 한다. 왜냐하면 메모리 누수가 발생하기 때문이다.

하지만 Widget 생성자에서 예외가 튀어나오면 pw에 포인터가 대입될 일은 절대로 안 생기기 때문에 사용자 코드에서는 이 메모리를 해제할 수 없다. 따라서 이 메모리의 해제는 C++ 런타임 시스템이 맡아줘야 한다.

 

C++ 런타임 시스템이 해 주어야 하는 일

간단하게 호출한 operator new 함수와 짝이 되는 operator delete 함수를 호출해야 한다. 따라서 어떤 것을 호출해야 하는지 런타임 시스템이 제대로 알고 있어야 한다.

기본형 operator new는 기본형 operator delete와 짝을 맞춘다. 그래서 표준 형태의 new, delete만 사용하면 런타임 시스템은 new의 동작을 되돌릴 방법을 알고 있는 delete를 찾아내는 데 어려움이 없다.

그러면 위치 지정 new를 사용한다면 어떨까?

 

위치 지정 new

위치 지정 new란 개념적으로 operator new 함수와 달리 매개변수를 추가로 받는 형태로 선언한 new를 말한다.

따라서 위치 지정 new는 가지각색 일 수 있다. 이 중 유용한 하나에 대해 알아보자.

 

어떤 객체를 생성시킬 메모리 위치를 나타내는 포인터를 매개변수로 받는 new

void * operator new(std::size_t, void *pMemory) throw();

이렇게 포인터를 추가로 받는 형태의 위치 지정 new는 유용성을 인정받아 이미 C++ 표준 라이브러리의 일부로도 들어가 있다. <new>만 #include 하면 된다.

이 버전의 new 함수는 표준 라이브러리의 여러 군데에서 쓰이고 있는데, 특히 vector의 경우에는 해당 벡터의 미사용 공간에 원소 객체를 생성할 때 이 위치 지정 new를 쓰고 있다. 또한 위치 지정 new의 원조이기도 하다.

 

다시 원래의 내용으로 돌아가서,

Widget *pw = new (std::cerr) Widget; // operator new를 호출하는데 cerr을 ostream인자로 넘기는데, 이때 Widget 생성자에서 예외가 발생하면 메모리가 누출된다.

위의 코드를 보면 메모리 할당은 성공했지만 Widget생성자에서 예외가 발생했을 경우, operator new에서 저지른 할당을 되돌리는 일은 C++런타임 시스템이 책임지고 해야 한다. 그런데 런타임 시스템은 호출된 operator new가 어떻게 동작하는 지를 알아낼 방법이 없으므로, 자신이 할당 자체를 되돌릴 수는 없다.

그 대신, 런타임 시스템은 호출된 operator new가 받아들이는 매개변수의 개수 및 타입이 똑같은 버전의 operator delete를 찾고, 호출한다.

하지만 operator new와 짝을 이루는 operator delete가 존재하지 않으면 어떤 operator delete도 호출하지 않는다.

결론적으로 짝을 이루는 operator delete가 존재해야 하며, 이를 위치 지정 delete라고 한다.

 

주의!! 이름 가리기

바깥쪽 유효 범위에 있는 어떤 함수의 이름과 클래스 멤버 함수의 이름이 같으면 바깥쪽 유효 범위의 함수가 이름만 같아도 가려진다.(항목 33 참고)

때문에 사용자 자신이 쓸 수 있다고 생각하는 다른 new(표준 버전 포함)를 클래스 전용의 new가 가리지 않도록 신경 써야 한다.

 

C++가 제공하는 전역 유효 범위의 operator new의 형태 3가지 표준

void *operator new(std::size_t) throw(std::bad_alloc);          // 기본형
void *operator new(std::size_t, void*) throw();                 // 위치지정
void *operator new(std::size_t, const std::nothrow_t&) throw(); // 예외불가

어떤 형태든 operator new가 클래스 안에 선언되면 표준 형태들이 전부 가려진다.

그리고 사용자가 표준 형태를 쓰지 못하게 막는 것이 의도한 게 아니라면 표준 형태들도 사용자가 접근할 수 있도록 해야 한다. 간단한 방법으로는 아래처럼 기본 형태를 가지고 있는 기본 클래스 하나를 만들고 new 및 delete의 기본 형태를 전부 넣어둔 뒤, 이를 상속받아 사용하는 형태가 있다.

class StandardNewDeleteForms 
{
public: 
  // 기본형
  static void* operator new(std::size_t size) throw(std::bad_alloc)
  {
    return ::operator new(size);
  }
  static void operator delete(void* pMemory) throw()
  {
    ::operator delete(pMemory);
  }
  
  //위치지정
  static void* operator new(std::size_t size, void* ptr) throw()
  {
    return ::operator new(size, ptr);
  }
  static void operator delete(void* pMemory, void* ptr) throw()
  {
    ::operator delete(pMemory, ptr);
  }
  
  //예외불가
  static void* operator new(std::size_t size, const std::nothrow_t& nt) throw()
  {
    return ::operator new(size, nt);
  }
  static void operator delete(void* pMemory, const std::nothrow_t& nt) throw()
  {
    ::operator delete(pMemory);
  }
};

// 상속과 using 선언을 사용하여 표준 형태를 파생 클래스 쪽으로 끌어와 외부에서 사용할 수 있게 만든 후에 사용자 정의 형태를 선언한다.
class Widget : public StandardNewDeleteForms 
{
public : 
  using StandardNewDeleteForms::operator new; 
  using StandardNewDeleteForms::operator delete;
  static void* operator new(std::size_t size, std::ostream& logStream) throw(std::bad_alloc);
  static void operator delete(void* pMemory, std::ostream& logStream) throw();
};

 

요약

  • operator new 함수의 위치 지정 버전을 만들 때는, 이 함수와 짝을 이루는 위치 지정 버전의 operator delete 함수도 꼭 만들어주자. 이 일을 빼먹었다가는, 찾아내기도 힘들며 또 생겼다가 안 생겼다 하는 메모리 누출 현상을 경험하게 된다.
  • new 및 delete의 위치 지정 버전을 선언할 때는, 의도한 바도 아닌데 이들의 표준 버전이 가려지는 일이 생기지 않도록 주의해 주자.
728x90
반응형