C++智能指针

1. 为什么要有智能指针?

我们先来看一个简单的例子:

int remodel(string & str, bool flag)
{
    string * ps = new string(str);
    if (flag)
        return -1;
    str = *ps; 
    delete ps;
    return 0;
}

当flag=true时,delete将不被执行,因此将导致内存泄露。

如何避免这种问题?有人会说,这还不简单,直接在return -1;之前加上delete ps;不就行了。是的,你本应如此,问题是很多人都会忘记在适当的地方加上delete语句(连上述代码中最后的那句delete语句也会有很多人忘记吧),如果你要对一个庞大的工程进行review,看是否有这种潜在的内存泄露问题,那就是一场灾难!

这时我们会想:当remodel这样的函数终止(不管是正常终止,还是由于出现了异常而终止),本地变量都将自动从栈内存中删除—因此指针ps占据的内存将被释放,如果ps指向的内存也被自动释放,那该有多好啊。

我们知道析构函数有这个功能。如果ps有一个析构函数,该析构函数将在ps过期时自动释放它指向的内存。但ps的问题在于,它只是一个常规指针,不是有析构凼数的类对象指针。如果它指向的是对象,则可以在对象过期时,让它的析构函数删除指向的内存

这正是 auto_ptr、unique_ptr和shared_ptr这几个智能指针的设计原因。
简单的总结下就是:将基本类型指针封装为类对象指针(这个类肯定是个模板,以适应不同基本类型的需求),并在析构函数里编写delete语句删除指针指向的内存空间

使用auto_ptr修改该函数的结果:

# include <memory>
using namespace std;
int remodel (string & str, bool flag)
{
    auto_ptr<string> ps (new string(str));
    if (flag)
        return -1; 
    str = *ps; 
    // delete ps; 不再需要
    return 0;
}

修改需要以下三步骤:
• 包含头文件memory(智能指针所在的头文件);
• 将指向string的指针替换为指向string的智能指针对象;
• 删除delete语句。

2. C++智能指针简单介绍

STL一共给我们提供了四种智能指针:auto_ptr、unique_ptr、shared_ptr和weak_ptr。
auto_ptr是C++98提供的解决方案,C+11已将将其摒弃,并提供了另外两种解决方案。然而,虽然auto_ptr被摒弃,但它已使用了好多年:同时,如果您的编译器不支持其他两种解决力案,auto_ptr将是唯一的选择。
下面是auto_ptr, unique_ptr,shared_ptr三种智能指针的使用举例:

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

注意:所有的智能指针类都有一个explicit构造函数,以指针作为参数。
因此不能自动将指针转换为智能指针对象,必须显式调用。调用方式必须是:

shared_ptr<T> ps(new T())
**不能用以下形式:**
T a;
shared_ptr<T> ps(&a);

因为变量a是存储在栈内存中,ps过期时,程序将把delete运算符用于非堆内存,这是错误的。

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和uniqiie_ptr 的策略,但unique_ptr的策略更严格。

创建智能更高的指针,跟踪引用特定对象的智能指针数。这称为引用计数。例如,赋值时,计数将加1,而指针过期时,计数将减1,。当减为0时才调用delete。这是shared_ptr采用的策略。

如上面的例子,用auto_ptr智能指针时,执行vocaticn = ps操作时,智能指针的所有权从ps转让给了vocaticn,ps已经变成了空指针,此时如果再从ps指针中取数据,程序就会崩溃

但这里如果把auto_ptr换成shared_ptr或unique_ptr后,程序就不会崩溃,原因如下:
使用shared_ptr时运行正常,因为shared_ptr采用引用计数,ps和vocaticn都指向同一块内存,在释放空间时因为事先要判断引用计数值的大小因此不会出现多次删除一个对象的错误。
使用unique_ptr时编译出错,与auto_ptr一样,unique_ptr也采用所有权模型,但在使用unique_ptr时,程序不会等到运行阶段崩溃,而在编译器因下述代码行出现错误。这也是unique_ptr优于auto_ptr的一点

这就是为何要摒弃auto_ptr的原因:避免潜在的内存崩溃问题

4. shared_ptr和weak_ptr区别

weak_ptr 是一种不控制对象生命周期的智能指针, 它指向一个 shared_ptr 管理的对象。
进行该对象的内存管理的是那个强引用的 shared_ptr. weak_ptr只是提供了对管理对象的一个访问手段。
使用 weak_ptr 解决 shared_ptr 因循环引用,不能释放资源的问题。

举个双向链表的例子来说下shared_ptr造成的循环引用

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

struct Node  
{  
    shared_ptr<Node> _pre;  
    shared_ptr<Node> _next;  

    ~Node()  
    {  
        cout << "~Node():" << this << endl;  
    }  
    int data;  
};  

void FunTest()  
{  
    shared_ptr<Node> Node1(new Node);  
    shared_ptr<Node> Node2(new Node);  
    Node1->_next = Node2;  
    Node2->_pre = Node1;  

    cout << "Node1.use_count:"<<Node1.use_count() << endl; 
    cout << "Node2.use_count:"<< Node2.use_count() << endl;
}  

int main()  
{  
    FunTest();  
    system("pause");  
    return 0;  
}

Node1和Node2的引用计数值都是2,永远都无法变成0,也就不会被释放。

针对上面出现的由于引用计数和管理空间的对象的个数导致空间不能释放的结果就是循环引用。解决方案是:使用弱引用的智能指针打破这种循环引用。

一个强引用是指当被引用的对象仍活着的话,这个引用也存在(也就是说,只要至少有一个强引用,那么这个对象 就不会也不能被释放)。share_ptr就是强引用

弱引用并不修改该对象的引用计数,这意味这弱引用它并不对对象的内存进行管理。它在功能上类似于普通指针,然而一个比较大的区别是,弱引用能检测到所管理的对象是否已经被释放,从而避免访问非法内存。

weak_ptr 设计的目的是为配合 shared_ptr 而引入的一种智能指针来协助 shared_ptr 工作, 它只可以从一个 shared_ptr 或另一个 weak_ptr 对象构造, 它的构造和析构不会引起引用记数的增加或减少。

weak_ptr的成员函数有:
weak_ptr()
没有重载*和->, 但可以使用 lock 获得一个可用的 shared_ptr 对象. 注意,weak_ptr 在使用前需要检查合法性.
expired():
用于检测所管理的对象是否已经释放, 如果已经释放, 返回 true; 否则返回 false.
lock():
用于获取所管理的对象的强引用(shared_ptr). 如果 expired 为 true, 返回一个空的 shared_ptr; 否则返回一个 shared_ptr, 其内部对象指向与 weak_ptr 相同.
use_count():
返回与 shared_ptr 共享的对象的引用计数.
reset():
将weak_ptr置空。
weak_ptr支持拷贝或赋值, 但不会影响对应的 shared_ptr 内部对象的计数。
使用示例:

#include <iostream>
#include <memory>

std::weak_ptr<int> gw;//全局变量

void f()
{
    if (auto spt = gw.lock()) // 使用之前必须复制到 shared_ptr
    {
        std::cout << *spt << "\n";
    }
    else
    {
        std::cout << "gw is expired\n";
    }
}

int main()
{
    {
        auto sp = std::make_shared<int>(42);
         gw = sp;
         f();
    }

    f();
}

输出:
42
gw is expired

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())。

猜你喜欢

转载自blog.csdn.net/okiwilldoit/article/details/80062220