목차

     

     


     

     

    Chapter 8  new와 delete를 내 맘대로


     

     

    항목49:  new 처리자의 동작 원리를 제대로 이해하자

     

    Q. new operator, delete operator를 클래스 내부 연산자로 오버로딩했습니다!  main()의 결과로 옳은 답은 몇번일까요?

    class Unseen
    {
    public:
    	Unseen()
    	{
    		cout << "Unseen()" << endl;	
    	}
    	
    	~Unseen()
    	{
    		cout << "~Unseen()" << endl;
    	}
    	
    	static void* operator new(size_t size) // static이 없어도 static인 것처럼 동작
    	{
    		cout << "Unseen new!" << size << endl;
    		void* ptr = ::malloc(size);
    		return ptr;
    	}
    
    	static void operator delete(void* ptr) // static이 없어도 static인 것처럼 동작
    	{
    		cout << "Unseen delete!" << endl;
    		::free(ptr);
    	}
    	
    	int32 _hp = 100;
    	int32 _mp = 10;
    }
    
    void* operator new[](size_t size)
    {
    	cout << “new[]! “ << size << endl;
    	void* ptr = ::malloc(size);
    	return ptr;
    }
    
    void operator delete[](void* ptr)
    {
    	cout << "delete[]!" << endl;
    	::free(ptr);
    }
    
    int main()
    {
    	Unseen* unseen = new Unseen();
    	delete unseen;
    }

     

    1번.

    Unseen()
    Unseen new! 8
    Unseen delete!
    ~Unseen()
    

    2번.

    Unseen new! 8
    Unseen()
    ~Unseen()
    Unseen delete!
    

    3번.

    Unseen new! 4
    Unseen()
    ~Unseen()
    Unseen delete!
    

     

    더보기

    답: 2번

    해제를 할 때에도 마찬가지로, 먼저 메모리를 해제한 후 소멸자를 호출해준다매크로로 만들어주는 것도 방법인데, 더 편한 방법은 Allocator 클래스, Memory 클래스 등을 만들어서 할당 정책을 관리하는 방법이라고 합니다.

    나쁜 방법은 아니지만 클래스마다 만들 때 넣어준다는 점이 귀찮고 아쉽습니다.

    먼저 메모리를 할당한 다음에, 생성자를 호출해줍니다

     

     

     

    Q.  아래와 같이 '믹스인 양식'을 사용해 '신기하게 반복되는 템플릿 패턴(curiously recurring remplate pattern: CRTP)'를 사용하면 좋은 점에 대해 말해보세요.

    template<typname T>
    class NewHandleerSupport{
    public:
        static new_handler set_new_handler(new_handler p) throw();
        static void* operator new(size_t size) throw(bad_alloc);
    
    private:
        static new_handler currentHandler;
    };
    
    template<typename T>
    new_handler
    NewHandleerSupport<T>::set_new_handler(new_handler p) throw()
    {
        new_handler oldHandler = currentHandler;
        currentHandler = p;
        return oldHandler;
    }
    
    template<typename T>
    void* NewHandlerSupport<T>::operator new(size_t size) throw(bad_alloc)
    {
        NewHandlerHolder h(set_new_handler(currentHandler));
        return ::operator new(size);
    }
    
    template<typename T>
    new_handler NewHandlerSupport<T>::currentHandler = 0;

     

     

     


     

     

    항목 50:  new 및 delete를 언제 바꿔야 좋은 소리를 들을지를 파악해 두자

     

    Q. new로 생성한 메모리에 delete를 하는 것을 잊어버리면 어떻게 될까요?

    더보기

    답:  메모리가 누출됩니다.

     

     

    Q. 생성한 메모리(= new한 메모리)를 두 번 이상 delete하면 무슨일이 벌어질까요?

    더보기

    답:  미정의 동작이 발생할 수 있습니다.  

    동일한 메모리를 다시 해제하려고 하면, 메모리 관리 시스템은 이미 해제된 메모리를 다시 해제하려는 시도를 이해할 수 없고, 이는 메모리 할당 테이블을 손상시키거나 프로그램이 잘못된 메모리 접근을 시도하도록 할 수 있습니다.

     

    int* ptr = new int;
    delete ptr;
    delete ptr;  // 두 번째 delete에서 오류 발생

    위 코드에서 두 번째 delete가 실행될 때, 이미 해제된 메모리를 다시 해제하려고 시도하기 때문에 문제가 발생합니다. 이러한 문제를 방지하기 위해, 포인터를 delete한 후에는 해당 포인터를 nullptr로 설정하는 것이 일반적인 관례입니다.

     

    [ 수정본 ]

    int* ptr = new int;
    delete ptr;
    ptr = nullptr;  // 포인터를 nullptr로 설정
    delete ptr;  // nullptr에 delete를 호출해도 안전함

    포인터가 더 이상 유효한 메모리를 가리키지 않도록 하여 double free 오류를 방지할 수 있습니다.

     

     

    Q.  사용자 정의 operator new를 사용하는 이유가 뭘까요?

    더보기

    답: 

    1.  잘못된 힙 사용을 탐지하기 위해 사용합니다.

    2.  동적 할당 메모리의 실제 사용에 관한 통계 정보를 수집하기 위해 사용합니다.

    3.  할당 및 해제 속력을 높이기 위해 사용합니다.

    4.  기존 메모리 관리자의 공간 오버헤드를 줄이기 위해 사용합니다.

    5.  임의의 관계를 맺고있는 객체들을 한 군데에 나란히 모아 놓기 위해 사용합니다.

    6.  그때그때 원하는 동작을 수행하도록 하기 위해 사용합니다.  

     

    사용자 정의 operator new를 사용하면, 요구된 크기보다 약간 더 메모리를 할당한 후에 사용자가 실제로 사용할 메모리의 앞과 뒤에 오버런/언더런 탐지용 바이트 패턴을 적어두도록 만들 수 있습니다.  또한 operator delete는 누군가가 이 경계표지에 손을 댔는지 안 댔는지 점검하도록 만듭니다. 만일 이 경계표지 부분에 원래와 다른 정보가 적혀 있다면 할당된 메모리 블록을 사용하는 도중에 오버런이나 언더런이 발생한 것이므로, operator delete는 이 사실을 로그로 기록함으로써 문제를 일으킨 포인터 값을 남겨 놓을 수 있습니다.  

    • 오버런(overrun):  할당된 메모리 블록의 끝을 넘어 뒤에 기록하는 것.
    • 언더런(underrun):   할당된 메모리 블록의 시작을 넘어 앞에 기록하는 것. 

     

     


     

     

     

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

     

    Q.  아래의 코드를 읽고 while(true)의 탈출 조건으로 틀린 것을 고르시오.

    void* operator new(std::size_t size) throw(std::bad_alloc)
    {
        using namespace std;
    
        if (size == 0) { // 0 바이트 요청이 들어오면, 1 바이트 요구로 간주
            size = 1;
        }
    
        while (true)
        {
            // size 바이트를 할당함
            if (할당에 성공)
            {
                return (할당된 메모리에 대한 포인터);
            }
    
            // 할당에 실패했을 경우, 현재의 new 처리자 함수가
            // 어느 것으로 설정되어 있는지 찾아낸다
            new_handler globalHandler = set_new_handler(0);
            set_new_handler(globalHandler);
    
            if (globalHandler) (*globalHandler)();
            else throw std::bad_alloc();
        }
    }

    1.  메모리 할당이 성공한 경우

    2.  new 처리자 함수가 가용 메모리를 늘려주는 경우

    3.  new 처리자 함수가 다른 new 처리자 함수를 설치하는 경우

    4.  bad_alloc 혹은 bad_alloc에서 파생된 타입의 예외를 던지는 경우

    5.  size 바이트가 변경된 경우

    더보기

    답:  5번.  size 값은 루프의 조건이나 탈출과 직접적인 연관이 없습니다. 

     

    ※ 참고) C++에는 모든 독립 구조의 객체는 반드시 크기가 0이 넘어야 한다.

    항목 39 참고

    2024.05.07 - [⭐ Programming/Effective C++] - [C++] Effective C++ : Chapter 6 상속, 그리고 객체 지향 설계


     

     

    항목 52:  

     

    Q.  아래 코드를 읽고 야기될 수 있는 문제점에 대해 설명해보세요.

    // 전역 operator new의 표준 형태 
    void* operator new(std::size_t size) throw(std::bad_alloc); // 기본
    void* operator new(std::size_t size, void* ptr) throw(); // 위치지정
    void* operator new(std::size_t size, const std::nothrow_t& nt) throw(); // 예외불가
    
    class Base{
    public:
    	static void* operator new(std::size_t size, std::ostream& logStream) throw();	
    	...
    };
    
    class Derived: public Base{
    public:
    	static void* operator new(std::size_t size) throw();
    	...
    };
    
    int main()
    {
        Base *pb = new Base;
        Base *pb = new (std::cerr) Base;
    
        Derived *pd = new (std::clog) Derived;
        Derived *pd = new Derived;
    }
    더보기

    Base 클래스 내의 operator new는 기존의 표준 operator new 를 가립니다.

    Derived 클래스 내에서 기본형 new 를 클래스 전용으로 다시 선언

     

    int main()
    {
        Base *pb = new Base; 			      // 에러. 전역의 operator new가 가려져 있음.
        Base *pb = new (std::cerr) Base; 	  // Base 클래스의 위치지정 new를 호출.
    
        Derived *pd = new (std::clog) Derived; // 에러. Base 클래스의 위치지정 new가 가려져 있음.
        Derived *pd = new Derived;			   // 정상.
    }