std::shared_ptr
개요
std::shared_ptr
는 하나의 리소스를 여러 포인터가 공유해서 사용할 수 있게 하는 스마트 포인터입니다. 참조 카운팅(Reference Counting) 방식을 사용하여, 자신을 참조하는 shared_ptr
가 몇 개인지 계산합니다. 참조 카운트가 0이 되면 자동으로 메모리를 해제합니다.
이를 통해 여러 객체가 동일한 메모리 리소스에 안전하게 접근하고 소유권을 공유할 수 있으며, 리소스의 생명 주기를 자동으로 관리하여 메모리 누수를 방지합니다.
C++ 버전별 주요 키워드 도입 시기
- C++11:
std::shared_ptr
가 표준 라이브러리에 처음 도입되었습니다. - C++11:
std::make_shared
가 도입되어 더 안전하고 효율적인shared_ptr
생성을 지원합니다. - C++17:
std::shared_ptr
가 배열을 지원하게 되었습니다.std::shared_ptr<T[]>
와 같이 사용할 수 있습니다.
내용 설명
std::shared_ptr
는 내부적으로 두 개의 포인터를 유지합니다.
- 관리 대상이 되는 리소스(객체)를 가리키는 포인터
- 참조 카운트, 약한 참조(weak count), 커스텀 삭제자(deleter) 등을 포함하는 제어 블록(Control Block)을 가리키는 포인터
shared_ptr
가 복사될 때마다 참조 카운트가 1씩 증가하고, shared_ptr
가 소멸될 때마다 참조 카운트가 1씩 감소합니다. 참조 카운트가 0이 되면 shared_ptr
는 제어 블록과 함께 관리하던 리소스를 해제합니다.
순환 참조 (Circular Reference) 문제
shared_ptr
의 가장 큰 단점은 두 개 이상의 객체가 서로를 shared_ptr
로 참조하는 순환 참조 구조가 만들어질 경우, 참조 카운트가 0이 되지 않아 메모리 누수가 발생할 수 있다는 점입니다. 아래 예제 코드의 f2
함수가 이 문제를 보여줍니다.
이 문제를 해결하기 위해 std::weak_ptr
를 사용할 수 있습니다. weak_ptr
는 리소스를 소유하지 않으므로 참조 카운트를 증가시키지 않고, 리소스에 대한 접근만 제공합니다. 순환 참조가 발생하는 경우, 한쪽의 참조를 weak_ptr
로 바꾸어 순환 고리를 끊을 수 있습니다.
예제 코드
#include <iostream>
#include <memory>
class ScopeTest
{
public:
ScopeTest(int val) : m_val(val)
{
std::cout << "Constructor: " << m_val << '\n';
}
~ScopeTest()
{
std::cout << "Destructor! val: " << m_val << '\n';
}
void test()
{
std::cout << m_val << '\n';
}
std::shared_ptr<ScopeTest> m_partner;
private:
int m_val;
};
// 기본 사용법 예제
void f1()
{
std::cout << "--- f1 execution ---" << std::endl;
auto t = std::make_shared<ScopeTest>(10);
t->test();
std::cout << "Count: " << t.use_count() << '\n'; // Count: 1
{
auto t2 = t; // 참조 카운트 증가
t2->test();
std::cout << "Count: " << t.use_count() << '\n'; // Count: 2
}
std::cout << "Count: " << t.use_count() << '\n'; // Count: 1
} // t가 소멸되며 참조 카운트 0, 소멸자 호출
// 순환 참조 문제 예제
void f2()
{
std::cout << "--- f2 execution (Circular Reference) ---" << std::endl;
auto t4 = std::make_shared<ScopeTest>(40);
std::cout << "Count t4: " << t4.use_count() << '\n'; // Count t4: 1
auto t5 = std::make_shared<ScopeTest>(50);
std::cout << "Count t5: " << t5.use_count() << '\n'; // Count t5: 1
t4->m_partner = t5; // t5의 참조 카운트 증가
std::cout << "Count t5: " << t5.use_count() << '\n'; // Count t5: 2
t5->m_partner = t4; // t4의 참조 카운트 증가
std::cout << "Count t4: " << t4.use_count() << '\n'; // Count t4: 2
} // t4, t5가 소멸되지만, 서로를 가리키고 있어 참조 카운트가 1로 남아 소멸자가 호출되지 않음 -> weak_ptr
int main()
{
f1();
std::cout << '\n';
f2();
std::cout << "\nmain function finished." << std::endl;
return 0;
}
실행 결과
--- f1 execution ---
Constructor: 10
10
Count: 1
10
Count: 2
Count: 1
Destructor! val: 10
--- f2 execution (Circular Reference) ---
Constructor: 40
Count t4: 1
Constructor: 50
Count t5: 1
Count t5: 2
Count t4: 2
main function finished.
f1
에서 생성된 객체(값 10)는 함수가 끝나면서 정상적으로 소멸자가 호출되었습니다. 하지만 f2
에서 생성된 두 객체(값 40, 50)는 main
함수가 끝날 때까지 소멸자가 호출되지 않았습니다. 이것이 바로 순환 참조로 인한 메모리 누수입니다.
활용팁
std::make_shared
사용:new
키워드를 직접 사용하는 것보다std::make_shared
를 사용하세요. 객체와 제어 블록을 한 번의 할당으로 생성하여 더 효율적이고, 예외 발생 시에도 안전합니다.- 순환 참조 주의: 클래스 설계 시 객체들이 서로를
shared_ptr
로 가리키는 구조가 될 수 있는지 항상 확인해야 합니다. 만약 그렇다면, 소유 관계가 없는 쪽(자식->부모 등)은std::weak_ptr
를 사용하여 순환 참조를 방지해야 합니다. - this 포인터: 클래스 내부에서 자기 자신을 가리키는
shared_ptr
가 필요할 때는std::enable_shared_from_this
를 상속받고shared_from_this()
멤버 함수를 사용해야 합니다. 그냥std::shared_ptr<T>(this)
를 사용하면 제어 블록이 새로 생성되어 이중 해제(double free) 문제가 발생할 수 있습니다.
'개발 > C++ (98,03,11,14,17,20,23)' 카테고리의 다른 글
Modern C++ : std::unique_ptr (11, 14) (0) | 2025.10.13 |
---|---|
Modern C++ : std::format (20) (0) | 2025.10.12 |
Modern C++ : std::ranges (20) (0) | 2025.10.11 |
Modern C++ : std::Attributes (11, 14, 17, 20) (0) | 2025.10.10 |
Modern C++ : std::any (17) (0) | 2025.09.30 |
Modern C++ : std::variant (17) (0) | 2025.09.29 |
Modern C++ : std::optional (17) (0) | 2025.09.28 |
Modern C++ : Type Traits 2 (11, 14, 17, 20) (0) | 2025.09.27 |