사용이 끝난 자원은 반드시 반환을 해서 다른 작업 때 사용할 수 있도록 해야 합니다. 메모리를 할당만 하고 해제를 하지 않는다면, 결국 메모리 부족으로 프로그램이 crash 될 수도 있음.
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 *pa
를 안해서 생성된 객체의 주소값을 가지는 포인터는 메모리 상에 존재하지 않게 됨.
또한, 아래와 같이 thrower()로 발생된 예외로 delete pa가 실행되지 않고 넘어가면 메모리 누수가 생기게 됨.
void thrower() {
// 예외를 발생시킴!
throw 1;
}
void do_something() {
A *pa = new A(); thrower();
// 발생된 예외로 인해 delete pa 가 호출되지 않는다!
delete pa;
}
RAII 패턴: 자원의 획득은 초기화다 - Resource Acquisition Is Initialization
자원의 관리를 스택에 할당한 객체를 통해 수행하는 것
stack unwinding : 예외가 발생해서 함수를 빠져나가도, 그 함수의 스택에 정의되어 있는 모든 객체들은 빠짐없이 소멸자가 호출된다. -> 예외가 발생하지 않으면 함수가 종료될 때 당연히 소멸자들이 호출됨.
이 개념을 이용해서, 포인터 객체라는 걸 도입하면, 함수가 종료되며 쌓인 객체 스택에서 소멸자들이 차례로 불리면서 포인터 자체와 포인터가 가리키는 객체 자체도 소멸될 수 있다(포인터 객체 내 소멸자에 가리키는 객체를 소멸하는 명령어 존재).
C++에서 메모리를 잘못된 방식으로 관리하는 경우 다음 두가지 종류의 문제점이 발생함
2번째 문제점인 double free bug 는 어떤 포인터에 객체의 소유권을 유일하게 부여해서 해결 가능.
-> unique_ptr
: 특정 객체에 유일한 소유권을 부여하는 포인터 객체
++ unique_ptr로 pa를 스택에 정의된 객체로 만들기 때문에, RAII 패턴 역시 자동 사용 가능.
std::unique_ptr<A> pa(new A());
pa->some();
// 아래와 동일
A* pa = new A();
unique_ptr은 -> 을 오버로드해서 마치 포인터를 다루는 것과 같이 사용할 수 있게 함.
#include <iostream>
class A {
public:
A(int a){};
A(const A& a) = delete;
// 복사 생성자를 호출하는 부분에서 오류가 발생함
};
int main() {
A a(3); // 가능
A b(a); // 불가능 (복사 생성자는 삭제됨)
}
컴파일하게 되면 복사 생성자를 호출하는 부분에서 오류가 발생함.
-> 복사 생성자를 명시적으로 삭제했기 때문
이와 같이, unique_ptr도 복사 생성자가 명시적으로 삭제됨.
-> unique_ptr는 어떠한 객체를 유일하게 소유해야 하기 때문!
unique_ptr를 복사 생성할 수 있다면, 특정 객체를 여러 unique_ptr들이 소유하게 되는 문제가 발생함.
각각의 unique_ptr들이 소멸될 때 전부 객체를 delete하려고 하기 때문에 double free bug가 발생함.
소유권은 이전이 가능: 이동 생성자 사용
#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(); }
unique_ptr은 복사 생성자는 정의되어 있지 않지만, 이동 생성자는 가능하다.
마치 소유권을 이동시킨다라는 개념으로 생각하면 됨.
소유권 이동 이후 기존 unique_ptr을 접근하지 않도록 조심해야 함.
unique_ptr를 함수 인자로 전달하고 싶다면: unique_ptr는 복사 생성자가 없음.
함수에 레퍼런스로 전달하면?
-> 소유권이라는 의미가 사라지게 됨...
void do_something(A* ptr) { ptr->do_sth(3); }
int main() {
std::unique_ptr<A> pa(new A());
do_something(pa.get());
// 실제 객체의 주소값을 리턴해 줌: 일반적인 포인터를 받을 수 있음.
}
소유권이라는 의미는 버린 채, do_something 함수 내부에서 객체로 접근 권한을 주는 것.
std::unique_ptr<Foo> ptr(new Foo(3, 5));
unique_ptr로 완벽한 전달을 수행함.
기본적으로 vector의 push_back 함수는 전달된 인자를 복사해서 집어넣기 때문에 컴파일 때 에러가 발생하게 됨.
#include <iostream>
#include <memory>
#include <vector>
class A {
int *data;
public:
A(int i) {
std::cout << "자원을 획득함!" << std::endl; data = new int[100];
data[0] = i;
}
void some() {
std::cout << "일반 포인터와 동일하게 사용가능!" << std::endl;
}
~A() {
std::cout << "자원을 해제함!" << std::endl;
delete[] data;
}
};
int main() {
std::vector<std::unique_ptr<A>> vec;
std::unique_ptr<A> pa(new A(1));
vec.push_back(pa); // ??
}
따라서 아래와 같이 push_back을 사용할 때 move로 명시적으로 이동 생성자 이용!
int main() {
std::vector<std::unique_ptr<A>> vec;
std::unique_ptr<A> pa(new A(1));
vec.push_back(std::move(pa)); // 잘 실행됨
}
emplace_back 함수를 이용하면 vector 안에 unique_ptr를 직접 생성해서 넣을 수 있음.
특정 자원을 몇 개의 객체에서 가리키는지를 추적한 다음에, 그 수가 0 이 되야만 비로소 해제를 시켜주는 방식의 포인터
객체를 소유하는 unique_ptr와는 다르게, shared_ptr로 객체로 가리킬 경우, 다른 shared_ptr 역시 그 객체를 가리킬 수 있음.
참조 개수가 0이 되어야 가리키고 있는 객체를 해제할 수 있음.
현재 shared_ptr 의 참조 개수가 몇 개 인지는 use_count 함수를 통해 알 수 있음.
std::shared_ptr<A> p1(new A());
std::shared_ptr<A> p2(p1); // p2 역시 생성된 객체 A 를 가리킨다.
std::cout << p1.use_count(); // 2
std::cout << p2.use_count(); // 2
개개의 shared_ptr들은 참조 개수가 몇개인 지 알고 있어야 함.
그런데 아래와 같은 경우 p2의 참조 개수는 증가시켜도 p1에 저장된 참조 개수는 건드릴 수 없음.
std::shared_ptr<A> p3(p2);
-> 이런 문제점 때문에
실제 객체를 가리키는 shared_ptr가 제어 블록(control block)을 동적으로 할당하고 shared_ptr들이 제어 블록에 필요한 정보를 공유하는 방식으로 구현됨.
shared_ptr는 복사 생성할 때마다 해당 제어 블록의 위치만 공유하고, shared_ptr가 소멸할 때마다 제어 블록의 참조 개수를 하나 줄이고, 생성 때마다 하나 늘리는 방식으로 작동함.
std::shared_ptr<A> p1(new A());
위 생성 방법으로는 동적 할당이 2번 발생
동적 할당은 아예 두개 합친 크기로 한번 할당하는 게 훨씬 빠름 -> 아래 참고
std::shared_ptr<A> p1 = std::make_shared<A>();
shared_ptr은 인자로 주소값이 전달되면,
마치 본인이 해당 객체를 첫번째로 소유하는 shared_ptr인 것처럼 행동함.
A* a = new A();
std::shared_ptr<A> pa1(a);
std::shared_ptr<A> pa2(a);
따라서 shared_ptr를 주소값을 통해 생성하는 걸 지양해야 함.
(이미 해제한 메모리를 또 해제한다는 오류가 날 수 있음)
또한, 순환 참조 문제가 발생할 수 있음(락).
위에서 말한 순환 참조 문제를 해결하기 위해 weak_ptr를 사용