5.2.2 C++11的智能指针

在C++98中,智能指针通过一个模板类型“auto_ptr”来实现。auto_ptr以对象的方式管理堆分配的内存,并在适当的时间(比如析构),释放所获得的堆内存。这种堆内存管理的方式只需要程序员将new操作返回的指针作为auto_ptr的初始值即可,程序员不用再显式地调用delete。比如:


auto_ptr(new int);


这在一定程度上避免了堆内存忘记释放而造成的问题。不过auto_ptr有一些缺点(拷贝时返回一个左值,不能调用delete[]等),所以在C++11标准中被废弃了。C++11标准中改用unique_ptr、shared_ptr及weak_ptr等智能指针来自动回收堆分配的对象。

这里我们可以看一个C++11中使用新的智能指针的简单例子,如代码清单5-8所示。

代码清单5-8


include <memory>

include <iostream>

using namespace std;

int main(){

unique_ptr<int> up1(new int(11));//无法复制的unique_ptr

unique_ptr<int> up2=up1;//不能通过编译

cout<<*up1<<endl;//11

unique_ptr<int> up3=move(up1);//现在p3是数据唯一的unique_ptr智能指针

cout<<*up3<<endl;//11

cout<<*up1<<endl;//运行时错误

up3.reset();//显式释放内存

up1.reset();//不会导致运行时错误

cout<<*up3<<endl;//运行时错误

shared_ptr<int> sp1(new int(22));

shared_ptr<int> sp2=sp1;

cout<<*sp1<<endl;//22

cout<<*sp2<<endl;//22

sp1.reset();

cout<<*sp2<<endl;//22

}

//编译选项:g++ -std=c++11 5-2-1.cpp


在代码清单5-8中,使用了两种不同的智能指针unique_ptr及shared_ptr来自动地释放堆对象的内存。由于每个智能指针都重载了运算符,用户可以使用up1这样的方式来访问所分配的堆内存。而在该指针析构或者调用reset成员的时候,智能指针都可能释放其拥有的堆内存。从作用上来讲,unique_ptr和shared_ptr还是和以前的auto_ptr保持了一致。

不过从代码清单5-8中还是可以看到,unique_ptr和shared_ptr在对所占内存的共享上还是有一定区别的。

直观地看来,unique_ptr形如其名地,与所指对象的内存绑定紧密,不能与其他unique_ptr类型的指针对象共享所指对象的内存。比如,本例中的unique_ptr<int> up2=up1;不能通过编译,是因为每个unique_ptr都是唯一地“拥有”所指向的对象内存,由于up1唯一地占有了new分配的堆内存,所以up2无法共享其“所有权”。事实上,这种“所有权”仅能够通过标准库的move函数来转移。我们可以看到代码中up3的定义,unique_ptr<int> up3=move(up1);一旦“所有权”转移成功了,原来的unique_ptr指针就失去了对象内存的所有权。此时再使用已经“失势”的unique_ptr,就会导致运行时的错误。本例中的后段使用*up1就是很好的例子。

而从实现上讲,unique_ptr则是一个删除了拷贝构造函数、保留了移动构造函数的指针封装类型(我们在7.2节中可以看到如何删除一个类的拷贝构造函数)。程序员仅可以使用右值对unique_ptr对象进行构造,而且一旦构造成功,右值对象中的指针即被“窃取”,因此该右值对象即刻失去了对指针的“所有权”。

而shared_ptr同样形如其名,允许多个该智能指针共享地“拥有”同一堆分配对象的内存。与unique_ptr不同的是,由于在实现上采用了引用计数,所以一旦一个shared_ptr指针放弃了“所有权”(失效),其他的shared_ptr对对象内存的引用并不会受到影响。代码清单5-8中,智能指针sp2就很好地说明了这种状况。虽然sp1调用了reset成员函数,但由于sp1和sp2共享了new分配的堆内存,所以sp1调用reset成员函数只会导致引用计数的降低,而不会导致堆内存的释放。只有在引用计数归零的时候,share_ptr才会真正释放所占有的堆内存的空间。

在C++11标准中,除了unique_ptr和shared_ptr,智能指针还包括了weak_ptr这个类模板。weak_ptr的使用更为复杂一点,它可以指向shared_ptr指针指向的对象内存,却并不拥有该内存。而使用weak_ptr成员lock,则可返回其指向内存的一个shared_ptr对象,且在所指对象内存已经无效时,返回指针空值(nullptr,请参见7.1节)。这在验证share_ptr智能指针的有效性上会很有作用,如代码清单5-9所示。

代码清单5-9


include <memory>

include <iostream>

using namespace std;

void Check(weak_ptr<int> &wp){

shared_ptr<int> sp=wp.lock();//转换为shared_ptr<int>

if(sp!=nullptr)

cout<<"still"<<*sp<<endl;

else

cout<<"pointer is invalid."<<endl;

}

int main(){

shared_ptr<int> sp1(new int(22));

shared_ptr<int> sp2=sp1;

weak_ptr<int> wp=sp1;//指向shared_ptr<int>所指对象

cout<<*sp1<<endl;//22

cout<<*sp2<<endl;//22

Check(wp);//still 22

sp1.reset();

cout<<*sp2<<endl;//22

Check(wp);//still 22

sp2.reset();

Check(wp);//pointer is invalid

}

//编译选项:g++ -std=c++11 5-2-2.cpp


在代码清单5-9中,我们定义了一个共享对象内存的两个shared_ptr——sp1及sp2。而weak_ptr wp同样指向该对象内存。可以看到,在sp1及sp2都有效的时候,我们调用wp的lock函数,将返回一个有效的shared_ptr对象供使用,于是Check函数会输出以下内容:


still 22


此后我们分别调用了sp1及sp2的reset函数,这会导致对唯一的堆内存对象的引用计数降至0。而一旦引用计数归0,shared_ptr<int>就会释放堆内存空间,使之失效。此时我们再调用weak_ptr的lock函数时,则返回一个指针空值nullptr。这时Check函数则会打印出:


pointer is invalid


这样的语句了。在整个过程当中,只有shared_ptr参与了引用计数,而weak_ptr没有影响其指向的内存的引用计数。因此可以验证shared_ptr指针的有效性。

简单情况下,程序员用unique_ptr代替以前使用auto_ptr的代码就可以使用C++11中的智能指针。而shared_ptr及weak_ptr则可用在用户需要引用计数的地方。事实上,关于智能指针的历史、使用及各种讨论还有很多,不过本书的重点不在于标准库,因此这里就不一一展开了。

总地来说,虽然智能指针能帮助用户进行有效的堆内存管理,但是它还是需要程序员显式地声明智能指针,而完全不需要考虑回收指针类型的内存管理方案可能会更讨人喜欢。当然,这种方案早已有了,就是垃圾回收机制。