I'm FanJae.

[C++ Intermediate] new/delete, placement new 본문

C++/Intermediate

[C++ Intermediate] new/delete, placement new

FanJae 2024. 9. 18. 21:36

1. new와 delete

 

1-1. new와 delete의 원리

#include <iostream>
class Point
{
   int x, y;
public:
   Point(int a, int b) : x{a}, y{b} { std::cout << "Point(int, int)" << std::endl; }
   ~Point()                         { std::cout << "~Point()" << std::endl; }
};

int main()
{
    Point* p1 = new Point(1,2);
    delete p1;
}

 

Point* p1 = new Point(1,2);

- 위와 같이 쓰면 크게 2가지 작업을 진행한다. 즉, new를 실행하면 아래와 같은 2가지가 실행되는 것이다.

 

① 메모리할당

 void *p = operator new(sizeof(Point))

② 생성자호출 

Point *p1 = new(p) Point(1,2);

- 여기서 new(p) Point(1,2); 와 같은 표기법을 placement new라고 한다.

 

delete p1;

- 위와 같이 쓰면 크게 2가지 작업을 진행한다. 즉, delete를 실행하면 아래와 같은 2가지가 실행되는 것이다.

 

① 소멸자호출

p1->~Point()

② 메모리해지

 

operator delete(p1);​

 

#include <iostream>
#include <new>
class Point
{
   int x, y;
public:
   Point(int a, int b) : x{a}, y{b} { std::cout << "Point(int, int)" << std::endl; }
   ~Point()                         { std::cout << "~Point()" << std::endl; }
};

int main()
{
    void * p = operator new(sizeof(Point));
    
    std::cout << p << std::endl;
    
    operator delete(p);
}

- 따라서 이와 같은 행위도 가능하다는 의미다.

- C로 따지면 malloc과 free를 한 것과 유사하다.

- <new> 헤더를 포함하면 사용할 수 있다.

 

1-2. operator new() / operator delete()

#include <iostream>
#include <new>
#include <memory>
class Point
{
   int x, y;
public:
   Point(int a, int b) : x{a}, y{b} { std::cout << "Point(int, int)" << std::endl; }
   ~Point()                         { std::cout << "~Point()" << std::endl; }
};

int main()
{
    void* p1 = operator new(sizeof(Point));
    Point* p2 = new(p1) Point(1, 2); // 생성자 호출
    
    operator delete(p1);
    
    Point* p3 = static_cast<Point*>(operator new(sizeof(Point)));
    std::construct_at(p3, 1, 2);
    
    operator delete(p3);

}

- 메모리를 할당/해지하는 C++ 표준 함수이다.

- <new> 헤더에 있다.

- std namespace가 아닌 global namespace이다.

void* operator new(std::size_t);
void operator delete (void* ptr) noexcept;

- 함수 정의는 다음과 같다.

- 따라서 casting을 통해 casting된 값을 받는것도 가능하다.

 

① Placement new

- 메모리 할당없이 이미 할당된 메모리에 대해 생성자를 명시적으로 호출하기 위한 new

new      Point(1,2); 새로운 메모리를 할당하고 객체 생성 ( 생성자 호출 )
new(p) Point(1, 2); 이미 할당된 메모리(p) 에 객체 생성 ( 생성자만 호출 ) placement new 

 

② 생성자를 명시적으로 호출하는 방법

new(p)  Point(1,2);
std::construct_at(p, 1, 2); C++20, <memory>

 

③ 소멸자를 명시적으로 호출 하는 방법

p->~Point();
std::destroy_at(p) C++17, <memory>

 

1-3. C++에서 객체를 생성/파괴하는 방법

① new/delete 사용

② 메모리 할당과 생성자 호출을 분리.

메모리 할당 operator new(sizeof(Point));
생성자 호출 new(p) Point(1, 2);      placement new
std::construct_at(p, 1, 2);      C++ 20
소멸자 호출 p->~point();
std::destory_at(p); // C++17
메모리 해지 operator delete(p);

 

1-4. Assembly Level

 

- 다음과 같이 placement new가 생성자 형태로 바뀐 것이다.

- delete의 경우도 소멸자를 부른 이후 ,operator delete(void *)를 진행한다.

- 이때 옵션에 -std=c++11로 해야만 이와 같이 나온다.

- 이는 다소 복잡한 얘기이므로, 뒤에서 다시 언급한다.

 

2. using placement new

2-1. 메모리 할당과 생성자 호출을 분리하는 이유

#include <iostream>
#include <new>
#include <memory>

class Point
{
    int x, y;
public:
    Point(int a, int b) : x{a}, y{b} { }
    ~Point()                         { }
};

int main()
{
    // Point 객체 한개를 힙에 생성하고 싶다.
    Point* p1 = new Point(0, 0);
    
    // Point 객체 3개를 힙에
    // 연속적으로 (배열 형태로) 생성하고 싶다.
    Point* p2 = new Point[3];
}

- 왜 메모리 할당과 생성자 호출을 분리 하는가?

- 본 예제의 핵심은 Default 생성자가 없다는 것에 있다.

new Point[3];

- 이 경우 Point 타입에는 반드시 디폴트 생성자가 있어야 한다. 디폴트 생성자가 없으면 에러가 발생한다.

Point* p2 = new Point[3]={{0,0},{0,0},{0,0}}; // C++ 11

- 위와 같이 사용해도 문제는 없다. 문제는 C++ 11부터 사용 가능하고  3개가 아니라 개수가 커진다면 어떻게 해야할까?

- 이와 같은 상황일때 메모리 할당과 생성자 호출을 분리하면 훨씬 편하다.

 

① 해결법

#include <iostream>
#include <new>
#include <memory>

class Point
{
    int x, y;
public:
    Point(int a, int b) : x{a}, y{b} { }
    ~Point()                         { }
};

int main()
{
    Point* p2 = static_cast<Point*> (operator new(sizeof(Point) * 3));
    
    for (int i = 0; i < 3; i++)
    {
        // new(&p2[i]) Point(0, 0);
        std::construct_at(&p2[i], 0, 0);
    }
    
    for (int i = 0; i < 3; i++)
    {
        p2[i].~Point();
        std::destory_at(&p2[i]);
    }
    operator delete(p2);
}

- 즉, 메모리 할당과 생성자 호출을 분리하면 유연하게 객체 생성이 가능해진다.

 

2-2. 생성자와 소멸자만 호출하는 예시

#include <iostream>
#include <vector>

struct X
{
   X() { std::cout << "X() get resource" << std::endl;
   ~X() { std::cout << "~X() release resource" << std::endl;
};

int main()
{
    std::vector<int> v(10);
    
    v.resize(7);
    
    std::cout << v.size() << std::endl; // 7
    std::cout << v.capacity() << std::endl; // 10
    
    v.resize(8);
    
    std::cout << v.size() << std::endl; // 7
    std::cout << v.capacity() << std::endl; // 10
}

- 일반적으로, 크기가 줄어드는 경우, 실제 메모리를 줄이지 않고 size 변수 값만 변경한다.

- 보통 여기서 Capacity와 size 값의 차이가 드러난다.

 

#include <iostream>
#include <vector>

struct X
{
    X() {
        std::cout << "X() get resource" << std::endl;
    }
    ~X() {
        std::cout << "~X() release resource" << std::endl;
    };
};
int main()
{
    std::vector<X> v(10);

    v.resize(7);

    std::cout << v.size() << std::endl; // 7
    std::cout << v.capacity() << std::endl; // 10

    v.resize(8);

    std::cout << v.size() << std::endl; // 7
    std::cout << v.capacity() << std::endl; // 10
}

- int와 다르게 객체를 담는 경우라면, 메모리는 제거되지 않는다.

- 하지만 이와 별개로 줄어든 객체의 소멸자는 호출해야할 필요가 존재하지 않을까?

- 또, Resize()를 진행해서 크기가 늘어난다면 인위적으로 생성자만 호출해야 하지 않을까?

- 즉, 메모리의 적절한 관리에 따라서 필요하고, 개발자는 메모리 관리에 대한 부담 없이 벡터 사용이 가능하다.

 

#include <iostream>
#include <vector>

struct X
{
    X() {
        std::cout << "X() get resource" << std::endl;
    }
    ~X() {
        std::cout << "~X() release resource" << std::endl;
    };
};
int main()
{
    std::vector<X> v(10);

    std::cout << "---------------" << std::endl;
    v.resize(7); // 소멸자만 명시적으로 3번 호출되고 있다. 
    std::cout << "---------------" << std::endl;

    std::cout << v.size() << std::endl; // 7
    std::cout << v.capacity() << std::endl; // 10
    std::cout << "---------------" << std::endl;
    v.resize(8); // 생성자만 명시적으로 1번 호출된다.
    std::cout << "---------------" << std::endl;

    std::cout << v.size() << std::endl; // 7
    std::cout << v.capacity() << std::endl; // 10
}

- vector는 사용자 정의 타입도 보관이 가능하다. 

- 따라서 메모리의 할당 해지가 없음에도 생성자 / 소멸자만 부르는 행위를 진행하고 있다.

- 실행해보면 다음과 같음을 알 수 있다.

#include <iostream>
#include <vector>

struct X
{
    X() {
        std::cout << "X() get resource" << std::endl;
    }
    ~X() {
        std::cout << "~X() release resource" << std::endl;
    };
};
int main()
{
    std::vector<X> v(10);

    std::cout << "---------------" << std::endl;
    v.resize(7); // 소멸자만 명시적으로 3번 호출되고 있다. 
    std::cout << "---------------" << std::endl;

    std::cout << v.size() << std::endl; // 7
    std::cout << v.capacity() << std::endl; // 10
    std::cout << "---------------" << std::endl;
    v.resize(8); // 생성자만 명시적으로 1번 호출된다.
    std::cout << "---------------" << std::endl;

    std::cout << v.size() << std::endl; // 7
    std::cout << v.capacity() << std::endl; // 10
}

- 이처럼, 메모리 관리 차원에서 사용될 수 있다.

Comments