프로그래밍/C++

#69. c++ 스마트 포인터 ( smart pointer )

코딩하는상후니 2022. 7. 23. 23:06

 


 

*스마트 포인터는 왜 필요할까??

 
=> 포인터 사본을 만들지 않도록 하는 것이 좋다.
=> 객체 생명주기로 주소값을 가지고 있는 객체들을 먼저 처리하고 참조되어있는 객체를 마지막으로 처리하는 것이 좋다.
=> 항상 실수 가능성이 있다는 것이 문제.
 
 

 

 

*댕글링포인터 ( 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/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