
스마트 포인터는 delete를 자동으로 해주는 보다 똑똑한 포인터를 의미한다. smart_ptr을 사용하면 new/delete를 직접 사용하지 않아도 동적 메모리를 안전하게 관리할 수 있다. (고맙다...)
#include <iostream>
class A {
int *data;
public:
A() {
data = new int[100];
std::cout << "자원을 획득함!" << std::endl;
}
~A() {
std::cout << "자원을 해제함!" << std::endl;
delete[] data;
}
};
void thrower() {
// 예외를 발생시킴!
throw 1;
}
void do_something() {
A *pa = new A();
thrower();
// 발생된 예외로 인해 delete pa 가 호출되지 않는다!
delete pa;
}
int main() {
try {
do_something();
} catch (int i) {
std::cout << "예외 발생!" << std::endl;
// 예외 처리
}
}
이런식으로 delete를 통해 메모리를 해제하려고 시도하지만 중간에 예외가 발생하면 바로 메모리 누수로 이어지게 된다. 따라서 스마트 포인터를 이용해서 관리하는 것이 정신건강에 이롭다. 참고로 auto_ptr 이란것도 있는데 이거는 여러 문제들이 발견되어 사용하면 안된다.
Smart Pointer에는 크게 3가지가 존재한다.
1. unique_ptr
2. shared_ptr
3. weak_ptr
이제 이 3가지에 대해서 하나씩 알아보자
오직 한 포인터만 그 객체를 소유한다. 복사 금지, 이동은 가능.
Data* data = new Data();
Date* data2 = data;
// data 의 입장 : 사용 다 했으니 소멸시켜야지.
delete data;
// ...
// data2 의 입장 : 나도 사용 다 했으니 소멸시켜야지
delete data2;
일단 이렇게 이미 해제된 메모리를 다시 해제하려고 시도하는 것을 double free라고 부르고 이는 프로그램을 죽게 만드는 치명적인 버그다.
이렇게 되는 이유는 객체의 소유권이 명확하지 않아서 그렇다. 그래서 이는 소유권을 부여해주는 unique_ptr을 통해 해결할 수 있다.
#include <iostream>
#include <memory>
class A {
int *data;
public:
A() {
std::cout << "자원을 획득함!" << std::endl;
data = new int[100];
}
void some() { std::cout << "일반 포인터와 동일하게 사용가능!" << std::endl; }
~A() {
std::cout << "자원을 해제함!" << std::endl;
delete[] data;
}
};
void do_something() {
std::unique_ptr<A> pa(new A());
pa->some();
}
int main() { do_something(); }
자원을 획득함!
일반 포인터와 동일하게 사용가능!
자원을 해제함!
std::unique_ptr<A> pb = pa;만약에 이런식으로 unique_ptr을 복사하려 한다면 당연히 오류를 발생한다.void do_something() {
std::unique_ptr<A> pa(new A());
std::cout << "pa : ";
pa->some();
// pb 에 소유권을 이전.
std::unique_ptr<A> pb = std::move(pa);
std::cout << "pb : ";
pb->some();
}
// 올바르지 않은 전달 방식
void do_something(std::unique_ptr<A>& ptr) { ptr->do_sth(3); }
int main() {
std::unique_ptr<A> pa(new A());
do_something(pa);
}
void do_something(A* ptr) { ptr->do_sth(3); }
int main() {
std::unique_ptr<A> pa(new A());
do_something(pa.get());
}
class Foo {
int a, b;
public:
Foo(int a, int b) : a(a), b(b) { std::cout << "생성자 호출!" << std::endl; }
void print() { std::cout << "a : " << a << ", b : " << b << std::endl; }
~Foo() { std::cout << "소멸자 호출!" << std::endl; }
};
int main() {
auto ptr = std::make_unique<Foo>(3, 5);
ptr->print();
}
먼저 STL(Standard Template Library) 컨테이너란 데이터를 넣어두는 그릇 클래스를 의미한다.
- vector, list, map, queue같은 것들이 전부 STL 컨테이너다.
int main() {
std::vector<std::unique_ptr<A>> vec;
std::unique_ptr<A> pa(new A(1));
vec.push_back(pa);
}
unique_ptr은 복사 생성자가 없기 때문이다.int main() {
std::vector<std::unique_ptr<A>> vec;
std::unique_ptr<A> pa(new A(1));
vec.push_back(std::move(pa)); // 잘 실행됨
}
int main() {
std::vector<std::unique_ptr<A>> vec;
// vec.push_back(std::unique_ptr<A>(new A(1))); 과 동일
vec.emplace_back(new A(1));
vec.back()->some();
}
unique_ptr<A>객체를 생성한다.
unique_ptr는 항상 딱 하나의 객체가 소유하는 포인터 였다면shared_ptr는 여거 객체가 같이 공유할 수 있는 포인터이다.
std::shared_ptr<A> p1(new A());
std::shared_ptr<A> p2(p1); // p2 역시 생성된 객체 A 를 가리킨다.
// 반면에 unique_ptr 의 경우
std::unique_ptr<A> p1(new A());
std::unique_ptr<A> p2(p1); // 컴파일 오류!
std::shared_ptr<A> p1(new A());
std::shared_ptr<A> p2(p1); // p2 역시 생성된 객체 A 를 가리킨다.
std::shared_ptr<A> p3(p2);
std::cout << p1.use_count(); // 3
std::cout << p2.use_count(); // 3
std::cout << p3.use_count(); // 3
shared_ptr은 인자로 주소값이 전달이 되면 자기가 해당 객체를 처음으로 소유하는 것 마냥 행동하게 된다.A* a = new A();
std::shared_ptr<A> pa1(a);
std::shared_ptr<A> pa2(a);
shared_ptr의 주소를 통해 생성하는 것을 피해야 한다.
make_shared은 shared_ptr을 쉽게 만들 수 있게 해주는 함수이다.
std::shared_ptr<A> p1 = std::make_shared<A>();
[객체][control block] 이런 형태로 분리되어서 메모리 공간에 할당이 된다.
하지만 make_shared를 사용하게 되면
[객체 | control block] 이런 형태로 단일 메모리 블록에 할당하게 된다.
// ✅ 올바른 사용법
auto pa1 = std::make_shared<A>(); // [A | control block] #1 생성
auto pa2 = pa1; // pa1과 control block 공유 (ref=2)
// 또는 복사
std::shared_ptr<A> pa2 = pa1; // 같은 control block 공유
A* a = new A();
auto pa1 = std::make_shared<A>(a);
auto pa2 = std::make_shared<A>(a);
class A {
int *data;
public:
A() {
data = new int[100];
std::cout << "자원을 획득함!" << std::endl;
}
~A() {
std::cout << "소멸자 호출!" << std::endl;
delete[] data;
}
std::shared_ptr<A> get_shared_ptr() { return std::shared_ptr<A>(this); }
};
int main() {
std::shared_ptr<A> pa1 = std::make_shared<A>();
std::shared_ptr<A> pa2 = pa1->get_shared_ptr();
std::cout << pa1.use_count() << std::endl;
std::cout << pa2.use_count() << std::endl;
}
shared_ptr로 넘기는 경우는 종종 일어나게 된다.shared_ptr<A>를 하게 되면 pa1과 pa2는 같은 A를 가르키지만 control block이 마찬가지로 각각 생기게 된다. 이러면 위와 같은 문제가 똑같이 일어날 수 있다.#include <iostream>
#include <memory>
class A : public std::enable_shared_from_this<A> {
int *data;
public:
A() {
data = new int[100];
std::cout << "자원을 획득함!" << std::endl;
}
~A() {
std::cout << "소멸자 호출!" << std::endl;
delete[] data;
}
std::shared_ptr<A> get_shared_ptr() { return shared_from_this(); }
};
int main() {
std::shared_ptr<A> pa1 = std::make_shared<A>();
std::shared_ptr<A> pa2 = pa1->get_shared_ptr();
std::shared_ptr<A> pa3 = pa1->get_shared_ptr();
std::shared_ptr<A> pa4 = pa3->get_shared_ptr();
std::cout << pa1.use_count() << std::endl;
std::cout << pa2.use_count() << std::endl;
std::cout << pa3.use_count() << std::endl;
}
enable_shared_from_this클래스를 상속받아서 해결할 수 있다.shared_from_this라는 멤버 함수를 정의하는데 이 함수는 이미 존재하는 control block을 이용해 shared_ptr을 생성한다.
weak_ptr이다.weak_ptr은
shared_ptr가 관리하는 객체를 약하게 참조하는 포인터다.
shared_ptr를 구현하는 참조 카운트의 영향을 받지 않는다는 특징이 있다.
.lock()이나 .expired()를 통해 하면 된다._uses와 _weaks 멤버 변수가 존재하는데, 이게 바로 강한 참조와 약한 참조를 확인하고 있는 변수라고 보면 된다.class Person
{
//... 생략
std::weak_ptr<Person> m_partner;
//... 생략
public:
bool SetParter(std::shared_ptr<Person>& p)
{
if (!p)
return false;
this->m_partner = p;
std::cout << m_name << " is now partnered with " << p->m_name << "\n";
return true;
}
string getPartnerName() const {
// lock()으로 안전하게 shared_ptr 변환 시도
if (auto partner = m_partner.lock()) {
return partner->m_name; // 안전하게 접근
}
return "No partner"; // 파트너가 삭제된 경우
}
}
{
// woong과 soo라는 Person의 shared_ptr 객체를 생성합니다.
auto pWoong = std::make_shared<Person>("woong");
auto pSoo = std::make_shared<Person>("soo");
// woong과 soo과 서로 파트너로 설정합니다.
// 내부적으로 서로가 서로를 참조하는 형태로 구현됩니다.
pWoong->SetPartner(pSoo);
pSoo->SetPartner(pWoong);
// 안전하게 파트너 이름 출력
std::cout << pWoong->getPartnerName() << std::endl; // "soo"
// soo가 먼저 삭제된 후
pSoo.reset();
std::cout << pWoong->getPartnerName() << std::endl; // "No partner"
}
auto partner = m_partner.lock() 부분에서 ref count가 1늘어났지만 스코프를 벗어나면서 자동적으로 정리가 되므로 다시 1이 줄어드는 흐름으로 동작하고 있는 것이다.
class Node {
std::vector<std::shared_ptr<Node>> children;
/* 어떤 타입이 와야할까? */ parent;
public:
Node(){};
void AddChild(std::shared_ptr<Node> node) { children.push_back(node); }
};
shared_ptr이 오면 앞서 확인한 순환 참조 문제가 발생할것이다.Node* 포인터를 하게 되면 dangling이 발생할 위험이 있다.weak_ptr은 일반 포인터와 shared_ptr사이에 위치한 포인터로 객체를 안전하게 참조할 수 있지만 count를 늘리지 않는다. (구경만 함.)weak_ptr자체로는 원래 객체를 참조할 수 없기 때문에 shared_ptr로 변환해서 사용해야 한다. 객체가 소멸된 상태면 빈 shared_ptr을 반환하고 아니라면 해당 객체를 가르키는 shared_ptr로 변환된다.#include <iostream>
#include <memory>
#include <string>
#include <vector>
class A {
std::string s;
std::weak_ptr<A> other;
public:
A(const std::string& s) : s(s) { std::cout << "자원을 획득함!" << std::endl; }
~A() { std::cout << "소멸자 호출!" << std::endl; }
void set_other(std::weak_ptr<A> o) { other = o; }
void access_other() {
// weak_ptr 를 shared_ptr 로 변환 시도
// 이게 변환인 이유는 other lock을 호출하면 mutex와 같은 락이 아니라 weak 상태를 shared 상태로 변환한다는 의미이다.
std::shared_ptr<A> o = other.lock();
if (o) {
std::cout << "접근 : " << o->name() << std::endl;
} else {
std::cout << "이미 소멸됨" << std::endl;
}
}
std::string name() { return s; }
};
int main() {
std::vector<std::shared_ptr<A>> vec;
vec.push_back(std::make_shared<A>("자원 1"));
vec.push_back(std::make_shared<A>("자원 2"));
vec[0]->set_other(vec[1]);
vec[1]->set_other(vec[0]);
// pa 와 pb 의 ref count 는 그대로다.
std::cout << "vec[0] ref count : " << vec[0].use_count() << std::endl;
std::cout << "vec[1] ref count : " << vec[1].use_count() << std::endl;
// weak_ptr 로 해당 객체 접근하기
vec[0]->access_other();
// 벡터 마지막 원소 제거 (vec[1] 소멸)
vec.pop_back();
vec[0]->access_other(); // 접근 실패!
}
자원을 획득함!
자원을 획득함!
vec[0] ref count : 1
vec[1] ref count : 1
접근 : 자원 2
소멸자 호출!
이미 소멸됨
소멸자 호출!
weak_ptr로 인자를 받아서 이를 넣는다. 즉 weak_ptr는 weak_ptr또는 shared_ptr를 받는다는 것을 알 수 있다.