bdfgdfg

[C++ 11] 스마트 포인터(Smart Pointer) - 1 본문

게임프로그래밍/C++

[C++ 11] 스마트 포인터(Smart Pointer) - 1

marmelo12 2022. 1. 26. 14:27
반응형

스마트 포인터(Smart Pointer)

C++은 new키워드를 통해 메모리를 동적할당 하였다면 메모리 누수를 막기 위해 꼭 다 사용하고 나서 delete를 해주어야 한다. 

C++11에서는 이러한 동적할당에 대해 더이상 직접 delete를 호출하지 않더라도 포인터가 필요하지 않게 되었을 때 자동으로 delete를 직접 호출해주는 기능이 추가 되었다. 그것이 바로 스마트 포인터.

자동으로 delete를 해준다는게 다른 객체지향 언어의 가비지 컬렉션의 개념과는 거리가 멀다.

이유는 스마트 포인터는 필요가 없어진 순간에 바로 알아서 delete를 하기 때문. (자동으로 삭제를 한다는점에선 같음)

 

스마트 포인터의 종류

1. unique_ptr

2. shared_ptr

3. weak_ptr

 

먼저 유니크 포인터를 본다.

 

유니크 포인터(unique_ptr)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <iostream>
#include <memory> // 스마트 포인터를 사용하기 위해 추가해야할 헤더
 
int main()
{
    int* data = new int();
    std::unique_ptr<int> myPtr(data);
    *myPtr = 50;
 
    std::cout << *data << " " << *myPtr << std::endl;
    std::cout << data << " " << myPtr;
 
    return 0;
}
cs

간단하게 사용한 유니크 포인터.

실행 결과.

유니크 포인터를 이용한 역참조 후 데이터 수정. 그리고 주소값 체크. 모두 같다는것을 알 수 있다.

유니크 포인터는 원시 포인터를 단독으로 소유한다.

원시 포인터는 누구하고도 공유되지 않으며 따라서 복사나 대입이 불가능하다.

또한 위에서 소개한 것처럼 unique_ptr의 변수가 해당 함수의 범위(scope)를 벗어나면 원시 포인터는 자동으로 delete된다.

1
2
3
4
5
6
7
8
9
10
11
12
13
int main()
{
    
    std::unique_ptr<Test> myPtr(new Test());
 
    //유니크 포인터가 가지는 원시 포인터는 공유 불가능.
    std::unique_ptr<Test> copyPtr = myPtr; //컴파일 에러
    std::unique_ptr<Test> copyPtr2(myPtr); //컴파일 에러
 
    return 0;
}
cs

 

온갖 에러가 뜨는것을 볼 수 있다. (이러한 복사는 불가능하지만 앞에서 배운 소유권 이전인 이동(move)을 쓰면 가능)

에러가 나는 코드를 지우고 이제 자동으로 소멸이 되는지 Test클래스의 소멸자 함수에 콘솔을 찍게하고 실행해보면

myPtr이 main함수의 범위를 벗어날 때 원시 포인터는 자동으로 delete된다.

이러한 자동 delete는 매우 편리한데, 유니크 포인터는 다음 두가지 경우에 적합하다.

 

1. 클래스의 멤버로 동적할당한 메모리를 가리키는 raw 포인터가 존재할 때.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Test
{
public:
    Test() { std::cout << "Test 생성자" << std::endl; }
    ~Test() { std::cout << "Test 소멸자" << std::endl; }
};
class Player
{
 
private:
    Test* m_test;
public:
    Player() : m_test(new Test()) { std::cout << "Player 생성자 호출!" << std::endl; }
    ~Player()
    {
        std::cout << "Player 소멸자 호출!" << std::endl;
    }
};
;
int main()
{
    std::unique_ptr<Player> myPtr(new Player());
    return 0;
}
cs

위 코드는 메모리 릭이 발생한다.

Player의 객체는 유니크 포인터가 가르켜서 자동 해제 되지만, Player의 멤버인 Test객체는 delete해주지 않았기 때문.

물론 소멸자에 delete를 붙여주면 그만이지만, 내부 객체 멤버조차도 unique_ptr을 사용한다면 소멸자에 delete 키워드를 까먹어 릭이 발생할 일도 존재하지 않는다.

1
2
3
4
class Player
{
private:
    std::unique_ptr<Test> m_test;
cs

저렇게 유니크 포인터로만 만들어주면

delete키워드가 필요없이 자동으로 소멸자가 호출이 된다.

 

2. STL 컨테이너의 요소로 동적할당한 데이터를 저장할 때.

1
2
3
4
5
6
7
8
9
10
11
int main()
{
    std::vector<Player*> vc;
vc.reserve(100);
    for (int i = 0; i < 100++i)
        vc.push_back(new Player());
 
    for (int i = 0; i < 100++i)
        delete vc[i];

vc.clear();
    return 0;
}
cs

위와같이 벡터 컨테이너의 요소에 동적할당한 메모리를 가리키는 raw Pointer를 집어넣을 때.

나중에 다 사용하고나면 동적할당한 메모리를 다시 delete를 해줘야 한다.

유니크 포인터를 사용하면? 그럴 필요가 없다.

1
2
3
4
5
6
7
8
9
10
int main()
{
    std::vector<std::unique_ptr<Player>> vc;
    vc.reserve(100);
    for (int i = 0; i < 100++i)
        vc.push_back(std::unique_ptr<Player>(new Player()));
 
    vc.clear();
    return 0;
}
cs

 

이렇게 깔끔하게 delete처리가 된다.

 

이러한 장점만 존재하는 유니크포인터를 잘못 사용하면 문제가 발생할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
int main()
{
    Player* player = new Player();
    // 원시 포인터를 공유해버린다.
    std::unique_ptr<Player> myPtr(player);
    std::unique_ptr<Player> anotherPtr(player);
    // unique_ptr의 대입연산은 불가능하지만
    // nullptr대입은 가능 -> 원시 포인터에 nullptr대입.
    anotherPtr = nullptr;
 
    return 0;
}
cs

위와 같이 원시 포인터를 먼저 생성 후 유니크 포인터에 넘겨줄 때.

두 유니크 포인터는 두 공유 포인터를 소유하고 있다.

여기서 player이든 myPtr이든 anotherPtr이든 nullptr이 대입되는 순간이라던지. player를 delete로 밀어버린다던지 하면

위와같은 문제가 발생한다.

(유니크 포인터가 범위를 벗어나면 자동으로 delete처리를 하므로)

이러한 해결책이 C++14에 나왔는데 바로 make_unique함수

make_unique함수는 넘긴 인자와 자료형으로 new 키워드를 호출해준다. -> 원시 포인터와 같음.

거기다 둘 이상의 unique_ptr이 원시 포인터를 공유할 수 없도록 막는다.

1
std::unique_ptr<Player> myPtr2 = std::make_unique<Player>();
cs

C++14를 쓸 수 없는 경우가 아니라면 make_unique를 통해 생성을 하도록 하자.

 

유니크 포인터 멤버 함수

5개 정도의 사용할 수 있는 멤버함수가 존재한다. 여기서 4개만 본다.

 

먼저 reset함수.

1
2
std::unique_ptr<Player> myPtr2 = std::make_unique<Player>();
myPtr2.reset(new Player());
cs

reset함수를 실행해보고 콘솔에 찍히는 로그를 확인해보면

인자로 넘겨준 Player객체의 생성자와 Test의 생성자를 호출하고, 기존의 myPtr2가 가리키던 원시 포인터가 가리키는 Player객체는 delete처리가 된다.

 

즉 reset함수는 유니크 포인터의 내부적으로 들고 있는 원시 포인터가 가리키는 메모리를 해제시킨다.

그런 다음 인자로 넘겨준 객체를 유니크 포인터의 원시 포인터가 새로이 가리키게 되는 것.

(즉 간단하게 말하면 원시 포인터 교체)

 

여기서 재밌는 점은 유니크 포인터에 nullptr은 대입이 가능하다고 했는데 nullptr을 대입할 시

reset과 같이 원시 포인터가 가리키는 객체를 자동으로 소멸시킨다.

 

그 다음은 get함수.

이 함수가 하는 일은 간단한데, 유니크 포인터의 내부 멤버인 원시 포인터를 반환한다.

1
2
std::unique_ptr<Player> myPtr2 = std::make_unique<Player>();
std::cout << myPtr2 << " " << myPtr2.get() << std::endl;
cs

이거는 조금 위험할수도 있는게 유니크 포인터는 스마트 포인터의 역할인 범위를 벗어나면 자동소멸의 역할도 있지만

원시 포인터가 가리키는 객체를 해당 유니크 포인터만이 가리키게끔 하는 역할도 있다.

1
2
3
4
5
6
7
8
9
10
11
12
void CheckPlayer(Player* player)
{
 
}
 
int main()
{
    std::unique_ptr<Player> myPtr2 = std::make_unique<Player>();
    CheckPlayer(myPtr2); // 컴파일 에러
    CheckPlayer(myPtr2.get()); // 원시 포인터를 반환하니 가능.
    return 0;
}
cs

위와 같은 경우에 사용될 수 있다.

 

그 다음으로 release 함수.

release 함수는 유니크 포인터의 멤버인 원시 포인터를 반환하고, 해당 원시 포인터를 nullptr로 만든다.

 -> 객체를 소멸시키지는 않음. 이름 그대로 놓아주는 개념.

1
2
3
4
5
6
7
int main()
{
    std::unique_ptr<Player> myPtr2 = std::make_unique<Player>();
    Player* player = myPtr2.release();
 
    return 0;
}
cs

실제로 release함수 호출 후 get함수를 호출해보면 nullptr이 반환된다.

 

마지막으로 swap함수.

말 그대로 swap. 두 유니크 포인터의 원시 포인터를 서로 swap한다.

1
2
3
4
5
6
7
8
int main()
{
    std::unique_ptr<Player> myPtr = std::make_unique<Player>();
    std::unique_ptr<Player> myPtr2 = std::make_unique<Player>();
    
    myPtr.swap(myPtr2);
    return 0;
}
cs

원래 가리키는 객체의 주소. swap함수 호출하면

이렇게 서로의 원시 포인터를 swap한다.

 

유니크포인터 소유권 이전

유니크 포인터는 복사가 금지되어 있다.

그 의미는 당연히 해당 객체를 가리킬 수 있는건 하나의 유니크 포인터만이 가능하다는 의미.

 

하지만 앞에서 배운 소유권을 이전한다는 개념을 적용시키면 가능하다.

바로 std::move함수의 사용

1
2
std::unique_ptr<Player> myPtr = std::make_unique<Player>();
std::unique_ptr<Player> myPtr2(std::move(myPtr));
cs

myPtr2라인을 실행하기전 메모리 상황

이동하고 나면

이동대상이었던 myPtr은 empty 비워버리고 myPtr2의 유니크 포인터로 내용을 모두 옮긴 모습.

 -> const 유니크 포인터는 move를 해도 이동이 불가능하다.

 

정리

- 스마트 포인터중 유니크 포인터는 단 하나의 객체만을 가리킬 수 있으며 공유가 불가능하다는 개념.

- 해당 범위를 벗어나면 자동 delete.

- 소유권을 이전하려면 std::move사용.

- 성능은 raw pointer만을 사용하는것과 거의 동일.

  -> 즉 자동 소멸을 해주지만 직접 관리하는 것과 속도가 거의 동일하단 의미

- unique_ptr을 사용할 수 있다면 사용하는게 좋다. (속도도 동일한데 사용자의 실수를 덜어줄 수 있으므로)

 

 

반응형
Comments