🤯 언리얼을 하기 위해 C++ 기억 되살리기 프로젝트
스마트 포인터
- unique_ptr
- shared_ptr
- 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;
}
|
댓글남기기