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을 사용할 때 주의해야 할 몇 가지 내용이 있습니다.

  1. 순환 참조(circular reference) 문제: shared_ptr은 reference counting 방식을 사용하기 때문에 객체가 서로를 참조하는 경우 순환 참조 문제가 발생할 수 있습니다. 이런 경우에는 weak_ptr을 사용하거나, 참조 구조를 재설계해야 합니다.
  2. shared_ptr에 raw pointer를 대입하지 않기: shared_ptr에 raw pointer를 대입하게 되면, 두 개 이상의 shared_ptr이 같은 객체를 참조할 때, 객체가 제대로 해제되지 않는 문제가 발생할 수 있습니다. 대신, make_shared() 함수를 사용하거나 shared_ptr의 생성자에 raw pointer를 전달하는 것이 좋습니다.
  3. C++11에서는 std::enable_shared_from_this를 상속받아야 shared_ptr의 안전한 사용이 가능합니다. 이 클래스를 상속한 객체는 자신을 참조하는 shared_ptr을 생성할 수 있습니다.
  4. 사용자 정의 deleter를 지정할 수 있습니다. shared_ptr은 reference counting을 이용하여 객체를 관리하지만, 사용자가 직접 메모리 할당을 하고 해제해야 하는 경우가 있습니다. 이때 사용자 정의 deleter를 지정하여 메모리 할당과 해제를 처리할 수 있습니다.

 

 

+ Recent posts