스마트 포인터
스마트 포인터는 C++11 이후에 추가된 키워드이다.
C++에서 new로 동적할당을 하면, 다 사용한 후에는 반드시 delete를 해주어야 한다.
그렇지 않으면 메모리 누수가 생겨, 쌓이면 메모리 공간부족과 여러 문제들이 생긴다.
하지만, 코드는 길어지고 스크립트들이 많아지면 이런 상황을 알아차리기 힘들고 결국 문제가 발생할 수 있다.
C#이나 JAVA 등의 최신 언어는 가비지 컬렉터를 지원하지만, C++은 기본적으로 가비지 컬렉터가 없다.
대신, C++은 STL을 통해 메모리를 알아서 해제해 주는 스마트 포인터를 지원한다.
스마트 포인터는 자동으로 필요가 없어진 순간에 알아서 delete를 한다. (함수의 범위를 벗어나던가?)
또한, get() 함수를 통해 원시 포인터(raw pointer)에 접근할 수도 있다.
스마트 포인터의 종류 : shared_ptr, weak_ptr, unique_ptr
일반 포인터(원시 포인터)
일반적인 원시 포인터에서는 a객체가 b객체의 주소를 가리키는 상황에서 b가 delete 되면 a는 엉뚱한 메모리를 가리킨다.
class Player
{
public:
Player() {}
virtual ~Player() {}
public:
Player* other = nullptr;
};
int main()
{
Player* p1 = new Player();
Player* p2 = new Player();
p1->other = p2;
delete p2;
// p1->other는 엉뚱한 메모리를 가리킴
p1->other;
}
이런 상황을 방지하려면 delete 할 객체의 주소를 가리키는 곳이 아무 곳도 없어야 한다.
shared_ptr
- shared_ptr은 여러 소유자를 허용하는 메모리 공간의 수명을 관리하기 위한 스마트 포인터다.
- shared_ptr을 초기화한 후 복사, 함수에 값으로 전달, 다른 shared_ptr 객체로 할당이 가능하다.
- 전체 참조 횟수가 0에 도달하면 메모리가 해제된다.
- use_count() 함수를 사용해 스마트 포인터가 관리하는 객체의 참조 횟수를 확인할 수 있으며 get() 함수로 관리하는 객체의 주소를 반환하는 것도 가능하다.
a객체가 b객체의 주소를 가리키는 상황이면 강한 참조 횟수가 1개 증가하고
더 이상 가리키지 않으면 다시 1개가 감소하여 자신을 가리키는 곳이 아무도 없으면, delete를 한다.
즉, 아직 주소를 사용하면 delete를 안 하고, 주소를 사용하는 곳이 없으면 delete 한다.
class Player
{
public:
Player() {}
virtual ~Player() {}
};
int main()
{
std::shared_ptr<Player> p1 = std::make_shared<Player>();
std::shared_ptr<Player> p2(p1); // 복사 가능
std::shared_ptr<Player> p3 = p2; // 복사 대입 가능
}
그러면 shared_ptr만 사용하면 메모리 누수 무적 아닌가? 너무 좋은데
하지만, shared_ptr의 단점은 순환 참조 구조에서 나타난다.
a가 b의 주소를 가리키고, b가 a의 주소를 가리킨다면 서로 순환 구조이기 때문에 다른 조치를 하지 않는 이상 절대 delete 되지 않는다.
이렇게 서로를 참조하고 있는 경우를 순환 참조하고 한다.
class Player
{
public:
Player() {}
virtual ~Player() {}
std::shared_ptr<Player> other = nullptr;
};
int main()
{
std::shared_ptr<Player> p1 = std::make_shared<Player>();
std::shared_ptr<Player> p2 = std::make_shared<Player>();
// 서로가 서로를 참조하고 있어서 자동으로 메모리 해제가 안된다.
p1->other = p2;
p2->other = p1;
}
shared_ptr의 구현 (안 봐도 됨)
shared_ptr의 구현이다. (공식 구현은 아니고, shared_ptr의 동작 원리정도로 보자)
class RefCountBlock {
public:
int _refCount = 1;
};
template<typename T>
class SharedPtr
{
public:
SharedPtr() { }
SharedPtr(T* ptr) :_ptr(ptr)
{
if (_ptr != nullptr)
{
_block = new RefCountBlock();
cout << "RefCount : " << _block->_refCount << endl;
}
}
SharedPtr(const SharedPtr& sptr) : _ptr(sptr._ptr), _block(sptr._block)
{
if (_ptr != nullptr)
{
_block->_refCount++;
cout << "RefCount : " << _block->_refCount << endl;
}
}
void operator=(const SharedPtr& sptr)
{
_ptr = sptr._ptr;
_block = sptr._block;
if (_ptr != nullptr)
{
_block->_refCount++;
cout << "RefCount : " << _block->_refCount << endl;
}
}
~SharedPtr()
{
if (_ptr != nullptr)
{
_block->_refCount--;
cout << "RefCount : " << _block->_refCount << endl;
// delete _ptr을 바로 하지 않고, 사용중인 곳이 없는지 확인 후 삭제
if (_block->_refCount == 0)
{
delete _ptr;
delete _block;
cout << "Delete Data" << endl;
}
}
}
public:
T* _ptr = nullptr;
RefCountBlock* _block = nullptr;
};
weak_ptr
스마트 포인터는 3종류 중 하나인, weak_ptr은 shared_ptr과 함께 사용할 수 있는 특별한 경우의 스마트 포인터이다.
shared_ptr은 강한 참조 카운트를 증가시키고, weak_ptr은 약한 참조 카운트를 증가시킨다.
약한 참조 카운트는 객체의 소멸의 관여하지 않는다.
약한 참조로 참조되는 객체는 강한 참조 카운트가 0이 되면 자동으로 소멸한다. (약한 참조 카운트에 상관없이)
weak_ptr은 shared_ptr의 단점인 순환 참조문제의 해결책이라 볼 수 있다.
- weak_ptr은 그 자체로는 객체에 접근할 수 없다.
- 이를 위해 weak_ptr은 lock() 함수를 제공한다.
- lock() 함수를 사용하면 shared_ptr로 변환할 수 있으며, 이미 객체가 소멸된 후에는 변환이 불가능하다. 따라서 lock() 함수를 사용한 뒤에 유효성을 검사하면 안전하게 포인터를 활용할 수 있다.
class Player
{
public:
Player() {}
virtual ~Player() {}
void SetOrder(std::weak_ptr<Player> other)
{
this->other = other;
std::shared_ptr<Player> otherPlayer = other.lock();
if (otherPlayer)
{
// Todo
}
}
private:
std::weak_ptr<Player> other;
};
int main()
{
std::shared_ptr<Player> p1 = std::make_shared<Player>();
std::shared_ptr<Player> p2 = std::make_shared<Player>();
p1->SetOrder(p2);
p2->SetOrder(p1);
}
unique_ptr
- unique_ptr은 포인터를 공유하지 않는다.
- unique_ptr은 다른 함수에 복사하거나, 값으로 전달하거나 복사본을 생성해야 하는 STL 알고리즘에는 사용할 수 없다.
- C++ 14부터 unique_ptr의 생성을 도와주는 make_unique 함수를 제공한다.
- unique_ptr을 사용하려면 <memory> 헤더를 추가해야 한다.
- 원시 포인터만큼 가볍고 효율적이며, STL 컨테이너에서 사용 가능하다.
아래는 두 unique_ptr 객체 사이의 소유권 이전이다.
#include <memory>
class Player
{
public:
Player() {}
virtual ~Player() {}
};
int main()
{
std::unique_ptr<Player> p1 = std::make_unique<Player>();
std::unique_ptr<Player> p2 = std::move(p1);
}

unique_ptr은 복사나 대입이 불가능한데, 이는 내부에서 명시적으로 복사 생성자와 복사 대입 연산자를 delete 했기 때문이다.
함수의 인자로 unique_ptr을 받아야 하는 경우에는 참조를 통해 가져오는 것이 좋다.
#include <memory>
class Player
{
public:
Player() {}
virtual ~Player() {}
};
void Func(const std::unique_ptr<Player>& p) {}
int main()
{
std::unique_ptr<Player> p1 = std::make_unique<Player>();
std::unique_ptr<Player>& p2 = p1; // 가능: 참조는 가능하다
std::unique_ptr<Player> p3(p1); // 오류: 복사 불가
std::unique_ptr<Player> p4 = p1; // 오류: 복사 대입 불가
// 포인터의 참조 전달이 가능하다.
Func(p1);
}
unique_ptr도 get 함수를 통해 원시 포인터를 가져올 수 있는데, 가져온 원시 포인터를 delete 할 수도 있다.
이렇게 의도적으로 하는 것은 막지 못한다.
#include <memory>
class Player
{
public:
Player() {}
virtual ~Player() {}
};
int main()
{
std::unique_ptr<Player> p1 = std::make_unique<Player>();
// 원시 포인터를 가져와서 명시적으로 메모리 해제 (위험)
// 굳이 자동으로 관리하는 스마트 포인터를 수동으로 해제할 이유가 없음
Player* ptr = p1.get();
delete ptr;
}
'📕Programming > 📝C/C++' 카테고리의 다른 글
| [C / C++] C++ 동작 방식 (0) | 2025.07.15 |
|---|---|
| [C / C++] 인라인 함수 (in-line function) (0) | 2023.12.28 |
| [C / C++] 완벽 전달(Perfect Forwarding)과 std::forward (0) | 2023.12.05 |
| [C / C++] 오른 값 참조(R-Value Reference)와 Move Semantics (0) | 2023.12.04 |
| [C / C++] delete (삭제된 함수) (0) | 2023.12.01 |