C++ 아이콘 제작자: Darius Dan - Flaticon
c++에선 직접 얻었던 자원은 직접 해제하지 않으면 프로그램이 끝날 때 까지 존재한다.
자원 관리는 어렵다.
사람인지라 깜빡할 수 있고 또 예외를 던지는 throw 아래에 delete가 있으면 예외 전달시 해제가 안됨.
==> 그래서 Resource Acquisition Is Initialization - RAII - 자원의 획득은 초기화다 패턴을 사용어떤 상황이 일어나도 할당된 자원들을 해제하는 패턴
표준 라이브러리에 이를 사용하는 스마트 포인터가 있다.
일반적인 포인터가 아닌 포인터 객체로 만들어 자신이 소멸될 때 가리키던 데이터도 같이 해제한다.
==> 객체를 통해 자원을 관리한다. 이 똑똑한 포인터가 스마트 포인터.
3가지의 스마트 포인터가 있다.
1) unique_ptr
2) shared_ptr
3) weak_ptr
이미 소멸된 객체를 다시 소멸시켜 에러를 발생 ==> double free 버그 라고 한다.
위와 같은 문제가 발생한 이유는 만들어진 객체의 소유권이 명확하지 않아서다.
- 어떤 포인터에 객체의 유일한 소유권을 부여해서, 이 포인터 말고는 객체를 소멸시킬 수 없다! 라고 한다면, 위와 같은 문제가 발생하지 않을 것이다.
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()); // A* pa = new A();와 똑같은 문장이라 생각하면 편하다.
pa->some(); // 생성된 스마트 포인터를 실제 포인터 처럼 활용한다.
}
int main() { do_something(); }
해당 스마트 포인터는 스택에 정의된 객체이기 때문에 해당 객체의 범위가 종료되면 소멸자가 자동으로 호출된다.
std::unique_ptr<\A> pb = pa;
==> 컴파일 오류가난다. ==> 삭제된 함수를 사용했기 때문에다.unique_ptr은 어떠한 객체를 유일하게 소유해야 하기에 복사 생성자가 명시적으로 삭제됨.
삭제된 함수?? A(const A& a) = delete; 처럼
= delete;
를 사용하면 명시적으로 함수를 사용못하게 막는다.
사용하면 컴파일 오류가 발생
std::unique_ptr<A> pb = std::move(pa); // pb 에 소유권을 이전.
이는 이동 생성자를 사용한 것으로 이전되면 아무것도 가리키지 않고 nullptr이 들어간다
해당 포인터를 댕글링 포인터라고 한다.
해당 포인터는 어떠한 객체의 소유권을 의미하는 것인데
함수에 인자로 전달한다면 함수 내부에서 unique_ptr은 더이상 유일한 소유권이라고 보기 어렵다.해당 스마트 포인터의 의미가 사라진다. 또한 규칙 위반이다.
- unique_ptr의 get함수는 실제 객체의 주소값을 리턴한다.
void do_something(A* ptr) { 무언가를 한다... } ==> 일반적인 포인터를 인자로 받는다 std::unique_ptr<\A> pa(new A()); do_something(pa.get()); } ==> 클래스 A의 객체의 주소를 넘겨줌
auto ptr = std::make_unique<A>(1, 2);
- make_unique 함수는 템플릿 인자로 전달된 클래스의 생성자에 인자들에 직접 완벽한 전달
- 따라서 기존 처럼 불필요하게
std::unique_ptr<\A> ptr(new A(1, 2 ));
할 필요가 없다
그냥 unique_ptr을 원소로 하는 vector에 해당 스마트 포인터를 push_back으로 넣으면 컴파일 오류가 남.
vector의 push_back함수는 전달 받은 인자를 복사해서 집어 넣는 것임.
그렇기에 unique_str을 vectre안으로 이동시켜줘야 한다.
vec.push_back(std::move(pa));
그런데 emplace_back을 사용하면 vector안에 unique_str을 직접 생성하면서 집어넣을 수 있다.
emplace_back 함수는 전달된 인자를 완벽한 전달로 직접
unique_ptr<\A>
의 생성자에 전달 해서 vector 맨 뒤에unique_ptr<\A>
객체를 생성
==> 따라서, 위에서 처럼 불필요한 이동 연산이 필요 없다.
std::shared_ptr<A> p1(new A());
std::shared_ptr<A> p2(p1); // p2 역시 생성된 객체 A 를 가리킨다.
참조 개수가 0이 되어야 가리키던 객체를 해제할 수 있다.
use_count로 현재 참조 개수가 몇개인지 확인할 수 있다. ex) std::cout << p.use_count();
처음으로 실제 객체를 가리키는 shared_ptr이 제어블럭(control block)을 동적으로 할당한다.
이 제어 블럭에서 서로 정보를 공유한다. ==> 참조 개수를 공유
해당 포인터를 생성할 때 마다 해당 제어 블럭의 위치를 공유하기만 하면 끝
std::shared_ptr<A> p1(new A());
동적 할당을 2번할 것을 알고 있으니 그냥 처음부터 2개를 합친 크기로 1번 할당하는게 훨씬 빠르다.
std::shared_ptr<A> p1 = std::make_shared<A>();
<>에 sharedptr이 가리킬 타입을 전달하면 알아서 해당 타입의 생성자와 제어 블럭까지 1번에 할당한다.
()에는 해당 타입 생성자의 인자를 전달하면 된다.해당 함수는 해당 타입의 생성자한테 완벽한 전달을 수행한다.
해당 포인터를 생성할 때 인자로 객체가 아닌 주소값을 전달하면
해당 객체를 첫번째로 소유하는 shared_str마냥 행동한다.그러면 여러개의 제어 블럭이 생기고 참조 개수가 서로 공유되지 않아
아직 스마트 포인터가 남아있음에도 가리키는 객체를 해제할 수 도 있다.
this 를 사용해서 shared_ptr 을 만들고 싶은 클래스가 있다면
enable_shared_from_this 를 상속 받으면 hared_from_this(); 함수를 사용할 수 있다.이 함수는 이미 정의되어 있는 제어 블럭을 사용해서 shared_ptr을 생성한다.
this를 사용할 자리에 그냥 해당 함수를 호출하면 된다.1가지 주의사항은 해당 함수를 사용하기 위해서는 반드시 해당 객체에 대한 shared_ptr 1개가 이미 있어야 한다.
==> 제어 블럭을 확인만 할 뿐 새로운 제어 블럭을 만들지 않기 때문이다.
class A {
int *data;
std::shared_ptr<A> other;
public:
A() {
data = new int[100];
std::cout << "자원을 획득함!" << std::endl; }
~A() {
std::cout << "소멸자 호출!" << std::endl;
delete[] data; }
void set_other(std::shared_ptr<A> o) { other = o; } };
int main() {
std::shared_ptr<A> pa = std::make_shared<A>();
std::shared_ptr<A> pb = std::make_shared<A>();
pa->set_other(pb);
pb->set_other(pa); } ==> 소멸자가 실행되지 않음
객체 1이 파괴 되기 위해서는 객체 1을 가리키고 있는 shared_ptr의 참조개수가 0이 되어야 하고
객체 2가 파괴 되기 위해서는 반대이다 => 이러지도 저러지도 못하는 상황이 된다.
==> 이 때 weak_ptr을 사용한다.
- shared_ptr을 사용하면서 참조 카운트에 영향을 받지 않는 스마트 포인터가 필요할 때 사용한다.
- weak_ptr을 사용하면 shared_ptr가 관리하는 자원(메모리)을 참조카운트에 영향을 미치지 않으면서
참조 타입으로 가질 수 있습니다. => 자원을 할당 받아도 잠조 카운팅에 영향을 미치지 않는다
auto ap = std::make_shared<A>();
std::weak_ptr<A> ap1(ap); // shared_ptr을 weak_ptr의 복사생성자로 weak_ptr(ap1) 객체 생성
std::weak_ptr<A> ap2 = ap1; // weak_ptr을 weak_ptr의 대입연사자로 weak_ptr(ap2) 객체 생성
- 만약 할당 받은 자원의 shared_ptr의 참조 카운트가 0이 되어 객체가 해제 된다면 더 이상 weak_ptr을 사용할 수 없고 비어 있는 상태가 되면 제어 블럭의 주소를 가지지 않는다.
weak객체에 접근할 수 있다고(weak가 가리키고 있다고) 해서 해당 자원에 접근할 수 있다는 것을 보장할 수 없기 때문에
weak_ptr은 할당 받은 자원을 직접적으로 사용하지 못한다
==> 그냥 빈 곳을 가리킬 수 있어서그래서 직접적인 사용을 막기 위해 해당 포인터의 함수에 ->, *, get() 연산자는 없다.
lock() 함수를 통해 해당 포인터 객체로 shared_ptr 객체를 만들어 사용한다.
std::shared_ptr<A> o = weak1.lock();
제어 블럭을 메모리에서 해제하기 위해서는 이를 가르키는 weak_ptr 역시 0개 여야한다
그래서 해당 포인터의 개수를 파악하기 위해 제어 블럭에 참조 개수와 함께
약한 참조 개수(weak count)를 기록한다.
개인 공부 기록용 블로그입니다.
틀린 부분 있으다면 지적해주시면 감사하겠습니다!!