指针和内存管理本来就是C++最令人难以理解,也是最头疼的两部分,而智能指针则正好是这两部分的综合。因此,理解起来难免有些吃力。

1. 为什么会引入智能指针

首先需要明确的是,智能指针并不是C++11新引入的概念,C++98提供了第一个智能指针:auto_ptr。那么,为什么需要引入智能指针?概括的来说,是为了防止内存泄露等问题,用一个对象来管理野指针,使得在该对象构造时获得该指针管理权,析构时自动释放(delete)。为了便于理解,可以参考以下代码:

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语句也会有很多人忘记吧)!这时我们会想:当remodel这样的函数终止(不管是正常终止,还是由于出现了异常而终止),本地变量都将自动从栈内存中删除——因此指针ps占据的内存将被释放。

自然地,我们会想到如果ps指向的内存也被自动释放,那该有多好啊。我们知道析构函数有这个功能。如果ps有一个析构函数,该析构函数将在ps过期时自动释放它指向的内存。但ps的问题在于,它只是一个常规指针,不是有析构凼数的类对象指针。如果它指向的是对象,则可以在对象过期时,让它的析构函数删除指向的内存。这正是 auto_ptr、unique_ptr和shared_ptr这几个智能指针背后的设计思想。所以智能指针的核心思想就是:将基本类型指针封装为类对象指针(这个类肯定是个模板,以适应不同基本类型的需求),并在析构函数里编写delete语句删除指针指向的内存空间。下图就很形象地展示了普通指针与智能指针的区别:

所以,对于我们一开始给出的例子,我们可以做如下改进:

# 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. C++11的智能指针

正如我们一开始所说,智能指针并不是C++11新提出的概念,早在C++98就出现了第一个智能指针:auto_ptr。但我们也注意到,在C++11中,摒弃了原有的auto_ptr指针,而引入了:shared_ptr,unique_ptr,weak_ptr三种新的智能指针。

2.1 为什么摒弃auto_ptr

自然地,我们会思考:为什么摒弃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和uniqiie_ptr 的策略,但unique_ptr的策略更严格
  • 创建智能更高的指针,跟踪引用特定对象的智能指针数。这称为引用计数。例如,赋值时,计数将加1,而指针过期时,计数将减1。当减为0时才调用delete。这是shared_ptr采用的策略

以上两种策略都各有自己用途,而auto_ptr为什么会被摒弃不妨参考以下示例:

#include <iostream>
#include <string>
#include <memory>
using namespace std;

int main() {
  auto_ptr<string> films[5] =
 {
  auto_ptr<string> (new string("Fowl Balls")),
  auto_ptr<string> (new string("Duck Walks")),
  auto_ptr<string> (new string("Chicken Runs")),
  auto_ptr<string> (new string("Turkey Errors")),
  auto_ptr<string> (new string("Goose Eggs"))
 };
 auto_ptr<string> pwin;
 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;
}

运行时发现程序崩溃(Segmentation fault: 11),原因在上面注释已经说的很清楚,films[2]已经是空指针了,下面输出访问空指针当然会崩溃了。

这里如果把auto_ptr换成shared_ptr,程序就不会崩溃。这是由于:使用shared_ptr时运行正常,因为shared_ptr采用引用计数,pwin和films[2]都指向同一块内存,在释放空间时因为事先要判断引用计数值的大小因此不会出现多次删除一个对象的错误。

这里如果把auto_ptr换成unique_ptr,程序会在编译时出错,与auto_ptr一样,unique_ptr也采用所有权模型,但在使用unique_ptr时,程序不会等到运行阶段崩溃,而在编译器因下述代码行出现错误,从而指导程序员发现潜在的内存错误。

因此,我们不难看出,unique_ptr比auto_ptr更加安全。

2.2 C++11智能指针

  • shared_ptr,基于引用计数的智能指针,会统计当前有多少个对象同时拥有该内部指针;当引用计数降为0时,自动释放;
  • weak_ptr,基于引用计数的智能指针(shared_ptr)在面对循环引用的问题将无能为力,因此C++11还引入weak_ptr与之配套使用,weak_ptr只引用,不计数
  • unique_ptr: 遵循独占语义的智能指针,在任何时间点,资源智能唯一地被一个unique_ptr所占有,当其离开作用域时自动析构。资源所有权的转移只能通过std::move()而不能通过赋值

其实,相较于shared_ptr与unique_ptr而言,weak_ptr的作用显得较为模糊。参照知乎上各位大神的理解来看,weak_ptr和shared_ptr的最大区别在于weak_ptr在指向一个对象的时候不会增加其引用计数。因此可以用weak_ptr去指向一个对象并且在weak_ptr仍然指向这个对象的时候析构它,此时你再访问weak_ptr的时候,weak_ptr其实返回的会是一个空的shared_ptr。
为什么要采取weak_ptr来解决刚才所述的环状引用的问题呢?需要注意的是环状引用的本质矛盾是不能通过任何程序设计语言的方式来打破的,为了解决环状引用,第一步首先得打破环,也就是得告诉C++,这个环上哪一个引用是最弱的,是可以被打破的,因此在一个环上只要把原来的某一个shared_ptr改成weak_ptr,实质上这个环就可以被打破了,原有的环状引用带来的无法析构的问题也就随之得到了解决。