[알게 된 것] - circular reference(순환 참조) 문제
java나 c# 같은 언어는 더 이상 접근할 방법이 없는 메모리에 대해 GC가 알아서 해당 메모리를 해제(관리)한다.
C, C++같은 경우 GC가 없기 때문에 동적으로 할당한 메모리에 대해 직접 delete를 이용하여 해제해야한다.
C++의 shared_ptr과 같은 스마트 포인터는 선언과 동시에 생성 후 referece counting을 이용하여
자동으로 메모리를 해제하는 특징을 지녔다.
여기서 reference counting이란 클래스의 생명주기 관리를 위해 클래스 객체를 참조하고 있는 포인터가 없는 경우 자동으로 참조하였던 객체를 delete시키는 기법이다. 물론 자바를 배울땐 이런 객체를 GC가 알아서 눈치채고 소멸시키기 때문에 신경쓸 이유가 없었지만 c++은 그런거 없으니 알아서 처리해야한다.
스마트 포인터를 사용하는 이유는 여기에 있다. 참조중인 객체를 가리키는 포인터가 사라지면 자동으로 객체를 delete 해주기 때문에 매우 편리하다.
- std::shared_ptr
스마트 포인터를 사용한다고 하면 shared_ptr을 보통 사용한다.
#include <iostream>
int main()
{
std::shared_ptr<int> sPtr = std::make_shared<int>(5);
std::cout << "\nsPtr reference count : " << sPtr.use_count() << std::endl;
// scope 탈출 시 reference count 1감소
{
std::shared_ptr<int> sPtr2 = sPtr;
std::cout << "\nsPtr reference count : " << sPtr.use_count() << std::endl;
std::cout << "\nsPtr2 reference count : " << sPtr2.use_count() << std::endl;
}
std::cout << "\nsPtr reference count : " << sPtr.use_count() << std::endl;
return 0;
}
실행 결과
sPtr reference count : 1
sPtr reference count : 2
sPtr2 reference count : 2
sPtr reference count : 1
기본 int형을 가리키는 스마트 포인터 sPtr을 생성후 scope내에서 sPtr2를 생성하여 해당 변수를 가리키게 하였다. 이때 use_count를 이용하여 reference count를 확인하였을 때는 2가 나오는 것을 알 수 있다.
이후 scope를 벗어나서 sPtr2가 사라지자 reference count가 다시 1로 내려가는 것 또한 알 수 있다.
int형이 아닌 클래스 객체를 따로 넣어서 다시 실험해보자.
#include <iostream>
class test
{
public:
test() {
std::cout << "\ncall constructor\n" << std::endl;
}
~test() {
std::cout << "\ncall destructor\n" << std::endl;
}
};
int main()
{
std::shared_ptr<test> sPtr = std::make_shared<test>(); // 생성자 호출
std::cout << "\nsPtr reference count : " << sPtr.use_count() << std::endl;
// scope 탈출 시 reference count 1감소
{
std::shared_ptr<test> sPtr2 = sPtr;
std::cout << "\nsPtr reference count : " << sPtr.use_count() << std::endl;
std::cout << "\nsPtr2 reference count : " << sPtr2.use_count() << std::endl;
}
// 아직 sPtr2가 참조하고 있으므로 객체가 소멸되지 않음
std::cout << "\nsPtr reference count : " << sPtr.use_count() << std::endl;
return 0;
}
실행 결과
call constructor
sPtr reference count : 1
sPtr reference count : 2
sPtr2 reference count : 2
sPtr reference count : 1
call destructor
객체를 생성하면서 그것을 스마트 포인터로 참조하게끔 한 후, 프로그램이 완전히 종료되면서 reference count가 0이 된 후 자동적으로 객체의 destructor가 호출 되는 것을 알 수 있다.
- circular reference(순환 참조) 문제
메모리 관리에 많은 도움이 되는 shared_ptr이지만 구조적으로 문제가 하나 존재한다. 클래스 내부적으로 다른 클래스를 참조 하게끔 되어있고, 필수불가결하게 클래스와 클래스가 서로를 참조하게 되는 상황을 circular reference라고 한다.
클래스 내부 멤버로 다른 클래스를 참조하고 있는 경우 프로그램이 종료 되어도 서로의 메모리는 해제 되지 않는다.
코드를 수정해서 다시 확인해보자.
#include <iostream>
class test
{
public:
test() {
std::cout << "\ncall constructor\n" << std::endl;
}
~test() {
std::cout << "\ncall destructor\n" << std::endl;
}
public:
void setPartner(std::shared_ptr<test> & t) {
this->partner = t;
}
private:
std::shared_ptr<test> partner;
};
int main()
{
std::shared_ptr<test> sPtr = std::make_shared<test>();
std::shared_ptr<test> sPtr2 = std::make_shared<test>();
// 순환 참조 발생
sPtr->setPartner(sPtr2);
sPtr2->setPartner(sPtr);
return 0;
}
실행 결과
call constructor
call constructor
이번엔 두개의 스마트 포인터를 선언하였고 서로를 자신의 파트너를 참조하도록 하였다. 이런 경우 순환 참조 문제가 발생하여 destructor가 호출 되지 않았고 결론적으로 할당된 메모리가 해제되지 않은 것을 확인할 수 있다. 이와 같이 순환 참조 문제는 메모리 누수를 발생시킨다.
circular reference문제를 해결하기 위해 std::weak_ptr을 사용한다. weak_ptr은 shared_ptr과 같이 다른 객체를 참조할 수 있지만 클래스의 생명주기에 영향을 주는 reference count를 증가시키지 않기 때문에 순환 문제를 해결 할 수 있다.
weak_ptr을 사용하여 코드를 수정하자.
#include <iostream>
class test
{
public:
test() {
std::cout << "\ncall constructor\n" << std::endl;
}
~test() {
std::cout << "\ncall destructor\n" << std::endl;
}
public:
void setPartner(std::shared_ptr<test> & t) {
this->partner = t;
}
private:
// std::shared_ptr<test> partner;
std::weak_ptr<test> partner;
};
int main()
{
std::shared_ptr<test> sPtr = std::make_shared<test>();
std::shared_ptr<test> sPtr2 = std::make_shared<test>();
// 순환 참조 발생
sPtr->setPartner(sPtr2);
sPtr2->setPartner(sPtr);
return 0;
}
실행 결과
call constructor
call constructor
call destructor
call destructor
weak_ptr을 사용하자 프로그램 종료 시 정상적으로 destructor가 호출되었고 결과적으로 메모리가 해제되었음을 확인 할 수 있다. 참고로 weak_ptr의 경우 shared_ptr과 달리 직접적으로 참조하여 객체의 값을 얻어오거나 할 수 없으며 weak_ptr을 이용하여 참조하는 객체를 얻어오고 싶은 경우 lock() 메소드를 호출하여 shared_ptr로 변환한 후 참조해야 한다.
이 이외의 기능은 shared_ptr과 동일하게 동작한다.