C++智能指针

智能指针

将基本类型指针封装为类对象指针(这个类肯定是个模板,以适应不同基本类型的需求),并在析构函数里编写 delete 语句删除指针指向的内存空间。

STL 一共给我们提供了四种智能指针:auto_ptr、unique_ptr、shared_ptr 和 weak_ptr。

模板 auto_ptr 是 C++ 98 提供的解决方案,C++11 已将将其摒弃,并提供了另外两种解决方案。然而,虽然 auto_ptr 被摒弃,但它已使用了好多年:同时,如果您的编译器不支持其他两种解决力案,auto_ptr 将是唯一的选择。

所有的智能指针类都有一个 explicit 构造函数,以指针作为参数。比如 auto_ptr 的类模板原型为:

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

pvac 过期时,程序将把 delete 运算符用于非堆内存,这是错误的。使用举例:

#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; }

为什么摒弃 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 过期时。要避免这种问题,方法有多种:

  1. 定义陚值运算符,使之执行深复制。这样两个指针将指向不同的对象,其中的一个对象是另一个对象的副本,缺点是浪费空间,所以智能指针都未采用此方案。
  2. 建立所有权(ownership)概念。对于特定的对象,只能有一个智能指针可拥有,这样只有拥有对象的智能指针的构造函数会删除该对象。然后让赋值操作转让所有权。这就是用于 auto_ptr 和 uniqiie_ptr 的策略,但 unique_ptr 的策略更严格。
  3. 创建智能更高的指针,跟踪引用特定对象的智能指针数。这称为引用计数。例如,赋值时,计数将加 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; }

运行下发现程序崩溃了,原因在上面注释已经说的很清楚,films[2] 已经是空指针了,下面输出访问空指针当然会崩溃了。但这里如果把 auto_ptr 换成 shared_ptr 或 unique_ptr 后,程序就不会崩溃,原因如下:

  1. 使用 shared_ptr 时运行正常,因为 shared_ptr 采用引用计数,pwin 和 films[2] 都指向同一块内存,在释放空间时因为事先要判断引用计数值的大小因此不会出现多次删除一个对象的错误。

  2. 使用 unique_ptr 时编译出错,与 auto_ptr 一样,unique_ptr 也采用所有权模型,但在使用 unique_ptr 时,程序不会等到运行阶段崩溃,而在编译器因下述代码行出现错误:

    unique_ptr<string> pwin; pwin = films[2]; // films[2] loses ownership.

    直到你发现潜在的内存错误。这就是为何要摒弃 auto_ptr 的原因,一句话总结就是:避免潜在的内存崩溃问题。

unique_ptr为何优于auto_ptr

可能大家认为前面的例子已经说明了 unique_ptr 为何优于 auto_ptr,也就是安全问题,下面再叙述的清晰一点。
请看下面的语句:

auto_ptr<string> p1(new string ("auto"); //#1 auto_ptr<string> p2; //#2 p2 = p1; //#3

在语句 #3 中,p2 接管 string 对象的所有权后,p1 的所有权将被剥夺。前面说过,这是好事,可防止 p1 和 p2 的析构函数试图刪同—个对象;

但如果程序随后试图使用 p1,这将是件坏事,因为 p1 不再指向有效的数据。下面来看使用 unique_ptr 的情况:

unique_ptr<string> p3 (new string ("auto"); //#4 unique_ptr<string> p4; //#5 p4 = p3; //#6

编译器认为语句 #6 非法,避免了 p3 不再指向有效数据的问题。因此,unique_ptr 比 auto_ptr 更安全。但unique_ptr 还有更聪明的地方。

有时候,会将一个智能指针赋给另一个并不会留下危险的悬挂指针。假设有如下函数定义:

unique_ptr<string> demo(const char * s) { unique_ptr<string> temp (new string (s)); 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 not allowed 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<string> 对象。

使用 move 后,原来的指针仍转让所有权变成空指针,可以对其重新赋值。

unique_ptr<string> ps1, ps2; ps1 = demo("hello"); ps2 = move(ps1); ps1 = demo("alexia"); cout << *ps2 << *ps1 << endl;

如何选择智能指针

在掌握了这几种智能指针后,大家可能会想另一个问题:在实际应用中,应使用哪种智能指针呢?下面给出几个使用指南。

  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<unique_ptr<int> > vp(size); 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 because arg is temporary for_each(vp.begin(), vp.end(), show); // use for_each() ... }

    其中 push_back 调用没有问题,因为它返回一个临时 unique_ptr,该 unique_ptr 被赋给 vp 中的一个 unique_ptr。另外,如果按值而不是按引用给 show() 传递对象,for_each() 将非法,因为这将导致使用一个来自 vp 的非临时 unique_ptr 初始化 pi,而这是不允许的。前面说过,编译器将发现错误使用 unique_ptr 的企图。

  3. 在 unique_ptr 为右值时,可将其赋给 shared_ptr,这与将一个 unique_ptr 赋给一个需要满足的条件相同。与前面一样,在下面的代码中,make_int() 的返回类型为 unique_ptr

    unique_ptr<int> pup(make_int(rand() % 1000)); // ok shared_ptr<int> spp(pup); // not allowed, pup as lvalue shared_ptr<int> spr(make_int(rand() % 1000)); // ok
  4. 模板 shared_ptr 包含一个显式构造函数,可用于将右值 unique_ptr 转换为 shared_ptr。shared_ptr 将接管原来归 unique_ptr 所有的对象。

  5. 在满足 unique_ptr 要求的条件时,也可使用 auto_ptr,但 unique_ptr 是更好的选择。如果你的编译器没有 unique_ptr,可考虑使用 Boost 库提供的 scoped_ptr,它与 unique_ptr 类似。

  6. weak_ptr 是用来解决 shared_ptr 相互引用时的死锁问题,如果说两个 shared_ptr 相互引用,那么这两个指针的引用计数永远不可能下降为 0,资源永远不会释放。它是对对象的一种弱引用,不会增加对象的引用计数,和 shared_ptr 之间可以相互转化,shared_ptr 可以直接赋值给它,它可以通过调用 lock 函数来获得shared_ptr。