导读
C++面试过程中,很多面试官都喜欢问智能指针相关的问题,比如你知道哪些智能指针?shared_ptr的设计原理是什么?如果让你自己设计一个智能指针,你如何完成?等等……。而且在看开源的C++项目时,也能随处看到智能指针的影子。这说明智能指针不仅是面试官爱问的题材,更是非常有实用价值。
本文参考自《C++ Primer Plus》。
目录
正文
1. 智能指针背后的设计思想
如果忘记在适当的地方加上delete语句,就会导致潜在的内存泄露问题。不仅需要考虑到函数正常终止路径,还要考虑由于出现了异常而终止的情况。
代码离开作用域的时候,本地变量都将自动从栈内存中删除。同样情况下,如果指针被声明为本地变量,那么指针本身占据的内存将被释放,如果该指针指向的内存也被自动释放,那该有多好啊。
我们知道析构函数有这个功能。如果指针变成一个类对象,具有一个析构函数,该析构函数将在指针过期时自动释放它指向的内存。但问题在于,一个常规指针不是有析构凼数的类对象。如果它是和指针相关的对象,则可以在对象过期时,让它的析构函数删除指针指向的内存。
2. C++智能指针简单介绍
STL一共给我们提供了四种智能指针:auto_ptr、 unique_ptr、 shared_ptr和weak_ptr(本文章暂不讨论)。
模板auto_ptr是C++98提供的解决方案,C+11已将将其摒弃,并提供了另外两种解决方案。然而,虽然auto_ptr被摒弃,但它已使用了好多年:同时,如果您的编译器不支持其他两种解决力案,auto_ptr将是唯一的选择。
使用注意点
· 所有的智能指针类都有一个explicit构造函数,以指针作为参数。比如auto_ptr的类模板原型为:
templet
class auto_ptr {
explicit auto_ptr(T* p = 0) ;
...
};
因此不能自动将指针转换为智能指针对象,必须显式调用:
shared_ptr<double> pd;
double *p_reg = new double;
pd = p_reg; // notallowed (implicit conversion)
pd = shared_ptr<double>(p_reg); // allowed (explicitconversion)
shared_ptr<double> pshared = p_reg; // not allowed (implicitconversion)
shared_ptr<double> pshared(p_reg); // allowed (explicitconversion)
· 对全部三种智能指针都应避免的一点:
string vacation("I wandered lonely as acloud.");
shared_ptr<string> pvac(&vacation); // No
pvac过期时,程序将把delete运算符用于非堆内存,这是错误的。
使用举例
#include
#include
#include
class report
{
private:
std::string str;
public:
report(const std::string s) : str(s){
std::cout<< "Objectcreated.\n";
}
~report() {
std::cout << "Object deleted.\n";
}
void comment() const {
std::cout << str << "\n";
}
};
int main() {
{
std::auto_ptr
ps->comment();
}
{
std::shared_ptr
ps->comment();
}
{
std::unique_ptr
ps->comment();
}
return 0;
}
3. 为什么摒弃auto_ptr?
先来看下面的赋值语句:
auto_ptr< string> ps (new string ("I reigned lonely as a cloud.”);
auto_ptr
vocaticn = ps;
上述赋值语句将完成什么工作呢?如果ps和vocation是常规指针,则两个指针将指向同一个string对象。这是不能接受的,因为程序将试图删除同一个对象两次——一次是ps过期时,另一次是vocation过期时。要避免这种问题,方法有多种:
· 定义陚值运算符,使之执行深复制。这样两个指针将指向不同的对象,其中的一个对象是另一个对象的副本,缺点是浪费空间,所以智能指针都未采用此方案。
· 建立所有权(ownership)概念。对于特定的对象,只能有一个智能指针可拥有,这样只有拥有对象的智能指针的构造函数会删除该对象。然后让赋值操作转让所有权。这就是用于auto_ptr(转移所有权)和unique_ptr (禁止所有权的转移)的策略,但unique_ptr的策略更严格。
· 创建智能更高的指针,跟踪引用特定对象的智能指针数。这称为引用计数。例如,赋值时,计数将加1,而指针过期时,计数将减1,。当减为0时才调用delete。这是shared_ptr采用的策略。
当然,同样的策略也适用于复制构造函数。
每种方法都有其用途,但为何说要摒弃auto_ptr呢?
下面举个例子来说明。
#include
#include
#include
using namespace std;
int main() {
auto_ptr
{
auto_ptr
auto_ptr
auto_ptr
auto_ptr
auto_ptr
};
auto_ptr
pwin = films[2]; // films[2]loses ownership. 将所有权从films[2]转让给pwin,此时films[2]不再引用该字符串从而变成空指针
cout << "The nominees for best avian baseballl film are\n";
for(int i = 0; i < 5; ++i)
cout << *films[i] << endl;
cout << "The winner is " << *pwin << endl;
cin.get();
return 0;
}
运行下发现程序崩溃了,原因在上面注释已经说的很清楚,films[2]已经是空指针了,下面输出访问空指针当然会崩溃了。但这里如果把auto_ptr换成shared_ptr或unique_ptr后,程序就不会崩溃,原因如下:
· 使用shared_ptr时运行正常,因为shared_ptr采用引用计数,pwin和films[2]都指向同一块内存,在释放空间时因为事先要判断引用计数值的大小因此不会出现多次删除一个对象的错误。
· 使用unique_ptr时编译出错,与auto_ptr一样,unique_ptr也采用所有权模型,但在使用unique_ptr时,程序不会等到运行阶段崩溃,而在编译器因下述代码行出现错误:
unique_ptr<string> pwin;
pwin = films[2]; // films[2] loses ownership.
Error 1 error C2280:'std::unique_ptr
指导你发现潜在的内存错误。
这就是为何要摒弃auto_ptr的原因,一句话总结就是:避免潜在的内存崩溃问题。
4. unique_ptr为何优于auto_ptr?
可能大家认为前面的例子已经说明了unique_ptr为何优于auto_ptr,也就是安全问题。
但unique_ptr还有更聪明的地方。
有时候,会将一个智能指针赋给另一个并不会留下危险的悬挂指针。假设有如下函数定义:
unique_ptr<string> demo(const char * s)
{
unique_ptr
return temp;
}
并假设编写了如下代码:
unique_ptr<string> ps;
ps = demo('Uniquely special");
demo()返回一个临时unique_ptr,然后ps接管了原本归返回的unique_ptr所有的对象,而返回时临时的 unique_ptr 被销毁,也就是说没有机会使用unique_ptr 来访问无效的数据,换句话来说,这种赋值是不会出现任何问题的,即没有理由禁止这种赋值。实际上,编译器确实允许这种赋值,这正是unique_ptr更聪明的地方。
总之,当程序试图将一个 unique_ptr 赋值给另一个时,如果源unique_ptr 是个临时右值,编译器允许这么做;如果源 unique_ptr 将存在一段时间,编译器将禁止这么做,比如:
unique_ptr<string> pu1(new string ("hello world"));
unique_ptr<string> pu2;
pu2 = pu1; // #1 notallowed
unique_ptr<string> pu3;
pu3 = unique_ptr<string>(new string ("You")); // #2 allowed
其中#1留下悬挂的unique_ptr(pu1),这可能导致危害。而#2不会留下悬挂的unique_ptr,因为它调用 unique_ptr 的构造函数,该构造函数创建的临时对象在其所有权让给pu3 后就会被销毁。这种随情况而已的行为表明,unique_ptr 优于允许两种赋值的auto_ptr 。
当然,您可能确实想执行类似于#1的操作。C++有一个标准库函数std::move(),让你能够将一个unique_ptr赋给另一个。下面是一个使用前述demo()函数的例子,该函数返回一个unique_ptr
使用move后,原来的指针仍转让所有权变成空指针,可以对其重新赋值。
unique_ptr<string> ps1, ps2;
ps1 = demo("hello");
ps2 = move(ps1);
ps1 = demo("alexia");
cout << *ps2 << *ps1 << endl;
5. 如何选择智能指针?
在掌握了这几种智能指针后,大家可能会想另一个问题:在实际应用中,应使用哪种智能指针呢?
下面给出几个使用指南。
(1)如果程序要使用多个指向同一个对象的指针,应选择shared_ptr。这样的情况包括:
· 有一个指针数组,并使用一些辅助指针来标示特定的元素,如最大的元素和最小的元素;
· 两个对象包含都指向第三个对象的指针;
· STL容器包含指针。很多STL算法都支持复制和赋值操作,这些操作可用于shared_ptr,但不能用于unique_ptr(编译器发出warning)和auto_ptr(行为不确定)。如果你的编译器没有提供shared_ptr,可使用Boost库提供的shared_ptr。
(2)如果程序不需要多个指向同一个对象的指针,则可使用unique_ptr。如果函数使用new分配内存,并返还指向该内存的指针,将其返回类型声明为unique_ptr是不错的选择。这样,所有权转让给接受返回值的unique_ptr,而该智能指针将负责调用delete。可将unique_ptr存储到STL容器在那个,只要不调用将一个unique_ptr复制或赋给另一个算法(如sort())。例如,可在程序中使用类似于下面的代码段。
unique_ptr<int> make_int(int n)
{
return unique_ptr<int>(new int(n));
}
void show(unique_ptr<int> &p1)
{
cout << *a << ' ';
}
int main()
{
...
vector
for(int i = 0; i < vp.size(); i++)
vp[i] = make_int(rand() % 1000); // copy temporary unique_ptr
vp.push_back(make_int(rand() % 1000)); // ok becausearg is temporary
for_each(vp.begin(),vp.end(), show); // usefor_each()
...
}
在满足unique_ptr要求的条件时,也可使用auto_ptr,但unique_ptr是更好的选择。如果你的编译器没有unique_ptr,可考虑使用Boost库提供的scoped_ptr,它与unique_ptr类似。
6. 自己构建智能指针
template
class SimplePointer
{
public:
explicit SimplePointer(_PointType_* _InPtr);
virtual ~SimplePointer();
virtual _PointType_& operator *();
virtual _PointType_* operator ->();
private:
SimplePointer(const SimplePointer<_PointType_>& other);
SimplePointer<_PointType_>& operator =(const SimplePointer<_PointType_>& other);
private:
_PointType_* m_Ptr;
};
template
SimplePointer<_PointType_>::SimplePointer(_PointType_* _InPtr)
{
this->m_Ptr = _InPtr;
}
template
SimplePointer<_PointType_>::~SimplePointer()
{
delete this->m_Ptr;
this->m_Ptr = nullptr;
}
template
_PointType_& SimplePointer<_PointType_>::operator*()
{
return *(this->m_Ptr);
}
template
_PointType_* SimplePointer<_PointType_>::operator->()
{
/* 因为特殊原因,所以C++中的Obj->set(5)等价于(Obj.operator->())->set(5)
* C++会给重载operator->返回的任何结果隐式地再应用一次operator->,所以自定义
* 类中的operator->()必须返回一个指针,而不是引用 */
return this->m_Ptr;
}