*스마트 포인터는 왜 필요할까??
=> 포인터 사본을 만들지 않도록 하는 것이 좋다.
=> 객체 생명주기로 주소값을 가지고 있는 객체들을 먼저 처리하고 참조되어있는 객체를 마지막으로 처리하는 것이 좋다.
=> 항상 실수 가능성이 있다는 것이 문제.
*댕글링포인터 ( Dangling Pointer )
=> 이미 존재하지 않는 객체의 주소를 가지고 있는 포인터
=> 객체가 삭제될 때 그 객체가 삭제되었다고 알려주어 해당 포인터를 nullptr 해야하는데
딱히 방법이 없다.
이런 상황을 해결하기 위해 스마트 포인터를 사용할 수 있다.
*shared_ptr
=> 참조 카운트 ( ref count ) 사용.
shared_ptr<Knight> k1 = make_shared<Knight>();
*왜 make_shared 를 사용해 shared_ptr 을 써야할까 ??
각각의 shared_ptr 은 참조 카운트를 공유하고 있어야 한다.
이 때,
'제어 블록 ( control block )' 을 사용한다.
처음 실제 객체를 가리키는 shared_ptr 이 제어 블록을 동적 할당하고
이후 복사되는 shared_ptr 들은 그 주소를 복사해 공유.
std::shared_ptr<A> p1(new A());
=> 이 연산은 두 번의 동적할당이 일어남.
1. A 의 생성
2. A 에 해당하는 제어블록 생성
위 2번의 연산을 한번에 처리해주는 것이 'make_shared'
class Player
{
public:
Player() : Data(new int(111)) { cout << "생성자" << endl; }
~Player() { if(Data) delete Data; }
Player(const Player& other)
{
*this = other;
cout << "복사생성자" << endl;
}
Player& operator=(const Player& other)
{
if (this == &other) return *this;
if (Data) { delete Data; }
Data = new int(*(other.Data));
return *this;
}
private:
int* Data = nullptr;
};
int main()
{
int useCount = 0;
// Player 생성자 호출.
std::shared_ptr<Player> P = make_shared<Player>();
// Player 복사 생성자 호출.
std::shared_ptr<Player> P2 = make_shared<Player>(*(P.get()));
std::shared_ptr<Player> P3 = P2;
useCount = P.use_count();
useCount = P2.use_count();
useCount = P3.use_count();
Player* Ref_P = P.get();
auto P8 = P;
auto P9 = P;
useCount = P.use_count();
}
=> 해당 make_shared 변수를 복사하게 되면 ( ex. auto P8 = P )
RefCount 가 증가하며 공유한다.
*주의사항
std::shared_ptr 생성 시, 주소를 넘겨주며 생성하는 것은
같은 주소를 가지는 객체를 만들지만 '제어블록을 따로 생성' 한다.
Player* NewP = new Player();
// std::shared_ptr<Player> NP2 = NP1 의 의미가 아니다.
// 그렇다고 NP1, NP2 가 각각 독립적인 shared_ptr 도 아니다.
std::shared_ptr<Player> NP1(NewP);
std::shared_ptr<Player> NP2(NewP);
=> std::shared_ptr<Player> NP2 = NP1 ( refcount++ ) 의 의미가 아니다.
( 제어블록을 따로 생성하기 때문. )
=> 그렇다고 NP1, NP2 가 각각 독립적인 shared_ptr 도 아니다.
( 같은 주소를 가리키기 때문. )
즉,
포인터가 같은 다른 제어블록의 shared_ptr.
결과적으로, 마지막에 삭제되는 객체로 인해 이미 삭제해버린 객체를 삭제.
=> 주소를 넘겨주며 shared_ptr 을 생성하지말자.
*enable_shared_from_this
=> 하지만 주소를 넘겨주며 shared_ptr 을 생성해야할 때가 존재.
'클래스에서 자기 자신의 shared_ptr 을 넘겨줄 때'
1. public std::enable_shared_from_this<T> 상속.
2. shared_from_this( ) 사용.
class Player : public std::enable_shared_from_this<Player>
void PrintData() { cout << *data << endl; }
std::shared_ptr<Player> GetSharedPT()
{
return shared_from_this();
}
Player p1;
auto sharedPT = p1.GetSharedPT();
sharedPT->PrintData();
=> 위 코드는 에러 why??
=> P1 을 shared_ptr 로 정의하지 않았기 때문.
shared_from_this ( ) 는 객체를 shared_ptr 로 만들지 않고
제어 블록 존재 여부만 판별하기 때문이다.
=> 아래와 같이 작성해야함.
shared_ptr<Player> p1 = make_shared<Player>();
auto sharedPT = p1->GetSharedPT();
sharedPT->PrintData();
*weak_ptr
*왜 shared_ptr 만으로는 부족할까 ??
서로를 shared_ptr 로 가리키고 있을 때, 순환 구조일 때, 메모리가 소멸이 되지 않음.
즉, 싸이클이 생성될 때, RefCnt = 0 이 될 수 없다.
즉, 이러한 순환구조가 발생할 수 있는 상황을 예방하는 방법으로
weak_ptr 을 쓰는 것.
=> shared_ptr 을 참조할 때, shared_ptr 의 referenceCount 를 증가시키지 않음.
대신, weak referenceCount 증가.
그 객체가 살아있는지 죽어있는지 간접적으로 판별할 수 있음.
따라서,
weak_ptr 을 쓸 때는 항상 객체가 살아있는지 아닌지 확인하는 작업 필요.
class Player : public std::enable_shared_from_this<Player>
{
int* hp = nullptr;
weak_ptr<Player> other;
public:
Player() : hp(new int(111)) { cout << "생성자" << endl; }
~Player() { if (hp) delete hp; }
Player(const Player& other)
{
*this = other;
cout << "복사생성자" << endl;
}
Player& operator=(const Player& other)
{
if (this == &other) return *this;
if (hp) { delete hp; }
hp = new int(*(other.hp));
return *this;
}
public:
void SetOther(weak_ptr<Player> _other)
{
other = _other;
}
void TakeDamage(int _damage) { *hp -= _damage; }
int GetCurrentHp() const { return *hp; }
void AttackOther(int _damage)
{
bool isAlive = !(other.expired());
auto other_sharedptr = other.lock();
if (other_sharedptr)
{
other_sharedptr->TakeDamage(_damage);
cout << _damage << " 만큼 맞았다.." << endl;
}
else
{
cout << "죽었다..ㅠ" << endl;
}
}
void PrintData() { cout << *hp << endl; }
std::shared_ptr<Player> GetSharedPT()
{
return shared_from_this();
}
};
expired() : 객체의 기한이 만료되었는지
lock() : 객체의 shared_ptr 반환
reset() : 객체를 empty 로 만든다.
int main()
{
shared_ptr<Player> p1 = make_shared<Player>();
shared_ptr<Player> p2 = make_shared<Player>();
p1->SetOther(p2);
p2->SetOther(p1);
p1->AttackOther(111);
if (p2->GetCurrentHp() <= 0)
{
p2 = nullptr;
//p2.reset();
}
p1->AttackOther(1111);
}
=> 약간의 추가 비용 발생.
( 객체 생사 여부 판별, shared_ptr 생성, 소멸 )
*weak_ptr 사용 시, 제어 블록 ( Control Block ) 제거 시점
=> 만약, weak_ptr 을 쓰게 되면 weak_refcount 를 따로 올려준다고 했다.
그로 인해 객체 소멸에 직접적인 영향을 미치지 않는다.
이 때,
weak_ptr 에서 객체가 소멸했는지 확인하기 위해선 제어 블록을 제거되어선 안된다.
때문에 위 코드에서,
p1->AttackOther(1111);
위 코드 진행 시점까지도 객체는 소멸되었지만 weak_refcount 가 남아있기에 아직 제어 블록은 살아있다.
따라서,
객체가 소멸했는지 확인하고 weak_ptr 을 reset() 으로 정리 가능.
이로 인해, shared_ptr<Player> p2 의 제어 블록까지 제거됨.
void AttackOther(int _damage)
{
bool isAlive = !(other.expired());
auto other_sharedptr = other.lock();
if (other_sharedptr)
{
other_sharedptr->TakeDamage(_damage);
cout << _damage << " 만큼 맞았다.." << endl;
}
else
{
cout << "죽었다..ㅠ" << endl;
other.reset(); //해당 부분 추가.
}
}
*unique_ptr
=> 복사(공유) 될 수 없는 포인터, 유일무이
=> std::move( ) 로 소유권 이전 가능.
unique_ptr<Player> p1 = make_unique<Player>();
//auto p2 = p1; // ERROR
p1 = nullptr;
=> unique_ptr 복사 생성자 delete.
unique_ptr<Player> p2 = std::move(p1);
=> 이동생성자를 이용한 객체 소유권 이동.
=> 실제 객체가 이동되어지는 것이 아님.
unique_ptr 이 p1 에서 p2 로 이동.
*함수의 매개변수
=> unique_ptr 을 함수의 매개변수로 넘겨주고 싶을 땐 어떻게 해야할까 ??
1. unique_ptr &
void PlayerPrintHp(unique_ptr<Player>& _p)
{
cout << _p->GetCurrentHp() << endl;
}
PlayerPrintHp(p2);
=> 해당 함수에서 unique_ptr 이 노출됨.
즉, 함수 내부에서
reset ( ) 을 수행할 수 있고
std::move 로 객체를 이동시켜버릴 수도 있고 swap 도 가능하게 됨.
2. raw pinter *
void PlayerPrintHp(Player* _p)
{
cout << _p->GetCurrentHp() << endl;
}
PlayerPrintHp(p2.get());
=> 해당 함수에서 _p 의 주소가 삭제될 수 있음.
참고한 사이트에서
위 2가지 경우 중 무엇이 더 나은 방안인지에 대해서 토론한 것을 읽어봤다.
어찌됐던 매개변수로 넘겨주는 목적이
해당 함수에서 원본 데이터로 접근하기 위함이라고 가정할 때,
내 생각은 2번 쪽이 더 좋은 것 같다.
두 가지 경우 원본 데이터로의 접근을 모두 충족한다.
하지만, 2번의 경우 객체의 삭제만 신경쓰면 되기 때문에
좀 더 위험의 여지가 좁지 않나 생각한다.
정 불편하다면 & 를 쓰는 것도 좋겠다.
void PlayerPrintHp(Player& _p)
{
cout << _p.GetCurrentHp() << endl;
}
PlayerPrintHp(*p2);
목적이 원본 데이터 접근이 아니라면
void PlayerPrintHp(const Player& _p)
결국, 틀리고 맞다 가 아니라
목적, 환경에 맞게 항상 적재적소에 잘 사용하는 것이 중요하겠다.
* unique_ptr<T> 컨테이너
=> unique_ptr<T> 를 가지고 있는 vector 를 만들어보자.
std::vector<unique_ptr<Player>> players;
unique_ptr<Player> p1 = make_unique<Player>();
players.push_back(std::move(p1));
players.push_back(make_unique<Player>(20));
players.emplace_back(make_unique<Player>(30));
players.emplace_back(new Player(50));
1. players.push_back(std::move(p1));
=> 기존의 unique_ptr 을 move 를 통해 vector 로 넣는다.
2. players.push_back(make_unique<Player>(20));
3. players.emplace_back(make_unique<Player>(30));
=> 위 2가지 경우, 과정은 비슷하다.
=> 두 문장의 차이점은 push_back / emplace_back 이다.
어셈블리로 봤을 때, 대략적인 과정은
처음 make_unique<player> 를 호출해 만들고
players 주소를 가져와 각각 push_back / emplace_back 실행한다.
흥미로운 것은 make_unique 을 사용해 push_back 했을 때,
emplace_back 함수로 넘어간다는 점이었다.
4. players.emplace_back(new Player(50));
=> 이 경우, 해당 객체를 만들고
make_unique<Player> = std::move() 과정과 동일하게 진행하고 있다.
*정리하면
1. make_unique<player> 과정 + std::move 과정 + push_back 함수 + emplace 함수
2. make_unique<player> 과정 + std::move 과정 + push_back 함수 + emplace 함수
3. make_unique<player> 과정 + std::move 과정 + emplace 함수
4. new Player 과정 + std::move 과정 + emplace 함수
=> 4번 과정이 제일 빠르다고 할 수 있다.
players.emplace_back(new Player(50));
*결론
스마트 포인터의 활용 방법을 재대로 알고 적절히 사용한다면
메모리 실수 여지와 관리에 대해서 에너지 낭비가 없을 것이다.
참고 링크
https://modoocode.com/229 ( unique_ptr )
https://modoocode.com/252 ( shared_ptr, weak_ptr )
본 내용은 인프런의 루키스님 강의를 듣고 정리한 내용입니다.
'프로그래밍 > C++' 카테고리의 다른 글
@.NET Framework (0) | 2022.08.27 |
---|---|
#68. 람다 (lambda) (0) | 2022.07.22 |
#67. 전달 참조 ( forwarding reference ) (0) | 2022.07.22 |
#66. 오른값참조 ( rvalue reference ) (0) | 2022.07.22 |
#65. enum class & override & final (0) | 2022.07.22 |