auto_ptr, shared_ptr, scoped_ptr - 그리고 지금은 표준 스마트 포인터
스마트포인터
메모리를 사용하는 과정에서 할당 이후 해제가 되지 않으면 해당 메모리 공간에는 다른 데이터를 넣을 수 없게 되어 사용 가능한 메모리 공간이 점점 줄어드는 메모리 누수(leak) 현상이 발생한다. 또한 해제된 메모리에 접근하면 쓰레기 값이 나오거나 프로그램이 오류를 일으킬 수도 있다. 따라서 할당(malloc, new 등) 이후에는 반드시 해제(free, delete 등)를 해줘야 하는데, 이 과정의 복잡함과 실수를 방지하기 위해 자동으로 메모리를 해제해주는 포인터가 바로 스마트 포인터다.
2026년 수정 안내 이 글은 2013년 당시 boost의 세 가지 스마트 포인터를 정리한 것이다. C++11(2011년 확정, 실무 보급은 이후 몇 년에 걸쳐 이루어졌다)부터는 이들이 표준 라이브러리로 흡수되었으므로, 지금 새로 쓴다면 boost가 아니라 표준을 쓰면 된다. 원문에 있던 두 가지 오류(
void main,use_count()의 시작 값)를 바로잡고, 표준 스마트 포인터로의 대응 관계를 정리해 덧붙인다.
1. auto_ptr
C++98의 표준 라이브러리에 있던 스마트 포인터. 객체가 특정 스코프({ ... })를 벗어날 때 소멸자가 호출된다는 원리를 이용한다.
기본적인 사용법
#include <iostream>
#include <memory>
int main()
{
std::auto_ptr<int> pInt( new int );
*pInt = 10;
std::cout << *pInt << std::endl;
return 0;
}
auto_ptr의 단점
- 배열에 쓰면 안 된다.
auto_ptr의 소멸자는 항상delete를 호출하지,delete[]를 호출하지 않는다.new[]로 할당한 배열을auto_ptr에 넣으면 배열의 나머지 원소들은 소멸자가 불리지 않고 메모리도 일부만 해제되어, 정의되지 않은 동작이 된다. (원래 이 글에는 “스코프 종료 시점에 해제하기 때문”이라고 적었는데, 정확한 원인은 이것이다.) auto_ptr을 대입(a = b)하면 소유권이 이동한다.a는b가 가리키던 것을 가져가고b는NULL이 된다. 같은 메모리를 두auto_ptr이 동시에 가리키다가 이중 해제되는 것을 막기 위한 설계다.- 위 이동 대입 방식 때문에 STL 컨테이너에 넣을 수 없다. 컨테이너 내부 연산이 복사를 요구하는데,
auto_ptr의 “대입”은 복사가 아니라 이동이라 컨테이너의 불변식을 깨기 때문이다.
auto_ptr은 C++11에서 deprecated되었고, C++17에서 표준에서 완전히 제거되었다. 최신 컴파일러라면 아예 컴파일되지 않는다.
2. shared_ptr
auto_ptr의 문제 때문에 boost에서 shared_ptr이 만들어졌다. 가리키는 객체에 대한 참조 횟수를 관리해서, 새로 가리킬 때마다 1씩 늘리고 소멸될 때마다 1씩 줄이다가 0이 되면 메모리를 해제한다. 이 방식 덕분에 같은 객체를 여러 shared_ptr이 동시에 가리킬 수 있다.
기본적인 사용법
#include <iostream>
#include <vector>
#include <boost/shared_ptr.hpp>
using namespace std;
using namespace boost;
class ClassA
{
public:
ClassA(){cout << "ClassA()" << endl;}
~ClassA(){cout << "~ClassA()" << endl;}
void hello(){cout << "# hello()" << endl;}
};
vector< shared_ptr<ClassA> > vec;
int main () {
shared_ptr<ClassA> obj(new ClassA());
cout << "* use_count (new): " << obj.use_count() << endl; // 1
vec.push_back(obj);
cout << "* use_count (push): " << obj.use_count() << endl; // 2
obj->hello();
vec.pop_back();
cout << "* use_count (pop): " << obj.use_count() << endl; // 1
obj.reset(new ClassA());
cout << "* use_count (reset): " << obj.use_count() << endl; // 1 (새 객체를 가리키므로 다시 1)
return 0;
}
shared_ptr로 할당된 객체는 use_count()로 참조 횟수를 알아낼 수 있다. 새로 만든 shared_ptr의 use_count()는 0이 아니라 1에서 시작한다. (원래 이 글에는 “0에서부터 1씩 증가시킨다”고 적었는데, 정확히는 생성되는 순간 이미 1이다. 0이 되는 시점은 마지막 소유자가 사라져 객체가 해제되는 순간뿐이다.)
shared_ptr의 단점
- 순환 참조에 취약하다.
A가B를 가리키는shared_ptr을 갖고,B도A를 가리키는shared_ptr을 가지면, 서로가 서로의 참조 횟수를 붙들고 있어 둘 다 0이 되지 않는다. 원형 연결 리스트처럼 순환 구조에서 흔히 나타난다. 이 문제는weak_ptr로 해결한다 — 순환을 이루는 한쪽 참조를weak_ptr로 바꾸면 참조 횟수에 영향을 주지 않으면서도 필요할 때lock()으로 접근할 수 있다.
3. scoped_ptr
동적으로 할당된 객체에 대한 포인터를 가진다. 자신이 소멸될 때 가리키던 객체도 자동으로 삭제한다. 복사가 불가능하도록 되어 있어 소유권이나 참조 카운트 문제가 발생하지 않는다.
기본적인 사용법
// ScopedPtrTest.h
#include <iostream>
#include <boost/scoped_ptr.hpp>
using std::cout;
using std::endl;
class Sample
{
public:
Sample() { cout << "생성" << endl; }
~Sample() { cout << "소멸" << endl; }
void print() { cout << "print" << endl; }
};
// ScopedPtrTest.cpp
#include "ScopedPtrTest.h"
typedef boost::scoped_ptr<Sample> ScopedPtr;
void PrintStr( ScopedPtr* ptr ); // 값 복사가 불가능하므로 포인터 형태로 넘겨야 한다.
int main()
{
ScopedPtr p1( new Sample() );
ScopedPtr p2( new Sample() );
p1->print();
PrintStr( &p1 ); // scoped_ptr의 주소를 넘긴다.
/*
// scoped_ptr - 자신이 가지는 포인터를 다른 인스턴스로 할당하거나 넘겨줄 수 없다.
// 한 번 가리키는 객체에 대한 삭제의 책임을 전적으로 진다.
// (복사가 빈번한 STL 컨테이너에는 넣을 수 없다.)
p2 = p1; // 에러! 할당 불가!
ScopedPtr p3( p1 ); // 에러!
*/
return 0;
}
void PrintStr( ScopedPtr* ptr )
{
(*ptr)->print();
}
scoped_ptr의 단점
- 포인터를 다른 변수에 복사하거나 할당할 수 없다.
- STL 컨테이너에 넣을 수 없다.
- 동적으로 생성한 객체 배열을 다루고 싶을 때는
scoped_array를 쓴다.
지금이라면 표준 스마트 포인터를 쓴다
이 글을 쓸 당시에는 boost가 필수였지만, C++11부터는 표준 라이브러리에 대응물이 모두 들어와 있다.
| boost | 표준 | 비고 |
|---|---|---|
boost::scoped_ptr |
std::unique_ptr |
단독 소유. 복사 불가, 이동은 가능하다는 점이 scoped_ptr과 다르다 |
boost::shared_ptr |
std::shared_ptr |
참조 카운트 공유 소유 |
| (shared_ptr의 순환 참조 해법) | std::weak_ptr |
shared_ptr과 짝을 이루는 비소유 참조 |
std::auto_ptr (C++98) |
std::unique_ptr |
auto_ptr은 C++17에서 제거됨 |
#include <memory>
// unique_ptr - scoped_ptr과 달리 이동은 가능하다
std::unique_ptr<Sample> p1 = std::make_unique<Sample>();
std::unique_ptr<Sample> p2 = std::move( p1 ); // 소유권 이동. p1은 이제 비어 있다
// shared_ptr - 순환 참조는 weak_ptr로 끊는다
struct Node
{
std::shared_ptr<Node> next;
std::weak_ptr<Node> prev; // 순환을 이루는 쪽은 weak_ptr로
};
auto node = std::make_shared<Node>();
make_unique(C++14)와 make_shared(C++11)를 쓰면 new를 직접 호출하지 않아도 되고, make_shared는 컨트롤 블록과 객체를 한 번에 할당해 더 효율적이다.
boost 스마트 포인터는 지금도 유효하고 계속 유지보수되지만, 표준 라이브러리만으로 충분하다면 별도 의존성을 추가할 이유가 없다.
댓글 남기기