이번글은 C++의 스마트 포인터에 대한 글입니다.
스마트 포인터
C++이후에 나온 언어들을 보면은 C#, JAVA 등등은 '가비지 컬렉터'라고 불리는 '자원 청소기'가 내장되어 있습니다.
프로그램 상에서 더이상 사용하지 않는 자원들을 자동으로 해제 해주기 때문에 사용자가 굳이 메모리 해제를 일일히 신경쓰면서 작업할 필요가 없습니다.
하지만 우리의 C++의 경우 이딴건 지원을 하지 않습니다! (이게 매력인거 같습니다)
그래서 스마트 포인터를 사용하는데 스마트 포인터를 사용하는 가장 큰 이유는 'RAII'때문입니다.
Resource Acquisition Is Intialization이라는 말인데요
'오브젝트와 자원 획득과의 초기화를 같이한다(생명주기를 같이한다)'라는 말입니다.
(따라서 모든 스마트 포인터는 'RAII'를 제공해야합니다.)
'자원'에는 크게 아래와 같이 있습니다.
- Heap
- Thread
- File Access
- Mutex
- DB Connection
- 등
위의 자원들과 오브젝트와의 생명주기를 같이하기 위해서 스마트 포인터를 사용합니다.
제한된 Resource는 'Heap'이고 오브젝트는 '스마트 포인터 객체'가 됩니다.
스마트 포인터는 일반적인 '쌩 포인터'가 아니라 '포인터 객체'입니다. 그래서 해당 객체가 소멸될 때 자신이 가르키고 있던 데이터들도 같이 delete해버림으로써 메모리 누수를 방지합니다.
(auto_ptr은 사용을 금지합니다. 자세한 이유는 글 맨마지막 참고한 글 부분에서 확인해주시면 되겠습니다)
std::unique_ptr
std::unique_ptr은 객체에 대한 '유일한' 소유권을 가집니다. 복사 생성자와 복사 대입연산자는 delete로 처리되어 있습니다.
따라서 복사는 불가능하고 오로지 '이동'만 가능합니다.
이동은 std::move로 가능하고 포인터 객체의 실제 주소는 get() 함수로 가져올 수 있습니다. 또한 메모리 해제는 reset()함수로 해제를 할 수 있습니다.
#include <iostream>
#include <memory>
int main() {
std::unique_ptr<int> f1(new int(3));
// auto f2 = f1; // 복사할 수 없다.
auto f2 = std::move(f1); // f2로 이동
std::cout << f1.get() << std::endl; // null_ptr기 때문에 0 출력 -> 000000000000
std::cout << f2.get() << std::endl;
f1.reset(); // 이미 이동되었기 때문에 아무 동작도 수행되지 않는다.
f2.reset(); // 메모리 해제
return 0;
}
그런데 위의 f2를 '참조'로 전달하면 어떻게 되는 것일까요?
#include <iostream>
#include <memory>
void RefFunc(std::unique_ptr<int>& ref)
{
cout << *ref << endl;
}
int main() {
std::unique_ptr<int> f1(new int(3));
// auto f2 = f1; // 복사할 수 없다.
auto f2 = std::move(f1); // f2로 이동
std::cout << f1.get() << std::endl; // null_ptr기 때문에 0 출력 -> 000000000000
std::cout << f2.get() << std::endl;
RefFunc(f2);
f1.reset(); // 이미 이동되었기 때문에 아무 동작도 수행되지 않는다.
f2.reset(); // 메모리 해제
return 0;
}
std::unique_ptr은 객체애 대한 '유일한' 소유권을 나타내는데 이렇게 되면 ref도 소유권을 가져버리게 되어 유일한 소유권이라는 원칙을 위배하게 됩니다.
그래서 그냥 아래처럼 포인터만 전달하는 식으로 유일한 소유권이라는 원칙을 위배 하지 않도록 해줍니다.
void RefFunc(int* ref)
{
cout << *ref << endl;
}
std::shared_ptr
std::shared_ptr은 unique_ptr과는 다르게 객체의 소유권을 다른 포인터들과 공유할 수 있습니다.
std::shared_ptr은 같은 객체를 가르키는 객체를 레퍼런스 카운팅으로 추적합니다.
포인터가 복사 될 때마다 레퍼런스 카운트가 1씩 증가되며, 해재 될 때마다 1씩 감소합니다.
0이 될때야 비로소 메모리가 해제가 됩니다.
#include <iostream>
#include <memory>
int main() {
std::shared_ptr<int> foo(new int(3)); // reference count = 1
auto bar = foo; // reference count = 2
std::cout << "reference count: " << bar.use_count() << std::endl;
foo.reset(); // reference count = 1
std::cout << "reference count: " << bar.use_count() << std::endl;
bar.reset(); // reference count = 0, 이떄 객체가 완전히 해제된다.
return 0;
}
하지만 문제점이 있습니다.
- 여전히 메모리 누수가 발생 할 수 있다는 점
위의 경우 'Cycle' 발생시 레퍼런스 카운트가 감소하지 않아 메모리 누수가 발생합니다.
아래와 같은 경우 사이클이 발생하여 메모리가 해제가 되지않고 메모리 누수가 발생하니 주의 해서 사용해야합니다.
#include <iostream>
#include <memory>
class Cat
{
public:
Cat()
{
cout << "Cat 생성자 호출!" << endl;
}
~Cat()
{
cout << "Cat 소멸자 호출!" << endl;
}
public:
std::shared_ptr<Cat> mFriend;
};
int main()
{
shared_ptr<Cat> cat1 = std::make_shared<Cat>();
shared_ptr<Cat> cat2 = std::make_shared<Cat>();
cat1->mFriend = cat2;
cat2->mFriend = cat1;
return 0;
}
make_shared를 사용하자
위의 경우 make_shared를 통해 객체를 생성해주었는데요. 아래처럼 new연산자를 통해 객체를 생성하는 방법은 바람직하지 않습니다.
shared_ptr<Cat> cat1(new Cat); // 바람직 하지 않다.
shared_ptr<Cat> cat1 = std::make_shared<Cat>(); // 바람직하다.
이유는 첫번째의 경우 new 연산자를 통해 한반 동적할당이 일어나고 그 다음에 shared_ptr의 제어 블록 역시 동적으로 할당해야하기 때문입니다. 즉 두번의 동적할당이 발생합니다.
shared_ptr 객체는 복사가 발생할 때 실제 객체를 가르키는 shared_ptr이 제어 블럭을 동적으로 할당한 후 제어 블럭에 필요한 정보들을 공유하는 방식으로 구현되어 있습니다.
따라서 new 동작할당과 제어 블럭을 두개를 합친 크기로 한번에 할당할 수 있는 make_shared를 사용하는 것이 바람직합니다.
vector
레퍼런스 카운트가 0이 될 때 delete를 호출하지만 delete[]를 호출하지는 않습니다.
따라서 아래의 arr처럼 사용하면 메모리 누수가 발생하기에 arr대신에 vec처럼 사용을 해주시면됩니다.
#include <memory>
#include <vector>
int main()
{
std::shared_ptr<int> arr(new int[1024]);
arr.reset();
for (auto& a : arr)
{
a.reset();
}
return 0;
}
하지만 위처럼 for 문을 사용해서 일일히 다 해제할 수 도 있지만 깔끔하지는 않습니다.
shared_ptr의 생성자를 찾아보면 두번째 인자로 소멸자를 받아주는 것처럼 보이는 버젼이 있는데요
굉장히 어지럽게 되있긴 한데 이를 사용하면 좀더 깔끔하게 스마트 포인터를 원소로 가지는 배열을 생성할 수 있습니다.
#include <memory>
template<typename T>
struct deleter {
void operator() (T* ptr) {
delete[] ptr;
}
};
int main() {
std::shared_ptr<int> arr1(new int[1024], deleter<int>());
std::shared_ptr<int> arr2(new int[1024], std::default_delete<int[]>());
std::shared_ptr<int> arr3(new int[1024], [](auto v) { delete[] v; }); // 람다
arr1.reset();
arr2.reset();
arr3.reset();
return 0;
}
위처럼 사용자 정의 deleter, std::default_delete, 람다 등을 두번째 인자로 넣어서 해제할 수 가 있습니다.
std::weak_ptr
위의 shared_ptr의 문제점인 사이클 문제를 해결해주는 스마트 포인터가 std::weak_ptr입니다.
weak_ptr은 일반 포인터와 shared_ptr 중간쯤에 위치한 스마트 포인터입니다. 일반 스마트 포인터처럼 객체를 안전하게 참조할 수 있게 해주지만 shared_ptr과는 다르게 레퍼런스 카운팅을 증가시키지 않습니다.
따라서 위의 Cat클래스에서 한쪽은 weak_ptr을 사용하여 순환 문제를 해결할 수 있습니다.
weak_ptr로 다른 객체를 참조하는 방식입니다.
참조에는 '강한 참조'와 '약한 참조'로 나뉘는데 오브젝트의 생명주기에 관여하는 참조가 '강한 참조'입니다. weak_ptr은 '약한 참조'입니다. 따라서 weak_ptr을 통해 멤버 변수나 함수에 접근할 수 없고 포인터에도 접근할 수 없습니다.
따라서 반드시 shared_ptr로 변환해서 사용해야합니다.
weak_ptr은 생성자로 shared_ptr이나 다른 weak_ptr을 받습니다.
lock() 이라는 멤버함수로 shared_ptr객체를 생성한다음에 그 객체를 통해서 포인터에 접근해야합니다.
lock() 사용할 때 weak_ptr이 가르키는 객체가 아직 메모리에 살아있다면 즉, 레퍼런스 카운터가 0이 아니라면 해당 객체가 가르키는 shared_ptr을 반환하고 이미 해제 되었다면 아무것도 가르키지 않는 shared_ptr을 반환합니다.
#include <memory>
class A
{
public:
A()
{
cout << "생성자" << endl;
}
~A()
{
cout << "소멸자" << endl;
}
public:
string name;
};
int main()
{
std::shared_ptr<A> foo = make_shared<A>(); // reference count = 1
std::weak_ptr<A> bar = foo; // reference count = 1
cout << foo.use_count() << endl;
{
auto baz = bar.lock(); // reference count = 2
cout << foo.use_count() << endl;
} // 이 closure를 벗어나면서 baz는 해제된다. reference count = 1
foo.reset(); // reference count = 0
return 0;
}
따라서 사이클이 발생했던 Cat클래스의 코드를 아래처럼 수정할 수 있습니다.
#include <iostream>
#include <memory>
class Cat
{
public:
Cat()
{
cout << "Cat 생성자 호출!" << endl;
}
~Cat()
{
cout << "Cat 소멸자 호출!" << endl;
}
void Print()
{
cout << "야옹" << endl;
}
public:
std::weak_ptr<Cat> mFriend;
};
int main()
{
shared_ptr<Cat> cat1 = std::make_shared<Cat>();
shared_ptr<Cat> cat2 = std::make_shared<Cat>();
cat1->mFriend = cat2;
cat2->mFriend = cat1;
cat1->mFriend.lock()->Print();
return 0;
}
참고 자료
https://blog.koriel.kr/cpp11-smart-pointer/
https://husk321.tistory.com/360
'CPP' 카테고리의 다른 글
[C++] union (DX Matrix) (0) | 2023.09.04 |
---|---|
[C++] initializer_list (0) | 2023.08.23 |
[C++] 가변 길이 템플릿과 후행 리턴 타입 (0) | 2023.08.20 |
[C++] boost 라이브러리 설치 방법 (0) | 2023.08.17 |
[C++] constexpr (0) | 2023.08.17 |
포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!