Publish:

태그: , , , ,

카테고리:


🤯 언리얼을 하기 위해 C++ 기억 되살리기 프로젝트

스마트 포인터

  • unique_ptr
    • 진짜 많이 씀! 중요!
  • shared_ptr
    • 밑의 weak와 묶여서 쓰임.
  • weak_ptr
1
2
3
4
5
6
7
8
9
10
11
12
13
// 예시: 포인터
#include "Vector.h"

int main()
{
    Vector* myVector = new Vector(10f, 30.f);

    // ...

    delete myVector;

    return 0;
}

이렇게 new 로 할당을 해줬으면 delete로 지워줘야 함.
그런데 중간에 반환을 해버리는 경우 delete까지 코드가 가지 않아
메모리가 누수가 발생하는 경우가 있을 수 있음.

💡 스마트 포인터를 쓰면, delete를 직접 호출할 필요가 없다!
그리고 가비지 컬렉션보다 빠르다!
( 쓰이지 않는 순간에 딱 지워지기 때문에. )

1. unique_ptr

포인터 소유자가 하나 밖에 없다.
나 말고 누구도 못쓰게 만드는 포인터.
소유자가 한 명이면 그 사람이 지우면 된다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <memory>
#include "Vector.h"

int main()
{
    std::unique_ptr<Vector> myVector(new Vector(10.f, 30.f));
    // 포인터 부호가 없음!

    myVector->Print();
    // 그러나 포인터처럼 동작할 수 있음.
    // 그리고 delete가 없음.

    return 0;
}
  • 포인터(원시 포인터? 기존 포인터?) 를 단독으로 소유
  • 소유한 원시 포인터는 누구하고도 공유되지 않았음
    • 따라서 복사나 대입 불가능!
  • unique_ptr가 범위(scope)를 벗어날 때, 원시 포인터는 지워짐 (delete)
    1
    2
    3
    4
    
    std::unique_ptr<Vector> myVector(new Vector(10.f, 30.f));
    
    std::unique_ptr<Vector> copiedVector1 = myVector;         // 컴파일 에러! 유니크 포인터 대입 안 됨.
    std::unique_ptr<Vector> copiedVector2(myVector);          // 컴파일 에러! 유니크 포인터 복사 못 함.
    

2. 유니크 포인트가 적합한 3가지 경우

예시1: 클래스에서 생성자/소멸자

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// OLD
// Player.h
class Player
{
    private:
        Vector* mLocation;
};

// Player.cpp
Player::Player(std::string name)
        : mLocation(new Vector())
{

}

Player::~Player()
{
    delete mLocation;           // 소멸자에서 지워줬어야 했음
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// NEW!
// Player.h
class Player
{
    private:
        std::unique_ptr<Vector> mLocation;
};

// Player.cpp
Player::Player(std::string name)
        : mLocation(new Vector())
{

}

// 소멸자에 delete 키워드 안 씀!
// Player 소멸될 때 알아서 지워짐.

예시2: 지역 변수

1
2
3
4
5
6
7
8
9
10
// OLD
#include "Vector.h"
int main()
{
    Vector* vector = new Vector(10.f, 30.f);

    vector ->Print();

    delete vector;
}
1
2
3
4
5
6
7
8
9
// NEW!
#include "Vector.h"
#include <memory>
int main()
{
    std::unique_ptr<Vector> vector(new Vector(10.f, 30.f));

    vector ->Print();
}

예시3: STL 벡터에 포인터 저장하기

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// OLD
#include <vector>
#include "Player.h"

int main()
{
    std::vector<Player*> players;
    players.push_back(new Player("Lulu"));
    players.push_back(new Player("Coco"));

    for (int i = 0; i < players.size(); ++i)
    {
        delete players[i];
    }

    players.clear();
    // ...
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// NEW!
#include <vector>
#include "Player.h"

int main()
{
    std::vector<std::unique_ptr<Player>> playerList;

    playerList.push_back(std::unique_ptr<Player>(new Player("Lulu")));
    playerList.push_back(std::unique_ptr<Player>(new Player("Coco")));

    players.clear();
    // 클리어 하면서 소멸자 호출되며 다 지워짐.

    // 사실 굳이 clear호출 안 해도 됨.
    // main() 스코프 나가면서 vector 소멸자 불러오게 됨.
    // 자체적으로 clear 호출되면서 지워짐.
    // ...
}

3. 유니크 포인터 만들기 (C++14 이후)

문제: 원시 포인터 공유

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
Vector* vectorPtr = new Vector(10.f, 30.f);         // 원시 포인터 생성
std::unique_ptr<Vector> vector(vectorPtr);
// 대입해서 넣어줌

std::unique_ptr<Vector> anotherVector(vectorPtr);
// 대입해서 넣어줌 2. 원시 포인터라 유니크 포인터에 대입 가능 함 ㅎ;;
// 같은 포인터를 또 넣어주게 된 것. 소유자 권한 개념이 깨짐!
// 그래서 nullptr을 대입하여 없애버릴 예정.

anotherVector = nullptr;            // 이거 연산자 오버로딩임
// 그.러.나.
// 이건 유니크 포인터가 null이 되는게 아니라, 그 안에 있는 원시 포인터를 null로 해주는 것.
// 포인터에 대한 소유권을 놓는 것이므로, 자동으로 소멸자 호출

// 결국 anotherVector는 empty가 되버리고,
// vectorPtr 값이 날아가게 됨.
// 그런데 vector는 아직도 본인이 포인터를 제대로 갖고있는 줄 앎.

// 그래서 나중에 vector가 소멸될 때 소멸자를 호출하게 되고,
// vectorPtr는 이미 날라갔으므로 에러가 발생하게 될 것...

해결책 (C++14 이후) : make_unique

1
2
3
4
5
6
7
8
9
10
11
#include <memory>
#include "Vector.h"

int main()
{
    std::unique_ptr<Vector> myVector = std::make_unique<Vector>(10.f, 30.f);

    myVector->Print();

    return 0;
}
  • std::make_unique()가 무슨 일을 할까?
    • 주어진 매개변수와 자료형으로 new 키워드 호출해줌
    • 둘 이상의 유니크 포인터가 원시 포인터를 공유할 수 없도록 막아줌

만들기 예시

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Vector* vectorPtr = new Vector(10.f, 30.f);

// 싹 다 에러남!
std::unique_ptr<Vector> vector1 = std::make_unique(vectorPtr);
std::unique_ptr<Vector> vector2 = std::make_unique<Vector>(vectorPtr);
std::unique_ptr<Vector> vector3 = std::make_unique<Vector*>(10.f, 30.f);

// C++11
std::unique_ptr<Vector> vector(new Vector(10.f, 30.f));
std::unique_ptr<Vector[]> vectors(new Vector[20]);

// C++14 (이렇게 쓰자)
std::unique_ptr<Vector> vector = std::make_unique<Vector>(10.f, 30.f);
std::unique_ptr<Vector[]> vectors = std::make_unique<Vector[]>(20);

유니크 포인터 재설정 : reset()

1
2
3
4
5
6
7
int main()
{
    std::unique_ptr<Vector> vector = std::make_unique<Vector>(10.f, 30.f);
    vector.reset(new Vector(20.f, 40.f));
    vector.reset();                         // 이건 nullptr 대입하는 거랑 같다!
    // ...
}
  • reset()을 호출하면 포인터를 교체한다
  • 재설정 될 때, 소유하고 있던 원시 포인터는 자동으로 소멸 된다!
  • nullptr을 꼭 써야 할까?
    • 아니다. 두 코드는 같음 (vector = nullptr;, vector.reset();)
    • nullptr이 reset()과 가독성이 더 높긴 함
    • 그러나 reset()은 vector가 원시 포인터가 아님을 확실히 보여줌

유니크 포인터 가져오기 : get()

유니크 포인터 안의 포인터를 받길 원할 때. (원시 포인터)
유니크 포인터는 본인이 소유하고 있는 거니까,
그 안의 원시 포인터를 밖으로 줄 땐 원시 포인터르 주는 게 나쁘지 않다!…

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// Vector.cpp
void Vector::Add(const Vector* other)
{
  mX += other->mX;
  mY += other->mY;
}

// -----------------------------------------
#include <memory>
#include "Vector.h"

int main()
{
  std::unique_ptr<Vector> vector = std::make_unique<Vector>(10.f, 30.f);
  std::unique_ptr<Vector> anotherVector = std::make_unique<Vector>(20.f, 40.f);

  vector->Add(anotherVector.get());     // <- 이렇게!
  vector->Print();

  return 0;
}
  • 원시 포인터를 반환한다

원시 포인터 소유권 박탈하기 : release()

원시 포인터를 지우지 않고 소유권을 놓아준다.
근데 좋은 함수는 아님… 나중에 지우는 걸 체크해야 하니까.
차라리 유니크 포인터가 갖게 두고 알아서 지워지게 두는게 낫다는 말.

1
2
3
4
5
6
int main()
{
  std::unique_ptr<Vector> vector = std::make_unique<Vector>(10.f, 30.f);
  Vector* vectorPtr = vector.release();     // <- 이렇게!
  // ...
}
  • 원시 포인터에 대한 소유권을 박탈하고 원시 포인터 반환
  • release() 호출 후 get() 을 호출하면 nullptr 반환
    1
    2
    3
    
    // vector는 유니크 포인터!
    Vector* vectorPtr = vector.release();
    Vector* vectorPtr2 = vector.get();          // nullptr 반환
    

유니크 포인터 소유권 이전하기 : std::move()

1
2
3
4
5
6
7
8
#include <memory>
#include "Vector.h"

int main()
{
  std::unique_ptr<Vector> vector = std::make_unique<Vector>(10.f, 30.f);
  std::unique_ptr<Vector> anotherVector(std::move(vector));     // <- 이거!
}

위 함수가 실행되면,
vector는 소유권을 잃었으니까 null 이 되버리고,
anotherVector 는 vector가 갖고있던 원시 포인터의 소유권을 갖게 됨.

  • std::unique_ptr 은 소유한 원시 포인터를 아무하고도 공유하지 않음
  • 즉, 주소 복사를 하지 않는다는 뜻
  • 대신, 소유권을 다른 std::unique_ptr 로 옮길 수 있음 -> std::move
    • std::move 를 사용할 경우 메모리 할당/해제가 일어나진 않음
  • 예외 : const std::unique_ptr
예시: const 유니크 포인터는 어떨까?
1
2
3
const std::unique_ptr<Vector> vector = std::make_unique<Vector>(10.f, 30.f);

std::unique_ptr<Vector> anotherVector(std::move(vector));   // <- 컴파일 에러!

const 니까 당연히 바뀔 수 없다… 그래서 에러.

4. 유니크 포인터 해부

1
2
3
4
5
6
7
8
9
10
11
12
template<typename T>                          // 유니크 포인터 아무거나 받도록 템플릿화
class unique_ptr<T> final                     // 상속 안되도록 final
{
  public:
    unique_ptr(T* ptr) : mPtr(ptr) {}         // 원시 포인터 받는 생성자. 포인터 변수에 저장하겠다!
    ~unique_ptr() { delete mPtr; }            // 지울 때. 포인터 변수 지운다!
    T* get() { return mPtr; }                 // 원시 포인터 반환하는 함수 get(). 포인터 변수 반환.
    unique_ptr(const unique_ptr&) = delete;   // 복사 안되도록 복사 생성자 지움.
    unique_ptr& operator=(const unique_ptr&) = delete;  // 대입 안되도록 대입 연산도 지움.
  private:
    T* mPtr = nullptr;                        
}

이슈 및 공부한 것을 기록해두는 개인 블로그 입니다. 댓글, 피드백 환영합니다 🙂

Update:

댓글남기기