heap
과 stack
프로그램이 운영체제로부터 할당받아 갖고 있는 메모리 공간에는 '힙 영역' 이라고 하는 것과 '스택 영역' 이라고 하는 것이 있다.
스택 영역은 함수 호출 시 생성되는 지역 변수와 매개변수가 저장되는 영역이다. 컴파일 타임
에 그 크기가 정해지며 프로그램에서 자동으로 관리해 준다. 함수에 필요한 지역 변수, 매개변수 등은 모두 정적인 데이터들이므로 스택 영역에는 '정적 데이터'
가 할당되고 해제된다. 또한 스택 영역은 그 이름답게 나중에 할당된 데이터가 먼저 해제되는 후입선출 (Last in First out, LIFO
) 방식으로 동작한다.
힙 영역은 프로그램이 동작하면서 동적으로 할당되고 해제되는
데이터들의 공간이다. C++에서는 new
, delete
등의 키워드를 통해 동적으로 메모리 공간을 할당하고 할당 해제할 수 있다. 또한, 프로그램이 동작하며 동적 할당 부분을 만날 때마다 할당되는 메모리 영역이므로 런타임
에 그 크기가 정해진다. 이 공간의 메모리 할당 및 해제의 책임은 사용자에게 있다. 또한 먼저 할당된 메모리가 먼저 해제되는 선입선출 (First in First out, FIFO
) 방식으로 동작한다.
garbage collection
Java에서는 garbage collector를 이용하여 garbage collection를 수행한다.
garbage collection이란, 프로그램에서 더 이상 사용되지 않는 메모리 공간을 자동으로 할당 해제하여 의미 없이 메모리 공간을 차지하고 있는 데이터를 막아 주는 기능이다.
만약 사용자가 동적 할당으로 메모리를 할당 받아서 사용한 이후 해제를 해 주지 않았을 때, 가비지 컬렉터가 없다면 메모리 누수
가 발생할 것이다.
C에는 이러한 가비지 컬렉터가 없어 사용자가 일일이 메모리 할당을 해제해 주어야 하지만, Java에서는 가비지 컬렉터가 자동으로 메모리를 할당 해제해 주기 때문에 사용자는 큰 스트레스 없이 개발에만 집중할 수 있게 된다.
이러한 가비지 콜렉팅 작업은 root로부터 그래프 순회를 통해 도달할 수 없는 객체를 찾아 이를 할당 해제해 주는 방식으로 실행된다.
C++에서는 메모리 누수(memory leak)로부터 프로그램의 안전성을 보장하기 위해 스마트 포인터
를 제공하고 있다.
C++에서의 스마트 포인터들은 모두 std
네임스페이스에서 사용할 수 있다.
unique_ptr
특정 객체를 하나의 스마트 포인터만이 가리킬 수 있게 하는 포인터가 unique pointer이다.
즉 어떤 unique 포인터가 A라는 객체를 가리키고 있으면, 다른 포인터에서 A를 가리킬 수 없게 하는 것이다. unique 포인터 하나가 한 객체를 온전히, 독점하여 가리킨다고 하여 객체에 '소유권 개념을 도입했다' 라고도 표현한다.
unique 포인터가 소멸되면 포인터가 가리키고 있던 객체 또한 소멸된다.
unique_ptr<A> ptr1(new A());
ptr1->someFunc();
또한 하나의 객체는 오직 하나의 unique 포인터에 의해서만 가리켜지므로 복사 생성자는 존재하지 않는다. 하지만 객체의 소유권의 이전은 move
함수를 사용하면 가능하다.
class A
{
public:
A()
{
cout << "메모리 자원 획득\n";
}
void someFunc()
{
cout << "출력\n";
}
~A()
{
cout << "메모리 자원 할당 해제\n";
}
};
int main()
{
unique_ptr<A> ptr1(new A());
ptr1->someFunc();
unique_ptr<A> ptr2 = move(ptr1);
ptr2->someFunc();
}
메모리 자원 획득
출력
출력
메모리 자원 할당 해제
A
객체의 소유권을 ptr1에서 ptr2로 넘겨받아서 일반 포인터처럼 사용할 수 있다. ptr1은 가리키는 객체가 사라지게 되며 널 포인터가 된다.
소유권이 이전된 unique_ptr를 댕글링 포인터(dangling pointer)
라고 하며 이를 재 참조할 시에 런타임 오류가 발생한다. 따라서 소유권 이전은, 댕글링 포인터를 절대 다시 참조하지 않겠다는 확신 하에 이루어져야 한다.
그렇다면 unique 포인터를 함수의 인자로 전달하고자 하면 어떨까.
그냥 포인터를 그대로 전달하면, 함수 내부에 복사 생성된 포인터가 생기게 되어 같은 객체를 가리킬 수 있게 된다. 물론 이 포인터는 함수가 종료되면 사라지겠으나 어쨌든 함수 내에서 unique 포인터가 가리키고 있는 객체를 가리킬 수 있다는 점에서 unique 포인터의 정의에 위배된다.
void some(unique_ptr<A> ptr)
{
ptr->someFunc();
}
int main()
{
unique_ptr<A> ptr1(new A());
some(ptr1);
}
이것에 대한 해결 방안은 생각보다 단순한데, 그냥 원래의 포인터 주소값을 전달해주면 된다. 그리고 포인터의 주소값을 얻는 것은 get()
함수로써 가능하다.
void some(A* ptr)
{
ptr->someFunc();
}
int main()
{
unique_ptr<A> ptr1(new A());
some(ptr1.get());
}
get
함수는 실제 객체의 주소값을 반환한다. 그리고 some
함수는 일반적인 포인터를 인자로 가진다.
만약 다른 함수에서 unique_ptr 가 소유한 객체에 일시적으로 접근하고 싶다면, get()
을 통해 해당 객체의 포인터를 전달하면 된다.
make_unique
로 간편하게 생성하기C++ 14부터는 간단히 make_unique
를 사용하여 unique ptr을 만들 수 있다. 두 문장은 같다.
auto ptr = std::make_unique<Foo>(3, 5);
std::unique_ptr<Foo> ptr(new Foo(3, 5));
reset()
멤버 함수를 이용하면 포인터가 가리키고 있는 객체의 메모리 할당을 해제할 수 있다.
unique_ptr<int> ptr(new int(5));
ptr.reset();
shared_ptr
shared_ptr
은 어떤 객체를 참조하는 스마트 포인터가 총 몇개인지를 참조하고 있다. 이렇게 참조하고 있는 스마트 포인터의 개수를 참조 횟수(reference count)라고 한다.
이 참조 횟수가 0이 되면 더 이상 이 객체를 참조하고 있는 포인터가 없다는 뜻이 되므로 메모리를 자동으로 해제한다.
이 참조 횟수는 use_count()
멤버 함수를 통해 확인할 수 있다.
shared_ptr<int> ptr1(new int(5));
cout << ptr1.use_count() << "\n";
auto ptr2(ptr1);
cout << ptr1.use_count() << "\n";
auto ptr3(ptr1);
cout << ptr1.use_count() << "\n";
1
2
3
make_shared
unique ptr과 마찬가지로 make_shared
함수를 이용해서 인스턴스를 안전하게 만들 수 있다.
weak_ptr
weak_ptr은 하나 이상의 shared_ptr 인스턴스가 소유하는 객체에 대한 접근을 제공하지만, 소유자의 수에는 포함되지 않는 스마트 포인터이다.
weak_ptr은 shard_ptr 인스턴스 간의 순환 참조를 제거하기 위해서 사용된다.
가령 두 객체가 상대방을 가리키는 shared_ptr를 가지고 있다면, 참조 횟수는 절대 0이 되지 않으므로 메모리는 영원히 해제되지 않는다. 이러한 상황을 순환 참조라고 한다.
https://www.tcpschool.com/cpp/cpp_template_smartPointer
https://min-zero.tistory.com/entry/C-STL-1-3-%ED%85%9C%ED%94%8C%EB%A6%BF-%EC%8A%A4%EB%A7%88%ED%8A%B8-%ED%8F%AC%EC%9D%B8%ED%84%B0smart-pointer