shared_ptr 을 사용하는 이유
C++에서 메모리 누수(memory leak)를 방지하고, 안전하게 객체를 공유하는데 사용됩니다.
C++에서는 메모리를 수동으로 할당하고 해제하는 것이 일반적이지만, 이러한 작업을 관리하는 것은 어려울 수 있습니다.
특히, 동적으로 할당된 메모리를 사용하는 경우, 메모리를 해제하는 것을 잊어버리면 메모리 누수가 발생할 수 있습니다.
shared_ptr은 동적으로 할당된 메모리를 관리하는 스마트 포인터(Smart Pointer) 중 하나입니다.
스마트 포인터는 C++에서 메모리 관리를 자동화하는데 사용되며, 메모리 누수를 방지할 수 있습니다.
shared_ptr은 객체에 대한 공유 소유권(shared ownership)을 제공합니다.
이는 동일한 객체를 여러 개의 shared_ptr에서 공유할 수 있으며, 모든 shared_ptr이 삭제될 때 객체가 자동으로 해제된다는 것을 의미합니다.
이러한 공유 소유권을 제공하는 것은 객체에 대한 참조를 안전하게 공유하고, 객체가 더 이상 필요하지 않을 때 자동으로 삭제하여 메모리 누수를 방지하는 데 도움이 됩니다.
또한, shared_ptr은 일반적으로 new 및 delete 연산자를 사용하여 메모리를 동적으로 할당하고 해제하는 것보다 안전합니다. shared_ptr을 사용하면 자동으로 메모리가 해제되므로 메모리 누수 및 다른 문제가 발생할 가능성이 줄어듭니다.
shared_ptr 의 원리
shared_ptr은 객체에 대한 공유 소유권(shared ownership)을 제공하며, 모든 shared_ptr이 삭제될 때 객체가 자동으로 해제됩니다.
shared_ptr의 작동 방식은 참조 계수(reference counting)를 사용합니다.
즉, shared_ptr은 객체에 대한 참조를 가지고 있으며, 객체를 참조하는 shared_ptr의 개수를 계산합니다.
이러한 개수를 참조 계수라고 합니다. 참조 계수가 0이면 객체가 자동으로 해제됩니다.
객체를 생성할 때, shared_ptr은 객체와 함께 참조 계수를 생성합니다.
새로운 shared_ptr이 객체를 참조할 때마다, 참조 계수를 증가시킵니다.
shared_ptr이 객체에 대한 참조를 해제하면, 참조 계수를 감소시킵니다.
참조 계수가 0이 되면, shared_ptr이 객체를 삭제하고 참조 계수도 함께 삭제합니다.
shared_ptr은 여러 개의 shared_ptr에서 객체를 공유할 수 있습니다.
이때 shared_ptr은 동일한 참조 계수를 공유하며, 모든 shared_ptr이 삭제될 때 참조 계수가 0이 되어 객체가 자동으로 해제됩니다.
이러한 공유 소유권을 제공하는 것은 객체에 대한 참조를 안전하게 공유하고, 객체가 더 이상 필요하지 않을 때 자동으로 삭제하여 메모리 누수를 방지하는 데 도움이 됩니다.
shared_ptr은 std::make_shared 함수를 사용하여 생성할 수 있습니다.
이 함수는 동적으로 할당된 메모리를 자동으로 해제하므로, 메모리 누수와 관련된 문제를 방지할 수 있습니다.
예를 들어, 다음과 같은 코드를 사용하여 shared_ptr을 생성할 수 있습니다.
#include <memory>
int main() {
// 객체를 생성하여 shared_ptr로 래핑합니다.
std::shared_ptr<int> ptr1(new int(42));
// make_shared 함수를 사용하여 shared_ptr을 생성합니다.
std::shared_ptr<int> ptr2 = std::make_shared<int>(42);
return 0;
}
코드에서 ptr1은 new 연산자를 사용하여 메모리를 동적으로 할당합니다.
반면에 ptr2는 std::make_shared 함수를 사용하여 메모리를 동적으로 할당합니다. 이러한 차이점은 메모리 누수 및 다른 문제를 방지하는 데 중요합니다
shared_ptr을 사용하여 객체를 공유하는 방법
#include <iostream>
#include <memory>
class MyClass {
public:
MyClass() {
std::cout << "MyClass 생성자 호출" << std::endl;
}
~MyClass() {
std::cout << "MyClass 소멸자 호출" << std::endl;
}
};
int main() {
// MyClass 객체를 생성하여 shared_ptr로 래핑합니다.
std::shared_ptr<MyClass> ptr1(new MyClass);
// ptr1과 동일한 MyClass 객체를 참조하는 또 다른 shared_ptr을 만듭니다.
std::shared_ptr<MyClass> ptr2(ptr1);
// ptr1과 ptr2 모두가 객체를 참조하고 있습니다.
std::cout << "ptr1.use_count(): " << ptr1.use_count() << std::endl;
std::cout << "ptr2.use_count(): " << ptr2.use_count() << std::endl;
// ptr1을 해제합니다.
ptr1.reset();
// ptr1이 해제되었으므로 ptr2만 객체를 참조하고 있습니다.
std::cout << "ptr1이 해제되었으므로 ptr2만 객체를 참조합니다." << std::endl;
std::cout << "ptr2.use_count(): " << ptr2.use_count() << std::endl;
return 0;
}
위 코드에서 MyClass는 간단한 클래스로, 객체가 생성될 때 메시지를 출력하고 객체가 삭제될 때 메시지를 출력합니다.
main 함수에서는 MyClass 객체를 생성하여 shared_ptr로 래핑합니다.
그런 다음, ptr1과 동일한 객체를 참조하는 또 다른 shared_ptr인 ptr2를 만듭니다.
이후, ptr1과 ptr2의 use_count 함수를 호출하여 객체를 참조하는 shared_ptr의 개수를 확인합니다.
ptr1을 해제한 후 ptr2의 use_count를 다시 확인합니다. ptr1이 해제되었으므로 ptr2만 객체를 참조하고 있습니다.
이 예제는 shared_ptr이 객체에 대한 공유 소유권을 제공하는 방법을 보여줍니다.
또한, shared_ptr의 참조 계수를 확인하여 객체가 얼마나 많은 shared_ptr에서 참조되고 있는지 확인할 수 있습니다.
shared_ptr을 사용하여 복잡한 상호 참조 구조를 만드는 예제
#include <iostream>
#include <memory>
class B;
class C;
class A {
public:
A() {
std::cout << "A 생성자 호출" << std::endl;
}
~A() {
std::cout << "A 소멸자 호출" << std::endl;
}
void set_b(std::shared_ptr<B> b) {
b_ = b;
}
void set_c(std::shared_ptr<C> c) {
c_ = c;
}
private:
std::shared_ptr<B> b_;
std::shared_ptr<C> c_;
};
class B {
public:
B() {
std::cout << "B 생성자 호출" << std::endl;
}
~B() {
std::cout << "B 소멸자 호출" << std::endl;
}
void set_a(std::shared_ptr<A> a) {
a_ = a;
}
void set_c(std::shared_ptr<C> c) {
c_ = c;
}
private:
std::shared_ptr<A> a_;
std::shared_ptr<C> c_;
};
class C {
public:
C() {
std::cout << "C 생성자 호출" << std::endl;
}
~C() {
std::cout << "C 소멸자 호출" << std::endl;
}
void set_a(std::shared_ptr<A> a) {
a_ = a;
}
void set_b(std::shared_ptr<B> b) {
b_ = b;
}
private:
std::shared_ptr<A> a_;
std::shared_ptr<B> b_;
};
int main() {
std::shared_ptr<A> a(new A);
std::shared_ptr<B> b(new B);
std::shared_ptr<C> c(new C);
a->set_b(b);
a->set_c(c);
b->set_a(a);
b->set_c(c);
c->set_a(a);
c->set_b(b);
std::cout << "a.use_count(): " << a.use_count() << std::endl;
std::cout << "b.use_count(): " << b.use_count() << std::endl;
std::cout << "c.use_count(): " << c.use_count() << std::endl;
return 0;
}
다음 예제는 두 개의 클래스 Person과 Address를 정의하고, Person 객체가 Address 객체를 참조하도록 구현합니다.
Person 객체와 Address 객체를 모두 shared_ptr로 관리합니다
#include <iostream>
#include <memory>
#include <string>
class Address {
public:
Address(const std::string& street, const std::string& city) : m_street(street), m_city(city) {
std::cout << "Address 생성자 호출: " << m_street << ", " << m_city << std::endl;
}
~Address() {
std::cout << "Address 소멸자 호출: " << m_street << ", " << m_city << std::endl;
}
std::string getStreet() const {
return m_street;
}
std::string getCity() const {
return m_city;
}
private:
std::string m_street;
std::string m_city;
};
class Person {
public:
Person(const std::string& name, const std::string& street, const std::string& city) : m_name(name), m_address(std::make_shared<Address>(street, city)) {
std::cout << "Person 생성자 호출: " << m_name << std::endl;
}
~Person() {
std::cout << "Person 소멸자 호출: " << m_name << std::endl;
}
std::string getName() const {
return m_name;
}
std::shared_ptr<Address> getAddress() const {
return m_address;
}
private:
std::string m_name;
std::shared_ptr<Address> m_address;
};
int main() {
// 두 개의 Person 객체를 생성합니다.
std::shared_ptr<Person> person1(new Person("Alice", "123 Main St", "Anytown"));
std::shared_ptr<Person> person2(new Person("Bob", "456 Elm St", "Anytown"));
// person1과 person2가 참조하는 Address 객체를 출력합니다.
std::cout << "person1이 참조하는 Address 객체: " << person1->getAddress()->getStreet() << ", " << person1->getAddress()->getCity() << std::endl;
std::cout << "person2가 참조하는 Address 객체: " << person2->getAddress()->getStreet() << ", " << person2->getAddress()->getCity() << std::endl;
// person1과 person2의 Address 객체가 같은지 확인합니다.
if (person1->getAddress() == person2->getAddress()) {
std::cout << "person1과 person2가 같은 Address 객체를 참조합니다." << std::endl;
} else {
std::cout << "person1과 person2가 다른 Address 객체를 참조합니다." << std::endl;
}
// person1을 삭제합니다.
person1.reset();
// person1이 참조하던 Address 객체를 출력합니다.
if (person2->getAddress()) {
std::cout << "person1이 참조하던 Address 객체: " << person2->getAddress()->getStreet() << ", " << person2->getAddress()->getCity() << std::endl;
} else {
std::cout << "person1이 참조하던 Address 객체
person1이 참조하던 Address 객체는 더 이상 참조되지 않으므로, 메모리에서 해제됩니다.
이 때, Address 클래스의 소멸자가 호출되고, 해당 객체가 속한 Person 객체도 메모리에서 해제됩니다.
person2는 여전히 Address 객체를 참조하고 있으므로, person2의 소멸자가 호출될 때까지 해당 Address 객체는 유효합니다.
shared_ptr은 복잡한 객체들을 관리하는 데 매우 편리한 기능을 제공합니다.
그러나 여러 shared_ptr이 하나의 객체를 참조하는 경우, 객체가 메모리에서 해제되는 시점이 예상과 다를 수 있으므로 주의해야 합니다. 또한, shared_ptr을 사용할 때는 서로 다른 스레드에서 동시에 접근하지 않도록 주의해야 합니다
게임 엔진에서 리소스 관리
게임 엔진에서 리소스 관리에 shared_ptr을 사용합니다.
게임 엔진에서는 이미지, 사운드, 모델 등 다양한 리소스를 로드하고 사용하는데, 이러한 리소스는 게임이 종료될 때까지 메모리에 유지되어야 하지만, 게임의 레벨이나 씬이 바뀔 때마다 리소스가 필요한 만큼만 로드하고, 사용이 끝나면 메모리에서 해제되어야 합니다.
shared_ptr은 리소스가 사용 중인지 아닌지를 추적할 수 있으므로, 여러 개의 객체에서 공유하며 사용하는 리소스를 안전하게 관리할 수 있습니다. 또한, shared_ptr은 객체가 더 이상 참조되지 않을 때 자동으로 메모리에서 해제되므로, 메모리 누수를 방지할 수 있습니다.
이러한 이유로, 많은 게임 엔진에서는 shared_ptr을 사용하여 리소스를 관리하고 있습니다. 예를 들어, Unreal Engine에서도 TSharedPtr이라는 이름으로 shared_ptr을 사용하여 객체를 관리합니다.
우선, Resource 클래스를 정의합니다.
이 클래스는 리소스의 기본 형태를 나타내며, 파일 이름과 크기를 멤버 변수로 가지고 있습니다.
class Resource {
public:
Resource(const std::string& name, int size)
: m_name(name), m_size(size) {}
std::string getName() const { return m_name; }
int getSize() const { return m_size; }
private:
std::string m_name;
int m_size;
};
다음으로, ResourceHolder 클래스를 정의합니다. 이 클래스는 리소스를 로드하고 관리하는 역할을 합니다.
ResourceHolder 클래스는 std::map을 사용하여 리소스를 이름으로 관리합니다.
class ResourceHolder {
public:
std::shared_ptr<Resource> getResource(const std::string& name) {
auto iter = m_resourceMap.find(name);
if (iter != m_resourceMap.end()) {
return iter->second;
}
else {
// Load the resource from file
std::shared_ptr<Resource> resource = std::make_shared<Resource>(name, 100);
m_resourceMap[name] = resource;
return resource;
}
}
private:
std::map<std::string, std::shared_ptr<Resource>> m_resourceMap;
};
getResource 함수는 먼저 std::map에서 해당 이름의 리소스를 찾습니다. 이미 로드되어 있는 경우에는 해당 리소스에 대한 shared_ptr을 반환하고, 아직 로드되어 있지 않은 경우에는 파일에서 리소스를 로드하고 shared_ptr을 생성하여 std::map에 추가합니다.
마지막으로, main 함수에서 ResourceHolder 클래스를 사용하여 리소스를 로드합니다.
int main() {
ResourceHolder resourceHolder;
std::shared_ptr<Resource> res1 = resourceHolder.getResource("image1.png");
std::shared_ptr<Resource> res2 = resourceHolder.getResource("sound1.wav");
std::shared_ptr<Resource> res3 = resourceHolder.getResource("image1.png");
std::cout << "Resource 1: " << res1->getName() << ", size: " << res1->getSize() << std::endl;
std::cout << "Resource 2: " << res2->getName() << ", size: " << res2->getSize() << std::endl;
std::cout << "Resource 3: " << res3->getName() << ", size: " << res3->getSize() << std::endl;
std::cout << "res1 and res3 are " << (res1 == res3 ? "same" : "different") << std::endl;
return 0;
}
main 함수에서는 ResourceHolder 클래스의 getResource 함수를 사용하여
image1.png와 sound1.wav 파일에서 리소스를 로드합니다.
image1.png 파일은 이미 한 번 로드되어 있기 때문에 getResource 함수는 새로운 shared_ptr을 생성하지 않고,
이미 로드된 리소스에 대한 shared_ptr을 반환합니다
주의사항
shared_ptr을 사용할 때 주의해야 할 몇 가지 내용이 있습니다.
- 순환 참조(circular reference) 문제: shared_ptr은 reference counting 방식을 사용하기 때문에 객체가 서로를 참조하는 경우 순환 참조 문제가 발생할 수 있습니다. 이런 경우에는 weak_ptr을 사용하거나, 참조 구조를 재설계해야 합니다.
- shared_ptr에 raw pointer를 대입하지 않기: shared_ptr에 raw pointer를 대입하게 되면, 두 개 이상의 shared_ptr이 같은 객체를 참조할 때, 객체가 제대로 해제되지 않는 문제가 발생할 수 있습니다. 대신, make_shared() 함수를 사용하거나 shared_ptr의 생성자에 raw pointer를 전달하는 것이 좋습니다.
- C++11에서는 std::enable_shared_from_this를 상속받아야 shared_ptr의 안전한 사용이 가능합니다. 이 클래스를 상속한 객체는 자신을 참조하는 shared_ptr을 생성할 수 있습니다.
- 사용자 정의 deleter를 지정할 수 있습니다. shared_ptr은 reference counting을 이용하여 객체를 관리하지만, 사용자가 직접 메모리 할당을 하고 해제해야 하는 경우가 있습니다. 이때 사용자 정의 deleter를 지정하여 메모리 할당과 해제를 처리할 수 있습니다.
'C++ > concept' 카테고리의 다른 글
[C++] virtual table 역할 및 필요성 (0) | 2023.03.24 |
---|---|
[C++] stream 클래스 상속 관계 (0) | 2023.03.24 |
헤더(header) 파일이 필요한 이유 (0) | 2023.03.17 |
조합(Composition)과 상속(Inheritance) 차이점 (0) | 2023.03.17 |
[C++] Message Passing Framework - 1 (0) | 2022.11.25 |