프로그래밍/C++

C++] 스마트 포인터에 대하여...

Hwan2 2020. 5. 31. 00:05
반응형

스마트 포인터란??



기본적으로 포인터라고 하면 *ptr을 떠올리게 됩니다.

그리고 주소 값을 해당 포인터 변수에 넣어주죠.

int *ptr = # 이런식으로요....


하지만 포인터가 new로 생성된 변수... 즉, Heap메모리에 할당된 주소 값을 가르키게 될 때

프로그래머의 실수로 Memory leak이 발생할 수 있습니다.

delete를 안해서 말이죠......


스마트 포인터를 사용하게 된다면 해당 걱정은 없어지게 됩니다.

Stack에 선언된 스마트 포인터가 pop이 된다면..... 

즉, {} 함수가 끝나게 된다면 Heap에 할당된 메모리는 자동적으로 초기화 시켜줍니다.


C++에서 스마트 포인터는 3가지가 있습니다.


1. unique_ptr

2. shared_ptr

3. weak_ptr



그럼 사용 방법에 대해 살펴보도록 하겠습니다.



1. unique_ptr


unique_ptr의 특징

unique_ptr은 요소 하나에 대해 여러개의 unique_ptr이 가르킬 수 없습니다.
무슨말이냐면....


요렇게 unique_ptr은 한 녀석당 한놈을 가르키게 됩니다.




사용법
#include <iostream>
#include <memory>

using namespace std;

class A {

};

int main(void) {
    
    unique_ptr<int[]> p1 = make_unique<int[]>(10);
    p1[0] = 1p1[1] = 2p1[2] = 3;    //p1스마트포인터 배열 10개 선언

    unique_ptr<int> p2 = make_unique<int>(5);   // unique_ptr<int>p2(new int(5));
    int * num = new int(10);
    unique_ptr<intp3(move(num));

    cout << p1[0] << endl;   //output : 1
    cout << *p2 << endl;     //output : 5
    cout << *p3.get() << endl;//output : 10
    cout << *num << endl;     //output : 10

    p3.reset();  //가르키고 있는 값 해제
    cout << *num << endl;     //output: -572662307(쓰레기 값)

    unique_ptr<A> a1 = make_unique<A>();
    unique_ptr<A> a2 = a1;  //error!!
    unique_ptr<A> a2 = move(a1);    //OK!!
    return 0;
}


선언 방법은 대충 이렇습니다.

물론 int형 배열 같은 경우에는 vector를 사용하는 방법이 옳고 new int를 저런식으로 받을 일도 드물지만

초기화 방법을 설명하기 위해 저렇게 예시로 들었습니다.


마지막 부분을 a1과 a2를 보면 일반적인 대입연산은 안되지만 move를 통한 소유권을 넘겨주는 것은 가능합니다.




그럼 memory leak을 실제로 확인해보겠습니다.



#include <iostream>
#include <memory>

using namespace std;

class A {
private:
    int* num;
public:
    A() : num(new int[100]) {
        for (int i = 0; i < 100; i++)
            num[i] = i;
    }

    ~A() {
        cout << "소멸자 호출!!" << endl;
        delete[] num;
    }

    int* return_ptr() {
        return num;
    }
};

int* fnc1() {       //delete 호출 안함!!        
    A* a = new A();
    int* p = a->return_ptr();

    cout << p[10] << endl;
    //delete a;
    return p;
}

int main(void) {

    int* p = fnc1();

    cout << p[10] << endl;

    return 0;
}



실행 결과



fnc1()함수에서 delete호출을 하지 않아 A Class에 있는 num이 메모리 해제가 되지 않은 모습입니다.





unique_str 사용


#include <iostream>
#include <memory>

using namespace std;

class A {
private:
    int* num;
public:
    A() : num(new int[100]) {
        for (int i = 0; i < 100; i++)
            num[i] = i;
    }

    ~A() {
        cout << "소멸자 호출!!" << endl;
        delete[] num;
    }

    int* return_ptr() {
        return num;
    }
};

int* fnc1() {
    unique_ptr<A> u_ptr = make_unique<A>();
    int* p = u_ptr->return_ptr();

    cout << p[10] << endl;
    return u_ptr->return_ptr();
}

int main(void) {

    int* p = fnc1();

    cout << p[10] << endl;
    return 0;
}



실행 결과


unique_ptr을 사용한 결과 함수가 끝남과 동시에 자동으로 num이 해제된 것을 볼 수 있습니다.







2. shared_ptr


shared_ptr의 특징


shared_ptr은 unique_ptr과 달리 하나의 변수를 동시에 참조가 가능 합니다.

참조하는 과정에서 shared_ptr내부적으로 count를 샙니다.

변수 num이란 놈을 shared_ptr 3개가 가르키게 된다면 count = 3이 됩니다.

그리고 count가 0이 되면 메모리 해제를 하게 됩니다.





사용방법


#include <iostream>
#include <memory>

using namespace std;

class A {
private:
    int* num;
public:
    A() : num(new int[100]) {}

    ~A() { 
        cout << "소멸자~" << endl;
        delete[] num; 
    }
    void print_ptr() { printf("%x\n", num); }

};

int main(void) {
    shared_ptr<A> sh_p1 = make_shared<A>();
    shared_ptr<A> sh_p2 = sh_p1;
    sh_p1->print_ptr();
    sh_p2->print_ptr();

    return 0;
}




실행결과




unique_ptr과 다르게 동시에 참조가 가능합니다.


그럼 보통 어느때 쓰느냐??


서로 참조해야 할 경우(스레드를 생성한 후 각 스레드에서 참조가 필요할 때)

게임을 예로 들자면 파티원을 만들 때.


하지만 shared_ptr의 가장 큰 단점이 있는데 그것은 Circular reference(순환 참조)가 일어날 수 있습니다.


이런식으로 말이죠......




이러한 문제를 해결하기 위해 


weak_ptr이란 걸 사용하게 됩니다.









3. weak_ptr 


weak_ptr 특징


weak_ptr은 shared_ptr과 같이 변수 하나를 동시에 여러게의 포인터가 참조할 수 있습니다.

단, 변수를 가르킬 때 count는 안새며 변수가 사라지면 weak_ptr는 자동으로 참조할 대상을 잃게됩니다.

때문에 shared_ptr보다 가벼우면서 Circular reference(순환 참조)를 예방할 수 있습니다.

메모리해제 기능은 없지만 말이죠.




사용 방법


#include <iostream>
#include <memory>

using namespace std;

class A {
private:
    int* num;
public:
    A() : num(new int[100]) {}

    ~A() { 
        cout << "소멸자~" << endl;
        delete[] num; 
    }
    void print_ptr() { printf("%x\n", num); }

};

void fnc(weak_ptr<Awp) {
    if (shared_ptr<A> temp = wp.lock()) {
        cout << "공유 포인터 있어요~" << endl;
        temp->print_ptr();
        cout << "(함수 안)공유 된 갯수 : " << wp.use_count() << endl;
    }
    else {
        cout << "공유 포인터 없어요~" << endl;
    }

    cout << "공유 된 갯수 : " << wp.use_count() << endl;
}

int main(void) {
    shared_ptr<A> sh_p1 = make_shared<A>();
    shared_ptr<A> sh_p2 = sh_p1;
    weak_ptr<A> wp1 = sh_p1;

    fnc(wp1);
    return 0;
}




실행결과



weak_ptr은 기본적으로 shared_ptr을 인자로 전달 받습니다.

그 후 .lock() 함수를 통해 새로운 shared_ptr을 만들어 냅니다.(기존에 전달 받은 참조 값으로 말이죠.)


main 함수를 보면 처음엔 shared_ptr이 총 2개를 가르키고 있고 fnc() 함수로 와서는

.lock() 함수 때문에 3개로 증가한 모습을 볼 수 있습니다.


.lock() 함수는 현재 weak_ptr이 가르키고 있는 공유 변수가 존재할 경우 값을 반환해주며 없을 경우엔 false를 반환합니다.

딱 봐도 안전하게 관리될 수 있는 모습이 보입니다.


해당 if문을 빠져나온 순간 .lock()으로 반환된 shared_ptr은 사라지고 "공유된 갯수 : 2"가 출력되는걸 확인할 수 있습니다.



weak_ptr기능중엔 .expired() 함수가 있는데, 이 함수는 weak_ptr이 전달받은 shared_ptr이 존재하는지 안하는지 확인하는 변수입니다.

이 기능들을 통해 좀 더 안전하게 포인터를 관리할 수 있습니다.






















반응형