우리가 사용하는 new의 정확한 동작 방식
- operator new() 함수를 호출해서 메모리 할당
- 1이 성공했을 때 생성하고자 하는 자료형이 클래스 타입이라면 생성자 호출
- 메모리 주소를 해당 타입으로 캐스팅해서 리턴
아래의 코드를 살펴보자.
#include <iostream>
using namespace std;
class Point {
public:
Point() { cout << "Point()" << endl; }
~Point() { cout << "~Point()" << endl; }
private:
int x, y;
};
int main(int argc, char* argv[]) {
Point* p1 = new Point;
delete p1;
Point* p2 = static_cast<Point*>(operator new(sizeof(Point)));
operator delete(p2);
return 0;
}
p1의 생성은 new에 의한 것이므로 위의 1~3의 과정을 거친다. 즉, 메모리를 할당하고 객체의 생성자를 호출한다. 반면에 p2는 1의 과정만 거친 것으로 Point 객체를 위한 메모리를 할당하고 그 메모리값 주소를 void* 형으로 반환한다.(그래서 캐스팅이 필요하다) delete쪽도 마찬가지로 operator delete는 메모리 해제만을 수행한다.
operator new 함수의 재정의
operator new와 operator delete는 사용자에 의한 재정의 및 오버로딩이 가능하다. 이를 통해서 사용자는 메모리 할당 방식을 변경할 수 있다. 실제로 많은 MMORPG에서는 성능 향상을 위해 자체 memory allocator를 가지고 있으며, 메모리의 할당 및 해제는 자체 allocator를 통하도록 operator new와 operator delete를 재정의해서 쓰고 있다.
// 자체 Memory 관리자 클래스 선언부
class MemoryManager {
public:
MemoryManager();
void* Alloc(const size_t size);
void Release(void* const memory);
// 인자가 두개인 오버로딩된 operator new()에서 사용할 함수
void* Alloc(const size_t size, const std::string& name);
private:
...
};
extern MemoryManager* GMemoryManger;
// MemoryManager 객체를 이용하는 operator new/delete 재정의
void* operator new(size_t size) {
cout << "operator new() using my memory manager" << endl;
return GMemoryManager->Alloc(size);
}
void operator delete(void* p) {
GMemoryManager->Release(p);
}
operator new의 오버로딩
operator new는 재정의뿐만 아니라 인자의 갯수를 달리하는 오버로딩도 가능하다. 이 때, 첫 번째 인자는 반드시 size_t 타입이어야 한다는 조건이 따른다. 그리고 호출시에는 두 번째 인자부터 아래와 같이 명시해서 쓴다.
#define _STRINGER(x) #x
#define STRINGER(x) _STRINGER(x)
#define __FILELINE__ \ __FILE__ "(" STRINGER(__LINE__) ")"
#define __FILELINEFUNCSIG__ \ __FILELINE__ " : " __FUNCSIG__
void* operator new(size_t size, const char* name) {
cout << "MemAlloc at " << name << "(Size: " << size << ")" << endl;
return GMemoryManager->Alloc(size, name);
}
int main(int argc, char* argv[]) {
Point* p = new(__FILELINEFUNCSIG__) Point;
// ... Do something ...
delete p;
return 0;
}
인자 두개를 받는 오버로딩된 operator new는 메모리가 할당된 위치정보를 함께 받고 있다. 그리고 이 정보를 Alloc 함수를 통해 MemoryManager에도 전달해서 차후에 메모리 누수를 확인할 때 사용할 수 있도록 한다.
Placement new
- 2개의 인자를 받는 operator new의 오버로딩
- 생성자의 명시적 호출을 위한 기법
class Point {
public:
Point() { cout << "Point()" << endl; }
~Point() { cout << "~Point()" << endl; }
private:
int x, y;
};
// C++ 표준에는 아래와 같은 형태의 함수가 이미 정의되어 있다.
void* operator new(size_t sz, void* p) {
return p; //< 메모리 할당없이 두 번째 들어온 포인터 값을 그냥 반환
}
int main(int argc, char* argv[]) {
Point p; //< 지역변수로 객체가 생성되면서 생성자 호출
p.~Point(); //< 소멸자의 명시적 호출. 문제없음.
// p.Point(); //< 생성자의 명시적 호출. 컴파일 에러
new(&p) Point; //< Placement new, p의 생성자를 호출
return 0;
}
Placement new는 두번째 인자가 포인터인 오버로딩된 operator new를 호출하면서 new의 동작방식을 수행한다. 결과적으로 처음에 설명한 1~3의 과정중 1과 3을 스킵하면서 이미 존재하는 객체의 생성자를 호출하는 표현이 된다. 그럼 이런 기능이 왜 필요한것일까?
생성자/소멸자의 명시적 호출의 필요성
- 디폴트 생성자가 없는 객체를 여러 개 heap에 생성할 때
- std::vector에서와 같은 효율적 메모리 관리 기술을 위해
- 고성능의 generic 알고리즘 개발 - ex) 안드로이드 Jellybean의 typehelpers.h
우선 Point 객체에 기본생성자가 아닌 x,y 좌표값을 받는 생성자가 정의되어 있다고 가정해보자. 그러면 컴파일러는 기본생성자를 자동으로 생성하지 않으며, 이 경우 아래와 같은 상황에서 문제가 된다.
Point* p1 = new Point(0, 0); //< Point 객체 1개 생성, 문제없음
Point* p2 = new Point[10]; //< 기본생성자가 없으므로 컴파일 에러
Point* p3 = new Point[10](0, 0); //< 허용되지 않는 문법
즉, 기본생성자가 없는 클래스 객체 배열은 일반적인 new를 통해 만들 수 없다. 이 문제에 대한 해법이 바로 Placement new이며, 적용 방법은 아래와 같다.
// 1. 객체 10개를 위한 메모리 할당
Point* p = static_cast<Point*>(operator new(sizeof(Point) * 10));
// 2. 10개 객체에 대해 생성자 호출(Placement new)
for (int i =0; i< 10; ++i) {
new(&p[i]) Point(0, 0);
}
다음으로 std::vector는 container 내에 존재하는 element의 수(size)보다 크거나 같은 메모리 용량(capacity)를 가진다. 이는 element가 삽입/삭제 될 때마다 new/delete를 하는 오버헤드를 가급적 줄이기 위함이다. 그럼 다음과 같은 상황을 생각해보자. 현재 vector내에 원소가 10개인데 resize 함수를 호출해서 size를 7로 줄인다면 이 상황에서 vector는 어떤 식으로 메모리를 관리해야 할까? 선택가능한 방법은 대충 아래와 같을 것이다.
- 3개의 객체를 소멸시키면서 실제 vector에 할당된 메모리 용량도 줄인다.
- 3개의 객체의 소멸자를 호출해서 객체 자체는 소멸처리하되, vector의 메모리 용량은 10으로 유지시킨다.
std::vector는 성능 향상을 위해서 두번째 방식을 택하고 있다. 1안을 적용할 경우 차후에 있을 새로운 원소의 삽입이나 현재 size보다 큰 값으로 resize함수 호출이 있을 때 다시 메모리를 할당해야 하는데 이로 인한 성능저하가 발생할 수 있기 때문이다. 이를 코드로 살펴보기 위해 DBConnection이라는 클래스를 가정하고 이 클래스로 생성된 객체는 생성시에 DB에 접속하고 소멸시에 연결을 close한다고 하자.
std::vector<DBConnection> vec(10); //< 10개의 DB Connection 생성
vec.resize(7); //< 메모리 해제는 없지만 3개의 커넥션을 닫아야 함 -- (1)
vec.resize(8); //< 메모리 재할당은 없지만 1개의 커넥션을 만들어야함 -- (2)
(1)의 경우 메모리 해제 없이 커넥션을 닫아야하므로 소멸자의 명시적 호출이 필요하다. 그리고 (2)의 경우에는 메모리 할당 없이 커넥션을 열어야하므로 생성자의 명시적 호출이 요구된다. 그러므로 효율적인 resize를 구현하기 위해서는 이미 존재하는 객체의 생성자/소멸자의 명시적 호출이 필요함을 알 수 있다.
'NC University > Advanced C++' 카테고리의 다른 글
2일차 - Template 부분 전문화(2) (0) | 2015.06.30 |
---|---|
2일차 - 템플릿 인자 (0) | 2015.06.30 |
2일차 - value_type (0) | 2015.06.29 |
2일차 - 멤버함수 템플릿 (0) | 2015.06.24 |
1일차 - Template meta programming (0) | 2015.06.18 |