자원 확보는 초기화다라는 뜻으로,
C++에서 주로 사용되는 프로그래밍 기법으로, 자원의 생명주기를 객체의 생명주기와 일치시켜 자원 관리를 자동화하는 방식입니다.
이렇게 하면 객체가 생성될 때 자동으로 자원을 얻고, 객체가 소멸될 때 (범위를 벗어나거나 delete 될 때) 자동으로 자원을 반환하게 됩니다.
RAII와 Scope
RAII 기법을 사용하는 객체는 주로 스택(stack)에 할당됩니다. 스택에 할당된 변수나 객체는 해당 변수가 선언된 범위(scope)를 벗어나면 자동으로 소멸됩니다.
이것이 중요한 이유는 다음과 같습니다.
자원 누수 방지: 함수가 정상적으로 끝나는 경우뿐만 아니라, 함수 중간에 return 문을 만나거나 예외(exception)가 발생하여 함수가 비정상적으로 종료되는 경우에도 스택에 있는 객체는 소멸됩니다. 이 과정에서 객체의 소멸자가 자동으로 호출되어 자원을 안전하게 해제할 수 있습니다.
간결한 코드: 개발자가 일일이 close(), free()와 같은 자원 해제 코드를 작성할 필요가 없습니다. 자원 관리를 컴파일러와 런타임 환경에 맡기게 되어 코드가 훨씬 간결하고 오류가 줄어듭니다.
RAII 객체의 생성과 소멸에 미세한 오버헤드가 발생할 수 있지만, 이는 대부분의 경우 무시할 수 있는 수준입니다.
<memory>
일반 포인터는 메모리 주소만 가리킬 뿐 해당 메모리에 대한 책임이 없지만, 스마트 포인터는 가리키는 메모리(자원)에 대한 소유권(Ownership)을 가지고 그 수명을 관리합니다.
이때, 소유권의 역할은 new를 통해 메모리를 할당한 사람이 반드시 delete를 통해 해제해야 하는 책임을 대신 가져와, 자신이 소멸될 때 소유하고 있는 자원을 자동으로 해제합니다.
단독 소유권을 가지는 스마트 포인터입니다. 하나의 객체는 오직 하나의 unique_ptr만이 소유할 수 있으며, 소유권 이전은 가능하지만 복사는 허용되지 않습니다. 소유권이 이전되면 원래 포인터는 null이 됩니다.
std::unique_ptr를 생성할 때 new 키워드를 직접 사용하기보다는 std::make_unique 함수를 사용하는 것이 C++14 이후의 표준이자 권장되는 방식입니다.
<예시>
#include <iostream>
#include <memory>
int main() {
std::unique_ptr<int> ptr1 = std::make_unique<int>(10);
std::cout << *ptr1 << std::endl;
std::unique_ptr<int> ptr2 = std::move(ptr1);
if (ptr1 == nullptr) std::cout << "ptr1 is now null" << std::endl;
std::cout << *ptr2 << std::endl;
return 0;
}
<결과>
공동 소유권을 가지는 스마트 포인터입니다. 여러 shared_ptr가 하나의 객체를 공유할 수 있으며, 객체에 대한 참조 횟수를 추적하는 참조 카운터를 사용합니다. 마지막 shared_ptr가 소멸될 때 객체가 해제됩니다.
std::shared_ptr를 생성할 때 new 키워드를 직접 사용하기보다는 std::make_shared 함수를 사용하는 것이 C++14 이후의 표준이자 권장되는 방식입니다.
<예시>
#include <iostream>
#include <memory>
int main() {
std::shared_ptr<int> ptr1 = std::make_shared<int>(20);
std::cout << "Reference count: " << ptr1.use_count() << std::endl;
std::shared_ptr<int> ptr2 = ptr1;
std::cout << "Reference count: " << ptr1.use_count() << std::endl;
ptr2 = nullptr;
std::cout << "Reference count: " << ptr1.use_count() << std::endl;
ptr1 = nullptr;
std::cout << "Reference count: " << ptr1.use_count() << std::endl;
return 0;
}
use_count() : 참조 카운트를 리턴하는 함수
<결과>
두 개 이상의 객체가 서로를 소유하거나 참조하면서 발생하는 상황을 말합니다. 이로 인해 한 객체의 소멸자가 다른 객체에 의해 호출되지 못하고, 결과적으로 메모리 누수(Memory Leak)가 발생할 수 있습니다.
<예시> | 순환 참조(Circular reference)
#include <iostream>
#include <memory>
class Child;
class Parent {
public:
std::shared_ptr<Child> child_ptr;
~Parent() {
std::cout << "Parent 소멸자 호출" << std::endl;
}
};
class Child {
public:
std::shared_ptr<Parent> parent_ptr;
~Child() {
std::cout << "Child 소멸자 호출" << std::endl;
}
};
void create_objects() {
std::shared_ptr<Parent> parent = std::make_shared<Parent>();
std::shared_ptr<Child> child = std::make_shared<Child>();
parent->child_ptr = child;
child->parent_ptr = parent;
}
int main() {
create_objects();
// RAII에 의해 함수가 종료되는 순간 함수 내부에서 생성된 객체는 소멸해야 합니다.
std::cout << "소멸자 호출 확인" << std::endl;
return 0;
}
순환 참조로 인해 소멸자가 호출되지 않는 것을 확인할 수 있습니다.
std::shared_ptr가 가지는 순환 참조 문제를 해결하기 위해 사용됩니다. 약한 참조를 제공하며, 객체를 소유하지 않고 관찰만 합니다.
lock() : weak_ptr가 가리키는 객체가 유효한지 확인하고, 유효하다면 그 객체를 가리키는 std::shared_ptr를 반환합니다. 만약 객체가 이미 소멸되었다면 nullptr를 반환합니다.
std::weak_ptr는 std::shared_ptr를 인자로 받아 생성되기 때문에 std::make_shared와 같은 헬퍼 함수가 필요하지 않습니다.
<예시>
#include <iostream>
#include <memory>
class Parent;
class Child {
public:
std::shared_ptr<Parent> parent_ptr;
~Child() {
std::cout << "Child 소멸자 호출" << std::endl;
}
};
class Parent {
public:
std::weak_ptr<Child> child_ptr; // weak_ptr로 변경
~Parent() {
std::cout << "Parent 소멸자 호출" << std::endl;
}
};
void create_objects() {
std::shared_ptr<Parent> parent = std::make_shared<Parent>();
std::shared_ptr<Child> child = std::make_shared<Child>();
parent->child_ptr = child; // weak_ptr에 할당
child->parent_ptr = parent;
}
int main() {
create_objects();
std::cout << "소멸자 호출 확인" << std::endl;
return 0;
}
<결과>