코딩하는 두식이
스마트 포인터 본문
1. 스마트 포인터(smart pointer)란?
자바의 경우 garbage collector를 통해 메모리를 관리하지만 c++은 사용자가 스스로 메모리를 할당 해제를 통해 관리해야 한다. c에서는 malloc, free로 메모리를 할당및 해제를 수행하고 c++은 new, delete를 사용한다. 이때 할당받은 메모리를 해제하지 않을경우 프로그램은 계속 사용하고 있는 메모리로 인지하고 해당 메모리를 사용하지 않는 메모리 누수(memory leak)가 발생한다. 이와 같은 메모리 누수를 방지하기 위해 스마트 포인터를 제공해준다. 스마트 포인터는 포인터 처럼 사용하는 클래스 템플릿으로 메모리를 자동으로 해제해 준다. 즉, delete를 자동으로 수행한다.
스마트 포인터를 자체적으로 간단하게 만들어보면
이런 SmartPtr클래스를 사용하면 소멸자는 객체의 사용이 끝나면 자동으로 호출된다. 이곳에 delete가 존재해 메모리를 직접 해제하지 않아도 자동으로 해제되는 간단한 과정이다. 따라서 이 클래스를 템플릿으로 일반화시켜 어떤 객체에 대해서도 메모리를 할당받은 경우에 자동으로 해제를 해주는 것이다.
즉, 스마트 포인터는 new 키워드를 사용해 일반 포인터가 실제 메모리를 가리키도록 초기화한 후 기본 포인터를 스마트 포인터에 대입하여 사용한다. 이렇게 정의된 스마트 포인터가 수명이 다하면 소멸자를 통해 delete 키워드를 자동으로 사용해 메모리를 해제한다. 따라서 따로 메모리를 해제할 필요가 없게 된다.
2. 스마트 포인터 종류
스마트 포인터 기능은 c++11이후 부터 추가되었다. memory 헤더파일을 include해야 사용 가능하다.
① shared_ptr
shared_ptr은 어떤 하나의 객체를 참조하는 스마트 포인터의 개수를 참조하는 스마트 포인터이다. 이렇게 참조하고 있는 스마트 포인터의 개수를 참조 카운트(reference count)라고 한다. 참조 카운트란 해당 메모리를 참조하는 포인터가 몇개인지 나타내는 값으로 shared_ptr가 추가될때 1씩 증가하고 수명이 다하면 1씩 감소한다.
따라서 마지막 shared_ptr의 수명이 다하거나 main()함수가 종료되면 참조 카운트가 0이되어 delete를 사용하여 메모리를 자동으로 해제한다.
문법)
shared_ptr<객체> 스마트 포인터명(new 객체);
shared_ptr<객체> 스마트 포인터명 = make_shared<객체>(인수);
make_shared() 함수는 전달받은 인수를 사용하여 지정된 객체를 생성하고 생성된 객체를 참조하는 shared_ptr을 반환하기 때문에 해당 함수를 사용하면 shared_ptr 객체를 예외발생에 대해 안전하게 생성할 수 있다.
예시를 통해 확인해 보면
use_count() 멤버 함수는 shared_ptr 객체가 현재 가리키고 있는 객체를 참조 중인 소유자의 수를 반환해주기 때문에 reference count와 동일한 값을 가진다.
shared_ptr s는 5라는 값을 가지는 int형 객체를 가리키게 하였다. 아직은 스마트 포인터 s 1개만 해당 객체를 가리키고 있으므로 reference count는 1이 출력되게 된다.
다음 s2가 s가 가리키는 객체와 같은 값을 가리키는 shared_ptr이 되도록 auto를 사용하였다. 그리고 5라는 값을 가지는 객체의 값을 7로 증가시키고 확인해보면 이제 7이란 객체를 s와 s2가 가리키므로 reference count는 2가된다.
마지막으로 s3를 s2가 가리키는 객체를 가리키도록 하였다. 그럼 s, s2, s3 총 3개의 스마트포인터가 7이란 객체를 가리키고 있으므로 reference count가 3이 나오는것을 확인할 수 있다.
즉, shared_ptr은 객체가 가진 값의 변경 여부는 관계없이 객체를 가리키는 스마트 포인터의 개수를 참조하는 것을 확인할 수 있다.
② unique_ptr
unique_ptr은 하나의 스마트 포인터만이 객체를 가리킬수 있도록 한다. 즉 shared_ptr과 다르게 reference count가 1을 넘길수 없다.
문법)
unique_ptr<객체> 스마트 포인터명(new 객체)
unique_ptr<객체> 스마트 포인터명 = make_unique<객체>(인수);
make_shared()함수 처럼 make_unique()함수를 사용하여 안전하게 인스턴스를 생성할 수 있다.
따라서 위와같이 동일한 객체를 다른 스마트 포인터 객체로 참조하려고 하면 에러가 발생한다.
단 하나의 스마트 포인터만 특정 객체를 가리킬수 있으므로 다른 스마트 포인터를 사용하고 싶다면 move()함수를 사용하여 가리키는 객체를 변경해야한다.
③ weak_ptr
weak_ptr은 하나 이상의 shared_ptr가 가리키는 객체를 참조할수 있지만 reference count를 늘리지않는 스마트 포인터이다. shared_ptr을 사용할때 발생할 수 있는 문제를 해결하기 위해 사용된다.
shared_ptr은 하나의 객체를 여러 스마트 포인터가 가리키고 reference count를 통해 동작한다. 그런데 만약 서로가 서로를 가리키는 shared_ptr을 가지게 되면 reference count가 0이 될 수가 없으므로 메모리가 해제되지 않는 순환 참조(circular reference)가 발생하게 된다. weak_ptr은 이러한 순환 참조를 제거하기 위해 사용된다.
서로 참조하는 shared_ptr의 경우
shared_ptr은 객체를 참조하는 개수가 0이되면 가리키는 객체를 메모리에서 해제시킵니다. 하지만, 객체를 더 이상 사용하지 않음에도 불구하고 참조 개수가 0이 될 수 없는 경우가 있습니다.
바로, 서로 참조하는 shared_ptr의 경우입니다.
출처 : https://modoocode.com/252
위의 경우에는 각 객체가 shared_ptr을 하나씩 가지고 있는데, 이 shared_ptr이 다른 객체를 가리키고 있습니다. 객체1이 소멸되기 위해서는 shared_ptr의 참조 개수가 0이 되어야하는데, 이는 객체2가 소멸되어야지 가능합니다. 반대로 객체2가 소멸되기 위해서는 객체1이 소멸되어야 가능합니다.
|
|
|
|
|
|
|
class A { |
|
int* data; |
|
std::shared_ptr<A> other; |
|
|
|
public: |
|
A() { |
|
data = new int[100]; |
|
std::cout << "Get Resources\n"; |
|
} |
|
|
|
~A() { |
|
std::cout << "Call Destructor\n"; |
|
delete[] data; |
|
} |
|
|
|
void set_other(std::shared_ptr<A> o) { |
|
other = o; |
|
} |
|
}; |
|
|
|
int main(void) { |
|
A* a = new A(); |
|
|
|
std::shared_ptr<A> p1 = std::make_shared<A>(); |
|
std::shared_ptr<A> p2 = std::make_shared<A>(); |
|
|
|
p1->set_other(p2); |
|
p2->set_other(p1); |
|
|
|
return 0; |
|
} |
위 코드를 실행시키면 아래의 출력 결과를 확인할 수 있습니다.
즉, 소멸자가 정상적으로 호출되지 않았습니다.
이 문제는 shared_ptr 자체의 문제이기 때문에 shared_ptr을 통해서 이 문제를 해결할 수는 없습니다. 이러한 순환 참조 문제를 해결하기 위해서는 weak_ptr 을 사용해야 합니다.
weak_ptr
순환 참조의 예로 트리 구조가 있으며, 아래와 같은 구조를 갖습니다.
출처 : https://modoocode.com/252
위와 같은 구조를 자료 구조로 나타내면 기본적으로 다음과 같이 나타낼 수 있습니다.
|
class Node { |
|
std::vector<std::shared_ptr<Node>> children; |
|
std::vector<std::weak_ptr<Node>> parent; |
|
|
|
public: |
|
Node() {}; |
|
void addChild(std::shared_ptr<Node> node) { |
|
children.push_back(node); |
|
} |
|
}; |
부모는 여러 개의 자식 노드들을 가지므로, shared_ptr들의 벡터로 나타낼 수 있고, 그 노드 역시 부모 노드가 있으므로 부모 노드를 가리키는 포인터를 가집니다.
이때, 부모 노드를 가리키는 포인터의 타입을 shared_ptr로 하게 된다면, 앞서 봤던 순환 참조 문제가 발생하게 됩니다. 부모와 자식이 서로를 가리키기 때문에 참조 개수가 절대로 0이 될 수 없고, 따라서, 이 객체들은 프로그램이 끝날 때까지 절대로 해제되지 않습니다. (일반 포인터는 메모리 해제를 잊어먹거나, 예외가 발생하여 적절하게 자원이 해제되지 않을 수 있는 가능성이 존재합니다.)
weak_ptr은 일반 포인터와 shared_ptr의 중간쯤에 위치한 스마트 포인터로써, 스마트 포인터처럼 객체를 안전하게 참조할 수 있게 해주지만, shared_ptr과는 다르게 참조 개수를 증가시키지 않습니다.
따라서, weak_ptr이 어떤 객체를 가리키고 있더라도, 다른 shared_ptr들이 가리키고 있지 않다면 이미 메모리에서 해제되었을 것입니다. 이 때문에 weak_ptr은 그 자체로는 원래 객체를 참조할 수 없고, 반드시 shared_ptr로 변환해서 사용해야 합니다. (이미 메모리에서 해제된 객체를 사용할 수 있는 가능성이 존재합니다.) 이때 weak_ptr이 가리키는 객체가 이미 소멸되었다면 빈 shared_ptr로 변환되고, 아닌 경우에는 해당 객체를 가리키는 shared_ptr로 변환됩니다.
weak_ptr은 아래처럼 사용할 수 있습니다.
|
|
|
|
|
|
|
|
|
|
|
class A { |
|
std::string s; |
|
std::weak_ptr<A> other; |
|
|
|
public: |
|
A(const std::string& s) : s(s) { |
|
std::cout << "Get Resources\n"; |
|
} |
|
|
|
~A() { |
|
std::cout << "Call Destructor\n"; |
|
} |
|
|
|
void set_other(std::weak_ptr<A> o) { |
|
other = o; |
|
} |
|
|
|
void access_other() { |
|
std::shared_ptr<A> o = other.lock(); |
|
if (o) { |
|
std::cout << "access : " << o->name() << std::endl; |
|
} |
|
else { |
|
std::cout << "already destroyed\n"; |
|
} |
|
} |
|
|
|
std::string name() { |
|
return s; |
|
} |
|
}; |
|
|
|
int main(void) { |
|
std::vector<std::shared_ptr<A>> vec; |
|
vec.push_back(std::make_shared<A>("Resource 1")); |
|
vec.push_back(std::make_shared<A>("Resource 2")); |
|
|
|
vec[0]->set_other(vec[1]); |
|
vec[1]->set_other(vec[0]); |
|
|
|
vec[0]->access_other(); |
|
|
|
vec.pop_back(); |
|
vec[0]->access_other(); |
|
|
|
return 0; |
|
} |
위 코드를 실행하면 아래의 출력 결과를 확인할 수 있습니다.
우선 set_other 멤버 함수부터 살펴보면, 이 함수는 weak_ptr<A>를 파라미터로 전달받고 있습니다. 여기서 main문 40, 41 line에서 shared_ptr을 파라미터로 전달하고 있습니다. 즉, weak_ptr은 생성자로 shared_ptr이나 다른 weak_ptr을 전달받습니다. 또한, shared_ptr과는 다르게, 이미 제어 블록이 만들어진 객체만이 의미를 가지므로, 평범한 포인터 주소값으로 weak_ptr을 생성할 수 없습니다.
그리고, access_other 멤버 함수를 살펴보겠습니다. 이 함수에서 weak_ptr을 사용하기 위해서 shared_ptr로 변환하고, weak_ptr에 접근하게 됩니다. (weak_ptr은 그 자체로는 원소를 참조할 수 없고, shared_ptr로 변환해야 합니다.)
이 변환 작업은 lock 함수를 통해서 수행할 수 있습니다. weak_ptr에 정의된 lock 함수는 weak_ptr이 가리키는 객체가 아직 메모리에 살아 있다면(참조 개수가 0이 아니라면), 해당 객체를 가리키는 shared_ptr을 반환하고, 이미 객체가 메모리 해제 됬다면 아무것도 가리키지 않는 shared_ptr을 반환합니다.
따라서, 46 line에서는 아직 vec[1] 객체가 살아있기 때문에 접근이 가능하여 access : Resource 2를 출력했지만, 49 line에서는 vec[1]이 삭제되었기 때문에 이미 해제됬다는 메세지가 출력됩니다.
'공부 > C++' 카테고리의 다른 글
Lvalue Rvalue std::move (0) | 2022.06.28 |
---|---|
Effective C++ 항목 4 : 객체를 사용하기 전에 반드시 그 객체를 초기화하자 (0) | 2022.05.07 |
Effective C++ 항목 9 : 객체 생성 및 소멸 과정 중에는 절대로 가상 함수를 호출하지 말자 (0) | 2021.05.10 |
Effective C++ 항목 7 : 다형성을 가진 기본 클래스에서는 소멸자를 반드시 가상 소멸자로 선언하자. (0) | 2021.05.10 |
Effective C++ 항목 3 : 낌새만 보이면 const를 들이대 보자! (0) | 2021.05.10 |