똑똑한 포인터
이름에서 알려주다시피 스마트포인터는 일반 포인터보다 똑똑하다. 스마트 포인터는 어떤 점을 개선하기 위해 나온 걸까?
메모리 오염
메모리오염은 할당되지 않는 메모리를 사용하는 것을 말한다. 주로 할당해제를 한 후에도 그 메모리를 가리키는 포인터를 사용할 때 일어난다.
이게 그렇게 심각한 건가?라고 생각이 들 수 있다. 오염시킨 그 공간이 비어있는 상태라면 괜찮지만,
만약 다른 객체들이 할당받았던 상태라면?? 갑자기 다른 객체의 값이 바뀔 수 있는 것이다.
이것을 방지하기 위해 똑똑한 스마트 포인터가 개발 됐다.
스마트 포인터의 구동 방식
스마트 포인터는 3가지가 존재한다.
- shared_pointer
- unique_pointer
- weak_pointer
각각 동작 방법은 모두 다르지만, 메모리 오염을 막는 데에 힘쓰고 있다. 이제 각각의 스마트포인터는 어떻게 동작하는지 알아보자.
shared_pointer
shared_pointer는 자신을 가리키고 있는 포인터 변수의 수를 카운팅 하여, 카운트가 0이 되기 전까진 메모리를 해제하지 않는다.
shared_pointer는 생성 당시 자기 자신이 참조하고 있으므로 Reference Counting은 1부터 시작한다.
가리키면 1이 증가되고 가리키던 포인터가 없어지면 1이 감소한다.
예시 코드
class Family {
public:
shared_ptr<Family> neighbor;
};
int main() {
shared_ptr<Family> f1 = make_shared<Family>();
{
shared_ptr<Family> f2 = make_shared<Family>();
f1->neighbor = f2;
}
.....
shared_pointer는 make_shared라는 함수로 생성가능하다.
원래라면 f2는 코드블록의 범위를 나갈 때 해제되어야 하지만, f1이 자신을 reference 하고 있어서 해제되지 않고 대기하게 된다.
주의점
reference counting을 하는 shared_pointer는 lock의 경우와 같이 순환구조가 되는 것을 주의해야 한다.
class Family {
public:
shared_ptr<Family> neighbor;
};
int main() {
shared_ptr<Family> f1 = make_shared<Family>();
shared_ptr<Family> f2 = make_shared<Family>();
f1->neighbor = f2;
f2->neighbor = f1;
.....
이 코드를 보면 f1과 f2가 서로를 가리키고 있어 reference count가 절대 0에 도달할 수 없게 됐다.
unique_pointer
unique_pointer는 딱 한 포인터만 가리킬 수 있도록 하여 메모리 오염을 방지하는 방법을 사용한다.
다른 포인터가 가리키려고 하면 컴파일 에러가 발생한다.
예시 코드
int main() {
unique_ptr<Family> f1 = make_unique<Family>();
unique_ptr<Family> f2 = f1; // 컴파일 에러
}
정말 간단한 코드다. unique_pointer는 make_unique라는 함수로 생성할 수 있다.
옮기기
unique_pointer에 있는 것을 다른 포인터로 옮기는 법은 정말 없는 걸까?? 당연히도 있다. 오른 값을 이용해 주면 된다.
오른 값을 간단히 설명하자면, 코드의 한 줄을 넘어가면 존재하지 않는, 사용하지 않는 값을 말한다. (ex. 상수)
오른 값으로 만들어주면 이제 존재하지 않기때문에 다른 변수로 옮겨줄 수 있다.
int main() {
unique_ptr<Family> f1 = make_unique<Family>();
unique_ptr<Family> f2 = std::move(f1);
}
move함수를 통해 오른값으로 전환할 수 있다.
weak_pointer
weak_pointer는 shared_pointer처럼 직접 생명주기에 관여하지 않는다. 또한 직접적으로 포인터를 할당받을 수 없다.
이게 무슨 말일까?
weak_pointer는 shared_pointer를 통해 사용할 수 있다. 그것도 shared_pointer의 referenceCounting엔 영향을 주지 않으면서!
예시 코드
weak_pointer는 코드를 보며 이해하는 것이 더 효과적이니, 예시 코드를 보며 이해해 보자.
int main() {
weak_ptr<Family> w_ptr;
{
shared_ptr<Family> f1 = make_shared<Family>();
w_ptr = f1;
}
if (w_ptr.expired()) {
cout << "f1 메모리 해제됨";
}
else {
shared_ptr<Family> f = w_ptr.lock();
// 사용
}
}
중간에 생성되는 f1 포인터는 코드 블록을 나가면 사라진다. 또한 w_ptr는 shared_pointer인 f1의 포인터를 받아 사용되고 있다. 그것을 생각하며 다음 설명을 보자.
현재 위 코드에서 나오는 함수는 2가지이다.
- expired()
- lock()
expired는 현재 weak_pointer(참조 중인 shared_pointer)가 해제되었다면 true를 반환하는 함수다. 이것으로 weak_pointer가 현재 유효한지 체크할 수 있다.
lock함수는 사용 시 shared_pointer를 반환하여 사용한다.
정리
weak_pointer를 쉽게 정리하자면 현재 포인터가 존재하는지 체크하고, shared_pointer로 반환하여 사용한다!라고 말할 수 있을 것 같다.
마무리
스마트 포인터 3가지는 모두 메모리오염을 막기위해 개발됐지만, 막는 방법이 모두 다르다. 상황에 맞춰 사용한다면 더욱 좋은 프로그램을 제작할 수 있을 것 같다.
'C++' 카테고리의 다른 글
람다(lamda) 표현식을 이해해보자 (6) | 2023.10.19 |
---|