본문 바로가기
Language & Framework & GIT/C++

[C++] 스마트 포인터(SMART POINTER)

by veganwithbacon 2023. 10. 27.
반응형

BAG 파일에 대해 다루다보니, 메모리 할당에 관한 코드를 보게 되어서 그런지 스마트 포인터들이 많이 보였다.

 

스마트 포인터는 C++에서 메모리 관리를 도와주는 중요한 기능이다.

일반적으로 new 키워드를 사용해 동적으로 할당받은 메모리는, 반드시 delete 키워드를 통해 해제해야 한다.

C++에서는 메모리 누수로부터 프로그램의 안전성 보장을 위한 스마트 포인터를 제공한다.

 

  스마트 포인터

 : 포인터처럼 동작하는 클래스 템플릿으로, 사용이 끝난 메모리를 자동으로 해제해준다.

 

자주 쓰이는 스마트 포인터는 unique_ptr, shared_ptr이다.

내가 보고 있는 코드만 봐도, 스택오버플로나 다른 참고자료도 대부분 위 두 개만 쓴다.

왜 쓰는 지를 모르는 상황에 갖다 붙여넣기만 하니, 답답해서 정리했다.

 

  unique_ptr

: 하나의 스마트 포인터만이 특정 객체를 소유할 수 있도록, 객체에 소유권 개념을 도입한 스마트 포인터

 

이 스마트 포인터는 해당 객체의 소유권을 가지고 있을 때만, 소멸자가 해당 객체를 삭제할 수 있다.

unique_ptr 인스턴스는 move() 멤버 함수를 통해 소유권을 이전할 수는 있지만, 복사할 수는 없다.

소유권이 이전되면, 이전 unique_ptr 인스턴스는 더는 해당 객체를 소유하지 않게 재설정된다.

unique_ptr<int> ptr01(new int(5)); // int형 unique_ptr인 ptr01을 선언하고 초기화함.
auto ptr02 = move(ptr01);          // ptr01에서 ptr02로 소유권을 이전함.
// unique_ptr<int> ptr03 = ptr01;  // 대입 연산자를 이용한 복사는 오류를 발생시킴. 
ptr02.reset();                     // ptr02가 가리키고 있는 메모리 영역을 삭제함.
ptr01.reset();                     // ptr01가 가리키고 있는 메모리 영역을 삭제함.

3번째 라인처럼, 스마트 포인터에 대한 대입 연산자를 이용한 복사는 오류를 발생시킨다.

일반적으로 C++ 객체에 대한 스마트 포인터가 필요한 상황에서 주로 unique_ptr을 사용한다

C++ 14부터는 make_unique()함수를 통해 unique_ptr 인스턴스를 안전하게 생성가능하다.

make_unique()함수는 전달받은 인수를 사용해 지정 타입의 객체를 생성하고, 생성된 객체를 가리키는 unique_ptr을 반환.

이 함수를 사용하면, 예외 발생에 대해 안전하게 대처가능하다.

 

#include <iostream>
#include <memory>
using namespace std;

 
class Person
{
private:
    string name_;
    int age_;
public:
    Person(const string& name, int age); // 기초 클래스 생성자의 선언
    ~Person() { cout << "소멸자가 호출되었습니다." << endl; }
    void ShowPersonInfo();
};

 
int main(void)
{
    unique_ptr<Person> hong = make_unique<Person>("길동", 29);
    hong->ShowPersonInfo();
    return 0;
}
 
Person::Person(const string& name, int age) // 기초 클래스 생성자의 정의
{
    name_ = name;
    age_ = age;
    cout << "생성자가 호출되었습니다." << endl;
}
 
void Person::ShowPersonInfo() { cout << name_ << "의 나이는 " << age_ << "살입니다." << endl; }

 

실행 결과 : 
생성자가 호출되었습니다.
길동의 나이는 29살입니다.
소멸자가 호출되었습니다.

앞서 언급한 것 처럼 Person 객체를 가리키는 unique_ptr 인스턴스 hong은 일반 포인터와 달리 사용 후, delete 키워드를 사용해 메모리를 해제할 필요가 없다.

 

❗ make_unique() 함수를 쓰려면 c++컴파일러가 c++14를 지원해야한다


  shared_ptr

: 하나의 특정 객체를 참조하여 스마트 포인터가 총 몇 개인지를 참조하는 스마트 포인터

 

참조 중인 스마트 포인터의 개수를 참조 횟수(reference count)라고 한다.

참조 횟수는 특정 객체에 새로운 shared_ptr이 추가될 때마다 1씩 증가하며, 수명이 다하면 1씩 감소한다.

마지막 shared_ptr의 수명이 다하면, 참조 횟수가 0이 되어 delete키워드를 통해 메모리를 자동 해제한다.

shared_ptr<int> ptr01(new int(5)); // int형 shared_ptr인 ptr01을 선언하고 초기화함.
cout << ptr01.use_count() << endl; // 1
auto ptr02(ptr01);                 // 복사 생성자를 이용한 초기화
cout << ptr01.use_count() << endl; // 2
auto ptr03 = ptr01;                // 대입을 통한 초기화
cout << ptr01.use_count() << endl; // 3  

위 예제의 use_count() 멤버 함수는 shared_ptr 객체가 현재 가리키고 있는 리소스를 참조 중인 소유자의 수를 반환해준다.

 

위 같은 방법 외에도 make_share()함수를 사용하여 shared_ptr 인스턴스를 안전하게 생성 가능하다.

make_shared()함수는 전달받은 인수를 사용해 지정된 타입의 객체를 생성하고, 생성된 객체를 가리키는 shared_ptr을 반환한다. 이 함수를 통해, 예외 발생에 대한 안전한 대처가 가능하다.

 

하기 예제는 Person 객체를 가리키는 chang을 shared_ptr를 make_shared()함수를 통해 생성하는 예제다.

shared_ptr<Person> chang = make_shared<Person>("길동", 29)
cout << "현재 소유자 수 : " << chang.use_count() << endl; //1
auto gpt = chang;
cout << "현재 소유자 수 : " << chang.use_count() << endl; //2
gpt.reset(); // shared_ptr인 gpt을 해제함.
cout << "현재 소유자 수 : " << chang.use_count() << endl; //1
실행 결과
생성자가 호출되었습니다.
현재 소유자 수 : 1
현재 소유자 수 : 2
현재 소유자 수 : 1
소멸자가 호출되었습니다.

  weak_ptr

: 하나 이상의 shared_ptr 인스턴스가 소유하는 객체에 대한 접근을 제공하지만, 소유자의 수에는 포함되지 않는 스마트 포인터

 

shared_ptr은 참조 횟수(reference count)를 기반으로 동작하는 스마트 포인터다.

만약 서로가 서로를 가리키는 shared_ptr를 가지고 있다면, 참조 횟수는 절대 0이 되지 않으므로 메모리는 영원히 해제되지 않는다. 이런 경우를 순환 참조(circular reference)라고 한다.

weak_ptr은 이런 상황의 순환 참조를 제거하기 위해 사용된다.

반응형

댓글