본문 바로가기
C,C++/정보정리

[C/C++] 자원 관리(feat. 스마트 포인터)

by 마두식 2023. 2. 10.
반응형
자원 관리
  • 자원 관리의 중요성

-  메모리를 할당만 하고 해제를 하지 않는다면, 결국 메모리 부족으로 프로그램이 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

댓글