C++ 스마트 포인터

  • C++11부터 스마트 포인터가 도입되면서 C에서 사용하던 포인터(*) 사용을 권하지 않습니다.
  • 스마트 포인터는 원시 포인터(Raw Pointer)를 세부적인 기능으로 구분하고 기능에 제한을 둔 포인터입니다.
  • 스마트 포인터의 종류는 다음과 같습니다.
    • std::unique_ptr
    • std::shared_ptr
    • std::weak_ptr
  • 스마트 포인터를 사용하기 위해 memory STL 헤더가 필요합니다.
  • 스마트 포인터의 장점 중 하나는 동적으로 할당한 객체의 메모리 해제가 자동으로 이뤄집니다. 더이상 생성한 객체의 흐름을 따라가며 delete를 할 필요가 없습니다.

스마트 포인터 예제

  • 스마트 포인터의 기능을 알아보기 위해 간단한 클래스를 작성합니다.
  • 사용자로부터 ID를 입력받고, ID를 반환하는 객체입니다.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class Object
{
public:
    Object() : 
        m_ID(0)
    {
        std::cout << "Concreted Object" << std::endl;
    }

    ~Object()
    {
        std::cout << "Deconcreted Object" << std::endl;
    }

    const int& getID()
    {
        return m_ID;
    }

    void setID(const int& id)
    {
        m_ID = id;
    }

private:
    int m_ID;
};

unique_ptr

  • unique_ptr은 단 하나의 포인터만이 해당 인스턴스를 가르킬 수 있습니다.
  • unique_ptr의 생성은 STL에서 권장하는 방식인 std::make_unique<T>를 사용합니다.
1
2
3
4
5
6
void someFunction()
{
    std::unique_ptr<Object> object = std::make_unique<Object>();
    object->setID(10);
    std::cout << "ID: " << object->getID() << std::endl;
}
  • 만약 다른 unique_ptr이 같은 객체를 가르키려 한다면 에러가 발생합니다.
1
2
3
4
5
6
7
8
9
void someFunction()
{
    std::unique_ptr<Object> object = std::make_unique<Object>();
    object->setID(10);
    std::cout << "ID: " << object->getID() << std::endl;

    // Error!
    std::unique_ptr<Object> otherObject = object;
}
  • unique_ptr의 소유권을 이전하려면 std::move() 함수를 사용합니다.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
void someFunction()
{
    std::unique_ptr<Object> object = std::make_unique<Object>();
    object->setID(10);
    std::cout << "ID: " << object->getID() << std::endl;

    std::unique_ptr<Object> otherObject = std::move(object);
    std::cout << "ID: " << otherObject->getID() << std::endl;

    // Error!
    std::cout << "ID: " << object->getID() << std::endl;
}
  • 소유권이 변경된 객체를 사용하려 한다면 런타임 에러가 발생합니다.
  • 객체의 소멸은 객체의 소유권을 가진 포인터가 없으면 자동으로 소멸자가 호출됩니다.
  • 또한 reset() 메서드를 사용하여 개발자가 의도적으로 소멸자를 호출할 수 있습니다.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
void someFunction()
{
    std::unique_ptr<Object> object = std::make_unique<Object>();
    object->setID(10);
    std::cout << "ID: " << object->getID() << std::endl;

    //object.reset();
    std::cout << "Is object deleted?" << std::endl;
}

int main(const int argc, const char* argv[])
{
    std::cout << "Function-In" << std::endl;
    someFunction();
    std::cout << "Function-Out" << std::endl;

    return 0;
}
  • 출력 결과
1
2
3
4
5
6
Function-In
Constructed Object
ID: 10
Is object deleted?
Deconstructed Object
Function-Out
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
void someFunction()
{
    std::unique_ptr<Object> object = std::make_unique<Object>();
    object->setID(10);
    std::cout << "ID: " << object->getID() << std::endl;

    object.reset(); // 소멸자가 먼저 호출된다.
    std::cout << "Is object deleted?" << std::endl;
}

int main(const int argc, const char* argv[])
{
    std::cout << "Function-In" << std::endl;
    someFunction();
    std::cout << "Function-Out" << std::endl;

    return 0;
}
  • 출력 결과
1
2
3
4
5
6
Function-In
Constructed Object
ID: 10
Deconstructed Object
Is object deleted?
Function-Out

sheard_ptr

  • shared_ptr은 공유 포인터로 객체를 가르키는 포인터의 갯수(Reference)를 카운팅합니다.
  • unique_ptr과는 다르게 여러 개의 포인터가 동일한 객체를 가르킬 수 있습니다.
  • 객체의 소멸 시점은 객체를 가르키는 포인터의 갯수가 0이 되면 자동으로 메모리가 해제됩니다.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
void someFunction()
{
    std::cout << "Function-In" << std::endl;

    std::shared_ptr<Object> objectA = std::make_shared<Object>();
    objectA->setID(10);
    std::cout << "Reference count : " << objectA.use_count() << std::endl; // 1

    // 동일한 객체를 objectB 포인터로 가르킨다.
    std::shared_ptr<Object> objectB = objectA;
    std::cout << "Reference count : " << objectA.use_count() << std::endl; //2

    // objectB로부터 동일한 객체를 가르킬수도 있다.
    std::shared_ptr<Object> objectC = objectB;
    // objectC도 동일한 객체를 가르키므로, objectA.use_count()와 동일한 결과가 나온다.
    std::cout << "Reference count : " << objectC.use_count() << std::endl; // 3

    // 첫 번째로 객체를 가르킨 포인터를 해제해도 objectC로 접근이 가능하다.
    objectA.reset();
    std::cout << "Reference count : " << objectC.use_count() << std::endl; // 2

    objectB.reset(); // 1
    objectC.reset(); // 0 -> 소멸자 호출
    // 여기서 객체의 소멸자가 호출된다. 

    std::cout << "Function-Out" << std::endl;
}
  • 출력 결과
1
2
3
4
5
6
7
8
Function-In
Constructed Object
Reference count : 1
Reference count : 2
Reference count : 3
Reference count : 2
Deconstructed Object
Function-Out

weak_ptr

  • weak_ptrshared_ptr과 비슷한 개념을 갖고 있습니다.
  • shared_ptr이 객체의 레퍼런스 갯수를 카운팅한다면, weak_ptr을 사용하면 카운팅에 포함되지 않습니다.
  • shared_ptr은 객체의 레퍼런스 갯수를 증가시켜 객체의 수명 주기를 결정하지만, 잘못 사용하면 객체가 영원히 소멸되지 않는 경우가 발생하기도 합니다.
  • 따라서 객체의 레퍼런스 갯수를 증가시킬 이유가 없다면 weak_ptr을 사용합니다.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
void someFunction()
{
    std::cout << "Function-In" << std::endl;

    std::shared_ptr<Object> objectA = std::make_shared<Object>();
    objectA->setID(10);
    std::cout << "Reference count : " << objectA.use_count() << std::endl; // 1

    std::weak_ptr<Object> objectB = objectA;
    std::cout << "Reference count : " << objectA.use_count() << std::endl; // 1

    std::weak_ptr<Object> objectC = objectB;
    std::cout << "Reference count : " << objectC.use_count() << std::endl; // 1

    objectA.reset(); // 원본 객체의 레퍼런스 갯수가 0이 되기 때문에 소멸자가 호출된다.
    std::cout << "ID : " << objectC->getID << std::endl; // Error! 객체가 존재하지 않는다.
    std::cout << "Reference count : " << objectC.use_count() << std::endl; // 0

    std::cout << "Function-Out" << std::endl;
}