Notice
Recent Posts
Recent Comments
Link
«   2024/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
05-19 10:17
관리 메뉴

nomad-programmer

[Programming/C++] new/delete 연산자 오버로딩 본문

Programming/C++

[Programming/C++] new/delete 연산자 오버로딩

scii 2021. 2. 5. 19:24

new와 delete도 연산자이기 때문에 오버로딩이 가능하다.

new 연산자

기본적으로 제공되는 new 연산자가 하는 일은 다음과 같다.

  1. 메모리 공간의 할당
  2. 생성자의 호출
  3. 할당하고자 하는 자료형에 맞게 반환된 주소 값의 형 변환

이 중 세 번째 내용은 C언어에서 사용하면 malloc함수와 달리, new 연산자가 반환하는 주소 값을 형 변환할 필요가 없음을 의미한다.

new 연산자의 오버로딩은 1번에 해당하는 메모리 공간의 할당만 오버로딩할 수 있다. 나머지 두 가지 작업은 C++ 컴파일러에 의해 진행이 되며, 오버로딩할 수 있는 대상도 아니다.

new 연산자 오버로딩은 다음과 같이 오버로딩 하도록 이미 약속이 되어있다.

void* operator new(size_t size) { ... }

반환형은 반드시 void* 형이어야 하고, 매개변수형은 size_t이어야 한다. 그리고 이렇게 오버로딩 된 함수는 컴파일러에 의해서 호출이 이뤄진다. 컴파일러는 Point 클래스를 대상으로 new 연산자가 오버로딩 된 상태에서 다음 문장을 만나면,

Point* ptr = new Point(5, 5);

먼저 필요한 메모리 공간을 계산한다. 그리고 그 크기가 계산되면, operator new 함수를 호출하면서 계산된 크기의 값을 인자로 전달한다. 여기서 중요한 것은 크기 정보는 "바이트 단위"로 계산되어 전달된다는 점이다. 따라서 다음의 형태로 operator new 함수를 정의해야 한다.

void* operator new(size_t size) {
  void* adr = new char[size];
  return adr;
}

컴파일러에 의해 필요한 메모리 공간의 크기가 바이트 단위로 계산되어 인자로 전달되니, 크기가 1바이트인 char 단위로 메모리 공간을 할당해서 반환한다. 
이는 operator new 함수가 반드시 해야 할 '메모리 공간의 할당' 이다. 

이렇게 해서 operator new 함수가 할당한 메모리 공간의 주소 값을 반환하면, 컴파일러는 생성자를 호출해서 메모리 공간을 대상으로 초기화를 진행한다. 그리고 완성된 객체의 주소 값을 Point 클래스의 포인터 형으로 형 변환해서 반환을 한다.

Point* ptr = new Point(5, 5);

위의 문장에서 new 연산자가 반환하는 값은 operator new 함수가 반환하는 값이 아니다. operator new 함수가 반환하는 값은, 컴파일러에 의해 적절히 형 변환이 된 값이다. 또한 생성자의 호출정보는 operator new 함수와 아무런 상관이 없다. 생성자의 호출은 여전히 컴파일러의 몫이기 때문에 이 정보는 컴파일러에 의해서 참조될 뿐이다.

size_t

일반적으로 size_t는 다음과 같이 정의되어 있다.
typedef unsigned int size_t;

그래서 0 이상의 값을 표현할 목적으로 정의된 자료형이다.

delete 연산자

Point* ptr = new Point(5, 5);

delete ptr;

delete ptr; 문장으로 객체 소멸을 명령하면, 컴파일러는 먼저 ptr이 가리키는 객체의 소멸자를 호출한다. 그리고 다음의 형태로 정의된 함수에 ptr에 저장된 주소 값을 전달한다.

void operator delete(void* adr) { ... }

따라서 delete함수는 다음의 형태로 정의해야 한다. 즉, 소멸자는 오버로딩 된 함수가 호출되기 전에 호출이 되니 오버로딩 된 함수에서는 메모리 공간의 소멸을 책임져야 한다.

void operator delete(void* adr) {
  delete[] adr;
}

참고로, 사용하는 컴파일러에서 void 포인터 형 대상의 delete 연산을 허용하지 않는다면, 위의 delete문을 다음과 같이 작성하면 된다. 즉, char포인터 형으로 변환해서 delete연산을 진행하면 된다.

delete[] ((char*)adr);

#include <iostream>
#include <cstdlib>

#pragma warning(disable: 4996)

using std::cout;
using std::cin;
using std::endl;


class Point {
private:
    int xpos, ypos;

public:
    explicit Point(int x=0, int y=0) : xpos(x), ypos(y) {}
    friend std::ostream& operator<<(std::ostream& os, const Point& pos);

    static void* operator new(const size_t size) {
        cout << "operator new: " << size << endl;
        void* adr = new char[size];
        return adr;
    }

    void operator delete(void* adr) {
        cout << "operator delete()" << endl;
        delete[] adr;
    }
};

std::ostream& operator<<(std::ostream& os, const Point& pos) {
    os << '[' << pos.xpos << ", " << pos.ypos << ']' << endl;
    return os;
}


 int main(const int argc, const char* const argv[]) { 
     Point* ptr = new Point(3, 4);

     Point* ptr2 = (Point*)Point::operator new(sizeof(Point));
     *ptr2 = Point(5, 7);

     cout << *ptr;
     cout << *ptr2;

     delete ptr;
     delete ptr2;

     return 0;
}

/* 결과

operator new: 8
operator new: 8
[3, 4]
[5, 7]
operator delete()
operator delete()

*/

바이트 단위로 메모리 공간을 할당하고 있다. 할당에 사용되는 크기정보는 컴파일러가 계산해서 전달해준다. 참고로 이렇듯 char형을 대상으로 new연산을 하는 문장은 malloc 함수의 호출문으로 대신할 수 있으며, 실제로 malloc함수를 대신 호출하기도 한다.

그리고 char형으로 할당된 메모리 공간을 해제하는 것이기 때문에 free 함수의 호출문으로 대신할 수 있으며, 실제로 free 함수를 대신 호출하기도 한다.

Point* ptr = new Point(3, 4);

위의 문장은 객체생성이 완성된 상태 전에 호출이 가능하다. 어떻게 그럴 수 있을까?

그것은 "static" 메소드라서 가능한 것이다. 

operator new함수와 operator delete 함수는 static함수이다.

비록 멤버함수의 형태로 선언을 해도 이 둘은 static 함수로 간주가 된다. 그래서 객체 생성의 과정에서 호출이 가능했던 것이다. 


operator new & operator new[] & operator delete & operator delete[]

new연산자는 다음 두 가지의 형태로 오버로딩이 가능하다.

  • void* operator new(size_t size) { ... }
  • void* operator new[](size_t size) { ... }

두 번째 함수는 new 연산자를 이용한 배열할당 시 호출되는 함수이다. 즉 다음의 문장을 만나면,

Point* arr = new Point[3];

컴파일러는 세 개의 Point객체에 필요한 메모리 공간을 바이트 단위로 계산해서, 이를 인자로 전달하면서 다음 함수를 호출한다.

void* operator new[](size_t size) { ... }

즉, 배열할당 시 호출되는 함수라는 사실을 제외하고는 operator new 함수와 차이가 없다. 마찬가지로 delete 연산자도 다음 두 가지의 형태로 오버로딩이 가능하다.

  • void operator delete(void* adr) { ... }
  • void operator delete[](void* adr) { ... }

두 번째 함수는 delete 연산자를 이용한 배열소멸 시 호출되는 함수이다. 즉, 다음의 문장을 만나면 호출된다.

delete[] arr;

컴파일러는 소멸자를 호출한 이후에 arr에 저장된 주소 값을 전달하면서 다음 함수를 호출한다.

void operator delete[](void* adr) { ... }

이렇듯 이 함수 역시 배열소멸 시 호출되는 함수라는 사실을 제외하면, operator delete함수와 차이가 없다.

#include <iostream>
#include <cstdlib>

#pragma warning(disable: 4996)

using std::cout;
using std::cin;
using std::endl;


class Point {
private:
    int xpos, ypos;

public:
    explicit Point(const int x = 0, const int y = 0) : xpos(x), ypos(y) {}

    friend std::ostream& operator<<(std::ostream&, const Point&);

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

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

    void operator delete(void* ptr) {
        cout << "operator delete()" << endl;
        delete[] ptr;
    }

    void operator delete[](void* ptr) {
        cout << "operator delete[]()" << endl;
        delete[] ptr;
    }
};


std::ostream& operator<<(std::ostream& os, const Point& pos) {
    cout << '[' << pos.xpos << ", " << pos.ypos << ']' << endl;
    return os;
}


 int main(const int argc, const char* const argv[]) { 
     Point* ptr = new Point(3, 4);
     Point* arr = new Point[3];

     delete ptr;
     delete[] arr;

     return 0;
}

/* 결과

operator new: 8
operator new[]: 24
operator delete()
operator delete[]()

*/
Comments