Pururoong

스마트 포인터 본문

Programming/Modern C++

스마트 포인터

Pururoong 2014. 6. 11. 03:08

C++에서는 new 연산자를 통해 클래스나 객체에 대한 메모리를 동적으로 할당할 수 있다.

할당된 메모리는 역할을 다하면 반드시 반환되어야 하며, 프로그래머가 명시해 주어야한다.

클래스의 경우, 소멸자를 통해 객체가 사라질 때, 메모리를 해제하도록 할 수 있지만, 

어디까지나 클래스가  정적으로 선언된 경우에만 해당한다. 

다음 예제는, 동적으로 선언된 클래스에서 소멸자가 호출되지 않음을 보여준다.


#include <iostream>

class Test {

public :
	
	Test()
	{
		std::cout<<"생성자 호출"<<std::endl;
	}

	~Test() 
	{
		std::cout<<"소멸자 호출"<<std::endl;
	}

private :

};

int main() {

         Test *t1 = new Test;
         //생성자만 호출 - 프로그램이 종료되어도 소멸자는 호출되지 않는다.
         //delete t1; 
	return 0;
}


이외에도 프로그래머는 delete를 잊거나, new와 delete의 형식을 맞추지 못하거나, delete를 두 번 사용하거나,
예외처리 구문을 통해 delete를 사용하는 등의 실수를 할 수 있다. 

이런 사소한(?) 메모리 누수를 방지하기 위해 스마트 포인터를 사용할 수 있다.


[std::auto_ptr]

스마트 포인터는 스택에서 생성되어 힙 영역에 할당된 객체를 가리키고, 스코프를
벗어나면 자동으로 소멸된다.(지역 변수처럼 관리)

int main() {

//std::auto_ptr을 사용하여 첫 번째 예제의 문제를 해결
Test *t1 = new Test;
std::auto_ptr<Test> p1; std::auto_ptr<Test> p2 (t1); p1 = p2; return 0; }


라인 8에서 객체의 복사가 이루어 지는데, 이 때 p2는 NULL을 가리키게 되고 메모리의 오너쉽을 p1에게 넘겨준다.
그리고 p1이 혼자서 메모리의 오너쉽을 가지고 있다가 스코프에서 벗어나게 되면 적절한 동작을 취한다.

auto_ptr은 단일 객체만 가리키도록 되어있기 때문에(delete만 호출하기 때문에) 다음과 같은 코드는 적절하지 않다.
auto_ptr<T> p1( new T );
//OK
auto_ptr<T> p2( new T[n] ); 
//delete[]를 호출하지 않기 때문에
//메모리 헤제가 제대로 이루어지지 않는다.

또한, 자원의 소유권을 전달하는 특이한 복사 방식 때문에 STL 컨테이너의 요소로 사용할 수 없다.
아래는 COAP(Container Of Auto_Ptr)라 불리는 auto_ptr의 잘못된 사용 예제이다. 
int main() {

	std::auto_ptr<int> p1(new int);
	std::vector<std::auto_ptr<int>> v;

	v.push_back(p1);

	return 0;
}

<에러 메세지>
error C2558: class 'std::auto_ptr<_Ty>' : 복사 생성자를 사용할 수 없거나 복사 생성자가 'explicit'으로 선언되었습니다.

 
C++ 표준 위원회에서 COAP를 방지하기 위해 컨테이너의 삽입 멤버 함수의 인자로 상수 참조(const T& arg)를 받도록 정의하였는데,
auto_ptr은 소유권 이전을 하기 때문에 복사 생성자에 상수 참조를 받지 못한다고 한다.

이런 문제점들 때문에 C++11에서는 공식적으로 auto_ptr을 폐기할 예정이라고 선언하였다.
대안으로 C++11사용자는 shared_ptr과 unique_ptr을 사용할 수 있다. C++11이전 사용자는 boost libraries의
shared_ptr을 사용할 수 있다.

포인터는 스코프를 벗어나면 삭제(메모리해제)가 잘 이루어 져야하는데, 코드중에서 어떤 포인터를 복제하여 누군가 참조하고 있을
경우도 있다. 이런 문제 상황을 엘리어싱(aliasing)이라 하는데, 이 문제를 해결하려면, 포인터가 가장 마지막에 사용
되는 곳에서 delete가 이루어져야 한다. 그러나 런타임에 따라 실행순서가 바뀌는 경우에는 어디에서 메모리가 가장
마지막에 사용되는지 알아내기가 어렵다. 

std::shared_ptr은 참조(Reference) 카운팅 방식으로 동작하는데, 객체의 참조를 카운트하고 있다가 포인터의
모든 오너가 포인터에 대한 참조를 마치게 되면(카운트가 0이 되면) 자동으로 실제 포인터를 delete한다.


[std::shared_ptr]
 
두 가지 방법으로 선언할 수 있다.

std::shared_ptr<Test> tp1(new Test);
auto tp2 = std::make_shared<Test>();

두 번째 방법에서 Test의 생성자가 파라매터를 요구하면 std::make_shared에 인자를 주면 된다.

메모리 해제시 delete만 실행되는 문제는 여전히 남아있다.

 std::shared_ptr<int> p1( new int[10] );
//메모리가 제대로 해제되지 않는다.

//다음과 같이 사용할 수 있다.
std::vector<std::shared_ptr<int>> pv;
pv.push_back(std::shared_ptr<int>(new int(4)));


혹은 boost의 shared_array를 사용할 수 있다.

boost::shared_array<int> p1(new int[10]); 

Comments