TL;DR
이 글은 씹어먹는 C++ 13-1. 객체의 소유권 글을 읽고 정리 및 요약한 글입니다.
자원 관리의 중요성
사용이 끝난 자원은 반드시 반환해서 다른 작업에서 사용할 수 있게 해야 한다. 메모리를 할당만 하고 해제를 하지 않는다면 메모리 부족으로 crash될 수 있다.
일반적인 GC의 역할은 프로그램 상에서 더이상 쓰이지 않는 자원을 자동으로 해제해주는 역할을 한다. 하지만 C++의 경우 한번 획득학 자원은 직접 해제하지 않는 이상 프로그램이 종료하기 전까지 남아있게 된다.(프로그램이 종료되면 운영체제가 해제해준다)
#include <iostream>
class A {
int *data;
public:
A() {
data = new int[100];
std::cout << "자원을 획득함!" << std::endl;
}
~A() {
std::cout << "소멸자 호출!" << std::endl;
delete[] data;
}
};
void do_something() { A *pa = new A(); }
int main() {
do_something();
// 할당된 객체가 소멸되지 않음!
// 즉, 400 바이트 (4 * 100) 만큼의 메모리 누수 발생
}
자원을 획득만 하고 소멸자를 호출하지 않았기 때문에 메모리 누수가 발생한다. delete를 해주지 않는다면 heap 어딘가에 A가 남아 있지만 do_something이 종료되면서 pa가 소멸되었기 때문에 메모리가 해제되지 못한채 heap에 남아있게 된다.
RAII(Resource Acquisition Is Initialization, 자원의 획득은 초기화다)
예외가 발생해 함수가 빠져나가더라도 함수의 스택에 정의되어 있는 모든 객체들은 모두 소멸자가 호출된다. 예외가 발생하지 않으면 함수가 종료될 때 소멸자가 호출된다.
이 소멸자 안에 다 사용한 자원을 해체하는 루틴을 넣는다면 소멸자가 자동으로 호출된다.
위의 코드에서 pa는 객체가 아니기 때문에 소멸자가 호출되지 않는다. 대신 pa를 포인터 객체로 만들어서 자신이 소멸될 때 자신이 가리키고 있는 데이터를 같이 delete할 수 있다.
이 포인터 객체를 스마트 포인터라고 부른다. C++에서는 unique_ptr과 shared_ptr을 제공한다.
unique_ptr
C++에서 메모리를 잘못된 방식으로 관리하였을 경우 두가지 종류의 문제점이 발생할 수 있다. 하나는 메모리 누수이다. 장시간 작동하는 프로그램의 경우 사용하는 메모리의 양이 점점 늘어나서 나중에 시스템 메모리가 부족해지며 서버가 종료되는 현상이 생길 수 있다. 두번째는 이미 해제된 메모리를 다시 참조하는 경우이다.
Data* data = new Data();
Date* data2 = data;
// data 의 입장 : 사용 다 했으니 소멸시켜야지.
delete data;
// ...
// data2 의 입장 : 나도 사용 다 했으니 소멸시켜야지
delete data2;
data2에서 이미 해제된 메모리를 다시 소멸시키려고 할 경우 메모리 오류가 나면서 프로그램이 죽는다. 소멸된 객체를 다시 소멸시켜서 발생하는 버그를 double free 버그라고 한다.
만약에 어떤 포인터에 객체의 유일한 소유권을 부여해서 이 포인터 말고는 객체를 소멸시킬 수 없다 라고 한다면?
C++에서는 이렇게 특정 객체에 유일한 소유권을 부여하는 포인터 객체를 unique_ptr이라고 한다.
#include <iostream>
#include <memory>
class A {
int *data;
public:
A() {
std::cout << "자원을 획득함!" << std::endl;
data = new int[100];
}
void some() { std::cout << "일반 포인터와 동일하게 사용가능!" << std::endl; }
~A() {
std::cout << "자원을 해제함!" << std::endl;
delete[] data;
}
};
void do_something() {
std::unique_ptr<A> pa(new A());
pa->some();
}
int main() { do_something(); }
unique_ptr을 정의하기 위해서는 템플릿에 인자로 포인터가 가리킬 클래스를 전달하면 된다. 또한 unique_ptr은 do_something()이 종료될 때 자동으로 소멸자가 호출된다. 만약 unique_ptr을 복사하려고 한다면 컴파일 오류가 생기게 된다. 이유는 복사 생성자를 명시적으로 삭제하였기 때문이다.
unique_ptr 소유권 이전하기
unique_ptr은 복사는 불가능하지만 소유권 이전은 가능하다.
#include <iostream>
#include <memory>
class A {
int *data;
public:
A() {
std::cout << "자원을 획득함!" << std::endl;
data = new int[100];
}
void some() { std::cout << "일반 포인터와 동일하게 사용가능!" << std::endl; }
~A() {
std::cout << "자원을 해제함!" << std::endl;
delete[] data;
}
};
void do_something() {
std::unique_ptr<A> pa(new A());
std::cout << "pa : ";
pa->some();
// pb 에 소유권을 이전.
std::unique_ptr<A> pb = std::move(pa);
std::cout << "pb : ";
pb->some();
}
int main() { do_something(); }
여기서 std::move를 통해 소유권 이전을 할 수 있다. 그리고 pa가 가리키고 있는 주소값을 확인해보면 nullptr이 나온다. 따라서 소유권을 이전시킨 후 기존의 unique_ptr을 접근하지 않아야 한다.
unique_ptr을 인자로 전달하기
#include <iostream>
#include <memory>
class A {
int* data;
public:
A() {
std::cout << "자원을 획득함!" << std::endl;
data = new int[100];
}
void some() { std::cout << "일반 포인터와 동일하게 사용가능!" << std::endl; }
void do_sth(int a) {
std::cout << "무언가를 한다!" << std::endl;
data[0] = a;
}
~A() {
std::cout << "자원을 해제함!" << std::endl;
delete[] data;
}
};
// 올바르지 않은 전달 방식
void do_something(std::unique_ptr<A>& ptr) { ptr->do_sth(3); }
int main() {
std::unique_ptr<A> pa(new A());
do_something(pa);
}
함수에 레퍼런스를 전달하면 do_something 내에서 ptr은 더이상 유일한 소유권을 의미하지 않는다. 함수에 unique_ptr을 전달하려면 원래의 포인터 주소값을 전달하면 된다.
unique_ptr 쉽게 생성하기
C++14부터 make_unique 함수를 제공한다.
auto ptr = std::make_unique<Foo>(3, 5);
기존의 std::unique_ptr<Foo> ptr(new Foo(3, 5));와 다르게 좀더 편리하게 unique_ptr을 만들 수 있다.
댓글
Discussion 원문