01. C++_Placement new
Placement new는 메모리를 할당하는 방식 중 하나인데, 이는 사용자가 이미 할당된 메모리 영역에 생성자를 명시적으로 호출하여 객체를 직접 생성하기 위한 방식입니다.
이는 일반적인 new 키워드를 사용하여 동적으로 메모리를 할당하는 것과는 조금 다릅니다. 보통 new 키워드를 사용하여 동적으로 메모리를 할당하면 시스템은 메모리를 할당하고 (메모리할당&생성자호출), 생성자를 호출하여 객체를 초기화 합니다. 반면 Placement new는 이미 할당된 메모리 영역에 생성자를 호출하여 객체를 직접 생성합니다. (생성자만 호출)
우선 기존의 new 키워드를 활용하여 동적할당을 하는 방법에 대해서 살펴보겠습니다.
- new Point(1,2)
#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()
{
Point* p1 = new Point(1,2);
delete p1;
}
다음 예제에서 동적으로 메모리를 할당하고 해제하는 과정은 아래와 같습니다.
동적 메모리 할당 (메모리 할당 + 생성자 호출(Placement new))
1) 메모리 할당 : void* p = operator new(sizeof(Point))
2) 생성자 명시적 호출 : Point* p1 = new(p) Point(1,2)
3) 소멸자 명시적 호출 : p1 -> ~Point();
4) 메모리 해지 : operater delete(p1);
위의 operator new()/ operator delete()는 메모리를 할당/해지하는 global namespace에 있는 c++ 표준함수입니다.
만약에 생성자는 호출하지 않고 메모리만 할당하고 싶다면
p1 = new Point(1,2)가 아닌, void* p1 = operator new(sizeof(Point)) 를 사용하면 됩니다. (이는 C언어의 malloc 함수와같습니다.)
마찬가지로 소멸자는 호출하지않고 메모리 해지만 하고 싶다면
delete p1 이아닌 operator delete(p1)을 해주면 됩니다.
new를 객체지향으로 사용할려면 (클래스에 대한 동적메모리를 할당) 생성자를 불러야하는데, 이미 할당된 메모리(주소가 p1)에 메모리 할당없이 생성자를 명시적으로 호출하는 기법을 Placement new 라고 합니다.
Placement new를 사용하여 생성자를 명시적으로 호출한 예제는 아래와 같습니다.
* new(p) Point(1,2)
#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); //void* 타입으로 넣었지만 객체가 생성되며 메모리를 가리키는 포인터가 Point*로 형식이 바뀐다.
p2 -> ~Point(); //소멸자를 명시적으로 호출
operator delete(p1);
}
객체에 대한 동적메모리를 할당하는 방법은 new/delete를 사용하는 방법이 있고,
operator new/ operator delete와 replacement new를 사용하면 객체에 대한 동적 메모리 할당 시에도 메모리 할당과 생성자 호출을 분리할 수 있습니다.
이처럼 메모리 할당과 생성자 호출을 분리하면 생성자 초기화, 메모리 효율화 등에 유리합니다.
생성자 초기화를 편리하게 할 수 있는 예시에 대해 알아보겠습니다.
#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* p1 = new Point[3] 만약 포인트 객체 3개를 배열 형태로 힙에 연속적으로 생성하고 싶다면?
//이 표기법은 3개에 객체에 대해서 디폴트 생성자를 부른다. 디폴트 생성자가 없다면 에러 (c++ 11부터 가능)
//Point* p1 = new Point[3]{{0,0},{0,0},{0,0}}; //3개를 만들건데 생성자는 이 모양으로 호출해달라고 표현해야해
//3개가 아니라 개수가 커진다면?
//메모리 할당과 생성자 호출을 분리하면 편리하다.
Point* p2 = static_cast<Point*>(operator new(sizeof(Point)*3)); //포인트 크기의 메모리를 3개를 할당하겠다.
for (int i=0; i<3; i++)
{
new(&p2[i]) Point(0,0); //3번을 돌건데, 이 주소에 생성자를 2개 인자를 갖는 생성자로 불러달라
}
for (int i=0; i<3; i++)
{
p2[i].~Point();
}
operator delete(p2);
}
이처럼 메모리할당과 생성자 호출을 분리하면 객체를 연속적으로 호출할 때 생성자 초기화에 효율적입니다.
아래 예제는 객체를 복사할 때 Placement new를 사용해서 생성자 호출 없이 복사 생성자만을 반복적으로 호출하는 예제에 대해 알아보겠습니다.
#include <iostream>
#include <type_traits>
struct Point
{
int x = 0;
int y = 0;
};
template<class T>
void copy_type(T* dst, T* src, std::size_t sz)
{
while(sz--)
{
std::cout << "using copy ctor" << std::endl;
new(dst) T(*src); //placement new기법을 사용하여 복사생성자를 호출
++dst, ++src;
}
}
}
//placement new 기법 : 메모리를 할당하고 그 메모리에 객체를 생성하는 기법 -> 복사생성자를 명시적으로 호출하는데 사용가능합니다.
//new 연산자를 사용하여 객체를 생성할 때는 생성자를 호출하게 되는데 placement new를 사용할 때에는 생성자 호출이 자동으로 되지 않습니다.
//따라서 T(*src)는 src의 값을 이용하여 T의 복사생성자를 명시적으로 호출합니다.
//이것은 src가 가리키는 객체의 값을 복사하여 새로운 객체를 생성합니다.
int main()
{
Point arr1[5];
// Point arr2[5]; 버퍼 복사 할때 보통 생성자를 부르지 않고 메모리만 할당하는 것이 더욱 효율적
Point* arr2 = static_cast<Point*>(operator new( sizeof(Point)* 5)); //복사를 위한 메모리 할당
copy_type(arr1, arr2, 5); //5개 짜리 배열을 옮기는 작업을 하겠습니다.
//만약 객체 정의 방식에 따라 복사 생성자가 하는길이 없다면 복사생성자를 일일히 호출하지않고 배열 전체를 memcpy나 memmove 등의 함수를 이용하여 복사하여도 됩니다.
}
위 예제는 강성민 강사님의 cpp intermediate 강좌의 내용을 인용하였습니다.
Course Status – ecourse 온라인 강의
Placement new 기법 정의는 cppreference에서 확인하실 수 있습니다.
new expression - cppreference.com