RAII 디자인 패턴은 자원의 생애 주기 (자원 할당 -> 자원 사용 -> 자원 해제) 를 객체의 생애 (객체의 Constructor 호출 -> 객체의 Destructor 호출) 에 바인드하여 자원 관리를 C++ 런타임에서 자동으로 맡기는 기법이다.
스마트 포인터는 RAII 패턴을 이용해 동적 메모리 관리를 프로그래머가 아닌 C++ 런타임이 관리하도록 만들어진 클래스이다.
기존 C++ 포인터는 프로그래머가 할당과 해제를 모두 신경써줘야했는데 이런 메모리 관리를 해결 할수 있어 사용했습니다.
댕글링 포인터(Dangling Pointer)
C++ 프로그래밍에서 매우 위험한 버그의 원인이 되는 문제로, 가리키는 대상이 더 이상 유효하지 않은 포인터를 말합니다.
포인터를 2개 또는 그 이상 나눠서 가졌을 때 잘못 Destroy하면 댕글링이됨.
(댕글링이 위험한 이유는 메모리 크러쉬로 인해 fatal 에러. 흔히 말해 프로그램 터지기 때문.)
1. std::share_ptr
std::share_ptr은 참조 횟수가 계산되는 스마트 포인터입니다. Shared_ptr 소유자가 범위를 벗어나거나 소유권을 포기할 때까지 삭제되지 않습니다.
std::shared_ptr 단점
(1) Destroy를 했는데 안지워지는 경우가 생김
// 순환참조
std::shared_ptr<A> PtrA = std::make_shared<A>();
std::shared_ptr<B> PtrB = std::make_shared<B>();
PtrA->BPtr = PtrB;
PtrB->APtr = PtrA;

순환 참조란 위 코드와 그림처럼 클래스 내부에서 Shared Pointer로 다른 클래스가 서로 가리키는 것을 의미한다. A 오브젝트와 B 오브젝트가 Shared Pointer A와 B에 의해서 가리키게 되고 내부의 mVar Shared Pointer가 서로의 오브젝트를 가리키게 되면 Shared Pointer가 main함수에서 메모리 해제가 되어도 각 오브젝트의 Count는 1로 메모리 해제가 안 되는 것을 말한다.
2. weak_ptr
shared_ptr과 함께 사용할 수 있는 특별한 경우의 스마트 포인터입니다.
weak_ptr은 하나 이상의 shared_ptr 인스턴스가 소유하는 개체에 대한 액세스를 제공하지만, 참조 수 계산에 참가하지 않습니다.
(레퍼런스 카운팅에 영향X)
std::weak_ptr는 std::shared_ptr에 의해 관리되는 자원 객체를 공유하지만 소유하지 않는 (std::shared_ptr의 약점(순환 참조)를 보강해주는)스마트 포인터입니다.
std::unique_ptr나 std::shared_ptr는 자신이 자원 객체를 관리하기 때문에 자원 객체가 살아있는지, 소멸했는지 알 수 있지만 std::weak_ptr는 자원 객체를 가리킬 뿐, 관리하지 않기 때문에 자신이 가리킨 자원 객체가 살아있는지 소멸했는지 알 수 없습니다.
#include <iostream>
#include <memory>
class NPC
{
private:
std::string sName;
int iAge = -1;
public:
NPC(const std::string& sInName, int iInAge) : sName(sInName), iAge(iInAge){
std::cout << "생성자 호출" << std::endl;
}
~NPC(){
sName.clear();
iAge = -1;
std::cout << "소멸자 호출" << std::endl;
}
};
int main()
{
_CrtSetDbgFlag(_CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF);
std::shared_ptr<NPC> pNPC = std::make_shared<NPC>("NPC1", 10);
std::weak_ptr<NPC> pNPCPtr1(pNPC);
std::weak_ptr<NPC> pNPCPtr2 = pNPCPtr1;
std::weak_ptr<NPC> pNPCPtr3;
pNPCPtr3 = pNPCPtr2;
std::cout << "pNPC 참조 개수: " << pNPC.use_count() << std::endl;
std::cout << "pNPCPtr1 참조 개수: " << pNPCPtr1.use_count() << std::endl;
std::cout << "pNPCPtr2 참조 개수: " << pNPCPtr2.use_count() << std::endl;
std::cout << "pNPCPtr3 참조 개수: " << pNPCPtr3.use_count() << std::endl;
}

shared_ptr 인스턴스 사이의 순환 참조를 차단
#include <iostream>
class A;
class B
{
public:
B()
{
}
void test()
{
std::cout << "B";
}
std::weak_ptr<A> aa; // 레퍼런스 카운트에 포함이 안됨
private:
};
class A
{
public:
A()
{
}
void test() // virtual 즉 가상함수 테이블 기반으로 형반환 하기에 가상함수가 없는 경운
{
std::cout << "A";
}
std::weak_ptr<B> bb; // 레퍼런스 카운트에 포함이 안됨
private:
};
int main()
{
_CrtSetDbgFlag(_CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF);
// 업 캐스팅S
// 자식(B) 객체를
std::shared_ptr<A> a = std::make_shared<A>();
std::shared_ptr<B> b = std::make_shared<B>();
// 맴버 변수로 가지고 있음
a->bb = b; // a(std::shared_ptr<B>)
b->aa = a; // b(std::shared_ptr<A>)
a->bb.lock()->test(); // lock() -> 이걸 사용해야지 해당 클래스의 함수 호출
int c = 0;
}
lock 멤버 함수는 원자적으로 연산되며 std::weak_ptr가 가리키는 자원 객체가 소멸했다면 아무 것도 가리키지 않는 std::shared_ptr를 반환하지만 존재한다면 std::weak_ptr와 동일한 자원 객체를 가리키는 std::shared_ptr를 반환합니다.
#include <iostream>
#include <memory>
#include <vector>
class NPC
{
private:
std::string sName;
int iAge = -1;
std::vector<std::weak_ptr<NPC>> pNeighbor_List;
private:
std::string GetName() { return sName; }
int GetAge() { return iAge; }
public:
NPC(const std::string& sInName, int iInAge) : sName(sInName), iAge(iInAge){
std::cout << "생성자 호출" << std::endl;
}
~NPC(){
std::cout << "소멸자 호출" << std::endl;
}
void Init(const std::vector<std::weak_ptr<NPC>>& pInNeighbor_List)
{
pNeighbor_List.resize(pInNeighbor_List.size());
pNeighbor_List = pInNeighbor_List;
}
void Reset()
{
sName.clear();
iAge = -1;
for(int iIndex=0; iIndex<pNeighbor_List.size(); iIndex++)
pNeighbor_List[iIndex].reset();
pNeighbor_List.clear();
}
void CallNeighborData()
{
for (int iIndex=0; iIndex<pNeighbor_List.size(); iIndex++){
if(pNeighbor_List[iIndex].lock() != nullptr)
std::cout << "접근 성공, Neighbor 정보 { " << pNeighbor_List[iIndex].lock()->GetName() << ", " << pNeighbor_List[iIndex].lock()->GetAge() << " }" << '\n';
else
std::cout << "접근 실패(참조한 객체가 소멸됨)" << '\n';
}
}
};
int main()
{
_CrtSetDbgFlag(_CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF);
std::shared_ptr<NPC> pNPC1 = std::make_shared<NPC>("NPC1", 10);
std::shared_ptr<NPC> pNPC2 = std::make_shared<NPC>("NPC2", 20);
std::vector<std::weak_ptr<NPC>> pNPC1Neighbor_List;
std::vector<std::weak_ptr<NPC>> pNPC2Neighbor_List;
pNPC1Neighbor_List.push_back(pNPC2);
pNPC1->Init(pNPC1Neighbor_List);
pNPC2Neighbor_List.push_back(pNPC1);
pNPC2->Init(pNPC2Neighbor_List);
std::cout << "pNPC1 참조 개수: " << pNPC1.use_count() << '\n'
<< "pNPC2 참조 개수: " << pNPC2.use_count() << '\n';
pNPC1->CallNeighborData();
pNPC1->Reset();
pNPC1.reset();
pNPC2->CallNeighborData();
}

3. unique_ptr
(1) 하나의 스마트 포인터만이 특정 개체에 대한 단일 소유권을 가집니다.
(2) std::move를 통해서 소유권을 다른 unique_ptr로 이전할 수 있고, 이 과정에서 원래의 unique_ptr은 null 상태
#include <iostream>
#include <memory>
#include <vector>
class NPC
{
private:
std::string sName;
int iAge = -1;
std::vector<std::weak_ptr<NPC>> pNeighbor_List;
private:
std::string GetName() { return sName; }
int GetAge() { return iAge; }
public:
NPC(const std::string& sInName, int iInAge) : sName(sInName), iAge(iInAge){
std::cout << "생성자 호출" << std::endl;
}
~NPC(){
std::cout << "소멸자 호출" << std::endl;
}
void Init(const std::vector<std::weak_ptr<NPC>>& pInNeighbor_List)
{
pNeighbor_List.resize(pInNeighbor_List.size());
pNeighbor_List = pInNeighbor_List;
}
void Reset()
{
sName.clear();
iAge = -1;
for(int iIndex=0; iIndex<pNeighbor_List.size(); iIndex++)
pNeighbor_List[iIndex].reset();
pNeighbor_List.clear();
}
void CallNeighborData()
{
for (int iIndex=0; iIndex<pNeighbor_List.size(); iIndex++){
if(pNeighbor_List[iIndex].lock() != nullptr)
std::cout << "접근 성공, Neighbor 정보 { " << pNeighbor_List[iIndex].lock()->GetName() << ", " << pNeighbor_List[iIndex].lock()->GetAge() << " }" << '\n';
else
std::cout << "접근 실패(참조한 객체가 소멸됨)" << '\n';
}
}
};
int main()
{
_CrtSetDbgFlag(_CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF);
std::unique_ptr<NPC> pNPC = std::make_unique<NPC>("NPC1", 10);
std::unique_ptr<NPC> pChangeNPC = std::move(pNPC);
if (pNPC == nullptr)
std::cout << "Is Nullptr" << std::endl;
if(pChangeNPC != nullptr)
std::cout << "Use OK" << std::endl;
}
std::enable_shared_from_this -> 본인 자신을 리턴해서 사용하고 싶은 경우에 사용
- enable_shared_from_this에서 파생된 개체는 멤버 함수에서 shared_from_this 메서드를 사용하여 기존 shared_ptr 소유자와 소유권을 공유하는 인스턴스의 shared_ptr 소유자를 만듭니다.

ex) 소유권을 공유하는 개념 레퍼런스 카운트 증가는 1
<- B
A(공유해서 가리킨다.) -> 레퍼런스 카운트 증가는 1
<- C
- 그렇지 않으면 this를 사용하여 새 shared_ptr를 만들 경우 기존 shared_ptr 소유자와 완전히 다르므로 잘못된 참조가 발생하거나 개체가 두 번 이상 삭제될 수 있습니다.
- 개체가 enable_shared_from_this 기본 클래스에서 파생될 경우 shared_from_this 템플릿 멤버 함수는 이 인스턴스의 소유권을 기존 shared_ptr 소유자와 공유하는 shared_ptr 클래스 개체를 반환합니다.
- 그렇지 않으면 this에서 새 shared_ptr를 만들 경우 기존 shared_ptr 소유자와 완전히 다르므로 잘못된 참조가 발생하거나 개체가 두 번 이상 삭제될 수 있습니다. shared_ptr 개체가 아직 소유하지 않은 인스턴스에서 shared_from_this를 호출하면 동작이 정의 해제됩니다.
(2) enable_shared_from_this의 사용은 이렇게 객체의 생성 및 소멸에 의한 참조 문제를 방지하기 위해 사용이 된다.
class A : public std::enable_shared_from_this<A>
{
....
}
int main ()
{
....
std::shared_ptr<A> ptr1, ptr2;
ptr1 = new A;
ptr2 = ptr1 -> shared_from_this();
}
// 예시
'C++ 개념 정리' 카테고리의 다른 글
| 11. 문자열 표준 정리 (0) | 2026.01.11 |
|---|---|
| 10. C++ 전처리문 (0) | 2026.01.11 |
| 8. Enum Vs Enum Class (0) | 2026.01.04 |
| 7. C++ 형변환 (1) | 2026.01.03 |
| 6. 생성자, 소멸자 (0) | 2026.01.03 |