: C++의 특별한 클래스 타입, 포인터처럼 동작하지만 스스로 메모리를 관리
메모리 누수
: 동적으로 할당한 메모리를 해제하지 않고 잊어버리면 그 메모리는 계속 시스템에 할당된 상태로 남게 됨
이렇게 되면 메모리 사용량이 증가하고, 결국은 시스템의 성능을 저하시키는 문제가 발생
해제 후 사용 (dangling pointer)
: 이미 해제된 메모리를 계속 사용하려는 경우 발생
이는 데이터 손실이나 시스템 충돌을 일으킬 수 있는 심각한 문제
: RAII(Resource Acquisition Is Initialization)이라는 원칙을 사용해 메모리를 자동으로 관리
💡 RAII (Resource Acquisition Is Initialization)
: 객체의 수명이 그 객체가 소유한 자원의 수명과 동일하게 관리되는 것
스마트 포인터는 내부적으로 '원시 포인터(raw pointer)'를 보관하고 있음
이 원시 포인터는 스마트 포인터가 가리키는 실제 메모리를 가리킴
그러나 사용자는 이 원시 포인터에 직접 접근할 수 없으며, 스마트 포인터가 제공하는 인터페이스를 통해서만 메모리에 접근할 수 있음
❗ 스마트 포인터를 사용하더라도 메모리 관리에 대한 주의는 필요함!
스마트 포인터를 사용하더라도 순환 참조와 같은 문제가 발생할 수 있기 때문
이름에서 알 수 있듯이 유일한 포인터
즉, 동일한 메모리를 가리키는 두 개의 unique_ptr 인스턴스가 동시에 존재할 수 없음
이러한 특성은 메모리 누수와 같은 일반적인 문제를 방지하는 데 도움이 됨
std::unique_ptr<int> ptr1(new int(5));
std::unique_ptr<int> ptr2 = ptr1; // 컴파일 에러
하지만, 이동이라는 개념을 사용하여 포인터의 소유권을 이전할 수 있음
std::unique_ptr<int> ptr1(new int(5));
std::unique_ptr<int> ptr2 = std::move(ptr1);
ptr1은 null이 되고, ptr2는 ptr1이 이전에 가리켰던 메모리를 가지게 됨
한편, unique_ptr은 성능 비용이 거의 없음
이는 내부적으로 일반 포인터를 사용해 객체를 추적하기 때문임
∴ 안전한 리소스 관리와 효율성을 동시에 달성하는 데 도움이 됨
이러한 속성 덕분에 다양한 유스케이스에서 유용하게 사용됨
ex. 함수에서 동적으로 할당된 메모리를 반환할 때, unique_ptr를 사용하면 호출자가 반환된 메모리의 소유권을 명확하게 이해할 수 있으며, 메모리 누수의 위험을 크게 줄일 수 있음
기본 사용법
#include <memory>
#include <iostream>
using namespace std;
class MyClass
{
public:
MyClass(int value) : value_(value) {}
void PrintValue()
{
cout << "Value : " << value_ << "\n";
}
};
int main()
{
// 첫 번째 예제
unique_ptr<MyClass> ptr1(new MyClass(3));
ptr1->PrintValue();
// 두 번째 예제
unique_ptr<MyClass> ptr2 = make_unique<MyClass>(10);
ptr2->PrintValue();
// 세 번째 예제
// unique_ptr<MyClass> ptr3 = ptr2; // 컴파일 에러
unique_ptr<MyClass> ptr3 = move(ptr2); // ptr2의 소유권이 ptr3로 옮겨짐
ptr3->PrintValue(); // Value : 10 출력
}
unique_ptr는 두 번째 예제처럼 make_unique 함수를 사용하여 생성하는게 좋은데, 그 이유는 객체 생성과 메모리 할당을 하나의 연산으로 결합하여 예외 안정성을 높일 수 있기 때문
함수 전달 / 반환
#include <memory>
using namespace std;
unique_ptr<int> CreateUniquePtr()
{
return unique_ptr<int>(new int(5));
// unique_ptr를 직접 반환하면, 소유권이 이전됨
// 이 때문에 함수가 끝난 후에도 메모리 누수는 발생하지 않음
}
void UseUniquePtr(unique_ptr<int> ptr)
{
// 함수에 전달된 unique_ptr는 이 함수 안에서만 유효
// 함수가 끝난 후에는 자동으로 메모리가 해제됨
}
int main()
{
unique_ptr<int> ptr = CreateUniquePtr();
UseUniquePtr(move(ptr));
// unique_ptr를 함수에 전달할 때는 move를 사용해야 함
// 이제 ptr는 nullptr임
}
#include <memory>
#include <cstdio>
using namespace std;
struct FileDeleter
{
void operator()(FILE* file) const
{
if(file)
{
fclose(file);
}
}
};
int main()
{
unique_ptr<FILE, FileDeleter> file_ptr(fopen("myfile.txt", "r"));
}
FileDeleter는 unique_ptr가 파일 핸들을 제거하는 방법을 정의하는 커스텀 deleter
메모리에 대한 공유 소유권을 제공하는 스마트 포인터
내부적으로 레퍼런스 카운팅을 수행하여, 메모리를 가리키는 shared_ptr 인스턴스의 수를 추적함
→ 여러 shared_ptr 인스턴스가 동일한 메모리를 카리킬 수 있음
std::unique_ptr<int> ptr1(new int(5));
std::unique_ptr<int> ptr2 = ptr1; // it's okay!
ptr1과 ptr2는 모두 동일한 메모리를 가리키며, 레퍼런스 카운트는 2임
{
std::shared_ptr<int> ptr1(new int(3));
{
std::shared_ptr<int> ptr2 = ptr1; // 레퍼런스 카운트 2
} // ptr2가 소멸되면서 레퍼런스 카운트 1
} // ptr1이 소멸되면서 레퍼런스 카운트가 0이 되고 메모리가 해제됨
레퍼런스 카운트가 0이 되면 즉시 메모리가 해제됨
but. 불필요한 레퍼런스 카운팅은 성능을 저하시킬 수 있으므로 반드시 필요한 경우에만 사용해야 함
❗ 또한, shared_ptr은 순환 참조 문제를 야기할 수 있음
→ weak_ptr를 통해 해결할 수 있음
기본 사용법
shared_ptr<int> p1(new int(5)); // 일반 포인터를 이용한 초기화
shared_ptr<int> p2 = make_shared<int>(5); // make_shared를 이용한 초기화
unique_ptr와 같이 두 가지 방법의 초기화가 있음
shared_ptr 또한 make_shared를 이용한 초기화를 사용하는 것이 좋음
참조 카운팅
shared_ptr<int> p1 = make_shared<int>(5);
cout << "p1 use count: " << p1.use_cout() << '\n'; // p1 use cout: 1 출력
shared_ptr<int> p2 = p1;
cout << "p1 use count: " << p1.use_cout() << '\n'; // p1 use cout: 2 출력
cout << "p2 use count: " << p2.use_cout() << '\n'; // p2 use cout: 2 출력
참조 카운트는 shared_ptr의 use_count() 메서드를 통해 알 수 있음
❗ 이 메서드는 디버깅이나 학습 목적 이외에는 사용을 권장하지 않음
use_count()의 반환 값이 순간적인 상태를 반영하기 때문에 멀티 스레드 환경에서는 신뢰할 수 없기 때문
std::shared_ptr와 유사하지만, 가리키는 객체의 수명에 영향을 주지 않는 약한 참조를 제공한다는 점에서 다름
→ 순환 참조를 피하는데 유용
💡 순환 참조
: 두 객체가 서로를 참조하고, 둘 다 shared_ptr를 사용하여 참조를 유지하는 경우에 발생
이 경우, 두 객체 모두 레퍼런스 카운트가 절대 0이 되지 않아 메모리 누수 발생
struct B;
struct A
{
std::shared_ptr<B> b_ptr;
};
struct B
{
std::shared_ptr<A> a_ptr;
};
std::shared_ptr<A> a(new A());
std::shared_ptr<B> b(new B());
a->b_ptr = b;
b->a_ptr = a; // 순환 참조 생성
weak_ptr를 사용하여 위와 같은 순환 참조 상황을 해결할 수 있음
struct B;
struct A
{
std::shared_ptr<B> b_ptr;
};
struct B
{
std::weak_ptr<A> a_ptr; // 약한 참조 사용
};
std::shared_ptr<A> a(new A());
std::shared_ptr<B> b(new B());
a->b_ptr = b;
b->a_ptr = a; // 이제 순환 참조가 발생되지 않음
이제 B는 weak_ptr를 사용하여 A를 참조하므로 순환 참조가 발생하지 않음
A가 파괴되면 weak_ptr는 자동으로 nullptr로 설정됨
shared_ptr<int> sp(new int(10));
weak_ptr<int> wp(sp); // shared_ptr로부터 weak_ptr 생성
weak_ptr는 약한 참조만을 제공하므로, 객체에 직접 접근하려면 shared_ptr로 변환해야 함 → 이를 락(lock)이라 부름
if(shared_ptr<int> sp = wp.lock())
{
// 객체에 접근
}
lock 메소드는 해당 객체가 아직 메모리에 있을 때 shared_ptr를 반환하며, 그렇지 않을때는 빈 shared_ptr를 반환함
→ lock 메서드를 사용하여 객체가 메모리에 있는지 확인할 수 있음
if (wp.expired())
{
// 객체는 더 이상 메모리에 없음
}
또한, weak_ptr는 expired 메소드를 제공하여 객체가 메모리에 있는지 확인할 수 있음
❗ 이 메소드는 객체의 상태를 확인하고 객체에 접근하는 사이에 객체가 해제될 수 있으므로, 안전한 객체 접근을 위해서는 항상 lock 메서드를 사용해야 함