이번 포스팅은 스마트 포인터 중 shared_ptr 에 대해서 정리해보겠다.
shared_ptr in C++ 11
shared_ptr 은 c++ 11 이후 제공되는 스마트 포인터 중 하나로 포인터를 더 이상 사용하지 않을 경우 메모리를 자동으로 해제해준다. 보통 unmaged 코드에서는 메모리를 개발자가 직접적으로 관리하는 경우가 많은데 이럴 경우 할당된 메모리를 해제하지 않는 실수를 많이 범할 수 있다. 이런 경우를 미연에 조금 더 방지할 수 있게 해준다.
다음 코드는 shared_ptr 을 사용하는 예제를 보여주고 있다.
#include <iostream>
#include <memory>
using namespace std;
int main()
{
shared_ptr<int> x(new int(1));
shared_ptr<int> y = make_shared<int>(1);
wcout << *x << endl;
wcout << *y << endl;
return 0;
}
위 코드는 두가지 방식을 보여주고 있다. shared_ptr 의 생성자를 이용하는 방식과 make_shared 를 이용하는 방식이다.
유의할 점은 shared_ptr 에 직접적인 힙메모리를 가리키게 해서는 안된다.
shared_ptr 이 가리키고 있는 메모리는 내부적으로 관리되는 참조 카운트에 의해서 해제되는데 해당 참조 카운터가 0이 되면 가리키는 메모리는 해제되게 되어있다. 다음 코드를 보자.
#include <iostream>
#include <memory>
using namespace std;
int main()
{
shared_ptr <int> x;
shared_ptr <int> y;
wcout << "x ref count" << x.use_count() << endl;
x = make_shared<int>(0);
wcout << "x ref count" << x.use_count() << endl;
y = x;
wcout << "x ref count" << x.use_count() << endl;
wcout << "y ref count" << y.use_count() << endl;
x = nullptr;
wcout << "x ref count" << x.use_count() << endl;
wcout << "y ref count" << y.use_count() << endl;
y.reset();
wcout << "y ref count" << y.use_count() << endl;
return 0;
}
우선 shared_ptr 변수 자체는 참조 카운터가 0이다. 그리고 다른 shared_ptr 이 대입되거나 객체를 생성하면 해당 참조 카운터는 1이 증가된다. 그리고 shared_ptr 에 nullptr 을 대입하게 해당 변수의 참조 포인터는 0이 되며 해당 변수와 다른 shared_ptr 이 연결되어 있다면 해당 shared_ptr 의 참조 카운터는 1 감소한다. reset() 함수 또한 해당 변수의 참조 카운터를 0으로 만든다.
shared_ptr 객체는 변수가 선언된 영역을 벗어나면 소멸자는 참조 카운터를 1 감소시킨다. 아래 코드로 해당 내용을 확인할 수 있다.
#include <iostream>
#include <memory>
using namespace std;
int main()
{
shared_ptr<int> x = make_shared<int>(0);
if(true)
{
shared_ptr<int> y = x;
wcout << "x ref count : "<< x.use_count() << endl;
wcout << "y ref count : "<< y.use_count() << endl;
}
wcout << "x ref count : " << x.use_count() << endl;
return 0;
}
위 코드에서 if 안의 shared_ptr y 변수는 영역을 벗어나면 소멸자에서 참조카운터를 1 감소시킨다.
함수 인자로 shared_ptr 를 사용할 경우 참조를 사용할 때는 해당 shared_ptr 의 참조 카운트가 증가하지 않는다. 다음 코드를 보면 그 예를 확인할 수 있다.
#include <iostream>
#include <memory>
using namespace std;
void func(shared_ptr<int>& p)
{
wcout << "p ref count : " << p.use_count() << endl;
}
int main()
{
shared_ptr <int> p = make_shared<int>(0);
wcout << "p ref count : " << p.use_count() << endl;
func(p);
wcout << "p ref count : " << p.use_count() << endl;
return 0;
}
다음은 std::move 함수를 통한 shared_ptr 이동에 대해서 알아보겠다. move 함수를 사용하면 shared_ptr 변수가 가리키는 포인터를 다른 shared_ptr 로 이동시킬 수 있다. 그리고 기존 shared_ptr 참조 카운터는 0이 된다. 다음 코드를 보자.
#include <iostream>
#include <memory>
using namespace std;
int main()
{
shared_ptr<int> p1 = make_shared<int>(0);
shared_ptr<int> p2;
wcout << "p1 ref count : " << p1.use_count() << endl;
p2 = move(p1);
wcout << "p1 ref count : " << p1.use_count() << endl;
wcout << "p2 ref count : " << p2.use_count() << endl;
return 0;
}
위 코드를 실행하게 되면 p1의 포인터가 p2 로 이동되고 p1 의 참조 카운터는 0이 되게 된다.
이번에는 shared_ptr 의 reset() 쓰임새에 대해 알아보자. reset() 함수는 인자가 없이 사용될 경우에는 참조 카운터를 1 감소시킨다. 아래 코드 확인으로 그 쓰임을 확인할 수 있다.
#include <iostream>
#include <memory>
using namespace std;
int main()
{
shared_ptr<int> p1 = make_shared<int>(0);
shared_ptr<int> p2;
p2 = p1;
wcout << "p1 ref count : "+ p1.use_count() + " p2 ref count : " + p2.use_count() << endl;
p1.reset();
wcout << "p1 ref count : "+ p1.use_count() + " p2 ref count : " + p2.use_count() << endl;
return 0;
}
위 코드를 실행해보면 reset() 함수 호출전에는 참조 카운터가 p1 :2, p2:2 이었다가 reset() 호출 후 0, 1 로 변경되는 것을 확인할 수 있다.
reset() 함수를 인자와 함께 사용하면 기존 가리키는 포인터 대신 인자로 들어온 새로운 포인터를 가리키게 되며 참조 카운터는 1이 된다. 아래 코드를 확인하자.
#include <iostream>
#include <memory>
using namespace std;
int main()
{
shared_ptr<int> p1 = make_shared<int>(0);
p1.reset(new int(1));
wcout << "p1 ref count : " << p1.use_count() << "value : " *p1 << endl;
return 0;
}
위에서 설명했듯이 참조 카운터가 0이되면 shared_ptr 객체가 가리키는 메모리가 해제된다. shared_ptr 클래스에서는 메모리 해제를 위해 기본적으로 delete 를 호출하지만 reset 함수를 통하여 사용자 정의 메모리 해제 함수를 호출하게 할 수도 있다. 아래 코드를 보자.
#include <iostream>
#include <memory>
using namespace std;
void dectl(int* p)
{
wcout << "custom func called." << endl;
delete [] p;
}
int main()
{
shared_ptr<int> sp;
shared_ptr<int> sp1;
int *p = new int[10];
int *p1 = new int[10];
sp.reset(p, [](int* u){
delete [] u;
}
);
sp1.reset(p1, dectl);
return 0;
}
위 코드를 보면 사용자 정의 메모리 해제 함수는 2가지 방식으로 가능하다. 함수 포인터를 넘겨주거나 람다 표현식으로 사용도 가능하다.
다음은 shared_ptr 의 null 비교법을 알아보자. 단순하다. 그저 null 값과 비교만 하면 된다.
#include <iostream>
#include <memory>
using namespace std;
int main()
{
shared_ptr<int> ptr = make_shared<int>(10);
ptr = nullptr;
if(!ptr){
// == if(ptr == nullptr)
wcout << "shared_ptr is null. " << endl;
}
return 0;
}
shared_ptr 사용시 유의할 점이 한가지 더있다. shared_ptr 은 thread safe 하지 않다는 것이다. 그렇기 스레드 사용할 때는 추가적인 동기화 객체를 사용하거나 atomic_ 관련 함수를 사용해야 한다. 하지만 사용시 성능 상의 문제도 잘 생각해서 사용해야 한다. 멀티 스레드에서 사용시 아래 코드와 같이 추가적인 동기화 메카니즘이 필요하다.
#include <atomic>
#include <memory>
using namespace std;
shared_ptr<int> g_ptr;
DWORD WINAPI threadfunc(void* param)
{
while(true){
shared_ptr<int> p = atomic_load(&g_ptr);
...
}
return 0;
}
DWORD WINAPI threadfunc2(void* param)
{
while(true){
shared_ptr<int> p = make_shared<int>(0);
atomic_store(&g_ptr, p);
...
}
return 0;
}
int main()
{
HANDLE hThread1 = CreateThread(NULL, 0, threadfunc, NULL, 0, NULL);
HANDLE hThread2 = CreateThread(NULL, 0, threadfunc2, NULL, 0, NULL);
WaitforSingleObject(hThread1, INFINITE);
WaitforSingleObject(hThread2, INFINITE);
return 0;
}
마지막으로 shared_ptr 와 유사하게 동작하도록 하는 클래스를 작성해보고 테스트해보자. reset() 는 구현하지 않았지만 기존 shared_ptr 과 비슷하게 동작한다.
test.h
#pragma once
template <typename T>
class nzshared_ptr
{
public:
nzshared_ptr() {
mRefCount = nullptr;
mPtr = nullptr;
}
explicit nzshared_ptr(T* ptr) {
mRefCount = nullptr;
mPtr = ptr;
addRef();
}
nzshared_ptr(const nzshared_ptr<T>& rhs)
{
mRefCount = rhs.mRefCount;
mPtr = rhs.ptr;
addRef();
}
~nzshared_ptr() {
release();
}
T* get() const noexcept {
return mPtr;
}
int use_count() const noexcept {
if (mRefCount == nullptr) return 0;
return (*mRefCount);
}
bool unique() const noexcept
{
if (mRefCount == nullptr) {
return false;
}
else {
if ((*mRefCount) == 0) {
return true;
}
else {
return false;
}
}
}
T* operator->() { return mPtr; }
T& operator* () { return *mPtr; }
operator bool() const {
return (mRefCount != nullptr);
}
nzshared_ptr<T>& operator=(const nzshared_ptr<T>& rhs)
{
mRefCount = rhs.mRefCount;
mPtr = rhs.mPtr;
addRef();
return *this;
};
template <typename U>
nzshared_ptr<T>& operator=(const U& rhs)
{
if (rhs == nullptr) {
(*mRefCount)--;
mPtr = nullptr;
mRefCount = nullptr;
}
else
throw exception("cant direct addr.");
return *this;
};
private:
void addRef() noexcept
{
if (mRefCount == nullptr) {
mRefCount = new int(0);
}
(*mRefCount)++;
}
void release() noexcept
{
(*mRefCount)--;
if (*mRefCount == 0)
{
delete mRefCount;
delete mPtr;
}
}
int* mRefCount;
T* mPtr;
};
main.cpp
#include <iostream>
#include <memory>
#include "test.h"
using namespace std;
int main()
{
nzshared_ptr<int> p1(new int(0));
nzshared_ptr<int> p2;
wcout << "p1 ref count : " << p1.use_count() << endl;
wcout << "p2 ref count : " << p2.use_count() << endl;
p2 = p1;
wcout << "p1 ref count : " << p1.use_count() << endl;
wcout << "p2 ref count : " << p2.use_count() << endl;
{
nzshared_ptr<int> p3;
wcout << "p3 ref count : " << p3.use_count() << endl;
p3 = p2;
wcout << "p1 ref count : " << p1.use_count() << endl;
wcout << "p2 ref count : " << p2.use_count() << endl;
wcout << "p3 ref count : " << p3.use_count() << endl;
}
wcout << "p1 ref count : " << p1.use_count() << endl;
wcout << "p2 ref count : " << p2.use_count() << endl;
p2 = nullptr;
wcout << "p1 ref count : " << p1.use_count() << endl;
wcout << "p2 ref count : " << p2.use_count() << endl;
return 0;
}
실제 실행해보면 참조 카운트가 올바르게 설정되는 것을 알 수 있다.
관련 다음 포스팅
2022.01.16 - [프로그래밍/C/C++] - [C++] 스마트포인터 unique_ptr [정보공유의 장]
[C++] 스마트포인터 unique_ptr [정보공유의 장]
C++ 스마트 포인터(Smart pointer) unique_ptr 이번 포스팅 정리는 스마트 포인터 중 unique_ptr 에 대해서 정리해 보자. unique_ptr 은 소유하는 포인터에 대해 다음과 같은 규칙을 갖는다. 1. 소유 포인터는 한
nanze.tistory.com
'프로그래밍 > C/C++' 카테고리의 다른 글
[C++] 스마트포인터 unique_ptr [정보공유의 장] (0) | 2022.01.16 |
---|---|
[C++] explicit - 정보공유의 장 (0) | 2022.01.15 |
C++ template with member function (0) | 2022.01.13 |
C++ Template and Integral_constant (0) | 2022.01.12 |
C++ template 과 type 추론 (0) | 2022.01.11 |