728x90

이 글은 [인프런-게임서버 만들기 by rookiss]를 내용을 토대로 개인적으로 학습한 내용을 종합하여 정리 했습니다.

 

CustomAllocator를 만들기 앞서 핵심이 되는 new, delete 연산자 오버로딩에 대한 설명이 필요하다.

new, delete 연산자는 메모리를 할당, 해제하는 연산자로 이 연산자를 오버로딩 하여 메모리를 효과적으로 관리할 수 있다. 

 

하지만 다양한 메모리 할당 전략(컴파일러에 의한)을 모른다면 오히려 독이 될 수도 있다. 그러니 다양한 메모리 할당, 해제 원리나 정책을 이해하고 사용 했을 때 new, delete 오버로딩은 빛을 발할 것이다. 하지만 효율적인 메모리 관리 또는 순수한 학습용이 아니라도 디버깅(개발시)에 메모리 이슈를 해결하기 위한 안전 도구를 만드는데도 유용할 것으로 보인다. 실제로 이 글은 메모리를 효과적으로 관리하기 위한 기법메모리 이슈를 쉽게 잡아 내기 위한 기법 두가지를 설명하는 것을 목표로 한다.

 

자, 그럼 new, delete 연산자의 오버로딩에 대해서 알아보자.

 

new 와 delete의 구체적인 동작 방식

new 표현식

  Knight* knight = new Knight();

위 문장을 new-표현식이라고 합니다. 이 문장은 두가지 일을 합니다. 먼저 operator new를 호출하여 Knight 객체에 대한 메모리를 할당하고, 객체의 생성자를 호출합니다. 생성자 실행이 끝나고 나서야 객체에 대한 포인터가 리턴 됩니다.

 

실제로 어셈블러를 통해서 'operator new 호출 -> 생성자 호출 -> 객체 주소 리턴' 되는 과정을 알 수 있습니다.

흥미로운 점은 operator new가 실제로 void*를 반환하는데 이때 바로 객체의 포인터를 포인터 변수에 대입하지 않고 생성자 부터 호출되네요. 그리고 생성자가 호출되기 전에 cmp 비교를 통해서 분기가 나뉘는 것도 볼 수 있네요... 아직은 제가 이해하기 힘든 영역 입니다... 

 

delete 표현식

delete knight;

위 문장은 delete-표현식이라고 합니다. 이 문장 역시 new-표현식과 마찬가지로 두가지 일을 합니다. 먼저 소멸자를 호출하고 operator delete를 호출해서 메모리르 해제 합니다.

 

new 표현식과 다르게 'scalar deleting destructor'를 호출 하고 scalar deleting destructor 내부에서 소멸자와 operator delete를 호출하는 것을 확인 할 수 있습니다. scalar deleteting destructor는 컴파일러가 사용하는 함수라고 하는데 왜 new-표현식과 다르게 동작하는지는 잘 모르겠습니다.. 추측하자면 어떤 버전의 operator delete 연산자가 호출 될지는 호출된 operator new 버전에 따라 바뀌기 때문에 어떤 operator delete를 호출해야 할지에 대한 정보가 필요하기 때문에.. 이런 과정을 거치는게 아닐까 생각 해봅니다... 

 

요약하자면,

new-표현식은 ==> '메모리 할당 -> 생성자 호출'

delete-표현식은 ==>  '소멸자 -> 메모리 해제'

 

여기서 또 한가지 중요한 부분은 new, delete 표현식 자체는 오버라이딩 할 수 없습니다. 단지 new, delete 연산자를 오버로딩 하는 것 입니다. 즉 메모리 할당, 해제를 커스터마이징 할 수 있는 것이지요.

 

new 표현식과 operator new

new-표현식에는 여섯가지 종류가 있고, 각 버전마다 적용되는 operator new가 따로 있다.

 

그중 2개는 배치 new 연산자로 placement new operator라 부른다. 이 연산자를 사용하면 기존에 확보된 메모리에서 객체를 생성하고(생성자를 호출) 한다. 이는 메모리풀을 만들 때 유용하다. 또한 이 배치 new 연산자는 오버로딩이 금지되어 있다.

//일반
void* operator new(size_t size);
void* operator new[](size t_size);

//익센션
void* operator new(size_t size, const std::nothrow_t&) noexcept;
void* operator new[](size_t size, const std::nothrow_t&) noexcept;

//placement new operator(오버로딩 금지)
void* operator new(size_t size, void* p) noexcept;
void* operator new[](size_t size, void* p) noexcept;

 

delete 표현식과 operator delete

delete-표현식은 두가지 종류가 있다. 하지만 operator delete는 operator new와 동일하게 6종류가 있다. 왜 일까?

operator delete는 operator new와 항상 대응되어 호출된다. operator new 종류를 보면 일반버전 2개, 익센션버전2개, 배치버전 2개가 있다. operator new의 익센션 버전이 호출된단다면 operator delete 익센션 버전이 호출된다. 하지만 이는 개발자가 예측하고 처리하는 부분이 아니다. 따라소 익센션 버전 delete-표현식은 존재하지 않는다. 배치버전 delete-표현식 역시 이와 동일한 이유다.

// 일반
void operator delete(void* ptr) noexcept;
void operator delete[](void* ptr) noexcept;

// 익셉션
void operator delete(void* ptr, const std::nothrow_t&) noexcept;
void operator delete[](void* ptr, const std::nothrow_t&) noexcept;

// placement delete operator
void operator delete(void* p, void*) noexcept;
void operator delete[](void* p, void*) noexcept;

 

operator new와 operator delete 오버로딩 하기

특정 new, delete 연산자를 오버로딩하여 내부적으로는 전역 new,delete 연산자를 호출하는 예제입니다.

#include <iostream>
using namespace std;

class MemoryDemo
{
public:
    virtual ~MemoryDemo() = default;

    //normal.
    void* operator new(size_t size) { cout << "operator new" << endl; return ::operator new(size); }
    void operator delete(void* ptr) noexcept { cout << "operator delete" << endl; ::operator delete(ptr); }

    void* operator new[](size_t size) { cout << "operator new[]" << endl; return ::operator new[](size); }
    void operator delete[](void* ptr) { cout << "operator delete[]" << endl; ::operator delete[](ptr); }

    //exception.
    void* operator new(size_t size, const std::nothrow_t&) noexcept { cout << "operator new nothrow" << endl; return ::operator new(size, nothrow); }
    void operator delete(void* ptr, const std::nothrow_t&) noexcept { cout << "operator delete nothrow" << endl; ::operator delete(ptr, nothrow); }

    void* operator new[](size_t size, const std::nothrow_t&) noexcept { cout << "operator new[] nothrow" << endl; return ::operator new[](size, nothrow); }
    void operator delete[](void* ptr, const std::nothrow_t&) noexcept { cout << "operator delete[] nothrow" << endl; ::operator delete[](ptr, nothrow); }

    //placement new 는 오버로딩 불가능.
};



int main()
{
    MemoryDemo* mem = new MemoryDemo();
    delete mem;
    mem = new MemoryDemo[10];
    delete[] mem;
    mem = new (nothrow) MemoryDemo();
    delete mem;
    mem = new (nothrow) MemoryDemo[10];
    delete[] mem;
}

 

new, delete 연산자 오버로딩시 주의할 점

- 전역 operator new와 delete 는 교체하지 말 것. 최소한 교체 했다면 그 내부에서 또다시 new를 호출 하지 말 것. 무한 재귀가 될 수도 있다.

- operator new를 오버로딩 하면 반드시 대응되는 operator delete도 오버로딩 할 것. 대응되게 하지 않으면 기본 c++메모리 정책을 따르므로 할당-해제 정책이 달라지는 결과 초래한다.

- operator new, delete를 오버로딩 했다면 default, delete를 사용하여 operator new, delete 여러 타입들의 사용, 미사용 여부를 명확하게 할 것. 메모리 할당, 해제 정책의 일관성을 유지하기 위해서.

 

operator new와 operator delete에 매개변수를 추가하여 오버로딩하기

#include <iostream>
using namespace std;

class MemoryDemo
{
public:
    virtual ~MemoryDemo() = default;

    void operator delete(void* ptr) noexcept 
    { 
        cout << "operator delete" << endl; 
        ::operator delete(ptr); 
    }

    //custom
    void* operator new(size_t size, int extra)
    { 
        cout << "operator new with extra int : " << extra << endl; 
        return ::operator new(size); 
    }

    // 이 delete를 직접 호출할 순 없다. operator new호출 할때 그객체의 생성자에서 익셉션을 던져야 호출된다.
    void operator delete(void* ptr, int extra) noexcept  
    { 
        cout << "operator delete with extra int : " << extra << endl; 
        ::operator delete(ptr);
    }
};



int main()
{
    MemoryDemo* mem = new (__LINE__) MemoryDemo();
    delete mem;
}

짝꿍으로 custom 오버로딩한 operator delete는 직접 호출되지 않는다. 그래서 위 코드에서는 기본 delete가 호출된다.

이 부분은 아직 잘 이해를 못하겠다. 기본 delete를 정의하지 않으면 delete 키워드는 적절한 operator delete를 찾을 수 없다고 불평을 한다. 

 

매개변수를 추가하여 메모리 누수가 발생한 지점의 파일 이름, 줄번호 등을 받아서 로그를 남길 수도 있다.

 

관련 C 매크로

__FILE__ :: 파일이름

__LINE__ :: 줄번호

__FUNCTION__ or __func__ :: 함수 이름

 

operator delete에 메모리 크기를 매개변수로 전달 하도록 오버로딩 하기

주의 할점은 매개변수를 받지 않는 operator delete를 사용하면 매개변수 없는 버전이 먼저 호출 되므로 매개변수 있는 타입을 쓰려면 그 버전만 정의해라.

#include <iostream>
using namespace std;

class MemoryDemo
{
public:
    virtual ~MemoryDemo() = default;

    //custom
    void* operator new(size_t size)
    { 
        cout << "operator new " << endl; 
        return ::operator new(size); 
    }

    // 이 delete를 직접 호출할 순 없다. operator new호출 할때 그객체의 생성자에서 익셉션을 던져야 호출된다.
    void operator delete(void* ptr, size_t size)  
    { 
        cout << "operator delete with extra int : " << size << endl;
        ::operator delete(ptr);
    }
};



int main()
{
    MemoryDemo* mem = new MemoryDemo();
    delete mem;
}

 

 

728x90

'Programming Language > C++' 카테고리의 다른 글

CustomAllocator] 3단계_메모리 페이징  (0) 2022.02.14
CustomAllocator] 2단계_BaseAllocator  (0) 2022.02.14
::/스코프 지정자  (0) 2022.02.11
std::function  (0) 2022.02.10
Modern C++/lambda/람다  (0) 2022.02.09

+ Recent posts