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则可用在用户需要引用计数的地方。事实上,关于智能指针的历史、使用及各种讨论还有很多,不过本书的重点不在于标准库,因此这里就不一一展开了。
总地来说,虽然智能指针能帮助用户进行有效的堆内存管理,但是它还是需要程序员显式地声明智能指针,而完全不需要考虑回收指针类型的内存管理方案可能会更讨人喜欢。当然,这种方案早已有了,就是垃圾回收机制。