• C++中的智能指针


    1. 从auto_ptr说起

    先来一段简单的代码,看看auto_ptr的使用:

    /************************************************************************
    > File Name: auto_ptrDemo.cpp
    > Author:Jelly
    > Mail:vipygd#126.com(#=>@)
    > Created Time: 2014年10月15日 星期三 12时00分33秒
    ************************************************************************/
    #include <iostream>          
    #include <memory>            
    using namespace std;         
    
    class A                      
    {                            
    public:                      
        A()                      
        {                        
            cout<<"Construct A Object."<<endl;
        }                        
    
        ~A()                     
        {                        
            cout<<"Destroy A Object."<<endl;
        }                        
    };                           
    
    int main()                   
    {                            
        auto_ptr<int> pInt(new int(10));
        cout<<*pInt<<endl;       
    
        auto_ptr<A> pAObj(new A);
    
        return 0;                
    }

     哦,被你发现了;auto_ptr的初衷是用来实现智能指针的,实现内存的自动回收。比如,代码中我new了一个A对象,但是却没有对应的delete A对象。嗯,这得解决多少麻烦事啊,真的是好东西,而且还这么好用。好用?如果好用,我就不用写这篇博文来进行总结了。说道智能指针,就不得不对它的实现原理简单说说。

    2. 智能指针

    智能指针,无非就是进行垃圾回收,主要可以分为以下两大类:

      1. 基于引用计数的垃圾回收器。简单的说,引用计数主要是使用系统记录对象被引用的次数。当对象被引用的次数变为0时,该对象即可被视为“垃圾”,从而可以被回收。使用引用计数做垃圾回收的算法的一个优点是实现很简单,与其它垃圾回收算法相比,该方法不会造成程序暂停,因为计数的增减与对象的使用时紧密结合的。 具体的引用实例可以参见: 
        1. 智能指针-引用计数实现
        2. COM中的引用计数1
        3. COM中的引用计数2;
      2. 基于跟踪处理的垃圾回收机制。相比于引用计数,跟踪处理的垃圾回收机制被更为广泛地应用。其基本方法是产生跟踪对象的关系图,然后进行垃圾回收。使用跟踪方式的垃圾回收算法主要有以下几种:
        • 标记-清除(Mark-Sweep)
          这个算法可以分为两个过程。首先该算法将程序中正在使用的对象视为“根对象”,从根对象开始查找它们所引用的堆空间,并在这些堆空间上做标记。当标记结束后,所有被标记的对象就是可达对象或活对象,而没有被标记的对象就被认为是垃圾,在第二步的清扫阶段会被回收掉;
          这种方法的特点是活的对象不会被移动,但是其存在会出现大量的内存碎片问题。
        • 标记-整理(Mark-Compact)
          这个算法标记的方法和标记-清除方法一样,但是标记完之后,不再遍历所有对象清扫垃圾了,而是将活跃的对象向“左”靠齐,这就解决了内存碎片的问题。
          标记-整理算法有个特点就是移动活的对象,因此相应的,程序中所有对堆内存的引用都要更新。
        • 标记-拷贝(Mark-Copy)
          这种算法的一大特点就是将堆空间分为两部分:From,To。开始的时候我们只在From里分配,当From分配满的时候出发垃圾收集,这个时候会找出From空间里所有的存活对象,然后将这些存活的对象拷贝到To空间里。这样From空间里剩下的就都全是垃圾,而且对象拷贝到To里,在To里是紧凑排列的。这个事儿做完了之后From和To的角色就转变了一下。原来的From变成了To,原来的To变成了现在的From。现在又可以在这个完全是空的From里分配了。这个算法实现起来也很简单,高效(Sun JVM的新生代的垃圾回收就使用了这种算法)。不过这个算法有一个问题,堆的利用率只有一半了,这对那些内存占用率比较低的对象还算好,如果随着应用的内存占用率的增高,问题就出现了,第一个要拷贝的对象太多了,还有可能无法回收内存了。程序失败了。

    3. 为什么auto_ptr难用?

    总结了一点理论性的东西,我是最讨厌理论的,但是没有办法,你越讨厌的东西,你还越要去看,无奈。话题再收回来,上面也说了,auto_ptr不好用,为什么说auto_ptr不好用呢?在继续阅读下面的内容之前,我建议阅读一下这篇文章:《C++中的RAII机制》。 是的,auto_ptr并没有使用上面介绍的几种垃圾回收技术中的任何一种技术,而是使用的一种叫做RAII的机制实现的,所以,auto_ptr本质上一点都不智能。来看下面这段代码:

    #include <iostream>
    #include <memory.h>
    using namespace std;
    
    class A
    {
    public:
        A() { cout << "Construct A Object." << endl; }
        ~A() { cout << "Destroy A Object." << endl; }
    
        void SetA(int value) { m_a = value; }
        int GetA() { return m_a; }
    
    private:
        int m_a;
    };
    
    void TestFunc(auto_ptr<A> Obj)
    {
        Obj->SetA(20);
        cout << Obj->GetA() << endl;
    }
    
    int main()
    {
        auto_ptr<A> pAObj(new A());
        //auto_ptr<A> pAObj1 = new A(); **This is wrong expression.
    
        pAObj->SetA(10);
        cout << pAObj->GetA() << endl;
    
        TestFunc(pAObj);
    
        //cout << pAObj->GetA() << endl; ** This is wrong.
    
        return 0;
    }

    看看,发生了什么。调用完TestFunc(pAobj)之后,我再调用pAObj->GetA()居然出错了。这就是auto_ptr的奇葩之处。为什么?在《C++中的RAII机制》中的使用RAII的陷阱一节有仔细的分析。

    4. auto_ptr的庐山真面目

    总是在说,总觉的缺点什么。看看auto_ptr的源代码吧(代码来源:SGI STL)。

    template <class _Tp> class auto_ptr {
    private:
        _Tp* _M_ptr; //实际wrap的指针
    
    public:
        typedef _Tp element_type;
    
        // 显式构造函数,防止auto_ptr<A> pAObj1 = new A();隐式构造
        explicit auto_ptr(_Tp* __p = 0) __STL_NOTHROW : _M_ptr(__p) {}
    
        // 复制构造函数,知道为什么参数是&吗?
        auto_ptr(auto_ptr& __a) __STL_NOTHROW : _M_ptr(__a.release()) {}
        template <class _Tp1> auto_ptr(auto_ptr<_Tp1>& __a) __STL_NOTHROW
            : _M_ptr(__a.release()) {}
    
        // 赋值构造函数
        auto_ptr& operator=(auto_ptr& __a) __STL_NOTHROW {
            if (&__a != this) {
                delete _M_ptr;
                _M_ptr = __a.release();
            }
            return *this;
        }
    
        template <class _Tp1>
        auto_ptr& operator=(auto_ptr<_Tp1>& __a) __STL_NOTHROW {
            if (__a.get() != this->get()) {
                delete _M_ptr;
                _M_ptr = __a.release();
            }
            return *this;
        }
    
        ~auto_ptr() { delete _M_ptr; }
    
        // 智能指针一般都要重载"*"和"->"操作符
        _Tp& operator*() const __STL_NOTHROW {
            return *_M_ptr;
        }
        _Tp* operator->() const __STL_NOTHROW {
            return _M_ptr;
        }
        _Tp* get() const __STL_NOTHROW {
            return _M_ptr;
        }
        _Tp* release() __STL_NOTHROW {
            _Tp* __tmp = _M_ptr;
            _M_ptr = 0;
            return __tmp;
        }
        void reset(_Tp* __p = 0) __STL_NOTHROW {
            if (__p != _M_ptr) {
                delete _M_ptr;
                _M_ptr = __p;
            }
        }
    };

    对于上面的代码,你有什么要问的么?阅读代码就会发现,auto_ptr不能共享内存,在同一时间,只有一个auto_ptr指向一个指定的内存。所以说,以下这种代码就会ptr2失效了。

    auto_ptr<int> ptr(p);
    auto_ptr<int> ptr2(p);
    ptr = ptr2;

    简直无法想象,一个赋值操作,导致右值失效了,怎么可以,这种问题,在实际开发中,我们不得不去注意。

    5. 我们要注意什么?

    是的,auto_ptr有不少坑,不可能列全了,但话又说回来了,以后基本上也会告别auto_ptr了,让我们再和auto_ptr愉快的玩最后一段时光吧。 当你、我,还有他在使用auto_ptr时,需要注意以下几点:

    • auto_ptr不能共享所有权;
    • auto_ptr不能指向数组(否则会造成内存泄漏问题);对于这点,我认为如果数组中存放的是POD类型时,或者含有trivial destructor时,是可以指向数组的;
    • auto_ptr不能作为容器的成员;C++标准已经明确禁止这么做了,否则可能会遇到不可预见的问题(STL容器在分配内存的时候,必须要能够拷贝构造容器的元素。而且拷贝构造的时候,不能修改原来元素的值。而auto_ptr在拷贝构造的时候,一定会修改元素的值。所以STL元素不能使用auto_ptr。)。

    6. C++11中的智能指针

    但是随着C++11的到来,auto_ptr已经不再了,即将成为历史;好的东西总是会受到大家的欢迎的,随着大家都在使用“准”标准库boost中的shared_ptr;C++标准委员会终于觉的是时候将shared_ptr加入到C++11中去了。欢呼声一片,至少我是这么觉的了;至少shared_ptr让我用起来,还是不错的。接下来,就总结一下C++11中的这些智能指针吧。

    先来一段简单的代码,看看C++11中到底有哪些智能指针。

    /*************************************************************************
    > File Name: SmartPointDemo.cpp
    > Author: Jelly
    > Mail: vipygd#126.com(#->@)
    > Created Time: 2014年10月16日 星期四 15时25分43秒
    ************************************************************************/
    
    #include <iostream>
    #include <memory>
    using namespace std;
    
    int main()
    {    
        unique_ptr<int> up1(new int(10)); // 不能复制的unique_ptr
        // unique_ptr<int> up2 = up1; // 这样是错的
        cout<<*up1<<endl;
    
        unique_ptr<int> up3 = move(up1); // 现在up3是数据唯一的unique_ptr智能指针
    
        cout<<*up3<<endl;
        // cout<<*up1<<endl; // 运行时错误
    
        up3.reset(); // 显式释放内存
        up1.reset(); // 即使up1没有拥有任何内存,但是这样调用也没有问题
        // cout<<*up3<<endl; // 已经释放掉up3了,这样会运行时错误
    
        shared_ptr<int> sp1(new int(20));
        shared_ptr<int> sp2 = sp1; // 这是完全可以的,增加引用计数
    
        cout<<*sp1<<endl;
        cout<<*sp2<<endl;
    
        sp1.reset(); // 减少引用计数
        cout<<*sp2<<endl;
    
        return 0;
    }

    C++11中主要提供了unique_ptrshared_ptrweak_ptr这三个智能指针来自动回收堆分配的对象。看看上面的代码,感觉用起来也还挺轻松的,也还不错,至少是比auto_ptr好点。

    7. unique_ptr

    C++11中的unique_ptrauto_ptr的替代品,它与auto_ptr一样拥有唯一拥有权的特性,与auto_ptr不一样的是,unique_ptr是没有复制构造函数的,这就防止了一些“悄悄地”丢失所有权的问题发生,如果需要将所有权进行转移,可以这样操作:

    unique_ptr<int> up3 = move(up1); // 现在up3是数据唯一的unique_ptr智能指针
    // 或者
    unique_ptr<int> up4(move(up1));

    只有在使用者显式的调用std::move之后,才会发生所有权的转移,这样就让使用者知道自己在干什么。再来一段代码,看看将unique_ptr作为函数参数和返回值的使用情况:

    /*************************************************************************
    > File Name: unique_ptrDemo.cpp
    > Author: Jelly
    > Mail: vipygd#126.com(#->@)
    > Created Time: 2014年10月16日 星期四 17时10分49秒
    ************************************************************************/
    
    #include <iostream>
    #include <memory>
    using namespace std;
    
    unique_ptr<int> Func(unique_ptr<int> a)
    {         
        cout<<*a<<endl;
        return a;
    }         
    
    int main()
    {         
        unique_ptr<int> up1(new int(10));
    
        up1 = Func(move(up1));
        cout<<*up1<<endl;
    
        return 0;
    }

    由于在unique_ptr中是没有拷贝构造函数的,所以在直接传参时,进行值传递时,建立临时变量时,就会出错了,所以需要显式的调用move,转移所有权;而函数的返回值已经进行了move操作,而不用显式的进行调用。

    8. shared_ptr

    在最开始的那段代码中,也简单的使用了一下shared_ptrshared_ptr名如其名,它允许多个该智能指针共享地“拥有”同一堆分配对象的内存;由于它的资源是可以共用的,所以也就可以透过operator=等方法,来分享shared_ptr所使用的资源。由于shared_ptr内部实现上使用的是引用计数这种方法,所以一旦一个shared_ptr指针放弃了“所有权”,其它的shared_ptr对对象的引用并不会发生变化;只有在引用计数归零的时候,shared_ptr才会真正的释放所占有的堆内存空间的。对于引用计数的问题,我这里就不再多总结了,可以参考以下文章:

    – 智能指针-引用计数实现
    – COM中的引用计数1
    – COM中的引用计数2

    我这里注重的总结shared_ptr的使用,并不会对shared_ptr进行源码级别的分析。再来一段简单的代码,看看shared_ptr的一些应用。

    #include <iostream>
    #include <memory>
    using namespace std;
    
    void Func1(shared_ptr<int> a)
    {
        cout<<"Enter Func1"<<endl;
        cout<<"Ref count: "<<a.use_count()<<endl;
        cout<<"Leave Func1"<<endl;
    }
    
    shared_ptr<int> Func2(shared_ptr<int> a)
    {
        cout<<"Enter Func2"<<endl;
        cout<<"Ref count: "<<a.use_count()<<endl;
        cout<<"Leave Func2"<<endl;
        return a;
    }
    
    int main()
    {
        shared_ptr<int> aObj1(new int(10));
        cout<<"Ref count: "<<aObj1.use_count()<<endl;
    
        {
            shared_ptr<int> aObj2 = aObj1;
            cout<<"Ref count: "<<aObj2.use_count()<<endl;
        }
    
        Func1(aObj1);
    
        Func2(aObj1);
    
        shared_ptr<int> aObj3 = Func2(aObj1);
        cout<<"Ref count:"<<aObj3.use_count()<<endl;
    
        return 0;
    }

    自己单独想想程序的输出。输出如下:

    Ref count: 1
    Ref count: 2
    Enter Func1
    Ref count: 2
    Leave Func1
    Enter Func2
    Ref count: 2
    Leave Func2
    Enter Func2
    Ref count: 2
    Leave Func2
    Ref count:2

    9. shared_ptr指向数组

    在默认情况下,shared_ptr将调用delete进行内存的释放;当分配内存时使用new[]时,我们需要对应的调用delete[]来释放内存;为了能正确的使用shared_ptr指向一个数组,我们就需要定制一个删除函数,例如:

    #include <iostream>
    #include <memory>
    using namespace std;
    
    class A
    {
    public:
        A() { cout<<"constructor"<<endl; }
        ~A() { cout<<"destructor"<<endl; }
    };
    
    int main()
    {
        shared_ptr<A> arrayObj(new A[5], [](A *p){delete[] p;});
    
        return 0;
    }

    上面的代码看不懂的,请参考这篇C++中的Lambda表达式文章。如果确实需要共享地托管一个对象,使用unique_ptr也许会更简单一些,比如:

    #include <iostream>
    #include <memory>
    using namespace std;
    
    class A
    {
    public:
        A() { cout<<"constructor"<<endl; }
        ~A() { cout<<"destructor"<<endl; }
    };
    
    int main()
    {
        unique_ptr<A[]> arrayObj(new A[5]);
    
        return 0;
    }

    10. 线程安全

    关于多线程中使用shared_ptr,有如下几点描述:

    1. 同一个shared_ptr被多个线程读,是线程安全的;
    2. 同一个shared_ptr被多个线程写,不是 线程安全的;
    3. 共享引用计数的不同的shared_ptr被多个线程写,是线程安全的。 对于第一点,没有什么说的;对于第二点,同一个shared_ptr在不同的线程中进行写操作不是线程安全的,那基于第三点,我们一般会有以下方案来实现线程安全:

    对于线程中传入的外部shared_ptr对象,在线程内部进行一次新的构造,例如: sharedptr AObjTmp = outerSharedptrObj;

    11. 环形引用

    对于使用引用计数实现的智能指针,总是避免不了这个问题的。如果出现类似下面的代码,那就出现了环形引用的问题了。

    class Parent
    {
    public:
        shared_ptr<Child> child;
    };
    
    class Child
    {
    public:
        shared_ptr<Parent> parent;
    };
    
    shared_ptr<Parent> pA(new Parent);
    shared_ptr<Child> pB(new Child);
    pA->child = pB;
    pB->parent = pA;

    要解决环形引用的问题,没有特别好的办法,一般都是在可能出现环形引用的地方使用weak_ptr来代替shared_ptr。说到了weak_ptr,那下面就接着总结weak_ptr吧。

    12. weak_ptr

    weak_ptr是最麻烦的,也比较拗口的;它可以指向shared_ptr指针指向的对象内存,却并不拥有该内存。但是,使用weak_ptr成员lock,则可返回其指向内存的一个shared_ptr对象,且在所指对象内存已经无效时,返回指针空值(nullptr)。由于weak_ptr是指向shared_ptr所指向的内存的,所以,weak_ptr并不能独立存在。例如以下代码:

    #include <iostream>
    #include <memory>
    using namespace std;
    
    void Check(weak_ptr<int> &wp)
    {
        shared_ptr<int> sp = wp.lock(); // 重新获得shared_ptr对象
        if (sp != nullptr)
        {
            cout<<"The value is "<<*sp<<endl;
        }
        else
        {
            cout<<"Pointer is invalid."<<endl;
        }
    }
    
    int main()
    {
        shared_ptr<int> sp1(new int(10));
        shared_ptr<int> sp2 = sp1;
        weak_ptr<int> wp = sp1; // 指向sp1所指向的内存
    
        cout<<*sp1<<endl;
        cout<<*sp2<<endl;
        Check(wp);
    
        sp1.reset();
        cout<<*sp2<<endl;
        Check(wp);
    
        sp2.reset();
        Check(wp);
    
        return 0;
    }

    所以,我们在使用weak_ptr时也要当心,时刻需要判断对应的shared_ptr是否还有效。对于上面的环形引用的问题,由于weak_ptr并不会增加shared_ptr的引用计数,所以我们就可以使用weak_ptr来解决这个问题。

    class Parent
    {
    public:
        weak_ptr<Child> child;
    };
    
    class Child
    {
    public:
        weak_ptr<Parent> parent;
    };
    
    shared_ptr<Parent> pA(new Parent);
    shared_ptr<Child> pB(new Child);
    pA->child = pB;
    pB->parent = pA;
  • 相关阅读:
    sqli-labs Less29-Less31
    sqli-labs Less23-Less28a
    sqli-labs Less20-Less22
    sqli-labs Less17--Less19
    sqli-labs Less13-Less16
    为什么选择centos,而不是Dibian、Ubuntu【转】
    sublime 安装常用插件
    Linux常用命令
    在UEFI下安装windows和Ubuntu双系统目前不可行
    nginx 环境搭建(基于linux)
  • 原文地址:https://www.cnblogs.com/alwayswangzi/p/7230969.html
Copyright © 2020-2023  润新知