관리 메뉴

기억을 위한 기록들

[CPP]스마트(Smart) 포인터에 관하여(1) - unique_ptr 본문

C & CPP/Smart Pointer

[CPP]스마트(Smart) 포인터에 관하여(1) - unique_ptr

에드윈H 2021. 2. 16. 20:53

스마트 포인터는 3가지가 있다.

- unique_ptr (유니크)
- shared_ptr (쉐어드)
- weak_ptr (위크)

 

 

 

우선 일반 포인터를 보면

//...

int main()
{
    Vector* myVector = new Vector(10.f, 30.f);
    
    //...
    
    delete myVector;   
    
    return 0;
}

이런식으로 delete를 해줘야 한다. 그러나

//...

int main()
{
    Vector* myVector = new Vector(10.f, 30.f);
    
    if(true) 
    {    
        return 0; //early return
    }
    
    delete myVector;   
    
    return 0;
}

중간에 끝나버리거나(try catch와 같은), delete을 잊으면 메모리 누수(memory leak)가 발생한다. "나중에 써야지.." 하

는것보다 바로 쓰는게 낫다.

 

뭐가 문제일까?

- 포인터가 필요하지 않을 때 메모리를 해제 해야함.

 

스마트 포인터를 쓰면 delete를 직접 호출할 필요가 없다. 그리고 가비지 컬렉션보다 빠르다.

 

 

3가지를 살펴보자

 

 

1. unique_ptr (유니크 포인터) (C++11)

 유니크하다. 하나 밖에 없다 라는 일반적인 뜻으로 소유자는 나 말고 못쓰게 만드는 포인터

 

유니크 포인터를 만드는 것을 보면

 

#include <memory>
#include "Vector.h"

int main()
{
   std::unique_ptr<Vector> myVector(new Vector(10.f,30.f)); //* 부호가 없음
   
   myVector->Print(); //그러나 포인터처럼 동작한다.   
   
   return 0; //delete는 없다.
}

위 예는 벡터형 유니크포인터라고 한다.

 

유니크 포인터의 특징은

- 포인터(원시 포인터라고 하자)를 단독으로 소유
- 원시(naked)포인터는 누구하고도 공유되지 않음.
- 따라서 복사대입불가
- 유니크포인터는 범위(scope)를 벗어날 때, 원시 포인터는 알아서 지워짐(delete).

 

유니포인트를 사용하기 좋은 경우 3가지

 

1. 클래스에서 생성자/소멸자

Class Player
{
private:
    std::unique_ptr<Vector> mLocation;
};

Player::Player(std::string name)
     : mLocation(new Vector()) //초기화만 해주면 된다.
{
}

//소멸자도 없고 delete도 없다.

한가지 단점으로는 해당 클래스는 다른클래스가 상속받을때 가상소멸자가 없어서 귀찮아진다. 근데 오버라이딩로 해결 가능.

 

 

2. 지역 변수 쓸 때

#include <memory>
#include "Vector.h"

int main()
{
   std::unique_ptr<Vector> myVector(new Vector(10.f,30.f)); 
   
   myVector->Print(); 
   
   //...
}

예를 들어 스택에 1mb 잡혀 있는데  10mb 넣으려고 하면 문제가 생긴다. 이럴 때 어쩔수 없이 힙메모리에서 가져와야하는데 이럴때 유니크 메모리를 쓰면 해결 된다.

 

 

3. STL벡터에 포인터 저장하기

#include <memory>
#include "Player.h"

int main()
{ 

   std::vector<std::unique_ptr<Player>> playerList;
   
   playerList.push_back(std::unique_ptr<Player>(new Player("lala")));
   playerList.push_back(std::unique_ptr<Player>(new Player("nono")));
   
   playerList.clear(); //벡터 비울때 알아서 같이 해제됨
   
   //for(int i=0;i<playerList.size();i++)
   //{
   //     delete playerList[i]; //원시 포인터 였다면 이런식으로 해제 해줘야함.
   //}
   
   //...
}

 

 

문제점 : 원시 포인터와 공유

Vector* vectorPtr = new Vector(10.f, 30.f); //1.원시포인터 할당했다
std::unique_ptr<Vector> vector(vectorPtr);  //2. 유니크 포인터에 복사해서 둘다 같은 주소값 가짐.
std::unique_ptr<Vector> anotherVector(vectorPtr); // 3.여기서 또 다른 유니크포인터가 위 2개와 주소값을 가짐.
anotherVector=null; //4. 그래서 지워줬더니  vectorPtr하고 vector둘다 주소가 가리키는 값이 지워짐.

 

 

그래서 C++14이후 해결책

 

#include <memory>
#include "Vector.h"

int main()
{ 
   //힙 할당 불필요. 단지 std::make_unique 보여주기 위함.
   std::unique_ptr<Vector> myVector = std::make_unique<Vector>(10.f,30.f);
    
   myVector->Print(); 
   return 0;
}

 

 

std::make_unique() 는 뭐 하길래?

- 주어진 매개변수와 자료형으로 new 키워드를 호출해줌
- 따라서 원시 포인터와 같음
- 둘 이상의 std::unique_ptr가 원시 포인터를 공유할수 없도록 막아준다. 

위 문제점 예시가 컴파일 난다.

Vector* vectorPtr = new Vector(10.f, 30.f); 
std::unique_ptr<Vector> vector1 =std::make_unique<Vector>(vectorPtr ); //컴파일 에러!    
//std::unique_ptr<Vector> anotherVector(vectorPtr); 
//anotherVector=null; 

 

C+11에서는

std::unique_ptr<Vector> myVector(new Vector(10.f,30.f));
std::unique_ptr<Vector[]> myVectorArr(new Vector[]);

C+14에서는 make_unique 이다. 

std::unique_ptr<Vector> myVector = std::make_unique<Vector>(10.f,30.f);
std::unique_ptr<Vector[]> myVectorArr = std::make_unique<Vector[]>(20);

 

유니크포인터 사용 함수

1. reset() : 포인터를 교체해준다. 재설정 될때, 소유하고 있던 원시포인터는 자동으로 소멸(null)

int main()
{
     std::unique_ptr<Vector> myVector = std::make_unique<Vector>(10.f,30.f);
     myVector.reset(new Vector(20.f,40.f)); //원래 있던걸 해제하고 새로 할당
     myVector.reset(); //원래 갖고 있던거 해제하고 null 
     //...
}

 

vector.reset(); 하고 vector = null; 은 같다. 개인취향이긴 한데 후자가 좀더 보기 좋다

2. get() : 내부에서 갖고 있는 원시 포인터 반환한다.

int main()
{
     std::unique_ptr<Vector> myVector = std::make_unique<Vector>(10.f,30.f);
     Vector* ptr= myVector.get();
    
     //...
}

 

3. release() : 소유권 이전(박탈하기)/ 잘쓰지 않음 (좋은 함수는 아니다.)

int main()
{
     std::unique_ptr<Vector> myVector = std::make_unique<Vector>(10.f,30.f);
     Vector* ptr= myVector.release();
    
     //...
}

 

relase함수보다는 아래의 방법이 낫다.

 

소유권 이전하기 관련

move 함수 사용

int main()
{
    std::unique_ptr<Vector> myVector = std::make_unique<Vector>(10.f,30.f);
    std::unique_ptr<Vector> anotherVector(std::move(myVector));
    
     //...
}

myVector가 가리키건 포인터를 anotherVector가 가리키고 myVector는 null이 된다. relase() 보다 안전함.

 

예외로는 const 유니크 포인터가 있다.

int main()
{
    const std::unique_ptr<Vector> myVector = std::make_unique<Vector>(10.f,30.f);
    //std::unique_ptr<Vector> anotherVector(std::move(myVector)); //컴파일 에러!
    
     //...
}

 

 

- std::unique_ptr는 소유한 원시 포인터를 아무하고도 공유하지 않음.
- 즉, 주소 복사를 하지 않는다는 뜻
- 대신, 소유권을 다른 std::unique_ptr로 옮길 수 있음
- 예외 : const std::unique_ptr

 

std::move 관련

- 개체A의 모든 멤버를 포기하고 그 소유권을 B에게 주는 방법
- 메모리 할당과 해제가 일어나지 않음
- 간단하게 A의 모든 포인터를 B에게 대입하고, A는 nullptr

 

 

stl 벡터에 요소 추가할때도 move 사용

#include <memory>
#include "Player.h"

int main()
{ 

   std::vector<std::unique_ptr<Player>> playerList;
   
   std::unique_ptr<Player> coco= std::make_unique<Player>("Coco");
   playerList.push_back(std::move(coco));
   
   std::unique_ptr<Player> lala= std::make_unique<Player>("Lala");
   playerList.push_back(std::move(lala));

}

 

 

정리 

- 직접 메모리 관리하는 것 만큼 빠르다.
- RAII 원칙에 잘 들어 맞음
  1. 자원 할당은 개체의 수명과 관련
  2. 생성자에서 new 그리고 소멸자에서 delete
  3. std::unique_ptr 멤버변수가 이걸 해줌
- 실수하기 어려움