C++智能指针
本文为《C++标准库》这本书的学习笔记
自C++11起,C++标准库提供两大类型的smart pointer:
- Class shared_ptr 实现共享式概念。多个smart pointer可以指向相同对象,该对象和棋相关资源会在“最后一个reference被销毁”时被释放。为了应对复杂的情境,标准库提供了weak_ptr、 bad_weak_ptr和enable_shared_from_this等辅助类。
- Class unique_ptr 实现独占式拥有或严格拥有概念,保证同一时间内只有一个smart pointer可以指向对象。
Class shared_ptr
Class shared_ptr提供了“当对象再也不被使用时就被清理”的共享式拥有语义。也就是说,多个shared_ptr可以共享同一个对象。对象的最后一个拥有者有责任销毁对象,并清理与该对象有关的所有资源。
使用shared_ptr
shared_ptr<string> pNico(new string("nico")); //OK
shared_ptr<string> pNico = new string("nico"); //ERROR
shared_ptr<string> pNico{new string("nico")}; //OK
使用赋值符,意味着需要一个隐式转化。但是,由于“接受单一pointer作为唯一实参”的构造函数是explicit,所以不能使用。然而新式初始化语法是被接受的。
也可以使用便捷函数make_shared():
shared_ptr<string> pNico = make_shared<string>("nico"); //OK
这种方式比较快,也比较安全,因为它使用一次而不是二次分配:一次针对对象,另外一次针对“shared pointer用以控制对象”的shared data。
另一种写法是,可以先声明shared pointer,然后对它赋值一个new pointer。然而你不可以使用assignment操作符,必须改用reset():
shared_ptr<string> pNico4;
pNico4 = new string("nico"); //ERROR
pNico4.reset(new string(nico)); //OK
定义一个Deleter
shared_ptr<string> pNico(new string("nico"),
[](string* p) {
cout << "delete " << *p << endl;
delete p;
});
...
pNico = nullptr;
//whoMadecoffee是一个vector<shared_ptr<string>>的容器,有3个元素
whoMadeCoffee.resize(2);
上诉伪代码会打印出:
delete nico
对付Array
shared_ptr提供的default deleter调用的是delete,不是delete[]。所以如下调用是错误的:
std::shared_ptr<int> p(new int[10]);
所以必须自己定义一个deleter。可以传递一份函数,函数对象或lambda,让它们针对传入的寻常指针调用delete[]。例如:
std::shared_ptr<int> p(new int[10],
[](int* p) {
delete[] p;
});
也可以使用为unique_ptr而提供的辅助函数作为deleter,其内部调用delete[]:
std::shared_ptr<int> p(new int[10],
std::default_delete<int[]>());
注意,shared_ptr和unique_ptr以稍稍不同的方式处理deleter。
std::unique_ptr<int[]> p(new int[10]); //OK
std::shared_ptr<int[]> p(new int[10]); //ERROR
此外,对于unique_ptr,你必须明确给予第二个template实参:
std::unique_ptr<int, void(*)(int*)> p(new int[10],
[](int* p){
delete[] p;
});
其他析构策略
如果清理工作不仅仅是删除内存,你必须明确给出自己的deleter。可以指定属于自己的析构策略。比如:
//伪代码
class Json_t_Deleter
{
void operator (json_t* ptJson) {
json_realease(ptJson);
}
}
char* strjson = "..."; //json字符串文本
std::shared_ptr<json_t> ptJson( json_load(strjson), Json_t_Deleter() );
实现说明
在典型的实现中,std::shared_ptr 只保存两个指针:
- 指向被管理对象的指针
- 指向控制块(control block)的指针
控制块是一个动态分配的对象,
其中包含:
- 指向被管理对象的指针或被管理对象本身
- 删除器
- 分配器(allocator)
- 拥有被管理对象的 shared_ptr 的数量
- 引用被管理对象的 weak_ptr 的数量
通过 std::make_shared 和 std::allocate_shared 创建 shared_ptr 时,控制块将被管理对象本身作为其数据成员;而通过构造函数创建 shared_ptr 时则保存指针。
shared_ptr 持有的指针是通过 get() 返回的;而控制块所持有的指针/对象则是最终引用计数归零时会被删除的那个。两者并不一定相等。
shared_ptr 的析构函数会将控制块中的 shared_ptr 计数器减一,如果减至零,控制块就会调用被管理对象的析构函数。但控制块本身直到 std::weak_ptr 计数器同样归零时才会释放。
make_shared和shared_ptr的区别
struct A;
std::shared_ptr<A> p1 = std::make_shared<A>();
std::shared_ptr<A> p2(new A);
区别是:std::shared_ptr构造函数会执行两次内存申请,而std::make_shared则执行一次。那个一次和两次的区别会带来什么不同的效果呢?
异常安全
考虑下面一段代码:
void f(std::shared_ptr &lhs, std::shared_ptr &rhs){...}
f(std::shared_ptr<Lhs>(new Lhs()),
std::shared_ptr<Rhs>(new Rhs())
);
因为C++允许参数在计算的时候打乱顺序,因此一个可能的顺序如下:
- new Lhs()
- new Rhs()
- std::shared_ptr
- std::shared_ptr
此时假设第2步出现异常,则在第一步申请的内存将没处释放了,上面产生内存泄露的本质是当申请数据指针后,没有马上传给std::shared_ptr,因此一个可能的解决办法是:
auto lhs = std::shared_ptr<Lhs>(new Lhs());
auto rhs = std::shared_ptr<Rhs>(new Rhs());
f(lhs, rhs);
而一个比较好的方法是使用 std::make_shared 。
f(std::make_shared<Lhs>(),
std::make_shared<Rhs>()
);
make_shared的缺点
因为make_shared只申请一次内存,因此控制块和数据块在一起,只有当控制块中不再使用时,内存才会释放,但是weak_ptr却使得控制块一直在使用。
Class weak_ptr
shared_ptr会自动释放"不再被需要的对象"的相应资源,但是有些时候却存在问题:
- cyclic reference(环式指向)。两个对象使用shared_ptr互相指向对方,这种情况下shared_ptr不会释放数据。
- 如果你“明确想共享但不愿拥有”某对象的情况。你的语义是:reference的寿命比其所指的寿命更长。如果使用寻常pointer可能不会注意到它们指向的对象已经不再有效,导致“访问已被释放的数据”的风险。
于是标准库提供了class weak_ptr,允许你“共享但不拥有”某个对象。一旦最末一个拥有该对象的shared pointer失去了拥有权,任何weak pointer就会自动成空。因此,在default和copy构造函数之外,class weak_ptr只提供“接受一个shared_ptr”的构造函数。
你不能够使用操作符*和->访问weak_ptr指向的对象。而是必须建立一个shared_pointer。这是合理的设计,两个理由:
- 在weak pointer之外建立一个shared pointer可因此检查是否存在一个相应对象。如果不,操作会抛出异常或建立一个empty shared pointer。
- 当指向的对象正在被处理是,shared pointer无法被释放。
如下例子:
class A{
public:
int m_a;
shared_ptr<B> m_ptB;
};
class B{
public:
int m_b;
shared_ptr<A> m_ptA;
};
void fn()
{
shared_ptr<A> ptA(new A);
shared_ptr<B> ptB(new B);
ptA.m_ptB.reset(ptB);
ptB.m_ptA.reset(ptA);
}
当fn函数执行完了以后,指向A对象和B对象的user_count都为1。如果,将class B中的shared_ptr<A> m_ptA;
修改为weak_ptr<A> m_ptA
就可以解决问题。
class B{
public:
int m_b;
weak_ptr<A> m_ptA;
};
使用weak_pointer时,我们必须轻微改变被指向对象的访问方式。不应该使用以下调用形式:
m_ptA->m_a
应该在式子内加上lock():
m_ptA.lock()->m_a
这会导致新产生一个得自于weak_ptr m_ptA的shared_ptr。如果这时对象的最末拥有也在此时释放了对象----lock()会产生一个empty shared_ptr。这种情况下调用*或->操作符会引发不明确行为。
如果不确定隐身于weak pointer背后的对象是否存活,可以有以下选择:
- 调用expired(),它会在weak_ptr不再共享对象时返回true。这等同于检查use_count()是否为0,但速度较快。
- 可以使用相应的shared_ptr构造函数明确将weak_ptr转化为一个shared_ptr。如果对象已经不存在,该构造函数会抛出一个bad_weak_ptr异常,那是一个派生自std:exception的异常,其what()会产生"bad_weak_ptr"。
- 调用use_count(),询问相应对象的拥有者数量。返回0表示不存在任何有效对象。但是,通常只应为了吊事而调用use_count();C++标准库明确告诉我们:“use_count并不总是很有效率。”
误用Shared Pointer
使用者必须确保某对象只被一组shared pointer拥有。下列代码错误:
int* p = new int;
shared_ptr<int> sp1(p);
shared_ptr<int> sp2(p); //ERROR
问题出在sp1,sp2都会在对视p的拥有权时释放相应资源(即调用delete)。所以应该在创建对象和其相应资源的那一刻直接设立smart pointer:
shared_ptr<int> sp1(new int);
shared_ptr<int> sp2(sp1); //OK
考虑一下代码:
shared_ptr<Person> mom(new Person(name+"'s mom"));
shared_ptr<Person> dad(new Person(name+"'s dad"));
shared_ptr<Person> kid(new Person(name));
ked->setParentsAndTheirKids(mom,dad);
class Person {
public:
stirng name;
shared_ptr<Person> mother;
shared_ptr<Person> father;
vector<weak_ptr<Person>> kids;
void setParentsAndTheirKids(shared_ptr<Person> m = nullprt
shared_ptr<Person> f = nullptr) {
mother = m;
father = f;
if (m != nullptr){
m->kids.push_back(shared_ptr<Person>(this)); //ERROR
}
if (f != nullprt){
f->kids.push_back(shared_ptr<Person>(this)); //ERROR
}
}
}
问题出在“得自this的那个shared pointer”的建立。我们需要一个shared pointer指向kid,而我们手上没有。如果,根据this建立起一个新的shared pointer并不能解决问题,因为这样一来就开启了一个新的拥有者团队。
解决方法之一:讲指向kid的那个shared pointer传递为第三参数。但C++标准库提供了另一个选项:class std::enable_shared_from_this<>。
可以从std::enable_shared_from_this<>派生出你自己的class,表现出“被shared pointer管理”的对象;做法是讲class名称当做template实参传入。然后你就可以使用shared_from_this建立起一个源自this的正确shared_ptr。
class Person : std::enable_shared_from_this<Person> {
public:
stirng name;
shared_ptr<Person> mother;
shared_ptr<Person> father;
vector<weak_ptr<Person>> kids;
void setParentsAndTheirKids(shared_ptr<Person> m = nullprt
shared_ptr<Person> f = nullptr) {
mother = m;
father = f;
if (m != nullptr){
m->kids.push_back(shared_from_this());
}
if (f != nullprt){
f->kids.push_back(shared_from_this());
}
}
}
注意,不能再构造函数内调用shared_from_this。因为shared_ptr本身被存放于Person的base class,所以,在初始化shared pointer的那个对象的构造期间,绝对无法建立shared pointer的循环引用。
转型
cast操作符可将一个pointer转为不同类型。其语义与其所对应的操作符相同,得到的是不同类型的另一个shared pointer。不可以使用寻常的cast操作符,因为那会导致不明确行为:
shared_ptr<void> sp(new int);
...
shred_ptr<int>(static_cast<int*>(sp.get())) //ERROR
static_pointer_cast<int*>(sp) //OK
Class unique_ptr
这个smart pointer实现了独占式拥有概念,意味着它可确保一个对象和其相应资源同一时间只被一个pointer拥有。一旦拥有者被销毁或变成empty,或开始拥有另外一个对象,先前拥有的那个对象就会被销毁,其任何相应资源亦会被释放。
Class unique_ptr继承class auto_ptr,后者由C++98引入但已不再被认可。Class unique_ptr提供了一个简明干净的接口,比auto_pointer更不易出错。
Class unique_ptr的目的
如下程序可能会有诸多问题。一个明显的问题是,有可能忘记delete对象,特别是如果你在函数中有个return语句。另外一个较不明显的危险是它可能抛出异常。那将立刻退离函数,末尾的delete语句也就没机会被调用,会导致内存泄漏。
void f()
{
ClassA* ptr = new ClassA;
...
delete ptr;
}
使用智能指针解决问题:
#include <memory>
void f()
{
std::unique<ClassA> ptr(new ClassA);
...
}
使用unique_ptr
unique_ptr有着与寻常pointer非常相似的接口,操作符*用来提领(dereference)指向对象,操作符->用来访问成员----如果被指向的对象来自class或struct。但是不提供point算术如++等。poniter算术运算符往往是麻烦的来源。
std::unique_ptr<std::string> up(new std::string("nico"));
(*up)[0] = 'N';
up->append("lai");
std::cout << *up << std::endl;
class unique_ptr<>不允许你以赋值语法将一个寻常的pointer当作初值。因此必须直接初始化:
std::unique_ptr<int> up = new int; //ERROR
std::unique_ptr<int> up(new int); //OK
unique_ptr不比一定拥有对象,也可以是empty。例如当它被default构造函数创建出来:
std::unique_ptr<std::string> up;
你也可以对它赋予nullptr,调用reset()或调用release()进行释放:
up = nullptr;
up.reset();
up.release();
你可以调用操作符bool()用以检查是否unique pointer拥有对象,也可以拿unique pointer和nullprt比较,或查询unique_ptr内的raw pointer:
if(up) {
std::cout << *up << std::endl;
}
if (up != nullptr)
if (up.get() != nullptr)
转移unique_ptr的拥有权
unique_ptr提供的语义是“独占式拥有”。但需要程序员来确保“没有两个unique pointer以同一个pointer作为初值”。以下程序为运行期错误:
std::string* sp = new std::string("hello");
std::unique_ptr<std::string> up1(sp);
std::unique_ptr<std::string> up2(sp); //ERROR
不可以对unique_ptr执行copy或assign。但是C++11起提供的move语义,可以使用copy构造函数或assignment操作符会将拥有权移交给另一个unique pointer。
std::unique_ptr<ClassA> up1(new ClassA);
std::unique_ptr<ClassA> up2(up1); //ERROR,编译期错误
std::unique_ptr<ClassA> up3(std::move(up1)); //OK
Assignment操作符的行为和上面所说很类似:
std::unique_ptr<ClassA> up1(new ClassA);
std::unique_ptr<ClassA> up2;
up2 = up1;
up2 = std::move(up1);
如果上述赋值动作之前up2原本拥有对象,会有一个delete动作被调用,删除该对象:
std::unique_ptr<ClassA> up1(new ClassA);
std::unique_ptr<ClassA> up2(new ClassA);
up2 = std::move(up1);
失去对象拥有权的unique_ptr并不会获得一个"指向无物"(refer to no object)的新拥有权。如果要赋新值给unique_ptr,新值必须也是个unique_ptr,不可以是寻常pointer:
std::unique_ptr<ClassA> ptr;
ptr = new ClassA; //ERROR
ptr = std::unique_ptr<ClassA>(new ClassA); //OK
赋值nullptr也可以,和调用reset()效果相同:
up = nullptr;
###源头和去处
拥有权的转移暗暗支出了unique_ptr的一种用途:函数可利用它们将拥有权一处给其他函数。这会发生在两种情况下:
- 函数是接收端。如果使用std::move()建立起来的unique_ptr以rvalue reference身份当作函数实参,那么被调用函数的参数将会取得unique_ptr的拥有权。因此,如果该函数不再转移拥有权,对象将会在函数结束时被deleted.
代码如下:
void sink(std::unique_ptr<ClassA> up)
{
...
}
std::unique_ptr<ClassA> up(new ClassA);
...
sink(std::move(up));
...
- 函数是供应端。当函数返回一个unique_ptr,其拥有权会转移至调用端场景内。在这里source()的return语句不需要std::move()的原因是,C++11语言规定,编译器应该自动尝试加上move。
代码如下:
std::unique_ptr<ClassA> source()
{
std::unique_ptr<classA> ptr(new ClassA);
...
return ptr;
}
void g()
{
std::unique_ptr<ClassA> p;
for (int i=0; i<10; ++i) {
p = source();
...
}
}
unique_ptr被当作成员
在class内部使用unique_ptr可避免资源泄漏,且不再需要析构函数,但还是需要copy构造函数和assignment操作符;此外unique_ptr也可协助避免“对象初始化期间因抛出异常而造成资源泄漏”。因为只有当构造函数完成的时候,析构函数才有可能被调用。所以,如果在构造函数中,第一个new成功,第二个new失败,就可能导致内存泄漏。
class ClassB{
private:
std::unique_ptr<ClassA> ptr1;
std::unique_ptr<ClassB> ptr2;
public:
ClassB(int val1, int val2)
: ptr1(new ClassA(val1)), ptr2(new ClassA(val2)) {
}
ClassB(const ClassB& x)
: ptr1(new ClassA(*x.ptr1)), ptr2(new ClassA(*x.ptr2)) {
}
const ClassB& operator= (const ClassB& x) {
*ptr1 = *x.ptr1;
*ptr2 = *x.ptr2;
return *this;
}
...
};
对付Array
默认情况下unique_ptr如果失去拥有权,会为其所拥有的对象调用delete。而对于数组需要调用delete[],所以一下语句错误:
std::unique_ptr<std::string> up(new std::string[10]);
shared_ptr需要自己定义deleter才能处理array,但是unique_ptr拥有一个偏特化版本:
std::unique_ptr<std::string[]> up(new std::string[10]);
但是这个偏特化版本不再提供操作符*和->,改而提供操作符[],用以访问其所指向的array中的某一个对象,并且索引的合法性需要程序员进行保证:
std::unique_ptr<std::string[]> up(new std::string[10]); //OK
...
std::cout << *up << std::endl; //ERROR
std::cout << up[0] << std::endl; //OK
同时这个class不接受"派生类型"的array作为初值。
其他相应资源的Deleter
不同于shared_ptr的deleter定义方式,unique_ptr需要具体致命deleter的类型作为第二个template实参。例如:
class ClassADeleter
{
public:
void operator () (ClassA* p) {
std::cout << "call delete for ClassA object" << std::endl;
delete p;
}
};
...
std::unique_ptr<ClassA, ClassADeleter> up(new ClassA());
如果你给的是个函数或lambda,必须声明deleter的类型为void(*)(T*)
或std::function<void(T*)>
,要不就使用decltype
。
Class auto_ptr
class auto_ptr是C++98标准库提供的一个smart pointer,但它已被C++11明确声明不再支持。它的目标是提供如今unique_ptr所提供的语义,不过却导致一些问题:
- 设计它时,C++语言尚未有move语义让构造函数和assignment操作符使用。
- 不存在deleter所表示的语义,因此你智能使用它处理“以new分配之单一对象”。
- 由于它是C++标准库提供的最早且唯一的smart pointer,所以常被误用,特别是他僭越地提供了如今class shared_ptr提供的拥有权共享语义。
考虑以下代码:
template<typename T>
{
if (p.get() == NLLL ) {
std::cout << "NULL";
}
else {
std::cout << *p;
}
}
std::auto_ptr<int> p(new int);
*p = 42;
bad_print(p); //控制权转移,在bad_print函数执行完后数据对象被析构
*p = 18; //RUNTIME ERROR
Smart Pointer结语
关于效能
Class shared_ptr由于存在拥有控制块,所以会有一些额外的开销。unique_ptr是无状态,其效能与raw pointer无异。
关于使用
Smart pointer并不完美,还是会有很多误用的情况。并且不是线程安全。