class A
{
int* data;
public:
A()
{
data = new int[100];
std::cout << "자원을 획득함!" <<std::endl;
}
~A()
{
std::cout << "소멸자 호출!" << std::endl;
delete[] data;
}
};
void do_something() { A* pa = new A(); }
int main()
{
do_something();
}
unique_ptr
는 포인터를 대신하는 객체로, 영역을 벗어나면 자동으로 소멸자가 호출되고 할당된 메모리 삭제Data* data = new Data();
Data* data2 = data;
// data : 사용이 끝났으니 소멸 해야지
delete data;
// data2 : 나도 사용 다 했으니 소멸해야지
delete data2;
unique_ptr
unique_ptr
의 특징
unique_ptr
는 자원 소유의 독점적인 책임을 갖는 스마트 포인터로, 복사 생성자와 할당 연산자가 삭제되어 있어 복사를 방지하고 이동 의미구조 강조 std::unique_ptr<int> up1(new int);
std::unique_ptr<int> up2(up1); // 컴파일 에러
복사 생성자와 할당 연산자가 삭제되어 있어 독점 포인터 복사 시 컴파일 에러 발생
자원의 독점적인 소유를 보장하기 위해 복사를 허용하지 않음
unique_ptr<int> cp(unique_ptr<int>(new int));
이동 의미구조를 활용하여 독점 포인터를 생성하고 초기화합니다.
unique_ptr<int>(new int)
는 rvalue로 취급되며, 이를 이동 생성자를 이용해 새로운 독점 포인터에 전달
이렇게 하면 소유권이 이전되고, 복사 대신 효율적인 이동이 이루어짐
class unique_ptr // 부분적으로 보여지는 인터페이스
{
public:
unique_ptr(unique_ptr &&other); // 여기에서 rvalue를 묶는다.
private:
unique_ptr(const unique_ptr &other);
};
unique_ptr
클래스는 이동 의미구조를 사용하기 위해 이동 생성자 제공
이동 생성자는 rvalue 참조를 매개변수로 받아와서 소유권을 이전하는 역할
복사 생성자는 삭제되어 있어 복사를 막고, 이동 생성자를 통해 효율적인 자원 이전
/*
unique_ptr
를 할당할 때 이동 의미구조가 사용된다.std::unique_ptr<int> up1(new int);
std::unique_ptr<int> up2(up1); // 컴파일 에러
unique_ptr
의 복사 생성자가 private 멤버이기 때문unique_ptr
는 rvalue 참조로부터 *할당하고 초기화하는 편의 기능을 제공*class unique_ptr // 부분적으로 보여지는 인터페이스
{
public:
unique_ptr(unique_ptr &&other); // 여기에서 rvalue를 묶는다.
private:
unique_ptr(const unique_ptr &other);
};
다음 예제는 이동 의미구조를 사용한다. 그래서 올바르게 컴파일된다.unique_ptr<int> cp(unique_ptr<int>(new int));
unique_ptr
는 동적으로 사용 가능한 메모리만 가리켜야 함unique_ptr
가 가리키면 안 됨unique_ptr
의 인터페이스는 이런 일이 일어나지 않도록 설계됨unique_ptr
객체가 영역을 벗어나면 자신이 가리키는 메모리를 삭제하고 할당된 그 메모리를 가리키는 다른 객체들도 즉시 날 포인터로 바꾼다.*/
unique_ptr<int> ptr1(new int(42));
unique_ptr<int> ptr2(std::move(ptr1));
ptr1
은 더 이상 소유권을 갖지 않고, ptr2
가 ptr1
의 소유권을 얻게 됨
동적으로 할당된 자원을 효과적으로 이동하면서 소유 권한을 전환 시 유용
std::unique_ptr
를 사용하여 파생 클래스(Derived
) 객체를 기본 클래스(Base
) 포인터로 독점적으로 소유하는 방법
다형성을 사용하지 않고, Derived
클래스의 소멸자가 자동으로 호출되는 것 확인 가능
deleter
함수는 Base
포인터를 받아서 static_cast
를 사용하여 Derived
포인터로 형변환한 후 delete
연산자를 통해 메모리를 해제unique_ptr
가 소유한 포인터를 get
함수로 가져와서 static_cast
를 사용하여 다시 Derived
포인터로 형변환한 후, process
멤버 함수를 호출합니다. 이 때, process
함수는 Derived
클래스에 존재하는 함수로 정상적으로 호출됩니다.unique_ptr
는 소유한 객체가 범위를 벗어나면 자동으로 소멸자를 호출합니다. 이 예제에서는 main
함수가 종료될 때 unique_ptr
객체 bp
가 범위를 벗어나면서 ~Derived
가 호출되어 Derived
객체의 메모리가 정상적으로 해제Derived
클래스가 Base
로부터 파생될 때, 새로 할당된 Derived
클래스 객체는 unique_ptr<Base>
에 할당할 수 있다.
Base
에 대하여 가상 소멸자를 정의할 필요가 없다.
unique_ptr
의 정의에 deleter
함수의 주소를 제공하기만 하면 독점 포인터 객체는 Base *
포인터를 돌려준다.
이 포인터를 그냥 정적으로 Derived
유형으로 변환하면 되며, Derived
클래스의 소멸자도 자동으로 호출된다.
class Base
{ ... };
class Derived: public Base
{
...
public:
// Derived에 void process() 멤버가 있다고 가정한다.
static void deleter(Base *bp);
};
void Derived::deleter(Base *bp)
{
delete static_cast<Derived *>(bp);
}
int main()
{
unique_ptr<Base, void (*)(Base *)> bp(new Derived, &Derived::deleter);
static_cast<Derived *>(bp.get())->process(); // OK!
} // 여기에서 ~Derived가 호출된다. 다형성을 요구하지 않는다.
unique_ptr
클래스에서 제공하는 여러 멤버 함수로 포인터 자체에 접근하거나 unique_ptr
가 또다른 메모리 블록을 가리키도록 만들 수 있다.
unique_ptr
정의하는 세가지 방법
unique_ptr
를 생성한다.unique_ptr<type> identifier;
unique_ptr
를 초기화한다.unique_ptr
인자는 더 이상 동적으로 할당된 메모리를 가리키지 않는다.unique_ptr<type> identifier(another unique_ptr for type);
unique_ptr
를 초기화하는 것deleter
)를 제공할 수 있음unique_ptr
의 포인터를 인자로 받는 자유 함수를 또는 함수객체를 소멸자로 건넬 수 있다.unique_ptr<type> identifier (new-expression [, deleter]);
unique_ptr
의 기본 생성자는 특정한 메모리 블록을 가리키지 않는다.
unique_ptr<type> identifier;
unique_ptr
가 제어하는 포인터는 0으로 초기화된다.
unique_ptr
객체 자체는 포인터가 아니지만 그 값은 0 과 비교할 수 있다
unique_ptr<int> ip;
if (!ip)
cout << "0-pointer with a unique_ptr object\n";
unique_ptr
는 유형이 같으면 rvalue 참조를 사용하여 초기화할 수 있다.
unique_ptr<type> identifier(other unique_ptr object);
다음 예제에 이동 생성자가 사용된다.
void mover(unique_ptr<string> &¶m)
{
unique_ptr<string> tmp(move(param));
}
비슷하게 할당 연산자를 사용할 수 있다.
unique_ptr
객체는 유형이 같은 임시 unique_ptr
객체에 할당할 수 있다.
역시 이동-의미구조가 사용된다.
#include <iostream>
#include <memory>
#include <string>
using namespace std;
int main()
{
unique_ptr<string> hello1(new string("Hello world"));
unique_ptr<string> hello2(move(hello1));
unique_ptr<string> hello3;
hello3 = move(hello2);
cout << // *hello1 << /\n' << // 세그먼트 폴트 에러가 일어날 것임
// *hello2 << '\n' << // 마찬가지
*hello3 << '\n';
}
// 출력: Hello world
hello1
은 동적으로 할당된 string
을 가리키는 포인터로 초기화unique_ptr
hello2
는 이동 생성자를 사용하여 hello1
이 제어하는 포인터를 획득hello1
을 0-포인터로 바꿈hello3
이 기본 unique_ptr<string>
로 정의된다.hello2
으로부터 획득한다. 결과적으로 역시 0-포인터로 바뀐다.hello1
이나 hello2
가 cout
으로 삽입되면 세그먼트 폴트 에러가 일어날 것이다.
0-포인터를 역참조했기 때문이다. 결국, hello3
만 실제로 원래 할당된 string
을 가리킨다.
주로 동적으로 할당된 메모리를 가리키는 포인터를 사용하여 초기화됨
unique_ptr<type [, deleter_type]> identifier(new-expression
[, deleter = deleter_type()]);
두 번째 deleter(_type)
인자는 선택적
할당된 메모리를 반납하는 자유 함수나 함수 객체를 참조
이중 포인터가 할당되어 있고 내포된 포인터마다 방문하여 할당된 메모리를 파괴하는 소멸자
다음 예제는 문자열 객체를 가리키는 독점 포인터를 초기화
unique_ptr<string> strPtr(new string("Hello world"));
생성자에 건네어진 인자는 operator new
가 돌려준 포인터이다.
유형에 그 포인터가 언급되지 않는 것을 눈여겨보라.
unique_ptr
의 생성에 사용된 유형은 new
표현식에 사용된 유형과 똑같다.
명시적으로 소멸자를 정의하여 어떻게 동적으로 할당된 문자열을 가리키는 포인터 배열 삭제
struct Deleter
{
size_t d_size;
Deleter(size_t size = 0) : d_size(size){}
void operator()(string **ptr) const
{
for (size_t idx = 0; idx < d_size; ++idx)
delete ptr[idx];
delete[] ptr;
}
};
int main()
{
unique_ptr<string *, Deleter> sp2(new string *[10], Deleter(10));
Deleter &obj = sp2.get_deleter();
}
unique_ptr
를 사용하면 new
표현식에 의해 할당된 실체의 멤버 함수에 도달할 수 있다.
unique_ptr
는 마치 동적으로 할당된 객체를 가리키는 평범한 포인터인 것처럼 이런 멤버에 도달할 수 있다.
다음 프로그램은 hello`' 단어 뒤에
C++`' 문자열을 삽입한다.
#include <iostream>
#include <memory>
#include <cstring>
using namespace std;
int main()
{
unique_ptr<string> sp(new string("Hello world"));
cout << *sp << '\n';
sp->insert(strlen("Hello "), "C++ ");
cout << *sp << '\n';
}
/*
출력:
Hello world
Hello C++ world
*/
unique_ptr
는 다음 연산자를 제공한다.
unique_ptr<Type> &operator=(unique_ptr<Type> &&tmp)
:
이 연산자는 이동 의미구조를 사용하여 rvalue
unique_ptr
가 가리키는 메모리를 lvalueunique_ptr
로 이전그래서 rvalue 객체는 자신이 가리키던 메모리를 잃어버리고 0-포인터로 바뀜
기존의
unique_ptr
를 또다른unique_ptr
에 할당할 수 있다.std::move
를 사용하여 먼저 rvalue 참조로 변환unique_ptr<int> ip1(new int); unique_ptr<int> ip2; ip2 = std::move(ip1);
Type &operator*()
:
이 연산자는 독점 포인터를 통하여 접근할 수 있는 정보를 참조로 돌려준다.
마치 평범한 포인터 역참조(dereference) 연산자처럼 행위한다.
Type *operator->()
:
이 연산자는 독점 포인터를 통하여 접근할 수 있는 정보를 포인터로 돌려준다.
이 연산자로 독점 포인터를 통하여 객체에 접근해 멤버를 선택할 수 있다.
unique_ptr<string> sp(new string("hello")); cout << sp->c_str();
unique_ptr
는 다음의 멤버 함수를 제공한다.
Type *get()
:독점 포인터가 통제하는 정보를 포인터로 돌려준다. 마치
operator->
처럼 행위한다.반환된 포인터를 조사할 수 있다. 만약 0이면 독점 포인터는 아무 메모리도 가리키지 않는다.
Deleter &unique_ptr<Type>::get_deleter()
:독점 포인터가 사용하는 소멸자를 참조로 돌려준다.
Type *release()
:독점 포인터를 통하여 접근할 수 있는 정보를 포인터로 돌려준다. 동시에 객체 자체는 0-포인터가 된다.
void reset(Type *)
:독점 포인터가 통제하는 동적으로 할당된 메모리를 공용 풀에 반납한다. 그 때부터 이 멤버 함수에 건넨 인자가 가리키는 메모리는 독점 포인터가 통제한다. 이 멤버 함수는 인자 없이 호출할 수도 있다. 그러면 독점 포인터는 0-포인터로 바뀐다. 이 멤버 함수를 사용하면 동적으로 할당된 새 메모리 블록을 독점 포인터에 할당할 수 있다.
동적으로 할당된 배열에 다음 구문을 사용할 수 있다.
[]
) 사용하여 동적으로 할당된 배열을 스마트 포인터가 통제하도록 지정한다.unique_ptr<int[]> intArr(new int[3]);
intArr[2] = intArr[0];
이 경우 스마트 포인터의 소멸자는 delete
가 아니라 delete[]
를 호출
공유 포인터를 정의하는 방법 네 가지
shared_ptr<type> identifier;
shared_ptr<string> org(new string("hi there"));
shared_ptr<string> copy(org); // 참조 횟수는 이제 2이다.
grabber
를 생성한다.grabber
의 생성자는 익명의 임시 객체를 받으므로 컴파일러는 공유 포인터의 이동 생성자를 사용한다.shared_ptr<string> grabber(shared_ptr<string>(new string("hi there")));
deleter
)를 제공할 수 있다.shared_ptr<type> identifier (new-expression [, deleter]);
shared_ptr<type> identifier;
shared_ptr<type> identifier(new-expression [, deleter]);
두 번째 인자(deleter
)는 선택적 할당된 메모리를 파괴하는 자유 함수나 함수 객체를 참조 이중 포인터가 할당되고 내포된 포인터마다 방문하면서 할당된 메모리를 파괴하는 소멸자 독점 포인터가 마주하는 상황에 사용 다음은 문자열 객체를 가리키는 공유 포인터를 초기화하는 예이다. shared_ptr<string> strPtr(new string("Hello world"));
생성자에 건네진 인자는 operator new
가 반환하는 포인터이다. 공유 포인터 생성에 사용된 유형은 new
표현식에 사용된 유형과 똑같다. 두 개의 공유 포인터가 실제로 정보를 공유하는 것을 보여준다. 한쪽에서 정보를 변경하면 변경된 정보가 다른 쪽에 보인다. int main()
{
shared_ptr<string> sp(new string("Hello world"));
shared_ptr<string> sp2(sp);
sp->insert(strlen("Hello "), "C++ ");
cout << *sp << '\n' <<
*sp2 << '\n';
}
/*
출력:
Hello C++ world
Hello C++ world
*/
공유 포인터와 조합하여 표준 C++ 스타일로 유형을 변환할 때 주의할 점
struct Base
{};
struct Derived: public Base
{};
새로 할당된 Derived
클래스 실체를 저장하기 위하여 shared_ptr<Base>
를 정의하면 Base *
유형이 반환된다.
이 반환 유형은 단독 포인터처럼 static_cast
를 사용하여 Derived *
으로 형변환할 수도 있다.
다형성은 요구하지 않는다. 공유 포인터를 재초기화하거나 공유 포인터가 영역을 벗어나더라도 슬라이싱(복사손실)은 일어나지 않는다.
Derived
객체는 Base
객체이기도 하므로 굳이 형변환하지 않아도 Derived
를 가리키는 포인터는 Base
를 가리키는 포인터로 간주해도 된다.
그러나 static_cast
를 사용하면 Derived *
를 Base *
로 번역하도록 강제할 수 있다.
Derived d;
static_cast<Base *>(&d);
그렇지만 Derived
객체를 가리키는 공유 포인터의 get
멤버를 사용하여 공유 포인터를 Base
로 변환할 때 평범한 static_cast
는 사용할 수 없다.
다음 코드는 결국 동적으로 할당된 Base
객체를 두 번 파괴하려고 시도하게 될 것이다.
shared_ptr<Derived> sd(new Derived);
shared_ptr<Base> sb(static_cast<Base *>(sd.get()));
sd 와 sb 는 같은 객체를 가리키기 때문에 sb 와 sd 가 영역을 벗어나면 ~Base
소멸자가 두 번 호출된다.
그 때문에 프로그램은 이르게 종료하게 된다.두 번 해제하는 에러가 일어나기 때문이다.
이 에러는 공유 포인터와 함께 사용되도록 특별히 설계된 형변환을 사용하면 방지할 수 있다.
이 형변환은 특정화된 생성자를 사용한다.
생성된 공유 포인터는 메모리를 가리키지만 소유권을 (즉, 참조 횟수를) 기존의 공유 포인터와 공유한다.
std::static_pointer_cast<Base>(std::shared_ptr<Derived> ptr)
:
Base
클래스를 가리키는 공유 포인터를 돌려준다. 반환된 공유 포인터는shared_ptr<Derived> ptr
가 참조하는Derived
클래스에서 바탕 클래스 부분을 참조한다.shared_ptr<Derived> dp(new Derived()); shared_ptr<Base> bp = static_pointer_cast<Base>(dp);
std::const_pointer_cast<Class>(std::shared_ptr<Class const> ptr)
:
Class
클래스를 가리키는 공유 포인터를 돌려준다. 반환된 공유 포인터는 가변Class
객체를 참조한다. 반면에ptr
인자는Class const
객체를 참조한다.shared_ptr<Derived const> cp(new Derived()); shared_ptr<Derived> ncp = const_pointer_cast<Derived>(cp);
std::dynamic_pointer_cast<Derived>(std::shared_ptr<Base> ptr)
:
Derived
클래스 객체를 가리키는 공유 포인터를 돌려준다.Base
클래스는 적어도 하나의 가상 멤버 함수를 가져야 하며,Base
를 상속받은Derived
클래스는Base
의 가상 멤버를 재정의할 수도 있다.Base *
으로부터Derived *
으로 동적 변환에 성공하면 반환된 공유 포인터는Derived
클래스 객체를 참조한다. 동적 형변환에 실패하면 공유 포인터의get
멤버는 0을 돌려준다. 예를 들어 (Derived
와Derived2
를Base
로부터 상속받았다고 가정함):shared_ptr<Base> bp(new Derived()); cout << dynamic_pointer_cast<Derived>(bp).get() << ' ' << dynamic_pointer_cast<Derived2>(bp).get() << '\n';
첫 번째
get
은 0-아닌 포인터를 돌려준다. 두 번째get
은 0을 돌려준다.
shared_array
클래스를 만드는 것은 어렵지 않다. shared_ptr
클래스로부터 상속받아 shared_array
클래스 템플릿을 만들고 거기에 소멸자만 주면 배열과 그의 원소들을 파괴해 준다. 게다가 인덱스 연산자를 정의하면 선택적으로 delete
를 사용하여 역참조 연산자를 선언할 수 있다. 다음은 어떻게 shared_array
를 정의하고 사용하는지 보여준다. struct X
{
~X()
{
cout << "destr\n"; // 객체의 소멸을 보여준다.
}
};
template <typename Type>
class shared_array: public shared_ptr<Type>
{
struct Deleter // 소멸자는 포인터를 받는다.
{ // 그리고 delete[]를 호출한다.
void operator()(Type* ptr)
{
delete[] ptr;
}
};
public:
shared_array(Type *p) // 다른 생성자는
: // 여기에 없다.
shared_ptr<Type>(p, Deleter())
{}
Type &operator[](size_t idx) // 인덱스 연산자
{
return shared_ptr<Type>::get()[idx];
}
Type const &operator[](size_t idx) const
{
return shared_ptr<Type>::get()[idx];
}
Type &operator*() = delete; // 포인터가 없는 멤버는 제거한다.
Type const &operator*() const = delete;
Type *operator->() = delete;
Type const *operator->() const = delete;
};
int main()
{
shared_array<X> sp(new X[3]);
sp[0] = sp[1];
make_shared' 그리고
make_unique' std::shared_ptr<string> sptr(new std::string("hello world"))
이 서술문은 메모리를 두 번 **할당한다. 하나는 std::string
를 할당하고, 다른 하나는 내부적으로 공유 포인터의 생성자 자체에서 사용 make_shared
템플릿을 사용하면 두 번의 할당을 하나의 할당으로 조합할 수 있음 std::make_shared
함수 템플릿 원형: template<typename Type, typename ...Args>
std::shared_ptr<Type> std::make_shared(Args ...args);
이 함수 템플릿은 Type
유형의 객체를 할당하고 args
를 생성자에 건네며 새로 할당된 Type
객체의 주소로 초기화된 공유 포인터를 돌려준다. 다음은 std::make_shared
를 사용하여 위의 sptr
객체를 초기화하는 방법 auto sptr(std::make_shared<std::string>("hello world"));
이렇게 초기화하고 나면 std::shared_ptr<std::string> sptr
이 정의되고 초기화 `std::cout << *sptr << '\n';` 같이 쓰일 수 있음
C++14 표준은 또 std::make_unique
을 제공한다. 이를 make_shared
처럼 사용할 수 있지만 공유 포인터가 아니라 독점 포인터를 만들어 준다.