안녕하세요, katte입니다.
이번 글에서는 스마트 포인터 클래스, auto_ptr, unique_ptr, shared_ptr에 대해서 정리하도록 하겠습니다.
포인터는 C와 C++의 강력한 도구이면서 동시에 고질적인 문제가 됩니다.
특히 동적 할당된 메모리를 참조하는 포인터는 메모리 누수의 문제를 떠안고 있죠.
이를 해결하기 위한 것이 스마트 포인터 클래스입니다.
스마트 포인터는 객체가 scope를 벗어나 소멸하게 될 때, 소멸자를 호출하는 것을 이용합니다.
스마트 포인터 클래스에는 힙에 할당된 주소를 저장할 포인터 멤버가 존재하고, 클래스의 객체가 소멸할 때 소멸자를 통해 delete를 호출하여 메모리를 해제합니다.
스마트 포인터에는 auto_ptr, unique_ptr, shared_ptr이 있으며,(+ weak_ptr)
auto_ptr은 되도록 unique_ptr로 대체하여 사용하는 것이 권장됩니다.
스마트 포인터는 std 네임스페이스에 저장되어 있습니다.
스마트 포인터에 주소를 대입할 때는 암시적 변환이 허용되지 않습니다.
이는 생성자와 대입연산자에서 모두 적용됩니다.
std::shared_ptr<double> pd;
double* p_reg = new double;
//대입연산자
//pd = preg; 허용(X)
pd = std::shared_ptr<double>(p_reg);
//생성자
//std::shared_ptr<int> pf = new int; 허용(X)
std::shared_ptr<int> pf(new int);
힙에 할당된 메모리를 동시에 여러 포인터로 참조할 때에는 주의를 기울여야 합니다.
이미 해제된 메모리를 다른 포인터가 가리키게 되는 댕글링 포인터 문제가 발생할 수 있기 때문입니다.
그렇다면 스마트 포인터는 이런 상황에 어떻게 대처할까요?
unique_ptr과 auto_ptr, 그리고 shared_ptr은 각각 이 문제에 대한 대처 방법이 다릅니다.
1. 먼저 unique_ptr과 auto_ptr은 이 문제에 대한 전략으로 move semantics 개념을 도입했습니다.
+) move semantics에 대해서는 이 글을 참고 바랍니다.
즉 소유권의 개념을 도입하여 동시에 여러 스마트 포인터 객체가 하나의 객체를 참조할 수 없도록 한 것입니다.
auto_ptr과 unique_ptr 모두 대입 연산을 사용하면 소유권이 다른 쪽으로 이동되고, 원래 그 객체의 주소를 소유하고 있던 포인터는 NULL 포인터가 됩니다.
단, 여기서 이 두 클래스의 차이는 unique_ptr의 소유권 이전은 rvalue 참조를 사용한 이동 대입 연산만 허용한다는 것입니다.
즉, unique_ptr의 경우 대입 연산에서의 우변이 rvalue일 때만 소유권 이전이 가능하고, 그 외에는 허용하지 않습니다.
반면 auto_ptr의 경우 C++11 이전에 등장한 것이기 때문에 이러한 안전장치를 가지고 있지 않습니다.
따라서 대입 연산에서 우변이 lvalue여도 컴파일 에러를 발생시키지 않습니다.
따라서 auto_ptr은 이미 소유권 이전을 하여 nullptr을 가리키게 된 객체에 여전히 접근할 수 있다는 문제가 생깁니다.
즉 nullptr을 역참조하게 되더라도 컴파일 에러를 일으키지 않는 것입니다.
그러나 unique_ptr은 rvalue만 대입 연산에 사용할 수 있기 때문에 조금 더 안전성이 보장됩니다.
(물론 move 함수를 사용하면 lvalue를 이용해서도 대입연산을 수행할 수 있긴 합니다.)
그러므로 auto_ptr보다는 주로 unique_ptr 사용을 권고하고 있습니다.
이 이유 뿐만 아니라 unique_ptr만 배열형을 참조할 수 있으므로(new[]를 사용해 할당한 주소를 저장해 배열처럼 사용하는 것이 가능함) unique_ptr을 사용하는 편이 좋습니다.
2. shared_ptr은 하나의 특정 개체를 참조하는 스마트 포인터의 개수를 세는 참조 카운팅을 사용합니다.
대입 연산을 사용할 때마다 참조 카운팅을 1씩 증가시키고 스마트 포인터가 소멸할 때마다 참조 카운팅을 감소시킵니다.
그리고 특정 개체를 참조하는 마지막 스마트 포인터가 소멸할 때 delete를 호출합니다.
따라서 동일 객체를 여러 포인터가 참조해야 할 필요가 있을 때에는 shared_ptr을 사용하는 것이 가장 좋습니다.
추가로, 대입연산시에 타이밍이 맞지 않는 경우가 생길 수 있으므로 다음과 같은 함수를 정의하여 사용할 수도 있습니다.
std::unique_ptr<string> demo(const char* s)
{
std::unique_ptr<string> temp(new string(s));
return temp;
}
int main()
{
std::unique_ptr<string> ps;
ps = demo("test");
}
함수 내에서 임시 객체를 만들어 rvalue로 리턴하고 그 리턴 값을 대입받으며, 임시 객체는 함수가 종료되면 소멸되기 때문에 문제가 생기지 않습니다.
* 해당 글은 'C++ 기초 플러스 6판'을 참고하여 작성되었습니다.
'Computer > C++' 카테고리의 다른 글
[C++] 예외 처리(1) - 예외 메커니즘과 스택 풀기 (0) | 2022.11.28 |
---|---|
[C++] 템플릿 클래스만 쓰면 링크 에러가 나는 이유가 뭘까 (0) | 2022.11.26 |
[C++] value categories (0) | 2022.11.17 |
[C++] rvalue 참조와 move semantics (0) | 2022.11.16 |
[C++] 함수 포인터 정리 (0) | 2022.11.11 |