본문 바로가기
NC University/Advanced C++

1일차 - new에 대해서

by 날쑤 2015. 6. 19.

우리가 사용하는 new의 정확한 동작 방식

  1. operator new() 함수를 호출해서 메모리 할당
  2. 1이 성공했을 때 생성하고자 하는 자료형이 클래스 타입이라면 생성자 호출
  3. 메모리 주소를 해당 타입으로 캐스팅해서 리턴

아래의 코드를 살펴보자.

#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을 스킵하면서 이미 존재하는 객체의 생성자를 호출하는 표현이 된다. 그럼 이런 기능이 왜 필요한것일까?

생성자/소멸자의 명시적 호출의 필요성

  1. 디폴트 생성자가 없는 객체를 여러 개 heap에 생성할 때
  2. std::vector에서와 같은 효율적 메모리 관리 기술을 위해
  3. 고성능의 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는 어떤 식으로 메모리를 관리해야 할까? 선택가능한 방법은 대충 아래와 같을 것이다.

  1. 3개의 객체를 소멸시키면서 실제 vector에 할당된 메모리 용량도 줄인다.
  2. 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