C++11 智能指针学习

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/qq_35865125/article/details/88918909

                                                                别人笑我太疯癫,我笑别人看不穿。--唐寅


C++程序设计中使用堆内存是非常频繁的操作,堆内存的申请和释放都由程序员自己管理。程序员自己管理堆内存可以提高程序的效率,但是整体来说堆内存的管理是麻烦的,C++11中引入了智能指针的概念,方便管理堆内存。

使用普通指针,容易造成堆内存泄露(忘记释放),二次释放,等问题,使用智能指针能更好的管理堆内存。

有时候我们会忘记释放内存,甚至有时候我们根本就不知道什么时候释放内存。特别时在多个线程间共享数据时,更难判断内存该何使释放。这种情况下就机器容易产生引用非法内存的指针。" 

智能指针的行为类似于常规指针。重要的区别是它负责自动释放所指向的对象。

理解智能指针:

1)从较浅的层面看,智能指针是利用了一种叫做RAII(Resource Acquisition Is Initialization,资源获取即初始化)的技术对普通的指针进行封装,这使得智能指针实质是一个对象,行为表现的却像一个指针。 

RAII是C++语言的一种管理资源、避免泄漏的惯用法。

2)智能指针的作用是防止忘记调用delete释放内存,和程序异常进入catch块忘记释放内存。另外指针的释放时机也是非常有考究的,多次释放同一个指针会造成程序崩溃,这些都可以通过智能指针来解决。


智能指针在C++11版本以及之后提供,包含在头文件<memory>中,有 shared_ptr、unique_ptr、weak_ptr。

shared_ptr允许多个指针指向同一个对象;unique_ptr则独占所指向的对象。标准库还定义了一个weak_ptr类,它是一种弱引用,指向shared_ptr所管理的对象。

1)shared_ptr

shared_ptr 多个指针指向相同的对象。shared_ptr 使用引用计数,每一个shared_ptr的拷贝都指向相同的内存。每使用它一次,内部的引用计数加1,每析构一次,内部的引用计数减1,减为0时,自动删除所指向的堆内存。shared_ptr内部的引用计数是线程安全的,但是对象的读取需要加锁。

只要将 new 运算符返回的指针 p 交给一个 shared_ptr 对象“托管”,就不必担心在哪里写delete p语句——实际上根本不需要编写这条语句,托管 p 的 shared_ptr 对象在消亡时会自动执行delete p。而且,该 shared_ptr 对象能像指针 p —样使用,即假设托管 p 的 shared_ptr 对象叫作 ptr,那么 *ptr 就是 p 指向的对象。”            

"多个 shared_ptr 对象可以共同托管一个指针 p,当所有曾经托管 p 的 shared_ptr 对象都解除了对其的托管时,就会执行delete p"


初始化:

智能指针是个模板类,可以指定类型,传入指针通过构造函数初始化。也可以使用make_shared函数初始化。不能将指针直接赋值给一个智能指针,一个是类,一个是指针。例如std::shared_ptr<int> p4 = new int(1);的写法是错误的。

解引用一个智能指针返回它指向的对象。如果在一个条件判断中使用智能指针,效果就是检测它是否为空。

常见的初始化:

int a = 10;
std::shared_ptr<int> ptra = std::make_shared<int>(a);


/*---------空指针------------*/
   shared_ptr<string> p1;

    if(!p1)                         //默认初始化的智能指针中保存着一个空指针!并不是""空字符串
        cout<<"p1==NULL"<<endl;

  /*---------初始化------------*/

  shared_ptr<string> p1(new string); 
  if(p1&&p1->empty()){         //需要注意 empty是属于string类的成员函数。 
    *p1="helloworld"; 
    cout<<*p1<<endl;
  }

//    shared_ptr<int> pa = new int(1);//error:不允许以暴露裸漏的指针进行赋值操作。

  //一般的初始化方式
    shared_ptr<string> pint(new string("normal usage!"));
    cout<<*pint<<endl;

    //推荐的安全的初始化方式
    shared_ptr<string> pint1 = make_shared<string>("safe uage!");
    cout<<*pint1<<endl;

    //通常我们用auto来定义一个对象来保存make_shared的结果,这种方式更为简单:
    auto pau = make_shared<string>("auto");    //!更简单,更常用的方式。
    cout<<*pau<<endl;

不推荐的初始化方式:

/*不推荐*/
    int * p = new int(32);
    shared_ptr<int> pp(p);
    cout<<*pp<<endl;

    /*意外的情况*/
//    delete p;               //!不小心把delete掉了。
//    cout<<*pp<<endl;·       //!pp也不再有效。


很好的例子:

    #include <iostream>
    #include <memory>
    using namespace std;
    class A
    {
    public:
        int i;
        A(int n):i(n) { };
        ~A() { cout << i << " " << "destructed" << endl; }
    };
    int main()
    {
        shared_ptr<A> sp1(new A(2)); //A(2)由sp1托管,
        shared_ptr<A> sp2(sp1);       //A(2)同时交由sp2托管
        shared_ptr<A> sp3;
        sp3 = sp2;   //A(2)同时交由sp3托管
        cout << sp1->i << "," << sp2->i <<"," << sp3->i << endl; //2,2,2
        A * p = sp3.get();      // get返回托管的指针,p 指向 A(2)
        cout << p->i << endl;  //输出 2
        sp1.reset(new A(3));    // reset导致托管新的指针, 此时sp1托管A(3)
        sp2.reset(new A(4));    // sp2托管A(4)
        cout << sp1->i << endl; //输出 3
        sp3.reset(new A(5));    // sp3托管A(5),然后,A(2)无人托管,被delete
        cout << "end" << endl;
        return 0;
    }

输出:

2,2,2
2
3
2 destructed
end
5 destructed
4 destructed
3 destructed

可以让多个 shared_ptr 对象去托管同一个指针,这些多个 shared_ptr 对象会共享一个对共同托管的指针的“托管计数”。有 n 个 shared_ptr 对象托管同一个指针 p,则 p 的托管计数就是 n。当一个指针的托管计数减为 0 时,该指针会被释放。shared_ptr 对象消亡或托管了新的指针,都会导致其原托管指针的托管计数减 1

shared_ptr 的 reset 成员函数可以使得对象解除对原托管指针的托管(如果有的话),并托管新的指针。原指针的托管计数会减 1。

main 函数结束时,sp1、sp2、sp3 对象消亡,各自将其托管的指针的托管计数减为 0,并且释放其托管的指针,于是输出5 destructed ...


!!!!!

只有指向动态分配的对象的指针才能交给 shared_ptr 对象托管。将指向普通局部变量、全局变量的指针交给 shared_ptr 托管,编译时不会有问题,但程序运行时会出错,因为不能delete一个没有指向动态分配(堆内存)的内存空间的指针。

注意,不能用下面的方式使得两个 shared_ptr 对象托管同一个指针:

A* p = new A(10);

shared_ptr <A> sp1(p), sp2(p);

sp1 和 sp2 并不会共享同一个对 p 的托管计数,而是各自将对 p 的托管计数都记为 1(sp2 无法知道 p 已经被 sp1 托管过)。这样,当 sp1 消亡时要析构 p,sp2 消亡时要再次析构 p,这会导致程序崩溃。


shared_ptr的一个最大的陷阱是循环引用----这时需要weak_ptr类啦:

weak_ptr是为了配合shared_ptr而引入的一种智能指针,因为它不具有普通指针的行为,没有重载operator*和->,它的最大作用在于协助shared_ptr工作,像旁观者那样观测资源的使用情况。

weak_ptr可以从一个shared_ptr或者另一个weak_ptr对象构造,获得资源的观测权。但weak_ptr没有共享资源,它的构造不会引起指针引用计数的增加。

使用weak_ptr的成员函数use_count()可以观测资源的引用计数,另一个成员函数expired()的功能等价于use_count()==0,但更快,表示被观测的资源(也就是shared_ptr的管理的资源)已经不复存在。

weak_ptr可以使用一个非常重要的成员函数lock()从被观测的shared_ptr获得一个可用的shared_ptr对象, 从而操作资源。但当expired()==true的时候,lock()函数将返回一个存储空指针的shared_ptr。

如果一块内存被shared_ptr和weak_ptr同时引用,当所有shared_ptr析构了之后,不管还有没有weak_ptr引用该内存,内存也会被释放。所以weak_ptr不保证它指向的内存一定是有效的,在使用之前需要检查weak_ptr是否为空指针。

循环引用问题举例:

#include<iostream>
using namespace std;
#include"boost/shared_ptr.hpp"
 
 
struct Node
{
    Node(int value)
    :_value(value)
    {
        cout << "Node()" << endl;
    }
    ~Node()
    {
        cout << "~Node()" << endl;
    }
    shared_ptr<Node> _prev;
    shared_ptr<Node> _next;
    int _value;
};
 
void Test2()
{
    shared_ptr<Node> sp1(new Node(1));
    shared_ptr<Node> sp2(new Node(2));
    cout << sp1.use_count() << endl;
    cout << sp2.use_count() << endl;
    sp1->_next = sp2;
    sp2->_prev = sp1;
    cout << sp1.use_count() << endl;
    cout << sp2.use_count() << endl;
}
 
int main()
{
    Test2();
    return 0;
}

在这里我们使用了双向链表来说明这个问题,并且双向链表中的指针都是使用shared_ptr来维护的,在这里我使用的是标准库里的shared_ptr智能指针。

从下图可以看出,构造的sp1和sp2在出它们的作用域时(即Test2())都没有被析构,从而造成了内存泄漏。

由于先构造的后释放,后构造的先释放可知,先释放的是sp2,那么因为它的引用计数为2,减去1之后就成为了1,不能释放空间,因为还有其他的对象在管理这块空间。但是sp2这个变量已经被销毁,因为它是栈上的变量,但是sp2管理的堆上的空间并没有释放。
接下来释放sp1,同样,先检查引用计数,由于sp1的引用计数也是2,所以减1后成为1,也不会释放sp1管理的动态空间。

通俗点讲:就是sp2要释放,那么必须等p1释放了,而sp1要释放,必须等sp2释放,所以,最终,它们两个都没有释放空间。

所以,造成了内存泄漏。
 


使用weak_ptr来解决上述的循环引用问题:

#include<iostream>
using namespace std;
#include"boost/shared_ptr.hpp"
 
struct Node
{
    Node(int value)
    :_value(value)
    {
        cout << "Node()" << endl;
    }
    ~Node()
    {
        cout << "~Node()" << endl;
    }
   weak_ptr<Node> _prev;
   weak_ptr<Node> _next;
    int _value;
};
void Test2()
{
    shared_ptr<Node> sp1(new Node(1));
    shared_ptr<Node> sp2(new Node(2));
    cout << sp1.use_count() << endl;
    cout << sp2.use_count() << endl;
    sp1->_next = sp2;
    sp2->_prev = sp1;
    cout << sp1.use_count() << endl;
    cout << sp2.use_count() << endl;
}
 
int main()
{
    Test2();
    return 0;
}

输 出:

“weak_ptr可以从一个shared_ptr或者另一个weak_ptr对象构造,获得资源的观测权。但weak_ptr没有共享资源,它的构造不会引起指针引用计数的增加。” ---》 解决了问题啊。

 


自己设计实现shared_ptr类:

shared_ptr对象每次离开作用域时会自动调用shared_ptr类的析构函数,而析构函数并不像其他类的析构函数一样,而是在释放内存是先判断引用计数器是否为0。等于0才做delete操作,否则只对引用计数器做减一操作。

一般来说,智能指针的实现需要以下步骤:

1.一个模板指针T* ptr,指向实际的对象。

2.一个引用次数(必须new出来的!!不然会多个shared_ptr里面会有不同的引用次数而导致多次delete)。

3.定义operator*和operator->,使得能像指针一样使用shared_ptr。

4.定义copy constructor,使其引用次数加一。

5.定义operator=,如果原来的shared_ptr已经有对象,则让其引用次数减一并判断引用是否为零(是否调用delete)。

 然后将新的对象引用次数加一。

6.定义析构函数,使引用次数减一并判断引用是否为零(是否调用delete)。

#include <iostream>
#include <memory>

template<typename T>
class SmartPointer {
private:
    T* _ptr;
    size_t* _count;
public:
    SmartPointer(T* ptr = nullptr) : _ptr(ptr) 
    {
        if (_ptr) {
            _count = new size_t(1);
        } else {
            _count = new size_t(0);
        }
    }

    //复制构造函数,用一个已经存在的对象去初始化一个新的对象。注意,两个对象的指针
    //成员_count指向同一个内存地址,所以最后两者的对_ptr的计数是相同的。我曾疑惑
    SmartPointer(const SmartPointer& ptr) {
        if (this != &ptr) {
            this->_ptr = ptr._ptr;
            this->_count = ptr._count;
            (*this->_count)++;
        }
    }

    SmartPointer& operator=(const SmartPointer& ptr) {
        if (this->_ptr == ptr._ptr) {
            return *this;
        }

        if (this->_ptr) {
            (*this->_count)--;
            if (this->_count == 0) {
                delete this->_ptr;
                delete this->_count;
            }
        }

        this->_ptr = ptr._ptr;
        this->_count = ptr._count;
        (*this->_count)++;
        return *this;
    }

    T& operator*() {
        assert(this->_ptr == nullptr);
        return *(this->_ptr);

    }

    T* operator->() {
        assert(this->_ptr == nullptr);
        return this->_ptr;
    }

    ~SmartPointer() {
        (*this->_count)--;
        if (*this->_count == 0) {
            delete this->_ptr;
            delete this->_count;
        }
    }

    size_t use_count(){
        return *this->_count;
    }
};

int main() {
    {
        SmartPointer<int> sp(new int(10));
        SmartPointer<int> sp2(sp);
        SmartPointer<int> sp3(new int(20));
        sp2 = sp3;
        std::cout << sp.use_count() << std::endl;
        std::cout << sp3.use_count() << std::endl;
    }
    //delete operator
}

标准容器

把对象直接存入容器中有时会有些麻烦。以值的方式保存对象意味着使用者将获得容器中的元素的拷贝,对于那些复制是一种昂贵的操作的类型来说可能会有性能的问题。此外,有些容器,特别是 std::vector, 当你加入元素时可能会复制所有元素,这更加重了性能的问题。最后,传值的语义意味着没有多态的行为。如果你需要在容器中存放多态的对象而且你不想切割它们,你必须用指针。如果你用裸指针,维护元素的完整性会非常复杂。从容器中删除元素时,你必须知道容器的使用者是否还在引用那些要删除的元素。

#include "boost/shared_ptr.hpp"
#include <vector>
#include <iostream>
class A {
public:
    virtual void sing()=0;
    protected:
    virtual ~A() {};
};
class B : public A {
public:
    virtual void sing() {
        std::cout << "Do re mi fa so la";
    }
};
boost::shared_ptr<A> createA() {
    boost::shared_ptr<A> p(new B());
    return p;
}
int main() {
    typedef std::vector<boost::shared_ptr<A> > container_type;
    typedef container_type::iterator iterator;
    container_type container;
    for (int i=0;i<10;++i) {
        container.push_back(createA());
    }
    std::cout << "The choir is gathered: \n";
    iterator end=container.end();
    for (iterator it=container.begin();it!=end;++it) {
        (*it)->sing();
    }
} 

这里有两个类, A和 B, 各有一个虚拟成员函数 sing。 B从 A公有继承而来,并且如你所见,工厂函数 createA返回一个动态分配的B的实例,包装在shared_ptr<A>里。

在 main里, 一个包含shared_ptr<A>的 std::vector被放入10个元素,最后对每个元素调用sing。如果我们用裸指针作为元素,那些对象需要被手工删除。而在这个例子里,删除是自动的,因为在vector的生存期中,每个shared_ptr的引用计数都保持为1;当 vector被销毁,所有引用计数器都将变为零,所有对象都被删除。

上面的例子示范了一个强有力的技术,它涉及A里面的protected析构函数。因为函数 createA返回的是 shared_ptr<A>, 因此不可能对shared_ptr::get返回的指针调用 delete。这意味着如果向某个需要裸指针的函数传送从shared_ptr中取出的裸指针,不会由于意外地被删除而导致灾难。这是非常有用的方法,用于给shared_ptr中的对象增加额外的安全性。  

https://baike.baidu.com/item/shared_ptr/2708164?fr=aladdin


shared_ptr与线程安全:


定制删除器 

 我们都会有一种普遍的认知,shared_ptr智能指针只能用来管理动态开辟的空间,实际不然,shared_ptr是用来管理资源的,这里的资源并不仅仅是动态开辟的空间,还有例如:打开文件,其维护一个文件指针,那么在程序快要结束时,需要关闭文件;

所以呢,我们就需要为其定制删除器。 

例子,尚未验证:

#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
using namespace std;
//为解决文件指针
struct Fclose
{
    void operator()(FILE*& fp)
    {
        cout<< "Fclose()" << endl;
        fclose(fp);
        fp = NULL;
    }
};
//为解决malloc开辟的空间
template<class T>
struct Free
{
    void operator()(T*& p)
    {
        free(p);
        p = NULL;
    }
};
//一般情况下(使用new动态开辟的空间)
template<class T>
class Delete
{
public :
    void operator()(T*& p)
    {
        delete p;
        p = NULL;
    }
};
template<class T, class Destory = Delete<T> >
class SharedPtr
{
public :
    SharedPtr(T* ptr = 0)//构造函数
        :_ptr(ptr)
        ,_pCount(NULL)
    {
        if (_ptr)
        {
            _pCount = new int(1);
        }
    }
    SharedPtr(const SharedPtr<T>& sp)//拷贝构造函数
        :_ptr(sp._ptr)
        , _pCount(sp._pCount)
    {
        if (_ptr)
        {
            ++GetRef();
        }
    }
    //sp1 = sp2
    SharedPtr<T>& operator=(const SharedPtr<T>& sp)
    {
        //可有三种情况:①sp1._ptr = NULL  ②sp1的引用计数为1  ③sp1的引用计数大于1
        if (this != &sp)
        {
            Release();
            _ptr = sp._ptr;
            _pCount = sp._pCount;
            ++GetRef();
        }
        return *this;
    }
    //辅助函数
    void Release()
    {
        if (_ptr && --GetRef() == 0)
        {
            Destory()(_ptr);
            delete _pCount;
            _pCount = NULL;
        }
    }
 //析构函数
    ~SharedPtr()
    {
        Release();
    }
 
    int& GetRef()
    {
        return *_pCount;
    }
private:
    T* _ptr;
    int* _pCount;
};
void Test2()
{
    FILE* sp1 = fopen("test.txt", "rb");
    SharedPtr<FILE, Fclose> sp2(sp1);
 
    int* sp3 = (int*)malloc(sizeof(int));
    SharedPtr<int, Free<int> >sp4(sp3);
 
    int* sp5 = new int(1);
    SharedPtr<int> sp6(sp5);
}
 
int main()
{
    Test2();
    return 0;
}

总结:

在以下情况时使用 shared_ptr :

  • 当有多个使用者使用同一个对象,而没有一个明显的拥有者时

  • 当要把指针存入标准库容器时

  • 当要传送对象到库或从库获取对象,而没有明确的所有权时

  • 当管理一些需要特殊清除方式的资源时

    通过定制删除器的帮助。


2)unique_ptr

unique_ptr“唯一”拥有其所指对象,同一时刻只能有一个unique_ptr指向给定对象(通过禁止拷贝语义、只有移动语义来实现)。

unique_ptr指针本身的生命周期:从unique_ptr指针创建时开始,直到离开作用域。离开作用域时,若其指向对象,则将其所指对象销毁(默认使用delete操作符,用户可指定其他操作)。

unique_ptr指针与其所指对象的关系:在智能指针生命周期内,可以改变智能指针所指对象,如创建智能指针时通过构造函数指定、通过reset方法重新指定、通过release方法释放所有权、通过移动语义转移所有权。

#include <iostream>
#include <memory>

int main() {
    {
        std::unique_ptr<int> uptr(new int(10));  //绑定动态对象
        //std::unique_ptr<int> uptr2 = uptr;  //不能賦值
        //std::unique_ptr<int> uptr2(uptr);  //不能拷貝
        std::unique_ptr<int> uptr2 = std::move(uptr); //轉換所有權
        uptr2.release(); //释放所有权
    }
    //超過uptr的作用域,內存釋放
}

Ref:

https://www.cnblogs.com/wxquare/p/4759020.html

https://www.cnblogs.com/wangkeqin/p/9351191.html

https://blog.csdn.net/worldwindjp/article/details/18843087#comments

http://c.biancheng.net/view/430.html

https://blog.csdn.net/qq_34992845/article/details/69218843

猜你喜欢

转载自blog.csdn.net/qq_35865125/article/details/88918909