반응형

C++의 shared_ptr  예제 및 사용방법을 다루고 있습니다. 

아직 C++에 익숙하지 못해서 제 맘대로 바꿔보면서 감을 익히는 중입니다. 



2020. 01. 01.  최초 작성



std::shared_ptr 이란

shared_ptr은 c++ 11이 제공하는 스마트 포인터 클래스 중 하나입니다.

포인터를 더 이상 사용하지 않는 경우 메모리를 자동으로 해제해줍니다.

잘 사용하면 메모리 해제를 제때 안해 발생하는 메모리 릭(memory leak) 문제를 방지할 수 있습니다.



shared_ptr 객체 생성

shared_ptr 객체 생성시 바로 메모리를 가리키는 포인터를 연결하는 방법은 아래 예제에 보이는  2가지가 있습니다. 

make_shared를 사용하는 것을 권장합니다. 



shared_ptr을 위한 힙 메모리에 다음 2개의 메모리가 할당됩니다.

1. 객체를 위한 메모리

2. 참조 카운터를 위한 메모리. 이 메모리를 공유하는 shared_ptr 객체의 개수를 의미. 초기값은 1.

 

* 연산자를 사용하여 shared_ptr이 가리키는 포인터에 저장된 값을 출력해볼 수 있습니다. 

 

#include <iostream>
#include <memory>

int main()
{
    std::shared_ptr<int> s1(new int(10));

    std::shared_ptr<int> s2 = std::make_shared<int>(10);

    std::cout << *s1 << std::endl;
    std::cout << *s2 << std::endl;

    return 0;
}



10

10




shared_ptr 객체에 포인터를 바로 대입하면 컴파일시 에러가 발생합니다.

왜냐하면 shared_ptr 생성자의 아규먼트는 명시적(Explicit)이어야 하는데 아래 코드는 암시적(Implicit)이기 때문입니다. 

 

std::shared_ptr<int> p1 = new int(); 



새로운 shared_ptr 객체를 생성하는 방법은 std::make_shared를 사용하는 겁니다.

std::make_shared는 객체와 참조 카운터를 위한 데이터 구조를 위한 메모리를 할당합니다. 

 

std::shared_ptr<int> p1 = std::make_shared<int>();




shared_ptr 객체의 참조 카운터

여러 shared_ptr 객체는 동일한 포인터를 공유할 수 있습니다. 

공유하는 변수가 늘어날 수록 참조 카운터가 증가합니다.

 

use_count() 함수를 사용하여 shared_ptr 객체의 참조 카운터를 확인할 수 있습니다.

 

(1) shared_ptr 변수의 참조 카운터는 0입니다.

(2) make_shared를 사용하여 shared_ptr 객체를 생성하면 참조 카운터는 1이 됩니다. 

(3) 다른 shared_ptr 변수에 대입하면 참조 카운터가 증가하여 2가 되며 대입한 변수도 동일한 참조 카운터를 갖게 됩니다.

(4) nullptr을 대입한 변수는 참조 카운터가 0이 되며 같은 포인터를 참조하고 있던 다른 변수는 참조 카운터가 1 감소합니다. 

(5) reset()를 사용해도 nullptr 처럼  참조 카운터가 0이 됩니다. 

 

참조 카운터가 0이 되면 더이상 shared_ptr 객체가 가리키는 메모리가 없는 상태입니다.

변수 s1의 경우 참조 카운터가 0이 되었으므로  기본적으로 지정되어 있는 delete를 사용하여 메모리를 해제합니다.

뒤에서 살펴보겠지만 사용자가 delete 대신에 다른 지정한 함수를 사용할 수도 있습니다.

 

#include <iostream>
#include <memory>

int main()
{
    std::shared_ptr<int> s;
   
    std::cout << s.use_count() << std::endl; // (1)

    s = std::make_shared<int>(10);

    std::cout << s.use_count() << std::endl; // (2)

    std::shared_ptr<int> s2;

    s2 = s;

    std::cout << s.use_count() << " " << s2.use_count() << std::endl; // (3)


    s = nullptr;


    std::cout << s.use_count() << " " << s2.use_count() << std::endl; // (4)


    s2.reset();

    std::cout << s.use_count() << " " << s2.use_count() << std::endl; // (5)

    return 0;
}



0

1

2 2

0 1

0 0




shared_ptr 객체가 스코프를 벗어나면 소멸자는 참조 카운터를 1 감소시킵니다.

(1) 초기 참조 카운터는 1입니다. 

(2) 다른 변수에 대입하여 참조 카운터가 1증가하였습니다. 

(3) 괄호 스코프를 벗어나서 변수 s2가 소멸되면서 변수 s1의 카운터가 1 감소합니다.

 

#include <iostream>
#include <memory>

int main()
{
    std::shared_ptr<int> s1 = std::make_shared<int>(10);
    std::cout << s1.use_count() << std::endl;   // (1)

    {
        std::shared_ptr<int> s2 = s1;
        std::cout << s1.use_count() << " " << s2.use_count() << std::endl;  // (2)
    }

    std::cout << s1.use_count() << std::endl;  // (3)

    return 0;
}

 

1

2 2

1




Call by Reference (참조 호출) 사용 여부에 따라 함수에서 참조 카운터 차이 

 

참조 호출 사용하지 않는 경우

(1) 함수에서 참조 카운터가 1 증가합니다.

 

#include <iostream>
#include <memory>

void f(std::shared_ptr<int> s)
{
    std::cout << s.use_count() << std::endl;  // (1)
}

int main()
{
    std::shared_ptr<int> s1 = std::make_shared<int>(10);
    std::cout << s1.use_count() << std::endl;  

    f(s1);

    std::cout << s1.use_count() << std::endl;

    return 0;
}



1

2

1



참조 호출 사용하는 경우 

(1) 함수에서 참조 카운터가 증가하지 않습니다.

 

#include <iostream>
#include <memory>

void f(std::shared_ptr<int> &s)
{
    std::cout << s.use_count() << std::endl;  // (1)
}

int main()
{
    std::shared_ptr<int> s1 = std::make_shared<int>(10);
    std::cout << s1.use_count() << std::endl;  

    f(s1);

    std::cout << s1.use_count() << std::endl;

    return 0;
}



1

1

1






move()

move를 사용하여 shared_ptr 변수가 가리키는 포인터를 다른 shared_ptr로 이동할 수 있습니다. 

기존 shared_ptr은 참조 카운터가 0이 됩니다.

 

(1) 새로운 shared_ptr 객체를 생성합니다. 

(2) shared_ptr 변수 s가 가리키는 포인터를 shared_ptr 변수 s2로 이동합니다. 

(3) 그 결과 shared_ptr 변수 s는 참조 카운터가 0이 되고 shared_ptr 변수 s2는 1이 됩니다. 

 

#include <iostream>
#include <memory>

int main()
{
    std::shared_ptr<int> s = std::make_shared<int>(10);  // (1)

    std::cout << s.use_count() << " " << *s << std::endl;


    std::shared_ptr<int> s2; 


    std::cout << s.use_count() << " " << s2.use_count() << std::endl;   


    s2 = std::move(s);  // (2)
 
    std::cout << s.use_count() << " " << s2.use_count() << std::endl;   // (3)

    return 0;
}



1 10

1 0

0 1



reset()

shared_ptr 객체가 가리키는 포인터를 연결하거나 연결해제하기 위해서 reset() 메소드를 사용합니다.  

파라미터 없이 reset 함수를 사용하면 상황에 따라 참조 카운터가 1 감소하거나 0이 됩니다.  

만약 참조 카운터가 0이되면 포인터가 삭제됩니다. 

 

(1) s1은 0이되며 s2는 참조 카운터가 1 감소한다.

 

#include <iostream>
#include <memory>

int main()
{
    std::shared_ptr<int> s1 = std::make_shared<int>(10);

    std::cout << s1.use_count() << std::endl;


    std::shared_ptr<int> s2 = s1;

    std::cout << s1.use_count() << " " << s2.use_count() << std::endl;

    s1.reset();
   
    std::cout << s1.use_count() << " " << s2.use_count() << std::endl; // (1)

    return 0;
}

 

1

2 2

0 1




파라미터와 함께 reset 함수를 사용하면 shared_ptr은 해당 포인터를 가리키게 되며 참조 카운터가 1이 됩니다. 

(1) 기존의 10을 가리키던 포인터가 34를 가리키는 포인터로 변경되었습니다. 

 

#include <iostream>
#include <memory>

int main()
{
    std::shared_ptr<int> s1 = std::make_shared<int>(10);

    std::cout << s1.use_count() << " " << *s1 << std::endl;


    s1.reset(new int(34));
   
    std::cout << s1.use_count() << " " << *s1 << std::endl; // (1)

    return 0;
}

 

1 10

1 34




shared_ptr에 nullptr을 대입하면 파라미터없이 reset 함수를 사용한 것과 똑같이 동작합니다. 

 

#include <iostream>
#include <memory>

int main()
{
    std::shared_ptr<int> s1 = std::make_shared<int>(10);

    std::cout << s1.use_count() << std::endl;


    std::shared_ptr<int> s2 = s1;

    std::cout << s1.use_count() << " " << s2.use_count() << std::endl;

    s1 = nullptr;
   
    std::cout << s1.use_count() << " " << s2.use_count() << std::endl;

    return 0;
}

 

1

2 2

0 1




shared_ptr과 Custom Deleter

shared_ptr 객체가 선언된 스코프를 벗어나게 되면 소멸자가 호출됩니다. 

(예를 들어 함수 내에 선언된 지역변수처럼 자신이 선언된 함수를 벗어나면 소멸자가 호출됩니다.)

 

소멸자 내에서 참조 카운터가 1 감소되며, 참조 카운트가 0이 된다면 shared_ptr이 가리키는 포인터가 삭제됩니다.

 

기본적으로 소멸자에서는 delete() 함수를 사용하여 포인터를 삭제합니다. 

 

delete Pointer;



shared_ptr이 배열 포인터를 가리킬 경우에는 기본 deleter를 사용하면 문제가 생길 수 있습니다.

 

std::shared_ptr<int> p3(new int[12]);



delete 대신에   delete []를 사용하도록 해야 합니다. 



shared_ptr에 custom deleter 추가하기

 

shared_ptr 생성자에 shared_ptr 소멸시 객체 삭제를 위한 콜백함수를 지정할 수 있습니다.

전달받은 포인터가 가리키는 메모리를 해제하기 위해 delete [] 를 호출하는 함수입니다.

 

함수 객체 또는 람다(lambda) 함수로 custom deleter를 지정할 수 있습니다.

 

#include <iostream>
#include <memory>


void Deleter(int* x) {
    delete[] x;
    printf("delete\n");
}


int main()
{
    int *buffer1 = new int[1024];
    std::shared_ptr<int> foo1;
    foo1.reset(buffer1, [](int *p){delete[]p; printf("delete\n");});
   

    int *buffer2 = new int[1024];
    std::shared_ptr<int> foo2;
    foo2.reset(buffer2, Deleter);
 

    return 0;
}




NULL인지 검사 

shared_ptr 객체를 생성한 후, 가리키는 포인터가 없으면 nullptr을 가리키게 됩니다. 

shared_ptr 객체가 포인터를 가리키고 있는지 확인하는 방법입니다.

 

std::shared_ptr<Sample> ptr3;

 

if(!ptr3)
    std::cout<<"Yes, ptr3 is empty" << std::endl;

if(ptr3 == NULL)
    std::cout<<"ptr3 is empty" << std::endl;

if(ptr3 == nullptr)
    std::cout<<"ptr3 is empty" << std::endl;



shared_ptr이 가리키고 있는 내부 포인터를 다음처럼 접근할 수 있습니다. 하지만 이 방법은 사용하지 않는 것이 좋습니다.

shared_ptr이 소멸되면 문제가 생길 수 있기 때문입니다..

 

std::shared_ptr<Sample> ptr = std::make_shared<Sample>();
Sample * rawptr = ptr.get();



shared_ptr 사용시 주의할 점

1. 둘 이상의 shared_ptr이 같은 포인터를 가리키도록 하면 하면 안됩니다. 

하나의 shared_ptr 객체가 소멸하면서 포인터의 메모리를 해제하더라도 다른 shared_ptr 객체는 여전히 포인터를 가리키고 있기 때문입니다. 

 

(1) s2 shared_ptr이 소멸되면서 s2가 가리키는 포인터의 메모리도 해제되었지만 s1은 여전히 해당 포인터를 가리키는 상태입니다. 

(2) s1의 가리키는 포인터가 가리키는 메모리는 이미 해제 되었기 때문에 엉뚱한 값이 출력됩니다. 

(3) 이미 메모리 해제된 포인터를 접근하려고 하면 에러가 납니다.

 

#include <iostream>
#include <memory>

int main()
{
    int *num = new int(10);
   
    std::shared_ptr<int> s1;
    std::shared_ptr<int> s2;
    s1.reset(num);
    s2.reset(num);

    std::cout << s1.use_count() << " " << *s1 << std::endl;
    std::cout << s2.use_count() << " " << *s2 << std::endl;

    s2.reset();   // (1)
   
    std::cout << s1.use_count() << " " << *s1 << std::endl;  // (2)
    std::cout << s2.use_count() << " " << *s2 << std::endl; // (3)

    return 0;
}



1 10

1 10

1 15735488

0 터미널 프로세스 "C:\Windows\System32\cmd.exe /d /c cmd /C D:\work\code\C_C++_Projects\shared_ptr_crash_test"이(가) 종료되었습니다(종료 코드: 3221225477).




2. 힙(heap) 대신에 스택(stack)을 가리키는 포인터를 사용하여 shared_ptr 객체를 생성하면 안됩니다. 

힙 메모리를 사용하면 스코프를 벗어나도(예를들어 지역 함수를 벗어나도) 포인터가 가리키는 메모리가 유지되지만

스택 메모리를 사용하면 스코프를 벗어나는 순간 사라지기 때문에 문제가 발생합니다. 

 

(1) 지역변수를 가리키는 포인터를 shared_ptr에서 가리키도록 합니다. 

(2) shared_ptr 변수인 s2는 여전히 지역변수 num2를 가리키고 있지만 num2는 함수를 벗어나는 순간 소멸되었기 때문에

shared_ptr 변수 s2가 가리키는 값을 *s2로 출력해보면 원하는 값이 출력되지 않습니다.  

프로그램 오류까지 발생합니다. 

 

#include <iostream>
#include <memory>

void f1(std::shared_ptr<int> &s1)
{
    int *num1 = new int(10);
    s1.reset(num1);

    std::cout << "in function : " << s1.use_count() << " " << *s1 << std::endl;
}

void f2(std::shared_ptr<int> &s2)
{
    int num2 = 20;
    s2.reset(&num2);  // (1)

    std::cout << "in function : " << s2.use_count() << " " << *s2 << std::endl;
}

int main()
{
    std::shared_ptr<int> s1;

    f1(s1);

    std::cout << "out of function : " <<  s1.use_count() << " " << *s1 << std::endl;

   
    std::shared_ptr<int> s2;

    f2(s2);

    std::cout << "out of function : " << s2.use_count() << " " << *s2 << std::endl; // (2)

    return 0;
}



in function : 1 10

out of function : 1 10

in function : 1 20

out of function : 1 0

터미널 프로세스 "C:\Windows\System32\cmd.exe /d /c cmd /C D:\work\code\C_C++_Projects\shared_ptr_crash_test"이(가) 종료되었습니다(종료 코드: 3221226356).

 

관련 포스팅

 

C++ shared_ptr 객체를 전달받은 함수에서 사용 후 해제하는 방법

https://webnautes.tistory.com/1468

 

 

참고

 

https://thispointer.com/learning-shared_ptr-part-1-usage-details/

 

https://thispointer.com/shared_ptr-and-custom-deletor/

 

https://thispointer.com/how-shared_ptr-object-is-different-from-a-raw-pointer/ 

 

https://thispointer.com/create-shared_ptr-objects-carefully/ 




반응형

포스트 작성시에는 문제 없었지만 이후 문제가 생길 수 있습니다.
질문을 남겨주면 가능한 빨리 답변드립니다.

여러분의 응원으로 좋은 컨텐츠가 만들어집니다.
지금 본 내용이 도움이 되었다면 유튜브 구독 부탁드립니다. 감사합니다 ~~

유튜브 구독하기


제가 쓴 책도 한번 검토해보세요 ^^

+ Recent posts