티스토리 뷰
※ 주의 사항 ※
- 이 글의 목적은 '지식의 전달'이 아닌 '학습의 기록'입니다.
- 따라서 제가 이해하는 그대로의 내용이 포함됩니다.
- 따라서 이 글은 사실과는 다른 내용이 포함될 수 있습니다.
선수 지식
C언어의 포인터를 이해하고 있으면 이 글의 내용의 이해가 쉽다!
C++ 클래스의 소멸자의 호출시점에 대해 이해하고 있으면 이 글의 내용의 이해가 쉽다!
스코프에 대해 이해하고 있으면 이 글의 내용의 이해가 쉽다!
레퍼런스 카운팅에 대해 이해하고 있으면 이 글의 내용의 이해가 쉽다!
스마트 포인터, 그게 뭔데?
C언어에서 포인터를 공부했다면 메모리를 동적으로 할당하고 해제하는 걸 직접 해봤을 것이다. 아직 이런 경험이 없다면 이 글은 조금 이르다.
C++ 에서는 new연산자를 이용해서 메모리를 동적으로 할당하고, delete연산자를 이용해서 할당했던 메모리 공간을 해제한다. new와 delete는 서로 쌍으로 사용되어야 한다. new로 할당한 메모리를 delete로 해제해주지 않으면 이 메모리 공간은 컴퓨터를 다시 재부팅할 때까지는 영영 사용할 수 없게 된다. 이를 메모리 누수라고 한다.
new 연산자는 프로그램을 개발하려면 어찌저찌 반드시 쓰게 되지만, 문제는 delete 연산자는 굳이 이걸 내가 안 써줘도 당장 프로그래밍 하는 데에 아무런 문제가 없기 때문에 많은 개발자들이 delete를 빼먹는 실수를 하게 되며, 이 별 거 없어 보이는 실수 때문에 C/C++ 개발자는 많은 적잖은 스트레스를 받아야 했다(적어도 스마트 포인터가 등장하기 전까진).
스마트 포인터는 delete의 괴롭힘으로부터 개발자를 자유롭게 해준다. 스스로 알아서 delete를 해주기 때문이다! 대체 이게 어떻게 가능한 걸까? 아래의 코드는 대충 직접 구현해본 스마트 포인터다.
#include <iostream>
#include <string>
template <typename T>
class mySmtPointer {
private:
// 내부에 C스타일 포인터
T* pnt;
public:
mySmtPointer(T value) {
// 생성자에서 메모리 할당
pnt = new T(value);
}
T getData() {
return *pnt;
}
virtual ~mySmtPointer() {
// 소멸자에서 메모리 해제
delete pnt;
}
};
int main() {
// 스마트 포인터 생성
mySmtPointer<std::string> smp("hello");
std::cout << smp.getData() << std::endl; // output: hello
return 0;
}
위 코드를 한번 정독하고 오시라! 스마트 포인터가 실제 저렇게 조잡하게 구현되어 있지는 않겠지만 대충 스마트 포인터가 어떻게 스스로 메모리를 할당하고 해제하는지 설명하기에는 충분하다고 생각한다.
핵심은 mySmtPointer 클래스의 소멸자에 있다. 소멸자의 호출 시점에 이해하고 있는가? 간단히 말해 어떤 객체의 소멸자는 그 객체가 선언된 스코프를 벗어날 때 자동으로 호출된다. 그리고 mySmtPointer 클래스에 구현된 소멸자를 보면 delete를 수행하고 있다! 즉, 우리는 mySmtPointer 객체를 생성만 하면 자동으로 메모리를 할당하고, 우리가 신경쓰지 않아도 이 객체가 스코프를 벗어나면 스스로 호출된 소멸자에 의해 delete가 수행된다!
스마트 포인터가 저렇게 클래스로 구현되어 있으면 아무래도 C스타일 포인터보다 속도면에서 불리하지 않을까 생각할 수 있다. 하지만 걱정하지 마시라. 스마트 포인터의 속도는 C스타일 포인터와 맞먹는다(이게 어떻게 가능한지는 나도 모른다 아무튼 그렇다고 한다). 그러니 우리는 스마트 포인터를 안 쓸 이유가 하~~나도 없다.
스마트 포인터는 딱 3종류가 있다
스마트 포인터 | 간단한 설명 |
unique_ptr | 복사가 원천봉쇄되어 있고 이동만 가능한 고유한 포인터 |
shared_ptr | 레퍼런스 카운팅 방식으로 복사를 구현한(실제 복사는 아님 공유에 가까움) 포인터 단, 순환 참조 문제가 발생하지 않도록 주의해야 함 |
weak_ptr | shared_ptr과 거의 같지만 특별히 다른 점이 있다면 복사할 때 레퍼런스 카운트를 증가시키지 않음 shared_ptr의 순환 참조 문제를 해결하는 데 주로 사용 |
스마트 포인터, 어떻게 사용하는지 간단하게 살펴보자
#include <iostream>
#include <string>
#include <memory> // 스마트 포인터가 들어 있는 헤더파일
class Person {
private:
std::string name;
int age;
public:
Person(std::string name = "koey", int age = 20) : name(name), age(age) {};
void hello() { std::cout << "Hello" << std::endl;}
std::string getName() { return name; }
int getAge() {return age;}
};
int main() {
// 이런 포인터를 이제는 원시 포인터라고 부른다!
Person* pA = new Person("AAA", 27);
// 스마트 포인터, 이렇게 생성한다!
auto upB = std::make_unique<Person>("BBB", 26); // unique_ptr
auto spC = std::make_shared<Person>("CCC", 25); // shared_ptr
// weak_ptr은 직접 생성할 수 없다! weak_ptr은 소유권을 가질 수 없기 때문!
// weak_ptr은 반드시 다른 스마트 포인터를 복사하여야만 생성할 수 있다!
// 남의 소유권을 빌려야만 존재할 수 있는 weak_ptr!
std::weak_ptr<Person> wp0(spC); // 복사 생성자 호출!
std::weak_ptr<Person> wp1 = wp0; // 복사 대입 연산 수행!
// unique_ptr은 다른 스마트포인터로 복사하는 것이 원천봉쇄되어 있다!
// std::shared_ptr<int> sp0(upB);
// std::shared_ptr<int> sp1 = upB;
// std::weak_ptr<int> wp2(upB);
// std::weak_ptr<int> wp3 = upB;
// unique_ptr은 std::move() 메서드를 통해 이동하는 것만 가능하다!
std::unique_ptr<Person> up0(std::move(upB)); // up0의 소유권이 up1로 이동하고, up0는 nullptr이 된다!
std::unique_ptr<Person> up1 = std::move(up0);
std::shared_ptr<Person> sp2(std::move(up1)); // shared_ptr로 소유권을 이동하는 것도 가능하다!
// 해보면 알겠지만 weak_ptr로 소유권 이동은 불가능하다!
// weak_ptr은 직접 소유권을 가질 수는 없고, 다른 스마트 포인터의 소유권을 빌려올 수만 있다!
// get() 메서드로 메모리 주소를 가져올 수 있다!
std::cout << sp2.get() << std::endl; // output: 0xe43cf0
// 스마트 포인터 사용은 C언어의 포인터 사용하듯이 사용하면 된다!
std::cout << sp2->getName() << std::endl; // output: 22
std::cout << sp2->getAge() << std::endl; // output: 22
sp2->hello();
// 입출력 연산자(<<, >>)는 unique_ptr과 shared_ptr에 대해서만 오버로딩되어 있다!
// weak_ptr.lock() 메서드는 가리키는 메모리 공간을 shared_ptr로 반환한다!
std::cout << wp1.lock() << std::endl; // output: (메모리 주소)
// 메모리를 직접 해제하고 싶으면 reset() 메서드!
sp2.reset();
return 0;
}
스마트 포인터 요점 정리
unique_ptr | shared_ptr | weak_ptr | |
소유권 | 있음 | 있음 | 없음 |
복사 | 불가능 | 가능 | 가능 |
이동 | 가능 | 가능 | 가능 |
사용 시 주의사항 | 소유권이 이동되고 nullptr이 된 포인터를 참조하지 않도록 주의! | shared_ptr을 남용하여 순환참조문제가 발생하지 않도록 주의! | 가급적 순환참조문제 발생 의심 시에만 사용하도록 주의! |
'공부 일지 > CPP 공부 일지' 카테고리의 다른 글
C++17 객체 지향 언어와 객체 그리고 클래스(class)(1) (2) | 2022.02.10 |
---|---|
C++17 객체 지향 언어와 객체(Object)의 개념 이해 (0) | 2022.02.08 |
C++17 Date and time utilities (0) | 2022.02.07 |
C++ | mutable 키워드 (0) | 2021.08.10 |
C++ | 범위 기반 for문 (0) | 2021.08.09 |