shared_ptr
unique_ptr은 객체에 대한 소유권을 하나의 스마트 포인터가 독점함을 보장했다. 하지만, 하나의 객체를 여러 포인터가 공유해야 하는 경우도 존재한다. 이럴 때는 shared_ptr을 사용할 수 있다. shared_ptr을 사용하면 개발자는 resource의 life cycle를 고려할 필요 없이 오브젝트의 소유권을 여러 scope에서 공유 가능하도록 만들 수 있다.
shared_ptr은 여러개의 포인터가 하나의 객체를 가리키고 있고, 하나의 객체를 몇 개의 포인터가 참조하고 있는지를 Reference count에 기록한다. 이 Reference count가 0이 될 경우 객체를 자유공간에서 해제한다. 이해를 돕기 위해서 아래의 코드를 살펴보자. use_count
함수는 현재 포인터가 가리키고 있는 객체의 Reference count를 반환한다.
#include <iostream>
#include <string>
#include <memory>
class A {
public:
A() { std::cout << "A()" << std::endl; }
~A() { std::cout << "~A()" << std::endl; }
};
int main() {
std::cout << "begin main()" << std::endl;
// scope 1
{
std::cout << "begin scope 1" << std::endl;
std::shared_ptr<A> pname1(new A());
std::cout << pname1.use_count() << std::endl;
// scope 2
{
std::cout << "begin scope 2" << std::endl;
// 다른 공유 포인터를 매개변수로 공유 포인터를 생성할 수 있다.
std::shared_ptr<A> pname2(pname1);
std::cout << pname1.use_count() << std::endl;
std::cout << "end scope 2" << std::endl;
}
std::cout << pname1.use_count() << std::endl;
std::cout << "end scope 1" << std::endl;
}
std::cout << "end main" << std::endl;
return 0;
}
출력 결과
begin main()
begin scope 1
A()
1
begin scope 2
2
end scope 2
1
end scope 1
~A()
end main
scpoe1 이 시작되고 공유 포인터를 활용하여 객체 A를 생성했다. 그리고 use_count
함수는 해당 객체를 참조하고 있는 공유 포인터의 수를 반환한다. scope2에서 pname2
는 pname1
이 가리키는 객체를 가리키게 된다. 그래서 use_count
로 객체에 대한 참조수를 출력하면 2개의 포인터가 객체를 참조하고 있다는 결과가 나온다. 이후 scope2가 끝나면서 pname2
포인터는 스택에서 제거된다. 따라서 A 객체를 가리키는 포인터 수는 1개로 줄어든다. scope1이 종료되면서 더이상 A 객체를 가리키는 포인터가 없기 때문에 A객체의 소멸자가 호출됨을 확인할 수 있다.
shared_ptr 생성
shared_ptr의 생성은 unique_ptr처럼 생성한 객체의 주소를 전달하는 방법과 make_shared<T> 함수를 사용하는 방법이 있다. 그리고 shared_ptr은 unique_ptr과 다르게 하나의 객체를 여러 포인터가 가리킬 수 있기 때문에 다른 shared_ptr 객체로 생성할 수도 있다. 아래의 코드는 공유 포인터를 생성하는 방법이다.
#include <iostream>
#include <string>
#include <memory>
class A {
public:
A(int n) : num(n) { std::cout << "A()" << std::endl; }
int GetNum() const { return num; }
~A() { std::cout << "~A()" << std::endl; }
private:
int num;
};
int main() {
// make_shared 활용 일반적인 방법.
std::shared_ptr<A> pA1 = std::make_shared<A>(10);
// 객체 생성 후 주소값을 전달
std::shared_ptr<A> pA2(new A(20));
// nullptr 공유 포인터 객체 생성
std::shared_ptr<A> pA3;
// 다른 공유포인터의 주소값을 복제
pA3 = pA1;
// 다른 공유 포인터의 주소값으로 공유 포인터 생성
std::shared_ptr<A> pA4(pA2);
std::cout << "pA1: " << pA1->GetNum() << " pA2: " << pA2->GetNum() << std::endl;
std::cout << "pA3: " << pA3->GetNum() << " pA4: " << pA4->GetNum() << std::endl;
return 0;
}
출력 결과
A()
A()
pA1: 10 pA2: 20
pA3: 10 pA4: 20
~A()
~A()
shared_ptr 초기화
shared_ptr 객체에 nullptr을 대입하면 저장된 주소가 nullptr로 바뀌고 객체에 대한 Reference count를 1 감소시킨다. 그리고 reset() 함수를 매개변수 없이 호출하면 동일한 결과를 확인할 수 있다.
int main() {
std::cout << "begin main()" << std::endl;
std::shared_ptr<A> pA1 = std::make_shared<A>(10);
pA1 = nullptr; // Reference count가 0이 되면서 객체 소멸
std::shared_ptr<A> pA2 = std::make_shared<A>(20);
pA2.reset(); // Reference count가 0이 되면서 객체 소멸
std::cout << "end main()" << std::endl;
return 0;
}
출력 결과
begin main()
A()
~A()
A()
~A()
end main()
reset()함수에 매개변수를 활용하면 새로운 객체로 공유 포인터를 초기화할 수 있다.
class A {
public:
A(int n) : num(n) { std::cout << "A() | num: " << num << std::endl; }
int GetNum() const { return num; }
~A() { std::cout << "~A() | num: " << num << std::endl; }
private:
int num;
};
int main() {
std::cout << "begin main()" << std::endl;
std::shared_ptr<A> pA = std::make_shared<A>(10);
pA.reset(new A(20)); // 새로운 객체로 초기화
std::cout << "end main()" << std::endl;
return 0;
}
출력 결과
begin main()
A() | num: 10
A() | num: 20
~A() | num: 10
end main()
~A() | num: 20
shared_ptr 원시 포인터 획득과 비교
unique_ptr과 마찬가지로 get()
함수로 원시 포인터를 획득할 수 있다.
#include <iostream>
#include <memory>
#include <string>
int main() {
std::shared_ptr<std::string> shrdStr = std::make_shared<std::string>("Algernon");
std::string* pStr = shrdStr.get();
std::cout << *pStr;
return 0;
}
출력 결과
Algernon
==
연산을 통해서 두 공유포인터 객체가 같은 객체를 가리키고 있는지 확인할 수 있다. 그리고 shared_ptr은 unique_ptr과 마찬가지로 bool타입으로 암시적으로 변환될 수 있다. 아래와 같이 nullptr 체크도 할 수 있다.
int main() {
std::shared_ptr<A> pA1 = std::make_shared<A>(10);
std::shared_ptr<A> pA2 = pA1;
if (pA1 == pA2)
std::cout << "pA1 == pA2" << std::endl;
pA1.reset(); // pA1 nullptr로 초기화
if (!pA1) //pA1 nullptr 체크
std::cout << "pA1 is nullptr!" << std::endl;
return 0;
}
A() | num: 10
pA1 == pA2
pA1 is nullptr!
~A() | num: 10
shared_ptr의 순환참조와 memory leak
shared_ptr을 사용하더라도 여전히 memory leak이 발생할 수 있는 경우가 있다. 아래의 예제 코드를 살펴보자.
#include <iostream>
#include <memory>
class Cat
{
public:
Cat()
{
std::cout << "Cat()" << std::endl;
}
~Cat()
{
std::cout << "~Cat()" << std::endl;
}
std::shared_ptr<Cat> mFriend;
};
int main()
{
std::shared_ptr<Cat> Jo = std::make_shared<Cat>();
std::shared_ptr<Cat> Moo = std::make_shared<Cat>();
Jo->mFriend = Moo;
Moo->mFriend = Jo;
}
출력 결과
Cat()
Cat()
출력 결과 소멸자가 호출되지 않았음(메모리 누수 발생)을 확인할 수 있다. 그 이유를 그림으로 살펴보자.
main 함수에서 Joo와 Moo는 각각 Heap 영역에 각자의 Cat 오브젝트를 가리키고 있다. Joo는 mfriend 포인터로 Moo를 가리키고 Moo는 다시 mfriend 포인터로 Joo를 가리키고 있다. 때문에 Joo 오브젝트와 Moo 오브젝트의 Reference count는 2가 된다. 이때 main 함수가 종료되면서 stack 영역에 있던 Joo와 Moo가 삭제되면 아래의 그림처럼 될 것이다.
main함수에서 Joo와 Moo가 삭제되었지만, 각 오브젝트는 mfriend 포인터로 서로를 여전히 참조하고 있기 때문에 Reference count는 여전히 1이다. 때문에 이들은 소멸되지 않는다. 이렇게 순환 참조가 발생하면 스마트 포인터를 사용하더라도 메모리 누수가 발생한다. 이 문제를 해결하기 위해서는 다음 글에서 다룰 weak_ptr을 사용할 수 있다.
<이전 글 Smart Pointer의 이해 - ① unique_ptr의 개념과 활용
참조:
유튜브 채널 - 코드없는 프로그래밍: https://www.youtube.com/channel/UCHcG02L6TSS-StkSbqVy6Fg
길벗 출판사 - C++14 STL 철저 입문