C++ 개념 정리

9. 스마트 포인터

CE : 하랑 2026. 1. 11. 16:59

 

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