Smart Pointer

일반적으로 pointer를 사용 시 매번 코드에 메모리를 생성하고 할당하는 작업을 같이 해줘야 하고, 이 작업을 해주지 않을 경우 메모리 누수나 메모리가 언제 할당되고 언제 해제 해야하는 지 프로그래머가 판단을 해야하는 입장에서 잘못된 로직으로 인해 할당 및 해제 작업이 제대로 이루어 지지 않아 메모리 낭비가 심해질 수 있다. 이를 방지 하기 위해 C++11 버전에서 메모리 할당 후 해제작업을 번거롭지 않게 자동으로 해제할 수 있도록 smart pointer 개념을 도입했고, java나 C#에서의 garbage collector의 메커니즘과 비슷하다.

 

① RAII(Resource Acquisition Is Initialization)

C++창시자인 비야네 스트로스트룹은 C++에서 자원을 관리하는 방법으로 RAII라 불리는 자원의 획득은 최기화 라는 디자인 패턴을 제안했다. 이는 자원 관리를 스택에 할당한 객체를 통해 수행하는 것이고, 스택에 정의되어 있는 모든 객체들은 빠짐없이 소멸자가 호출되기 때문에 포인터를 멤버에 넣고 소멸자 호출 시 메모리 해제 코드를 자연스럽게 넣어 로직상에 메모리 해제 코드를 없앨 수 있는 메커니즘을 제공하는 것을 말한다.

#include <iostream> 
using namespace std; 

// A generic smart pointer class 
template <class T> 
class SmartPtr { 
	T* ptr; // Actual pointer 
public: 
	// Constructor 
	explicit SmartPtr(T* p = NULL) { ptr = p; } 

	// Destructor 
	~SmartPtr() { delete (ptr); } 

	// Overloading dereferncing operator 
	T& operator*() { return *ptr; } 

	// Overloading arrow operator so that 
	// members of T can be accessed 
	// like a pointer (useful if T represents 
	// a class or struct or union type) 
	T* operator->() { return ptr; } 
}; 

int main() 
{ 
	SmartPtr<int> ptr(new int()); 
	*ptr = 20; 
	cout << *ptr; 
	return 0; 
} 

 

(1) unique_ptr

unique_ptr smart pointer의 경우 하나의 포인터만이 메모리를 가리킬 수 있도록 하는 smart pointer이다. 즉 하나의 소유자만 있고, 새 소유자의 이동은 가능하지만, 복사나 공유는 불가능하다. 소유권이 이전된 포인터를 Dangling Pointer라 하며 Dangling Pointer를 다시는 참조하지 않겠다는 확신하에 소유권을 넘겨줘야 한다.

 

① move function

소유권을 이전하고자 할 때 사용되는 함수이다. 소유권을 넘겨주게 되면 넘겨준 포인터는 NULL값을 가지게 된다.

#pragma once
#include <memory>
#include <iostream>

using namespace std;

class point {
	int x;
	int y;
public:
	point(int x, int y) : x(x), y(y) {}
	point() { x = 0, y = 0; }

	void print() {
		cout << "X : " << x << " Y : " << y << endl;
	}

	void print(int x, int y) {
		cout << "X : " << x << " Y : " << y << endl;
	}
};

void execute() {
	unique_ptr<point> a1(new point());
	a1->print();

	unique_ptr<point> a2 = move(a1);

	a2->print(10,10);

	a1->print(20,20);

	if (a1 == NULL)
		cout << "A1 is NULL" << endl;
}

하지만 소유권을 넘겨주더라도 a1의 print는 호출됨 위 결과는 컴파일러에 따라 다르게 나올 수 있다.

 

② get function

소유한 객체에 일시적으로 접근하고자 할 때 get을 사용하여 해당 객체에 대한 주소값을 전달해 준다. 

#include <memory>
#include <iostream>

using namespace std;

class point {
	int x;
	int y;
public:
	point(int x, int y) : x(x), y(y) {}
	point() { x = 0, y = 0; }

	void print() {
		cout << "X : " << x << " Y : " << y << endl;
	}

	void print(int x, int y) {
		cout << "X : " << x << " Y : " << y << endl;
	}

	~point() {
		cout << "자원을 해제함!" << endl;
	}
};

void bad_dosomething(unique_ptr<point>& p1) {
	p1->print();
}

void success_dosomething(point* p1) {
	p1->print();
}

void execute() {
	unique_ptr<point> a1(new point(5, 7));
	unique_ptr<point> a2(new point(5, 7));
	
	bad_dosomething(a1);
	success_dosomething(a2.get());
}

③ reset function

pointer가 가리키고 있는 메모리 영역을 삭제한다.

unique_ptr<int> ptr01(new int(5)); // int형 unique_ptr인 ptr01을 선언하고 초기화함.
auto ptr02 = move(ptr01);          // ptr01에서 ptr02로 소유권을 이전함.
// unique_ptr<int> ptr03 = ptr01;  // 대입 연산자를 이용한 복사는 오류를 발생시킴. 
ptr02.reset();                     // ptr02가 가리키고 있는 메모리 영역을 삭제함.
ptr01.reset();                     // ptr01가 가리키고 있는 메모리 영역을 삭제함.

④ make_unique

make_unique smart pointer의 경우 unique와 다르게 new 연산자를 사용하지 않으며, exception 발생 시 안전하게 작업이 이루어진다. 즉 안전하게 smart pointer를 사용하고 자 할때 사용된다.

void processWidget(std::shared_ptr<Widget> spw, int priority);

위 함수의 경우 widget객체 생성 후 shared_ptr을 넘겨주지만, 컴파일러의 잘못된 판단으로 인해 순서가 뒤바뀔 수도 있다. 따라서 make_unique 스마트 포인터를 사용하여 안전하게 객체를 생성하여 사용하는 방법을 제공한다.

 

(2) shared_ptr

unique_ptr과 반대로 여러 포인터들이 하나의 메모리를 공유하여 사용하고자 할 때 사용한다. shared_ptr에는 get function이 없다.

#include <iostream> 
using namespace std; 
#include <memory> 

class Rectangle { 
	int length; 
	int breadth; 

public: 
	Rectangle(int l, int b) 
	{ 
		length = l; 
		breadth = b; 
	} 

	int area() 
	{ 
		return length * breadth; 
	} 
}; 

int main() 
{ 

	shared_ptr<Rectangle> P1(new Rectangle(10, 5)); 
	// This'll print 50 
	cout << P1->area() << endl; 

	shared_ptr<Rectangle> P2; 
	P2 = P1; 

	// This'll print 50 
	cout << P2->area() << endl; 

	// This'll now not give an error, 
	cout << P1->area() << endl; 

	// This'll also print 50 now 
	// This'll print 2 as Reference Counter is 2 
	cout << P1.use_count() << endl; 
	return 0; 
} 

 

① Circular references

shared_ptr의 경우 레퍼런스 참조 기반이기 때문에 순환참조에 대해 문제가 발생될 수 있다. 즉 A와 B가 서로에 대한 shared_ptr을 들고 있으면 레퍼런스 카운트가 0이 되지 않아 메모리가 해제되지 않는다. 따라서 Circual references로 인한 DeadLock, Dangling Pointer, 검색 비용 회피를 방지하고 객체를 참조만 하고 싶다면 weak_ptr를 사용하여 예방할 수 있다. 만약 weak_ptr pointer에서 reference count를 하고 싶다면 lock함수를 호출하고, shared_ptr의 상태를 확인하고자 할때는 expired function을 사용하면 된다.

 

※ lock function

shared_ptr<_Ty> lock() const
{      
    // convert to shared_ptr
    return (shared_ptr<_Elem>(*this, false));
}

※ expired function

bool expired() const
{      
    // return true if resource no longer exists
    return (this->_Expired());
}

'프로그래밍 > C & C++' 카테고리의 다른 글

Stack Unwinding(스택 되감기)  (0) 2020.04.24
Virtual Function  (0) 2020.04.21
Dangling Pointer(허상 포인터)  (0) 2020.04.17
Vector 컨테이너  (0) 2020.04.09
STL Vector와 List의 차이점  (0) 2020.03.25
더보기

댓글,

야미야미얌얌

프로그래밍 및 IT 기술