자원 관리
- 자원 관리의 중요성
- 메모리를 할당만 하고 해제를 하지 않는다면, 결국 메모리 부족으로 프로그램이 crash 될 수 있다.
- C++ 이후에 나온 많은 언어들은 대부분 가비지 컬렉터(Garbage Collector - GC)라 불리는 기능이 기본적으로 내장되어 있다.
=> 가비지 컬렉터는 프로그램 상에서 더 이상 쓰이지 않는 자원을 자동으로 해제해주는 역할을 한다.
==> 프로그래머들이 코드를 작성할 때, 자원을 해제하는 일에 대해 크게 신경 쓸 필요가 없다.
- C++은 프로그래머가 한 번 획득한 자원은, 직접 해주지 않는 이상 프로그램 종료 전까지 영원히 남아있게 된다.
=> 프로그램 종료시에는 운영체제가 알아서 해제해준다.
- delete가 제대로 되지 않아서 메모리 누수가 발생하는 경우가 종종 생긴다.
- Resource Acquisition Is Initialization - RAII
- C++에서 자원을 관리하는 디자인 패턴
=> 자원 관리를 스택에 할당한 객체를 통해 수행한다.
- 클래스 안에서 자원에 대한 초기화 및 해제를 해준다.
=> 외부 함수 등에선 일반 객체를 통해서 접근하게 되고, 함수가 종료되거나 예외 상황이 발생하는 등의 경우가 생기면 스택이 비워지면서 해당 객체의 소멸자가 호출된다. 이 때, 소멸자 내부에서 자원에 대한 해제를 해주면 자동으로 자원 관리가 된다.
또한, 객체를 생성하는 시점에 자원에 대한 초기화가 완료되어야 한다.
- 스마트 포인터(smart pointer)
- unique_ptr - 객체의 유일한 소유권
- 메모리 관리에 실패했을 때 발생하는 두 가지 종류의 문제점
1. 메모리 사용 후 해제하지 않은 경우 - 메모리 누수(memory leak)
=> 장시간 작동하는 프로그램의 경우 시간이 지남에 따라 점점 사용하는 메모리 양이 늘어나서 결과적으로 나중에 시스템 메모리가 부족해서 프로그램이 죽어버릴 수 있다.
==> 이 문제는 위에서 언급한 RAII 패턴을 통해 해결할 수 있다.
2. 이미 해제된 메모리를 다시 참조하는 경우
- 이미 소멸된 객체를 다시 소멸시켜서 발생하는 버그를 double free 버그라고 부른다.
=> 객체의 소유권이 명확하지 않아서 발생하는 버그다.
- 특정 객체에 유일한 소유권을 부여하는 포인터 객체를 unique_ptr 라고 한다.
=> std::unique_ptr<class name> 변수명(new class());
==> 템플릿에 인자로, 포인터가 가리킬 클래스를 전달한다.
==> 즉, 위의 코드는 class* 변수명 = new class(); 와 같다.
- unique_ptr로 정의한 포인터는 함수가 종료되는 등 생명이 끝나면 메모리를 자동으로 해제해준다.
=> 메모리를 할당하지 않았다면 core가 발생하거나 오류가 발생할 수도 있다.
- 삭제된 함수
- 사용을 원치 않는 함수를 삭제시키는 방법으로 = delete; 를 사용하게 되면 프로그래머가 명시적으로 '해당 함수는 사용 불가' 라고 표현할 수 있다.
=> 사용하게 되면 컴파일 오류가 발생한다.
- unique_ptr의 복사는 불가능하다.
=> unique_ptr는 어떠한 객체를 "유일하게" 소유해야 한다.
==> unique_ptr의 복사 생성자가 명시적으로 삭제되어 있다.
- unique_ptr 소유권 이전
- unique_ptr의 복사생성자는 정의되어 있지 않지만, 이동 생성자는 가능하다.
=> 소유권을 이동시킨다 라는 개념으로 생각하면 되기 때문
=> 소유권을 이동시킨 후에 기존의 unique_ptr 접근에 유의해야 한다.
- 소유권이 이전된 unique_ptr를 댕글링 포인터(dangling pointer)라고 한다.
=> 재참조 할 경우 런타임 오류가 발생한다.
- unique_ptr를 함수 인자로 전달하기
- unique_ptr은 객체의 유일한 소유권을 의미하는 것이기 때문에, 함수의 인자로 레퍼런스로 전달해서는 안된다.
=> 실행은 되지만 의도와는 맞지 않다.
- 원래의 포인터 주소값을 전달해준다.
=> unique_ptr의 get 함수를 호출하면, 실제 객체의 주소값을 리턴해준다.
==> 해당 주소값을 매개변수로 넘겨주면 된다.(포인터 사용)
- unique_ptr 쉽게 생성
- C++ 14부터 std::make_unique 함수가 제공된다.
- make_unique 함수는 템플릿 인자로 전달된 클래스의 생성자에 인자들을 직접 완벽하게 전달한다.
ex) std::unique_ptr<Foo> ptr(new Foo(3, 5)); => auto ptr = std::make_unique<Foo>(3, 5);
- unique_ptr를 원소로 가지는 컨테이너
- unique_ptr은 다른 타입들과 큰 차이는 없지만, 복사 생성자가 없다 라는 특성때문에 조금 어려울 수 있다.
- vector의 push_back 함수는 전달된 인자를 복사해서 집어넣는 방식이기 때문에, unique_ptr을 사용하게 되면 복사 생성자로 인한 문제가 발생한다.
=> unique_ptr 변수를 vector 안으로 이동시켜야 한다. 즉, push_back에 우측값 레퍼런스를 넣어줘야 한다.
- emplace_back은 unique_ptr을 직접 생성하면서 집어넣을 수 있다.
=> 불필요한 이동 과정을 생략할 수 있다.
- unique_ptr 정리
- unique_ptr은 어떤 객체의 유일한 소유권을 나타내는 포인터이며, unique_ptr가 소멸될 때, 가리키던 객체 역시 소멸된다.
- 다른 함수에서 unique_ptr가 소유한 객체에 일시적으로 접근하려면, get을 통해 해당 객체의 포인터를 전달하면 된다.
- 소유권 이전을 원한다면, unique_ptr를 move 하면 된다.
- unique_ptr의 생성자는 noexcept로 선언되어서 예외를 던지지 않는다.
=> 메모리 할당이 실패할 경우 예외를 throw 하게 되지만, noexcept로 선언되어 있으므로 바로 std::terminate가 호출된다.(terminate는 기본적으로 abort를 호출함으로 오류창이 발생한다.)
- 자원의 공유 - shared_ptr
- shared_ptr로 객체를 가리킬 경우, 다른 shared_ptr 역시 그 객체를 가리킬 수 있다.
=> 몇 개의 shared_ptr 들이 원래 객체를 가리키는지 알아야만 한다.
==> 이를 참조 개수(reference count)라고 하며, 참조 개수가 0이 되어야 가리키고 있는 객체를 해제할 수 있다.
- unique_ptr 와는 다르게 shared_ptr의 경우 객체를 가리키는 모든 스마트 포인터들이 소멸되어야만 객체를 파괴할 수 있다.
- 같은 객체를 가리키는 shared_ptr끼리 동기화 시키는 방법은 처음으로 실제 객체를 가리키는 shared_ptr가 제어 블록(control block)을 동적으로 할당한 후, shared_ptr 들이 제어 블록에 필요한 정보를 공유하는 방식으로 구현된다.
- shared_ptr는 복사 생성할 때마다 해당 제어 블록의 위치만 공유하면 되고, shared_ptr가 생성 혹은 소멸할 때마다 제어 블록의 참조 개수를 조절하기만 하면 된다.
- make_shared
- std::stared_ptr<Foo> p1(new Foo()); 는 바람직한 shared_ptr 생성 방법이 아니다.
=> Foo를 생성하기 위해 동적 할당이 한 번 발생하고, shared_ptr의 제어 블록 역시 동적으로 할당된다.
==> 즉, 두 번의 동적 할당이 발생하는데 동적 할당은 상당히 비싼 연산이다.
==> 두 번의 동적 할당을 할 것을 알고 있다면, 아예 두 개 합친 크기로 한 번 할당하는 것이 훨씬 빠르다.
- std::shared_ptr<Foo> p1 = std::make_shared<Foo();
=> make_shared 함수는 Foo의 생성자의 인자들을 받아서 이를 통해 객체 Foo와 shared_ptr의 제어블록까지 한 번에 동적할당한 후 만들어진 shared_ptr을 리턴한다.
=> Foo의 생성자에 인자가 있다면 make_shared에 인자로 전달해주면 된다.
- shared_ptr 생성 시 주의사항
- shared_ptr은 인자로 주소값이 전달된다면, 마치 자기가 해당 객체를 첫번째로 소유하는 shared_ptr인 것 처럼 행동한다.
=> shared_ptr를 주소값을 통해서 생성하는 것을 지양해야 한다.
ex) Foo* a = new Foo();
std::shared_ptr<Foo> pa1(a);
std::shared_ptr<Foo> pa2(a);
이렇게 shared_ptr을 사용하면, a에 대한 각각의 제어 블록이 따로 생성되므로 문제가 발생한다.
- 객체 내부에서 자기 자신을 가리키는 shared_ptr를 만들면 이러한 문제에 부딪힐 수 있다.
- enable_shared_from_this를 통해 해결이 가능하다.
=> this를 사용해서 shared_ptr을 만들고 싶은 클래스가 있다면, enable_shared_from_this를 상속받으면 된다.
- enable_shared_from_this 클래스에는 shared_from_this 라는 멤버 함수가 정의되며, 해당 함수는 이미 정의된 제어블록을 사용해서 shared_ptr을 생성한다.
=> 이전 처럼 같은 객체에 두 개의 다른 제어 블록이 생성되는 일을 막을 수 있다.
- shared_from_this가 잘 작동하려면 해당 객체의 shared_ptr 가 반드시 먼저 정의되어 있어야 한다.
=> shared_from_this는 이미 존재하는 제어 블록을 확인만 할 뿐, 없는 제어 블록을 만들지는 않는다.
- 서로 참조하는 shared_ptr
- 각 객체가 shared_ptr를 하나씩 가지며, 해당 shared_ptr이 서로의 객체를 가리키게 되면 문제가 발생한다.
=> 이러한 순환 참조 문제를 해결하기 위해 나타난 것이 바로 weak_ptr 이다.
- weak_ptr
- 일반 포인터와 shared_ptr 사이에 위치한 스마트 포인터로, 스마트 포인터처럼 객체를 안전하게 참조할 수 있게 해주지만, shared_ptr와는 다르게 참조 개수를 늘리지는 않는다.
- 어떤 객체를 weak_ptr 가 가리키고 있다 해도, 다른 shared_ptr들이 가리키고 있지 않다면 이미 메모리에서 소멸되었을 수 있다.
=> weak_ptr 자체로는 원래 객체를 참조할 수 없고, 반드시 shared_ptr로 변환해서 사용해야 한다.
==> 가리키고 있는 객체가 이미 소멸되었다면 빈 shared_ptr로 변환되고, 아닐 경우 해당 객체를 가리키는 shared_ptr로 변환된다.
- weak_ptr는 생성자로 shared_ptr나 다른 weak_ptr를 받는다.
- shared_ptr과는 다르게, 이미 제어 블록이 만들어진 객체만이 의미를 가지므로 평범한 포인터 주소값으로 weak_ptr를 생성할 수는 없다.
- weak_ptr 자체로는 원소를 참조할 수 없으므로 shared_ptr로 변환해야 한다.
=> 해당 작업은 lock 함수를 통해서 수행할 수 있다.
==> weak_ptr에 정의된 lock 함수는 만일 weak_ptr 가 가리키는 객체가 아직 메모리에 살아있다면(참조 개수가 0이 아니라면) 해당 객체를 가리키는 shared_ptr를 반환하고, 이미 해제되었다면 아무것도 가리키지 않는 shared_ptr를 반환한다.
==> 아무것도 가리키지 않는 shared_ptr은 false로 형변환 된다.
- 제어 블록의 참조 개수가 0이 되어도 제어 블록은 메모리에서 해제되지 않는다.
=> shared_ptr은 0개여도 weak_ptr가 남았다면, 제어 블록을 메모리에서 해제했을 때 제어 블록의 참조 카운트가 0이라는 사실을 알 수 없게 된다.
==> 제어 블록을 메모리에서 해제하기 위해서는 이를 가리키는 weak_ptr 역시 0개여야 한다.
===> 제어 블록에는 참조 개수와 더불어 약한 참조 개수(weak count) 를 기록하게 된다.
틀린 부분이나 이상한 부분이 있으면 댓글로 편하게 지적해주세요!
감사합니다!
참고
씹어먹는 C ++ - <13 - 1. 객체의 유일한 소유권 - unique_ptr> (modoocode.com)
씹어먹는 C ++ - <13 - 2. 자원을 공유할 때 - shared_ptr 와 weak_ptr> (modoocode.com)
'C,C++ > 정보정리' 카테고리의 다른 글
[C/C++] Thread - (1) (0) | 2023.02.15 |
---|---|
[C/C++] Callable (0) | 2023.02.11 |
[C/C++] Lvalue, Rvalue (0) | 2023.02.10 |
[C/C++] 문자열 및 예외처리 (0) | 2023.02.03 |
[C/C++] 표준 템플릿 라이브러리(STL) - (3) (0) | 2023.02.03 |
댓글