네이키드 포인터 (naked pointer)
우리가 흔히 말하는 *
를 이용하여 생성된 그 포인터를 말함 (plain pointer).
#include <memory>
struct X { int a,b,c; };
int main()
{
X* np = new X;
delete np;
return 0;
}
여기서 np
변수가 네이키드 포인터를 말한다. np
는 동적으로 할당되어 힙메모리 안에 저장이 되는데, 자원의 활용이 끝나면 delete
를 활용하여 객체에 할당된 메모리를 반드시 해제해 줘야만 메모리 누수를 막을 수 있다. 해제하지 않으면 힙메모리가 자원을 계속 들고 있어서 성능상 문제가 발생하기 때문이다.
이렇게 개발자가 직접 메모리 해제에 관여해줘야 하는 네이키드 포인터와 달리, 개발자가 직접 메모리 해제에 관여하지 않아도 되도록 설계된 포인터가 스마트 포인터.
스마트 포인터
C++11부터 등장한 새로운 포인터 선언 방식.
스마트 포인터 사용을 위해서는 위의 코드에서 보이듯 #include <memory>
선언을 해줘야 한다.
[예제 1-1] 스마트 포인터를 활용한 예제 코드
#include <iostream>
#include <memory>
using namespace std;
class Blablabla {
public:
Blablabla() {
cout << "자원 획득" << endl;
}
void call() {
cout << "did you call me?" << endl;
}
~Blablabla() {
cout << "자원 해제" << endl;
}
};
int main() {
unique_ptr<Blablabla> mB1(new Blablabla());
mB1->call();
return 0;
}
여기서 unique_ptr
이 스마트 포인터인데, 스마트 포인터 종류로는 unique_ptr
, shared_ptr
, weak_ptr
세가지가 있다. 우선 이 문서에서는 unique_ptr
, shared_ptr
둘만 다루도록 하겠다.
(1) unique_ptr
여기서 unique
는 유일하다는 의미로 쓰여서 '리소스에 대한 유일한 소유권'을 갖는 포인터라는 뜻. 달리 말해서 다른 객체가 unique_ptr
로 선언된 리소스에 대한 소유권을 가질 수 없다. 여기서 말하는 소유권을 가질 수 없다는 것은 "타 객체가 이 객체로의 접근을 할 수 없다"라는 뜻 정도 되는것 같다.
[예제 1-2] unique_ptr
로 선언된 리소스에 대하여 다른 객체로부터 접근 시도
unique_ptr<Blablabla> mB2 = mB1;
mB2->call();
위의 예제에서는 mB2
객체를 선언한 후, 이 객체가 mB1
에 접근하도록 선언해 주고 있는데, 이를 컴파일 하면 에러가 발생한다.
marshall@marshall-IdeaPad-5-15ITL05:~/learn-cpp$ g++ smart_pointer.cpp
smart_pointer.cpp: In function ‘int main()’:
smart_pointer.cpp:26:30: error: use of deleted function ‘std::unique_ptr<_Tp, _Dp>::unique_ptr(const std::unique_ptr<_Tp, _Dp>&) [with _Tp = Blablabla; _Dp = std::default_delete<Blablabla>]’
26 | unique_ptr<Blablabla> mB2 = mB1;
| ^~~
In file included from /usr/include/c++/9/memory:80,
from smart_pointer.cpp:2:
/usr/include/c++/9/bits/unique_ptr.h:414:7: note: declared here
414 | unique_ptr(const unique_ptr&) = delete;
| ^~~~~~~~~~
에러 메시지에서 말하길 삭제된 함수를 사용하려 한다고 하는데, 이 메시지의 내용을 좀 더 들여다볼 필요가 있다.
(2) shared_ptr
(공유 포인터)
unique_ptr
과 다르게 shared_ptr
을 이용하면 객체 공유가 가능해진다. 예제 1-1의 메인함수 내용을 다음과 같이 고친다.
shared_ptr<Blablabla> mB1(new Blablabla());
mB1->call();
shared_ptr<Blablabla> mB2 = mB1;
mB2->call();
이를 실행하면 다음과 같이 출력한다.
자원 획득
did you call me?
did you call me?
자원 해제
mB1
, mB2
가 동일한 객체를 소유하고 있다 보니, 함수 종료 후 소멸자 호출도 하나의 객체에서만 이루어짐으로써 '자원 해제'라는 메시지가 한번만 나오는 것을 확인할 수 있다.
shared_ptr
를 쓸 때 유의할 점
공유 포인터의 특징은 앞에서 말했듯 한 객체에 여러 공유 포인터가 접근할 수 있다는 점이다.
ClassA* a = new ClassA();
shared_ptr<A> pA1(a);
shared_ptr<A> pA2(a);
이렇게 선언을 하면 pA1
, pA2
둘 다 a
를 바라보게 된다. 각 공유 포인터 pA1
, pA2
는 어떤 다른 공유 포인터가 같은 객체를 공유하고 있는지를 알 수 없다. 다시 말해서 각 공유 포인터는 각자의 고유 레퍼런스 카운트를 갖고 있게 된다. 만일 pA1
의 레퍼런스 카운트가 0이 된다면 프로그램에서는 pA1
이 가리키고 있는 객체 a
를 소멸시켜 버릴텐데, 그러면 pA2
가 아직 레퍼런스 카운트가 0이 아닌 상태인데, 여기서 a
에 대한 접근을 시도하게 될 경우 참조 에러가 발생한다.
[예제 2] 두 공유 포인터
#include <iostream>
#include <memory>
using namespace std;
class Blablabla {
public:
Blablabla() {
cout << "자원 획득" << endl;
}
void call() {
cout << "did you call me?" << endl;
}
~Blablabla() {
cout << "자원 해제" << endl;
}
};
int main() {
Blablabla *b = new Blablabla();
shared_ptr<Blablabla> mB1(b);
shared_ptr<Blablabla> mB2(b);
cout << mB1.use_count() << endl;
cout << mB2.use_count() << endl;
return 0;
}
위 코드 자체는 문제없이 컴파일 된다. use_count
함수는 동일하게 참조되는 객체를 참조하는 공유포인터의 수를 리턴한다. [https://en.cppreference.com/w/cpp/memory/shared_ptr/use_count](여기 참조). 다만 실행해 보면 다음과 같이 정상적으로 실행되지 않는 것을 볼 수 있다.
marshall@marshall-IdeaPad-5-15ITL05:~/learn-cpp$ ./a.out
자원 획득
1
1
자원 해제
자원 해제
free(): double free detected in tcache 2
Aborted (core dumped)
같은 객체에 대해 메모리 해제를 두번 해주려 하다가 문제가 발생했다는 메시지다.
다음 시간에는 weak_ptr
개념에 대한 소개, shared_ptr
포인터 사용시 발생하는 문제점을 해결하는 방법 등에 대한 내용을 싣겠다.
'C++' 카테고리의 다른 글
[C++] 네이키드 포인터를 썼을 때 발생할 수 있는 문제 && 이를 방지할 방안 (스마트 포인터) (0) | 2022.12.18 |
---|---|
[C++] 두 수의 합/차가 overflow가 발생하는 경우 감지하는 코드 작성 (0) | 2022.12.14 |
[C++] 정수 연산시 오버플로우가 발생하는 문제점 (0) | 2022.12.13 |
C++에서 어마어마하게 큰 숫자를 활용해야 하는 경우 어떻게 할 것인가? (0) | 2016.11.07 |
GCC로 .cpp 파일 컴파일 하는 법 (0) | 2016.04.14 |