智能指针和普通指针的区别在于智能指针实际上是对普通指针加了一层封装机制,这样的一层封装机制的目的是为了使得智能指针可以方便的管理一个对象的生命期。尤其的完成指针的自动删除以及指针指向内存区域的清理。否则有可能导致野指针或者内存泄漏问题。在智能指针中,一个对象什么时候和在什么条件下要被析构或者是删除是受智能指针本身决定的,用户并不需要管理。本质是C++RAII机制的体现。
C++面试过程中,很多面试官都喜欢问智能指针相关的问题,比如你知道哪些智能指针?shared_ptr的设计原理是什么?如果让你自己设计一个智能指针,你如何完成?等等……。而且在看开源的C++项目时,也能随处看到智能指针的影子。这说明智能指针不仅是面试官爱问的题材,更是非常有实用价值。本wiki将分两篇文章展开,第一篇主要介绍,第二篇自己实现。
1 智能指针设计思想
void remodel(std::string & str)
{
std::string * ps = new std::string(str);
...
if (weird_thing())
throw exception();
str = *ps;
delete ps;
return;
}
当出现异常时(weird_thing()返回true),delete将不被执行,因此将导致内存泄露。
如何避免这种问题?有人会说,这还不简单,直接在throw exception();之前加上delete ps;不就行了。是的,你本应如此,问题是很多人都会忘记在适当的地方加上delete语句(连上述代码中最后的那句delete语句也会有很多人忘记吧),如果你要对一个庞大的工程进行review,看是否有这种潜在的内存泄露问题,那就是一场灾难!
这时我们会想:当remodel这样的函数终止(不管是正常终止,还是由于出现了异常而终止),本地变量都将自动从栈内存中删除—因此指针ps占据的内存将被释放,如果ps指向的内存也被自动释放,那该有多好啊。
我们知道析构函数有这个功能。如果ps有一个析构函数,该析构函数将在ps过期时自动释放它指向的内存。但ps的问题在于,它只是一个常规指针,不是有析构凼数的类对象指针。如果它指向的是对象,则可以在对象过期时,让它的析构函数删除指向的内存。
这正是 auto_ptr、unique_ptr和shared_ptr这几个智能指针背后的设计思想。我简单的总结下就是:将基本类型指针封装为类对象指针(这个类肯定是个模板,以适应不同基本类型的需求),并在析构函数里编写delete语句删除指针指向的内存空间。
因此,要转换remodel()函数,应按下面3个步骤进行:
- 包含头义件memory(智能指针所在的头文件);
- 将指向string的指针替换为指向string的智能指针对象;
- 删除delete语句。
下面是使用auto_ptr修改该函数的结果:
# include <memory>
void remodel (std::string & str)
{
std::auto_ptr<std::string> ps (new std::string(str));
...
if (weird_thing ())
throw exception();
str = *ps;
// delete ps; NO LONGER NEEDED
return;
}
2 智能指针常用介绍
STL一共给我们提供了四种智能指针:auto_ptr、unique_ptr、shared_ptr和weak_ptr(本文章暂不讨论)。
模板auto_ptr是C++98提供的解决方案,C+11已将将其摒弃,并提供了另外两种解决方案。然而,虽然auto_ptr被摒弃,但它已使用了好多年:同时,如果您的编译器不支持其他两种解决力案,auto_ptr将是唯一的选择。weak_ptr和shared_ptr的最大区别在于weak_ptr在指向一个对象的时候不会增加其引用计数,因此你可以用weak_ptr去指向一个对象并且在weak_ptr仍然指向这个对象的时候析构它,此时你再访问weak_ptr的时候,weak_ptr其实返回的会是一个空的shared_ptr。故这里主要讨论前三个。
1 所有智能指针类的构造函数都加了explicit关键字,即不能隐式转换,只能显式调用:
templet<class T>
class auto_ptr {
explicit auto_ptr(X* p = 0) ;
...
};
因此不能自动将指针转换为智能指针对象,必须显式调用:
shared_ptr<double> pd;
double *p_reg = new double;
pd = p_reg; // not allowed (implicit conversion)
pd = shared_ptr<double>(p_reg); // allowed (explicit conversion)
shared_ptr<double> pshared = p_reg; // not allowed (implicit conversion)
shared_ptr<double> pshared(p_reg); // allowed (explicit conversion)
string vacation("I wandered lonely as a cloud.");
shared_ptr<string> pvac(&vacation); // No
#include <iostream>
#include <string>
#include <memory>
class report
{
private:
std::string str;
public:
report(const std::string s) : str(s) {
std::cout << "Object created.\n";
}
~report() {
std::cout << "Object deleted.\n";
}
void comment() const {
std::cout << str << "\n";
}
};
int main() {
{
std::auto_ptr<report> ps(new report("using auto ptr"));
ps->comment();
}
{
std::shared_ptr<report> ps(new report("using shared ptr"));
ps->comment();
}
{
std::unique_ptr<report> ps(new report("using unique ptr"));
ps->comment();
}
return 0;
}
3 不使用auto_ptr
先来看下面的赋值语句:
auto_ptr< string> ps (new string ("I reigned lonely as a cloud.”);
auto_ptr<string> vocation;
vocaticn = ps;
上述赋值语句将完成什么工作呢?如果ps和vocation是常规指针,则两个指针将指向同一个string对象。这是不能接受的,因为程序将试图删除同一个对象两次——一次是ps过期时,另一次是vocation过期时。要避免这种问题,方法有多种:
- 定义陚值运算符,使之执行深复制。这样两个指针将指向不同的对象,其中的一个对象是另一个对象的副本,缺点是浪费空间,所以智能指针都未采用此方案。
- 建立所有权(ownership)概念。对于特定的对象,只能有一个智能指针可拥有,这样只有拥有对象的智能指针的构造函数会删除该对象。然后让赋值操作转让所有权。这就是用于auto_ptr和unique_ptr 的策略,但unique_ptr的策略更严格。
- 创建智能更高的指针,跟踪引用特定对象的智能指针数。这称为引用计数。例如,赋值时,计数将加1,而指针过期时,计数将减1,。当减为0时才调用delete。这是shared_ptr采用的策略。
当然,同样的策略也适用于复制构造函数。
每种方法都有其用途,但为何说要摒弃auto_ptr呢?
下面举个例子来说明。