스토리텔링 개발자

[More Effective C++] 8. new / delete 연산자와 operator new / delete 본문

개발/More Effective C++

[More Effective C++] 8. new / delete 연산자와 operator new / delete

김디트 2024. 8. 8. 11:12
728x90

항목 8 : new와 delete의 의미를 정확히 구분하고 이해하자

 

 

 

‘new 연산자’와 ‘operator new’의 차이
string* ps = new string("Memory Management"); // new 연산자가 사용되었다.
  • new 연산자
    • C++에서 기본으로 제공한다.
    • sizeof가 그런 것처럼, 동작 원리를 바꾸는 것이 불가능하다.
    • 동작 단계
      1. 요청 타입의 객체를 담을 수 있는 크기의 메모리를 할당한다.(이 동작이 operator new 함수 호출로 동작된다.)
      2. 객체의 생성자를 호출하여 할당된 메모리의 객체 초기화를 수행한다.
  • operator new
    • 객체로 담을 메모리를 할당하는 방법이다.
    • new 연산자는 필수적인 메모리 할당을 위해 operator new를 호출하게 되어 있다.

 

 

 

operator new의 형태
void* operator new(size_t size);
  • 반환 타입 void*
    • 이 함수는 초기화되지 않은 원시 메모리의 포인터를 반환한다.
    • 초기화하고 반환하도록 커스텀할 수도 있겠지만, 대개 그렇게 하지 않는다.
  • size_t 매개변수
    • 할당된 메모리의 크기를 지정한다.
    • 매개변수를 추가한 버전을 오버로딩(위치지정 new) 할 수 있다.
      • 하지만 첫번째 매개변수는 항상 size_t여야 한다.(위치지정 new)
    • 위치지정 new(Effective C++ 항목 52 참조)

 

 

 

operator new의 특징
  • operator new는 직접 호출할 수도 있다.
void* rawMemory = operator new(sizeof(string)); // operator new 직접 호출
  • operator new는 메모리만 할당한다.
    • malloc과 마찬가지의 역할만 맡는다. 
    • 그러므로 어떤 생성자를 호출해줘야 할 지는 알 방도가 없다.
      • 할당된 메모리를 받아서 객체 구실을 할 수 있도록 하는 것은 new 연산자의 일이다.
// 컴파일러가 다음 문장을 만난다면?
string* ps = new string("Memory Management");

// 아래와 비슷한 코드를 만들어낼 것이다.
void* memory = operator new(sizeof(string)); // 1. raw 메모리 획득
... // 2. *memory에 대해 string::string("Memory Management")를 호출한다.(객체를 초기화한다.)
string* ps = static_cast<string*>(memory): // 3. ps가 새 객체를 가리키게 한다.
  • 위 예제의 2번 과정(raw 메모리에 대해 생성자 호출 단계)은 유저가 손 댈 수 있는 부분이 아니다.
    • 그러므로 객체를 힙에 할당하려면 방법은 컴파일러가 제공하는 new 연산자를 사용하는 법 뿐이다.
    • 이렇게 유저가 손댈 수 없는 성질의 것은 vtbl도 포함된다.(항목 24 참조)

 

 

 

위치지정 new(placement new)
  • 그럼에도 미초기화 메모리가 덩그러니 있고, 생성자를 호출해주고 싶은 상황이 있을 수 있다.
  • 그 경우에는 위치지정 new를 사용하여 new 연산자 로직을 통과시켜 객체를 초기화 시켜줄 수 있다.
class Widget
{
public:
    Widget(int widgetSize);
    ...
};

// 위치지정 new로 raw 메모리를 그저 반환해주는 operatr new 를 만든다.
// size_t는 사용하지 않으므로 이름이 없도록 처리한다.(항목 6 참조)
// #include <new>로 포함하면 위치지정 new를 아래처럼 직접 구현할 필요조차 없다.
void* operator new(size_t, void* location)
{
    return location;
}

Widget* constructWidgetInBuffer(void* buffer, int widgetSize)
{
    // 위치지정 new를 통해 new 연산자 로직을 통과시킬 수 있다.
    return new (buffer) Widget(widgetSize);
}

 

 

 

객체 삭제와 메모리 해제(deletion & deallocation)
  • delete 연산자 / operator delete 역시 new의 경우와 동일하다.
void operator delete(void* memoryToBeDeallocated); // operator delete

string* ps;
...
delete ps; // delete 연산자를 사용한다.

// delete ps; 에 대해 컴파일러는 아래와 비슷한 코드를 만들어낸다.
ps->~string(); // 소멸자 호출
operator delete(Ps); // operator delete 함수 호출로 메모리 해제

 

 

 

 유의사항
  • raw 메모리만으로 무언가를 할 때는?
    • new / delete 연산자를 건너뛰고 operator new / delete를 사용해야 한다.
    • 즉 operator new / delete는 C++ 버전의 malloc / free인 셈이다.
    • 그 이유는, 생성자, 소멸자 호출이 불필요하기 때문이다.
void* buffer = operator new(50 * sizeof(char)); // 50개 문자를 담는 메모리 할당
...
operator delete(buffer); // 소멸자 호출 없이 메모리 해제
  • 위치지정 new를 사용한 메모리에는 절대로 delete 연산자를 쓰지 말아야 한다.
    • 원칙적으로 operator new로 할당된 것이 아니기 때문이다.
    • 사용한 그 메모리 포인터가 어디에서 왔는지는 유저만이 알 것이다.
    • 즉, 생성자가 한 일을 돌려놓기만 하되(소멸자만 호출하되), 메모리 해제는 불필요하다.
// 공유 메모리에 메모리 할당, 해제 함수
void* mallocShared(size_t size);
void freeShared(void* memory);


void* sharedMemory = mallocShared(sizeof(Widget)); // 공유 메모리 할당
Widget* pw = constructWidgetInBuffer(sharedMemory, 10); // 위치지정 new로 생성자 호출

...

delete pw; // 미정의 결과!!

pw->~Widget(); // 소멸자만 호출한 후,
freeShared(pw); // 공유 메모리 해제(문제 없음)

 

 

 

배열 할당의 경우
string* ps = new string[10]; // 객체 배열을 할당한다.
// 이 경우 new 연산자는 어떻게 동작할까?
  • 배열 new 연산자와 단일 객체 new 연산자의 차이점
    1. 메모리를 할당할 때 배열 할당용 버전인 operator new[]를 호출한다.("array new")
      • 오버로딩이 가능하다.
      • 허나, operator new[]를 지원하지 않는 컴파일러 환경에서는, 오버로딩을 매우 매우 신중하게 생각해야 한다.
        • 이 경우 operator new의 전역 함수 버전을 사용하여 배열을 할당한다.
        • 헌데 operator new 전역 함수 버전을 다시 작성한다는 건 절대 가벼운 일이 아니다.
        • operator new의 함수 매커니즘이 변경되면 어떤 이슈가 생길지 예측할 수 없으므로.
        • 더군다나 다른 라이브러리와 호환되지 않게 된다.(항목 27 참조)
    2. 생성자를 호출하는 횟수가 더 많다.
      • 배열 요소에 대해 일일이 생성자를 호출하기 때문이다.
string* ps = new string[10]; // new 연산자. operator new[] 호출 후 각 요소에 생성자 호출
...
delete[] ps; // delete 연산자. 각 요소에 소멸자 호출 후 operator delete[] 호출

 

 

 

결론
  • new / delete 연산자는 C++에서 기본으로 제공되는 것이므로 바꿀 수 없다.
  • 하지만 내부적으로 사용하는 메모리 할당, 해제 함수는 고칠 수 있다.(operator new / delete)

 

 

 

참조
 

[Effective C++] 50. operator new / delete는 언제 커스텀해야 할까?

항목 50 : new 및 delete를 언제 바꿔야 좋은 소리를 들을지를 파악해두자   operator new / delete를 커스텀하는 이유잘못된 힙 사용 탐지를 위해new 한 메모리에 대해 delete를 잊으면 메모리 누수가 발생

delightlane.tistory.com

 

[Effective C++] 51. operator new / delete 커스텀 관례

항목 51. new 및 delete를 작성할 때 따라야 할 기존의 관례를 잘 알아두자   operator new 구현 시 요구사항반환값이 제대로 되어 있어야 한다.가용 메모리가 부족할 경우 new 처리자 함수를 호출해야

delightlane.tistory.com

 

[Effective C++] 52. 위치지정 new / delete

항목 52 : 위치 지정 new를 작성한다면 위치 지정 delete도 같이 준비하자.   new 중 기본 생성자에서 예외가 발생한다면?Widget* pw = new Widget;위 코드는 실행 중 두 개의 함수가 순차적으로 호출된다.

delightlane.tistory.com

 

728x90
Comments