zoukankan      html  css  js  c++  java
  • [C++11新特性] shared_ptr共享的智能指针

    C++ 程序设计中使用堆内存是非常频繁的操作,堆内存的申请和释放都由程序员自己管理。但使用普通指针,容易造成内存泄露(忘记释放)、二次释放、程序发生异常时内存泄露等问题等。所有 C++11 就引入了智能指针。

    一、原始指针容易发生内存泄漏

    C 语言中最常使用的是malloc()函数分配内存,free()函数释放内存,而 C++ 中对应的是newdelete关键字。malloc()只是分配了内存,而new则更进一步,不仅分配了内存,还调用了构造函数进行初始化。使用示例:

    int main()
    {
        // malloc返回值是 void*
        int* argC = (int*)malloc(sizeof(int));
        free(argC);
    
        char *age = new int(25); // 做了两件事情 1.分配内存 2.初始化
        delete age;
    }
    

    newdelete必须成对出现,有时候是不小心忘记了delete,有时候则是很难判断在这个地方自己是不是该delete,这个和资源的生命周期有关,这个资源是属于我这个类管理的还是由另外一个类管理的(其它类可能要使用),如果是别人管理的就由别人delete

    如果需要自己管理内存的话,最好显示的将自己的资源传递进去,这样的话,就能知道是该资源确实应该由自己来管理。

    char *getName(char* v, size_t bufferSize) {
        //do something
        return v;
    }
    

    上面还是小问题,自己小心一点,再仔细看看文档,还是有机会避免这些情况的。但是在 C++ 引入异常的概念之后,程序的控制流就发生了根本性的改变,在写了 delete 的时候还是有可能发生内存泄漏。如下例:

    void badThing(){
        throw 1;// 抛出一个异常
    }
    
    void test() {
        char* a = new char[1000];
    
        badThing();
        // do something
        delete[] a;
    }
    int main() {
        try {
            test();
        }
        catch (int i){
            cout << "error happened " << i << endl;
        }
    }
    

    上面的newdelete是成对出现的,但是程序在中间的时候抛出了异常,由于没有立即捕获,程序从这里退出了,并没有执行到delete,内存泄漏还是发生了。

    二、使用构造函数和析构函数解决内存泄漏

    C++ 中的构造函数和析构函数十分强大,可以使用构造和析构解决上面的内存泄漏问题,比如:

    class SafeIntPointer {
    public:
        explicit SafeIntPointer(int v) : m_value(new int(v)) { }
        ~SafeIntPointer() {
            delete m_value;
            cout << "~SafeIntPointer" << endl;
        }
        int get() { return *m_value; }
    private:
        int* m_value;
    };
    
    void badThing(){
        throw 1;// 抛出一个异常
    }
    
    void test() {
        SafeIntPointer a(5);
    
        badThing();
    }
    
    int main() {
        try {
            test();
        }
        catch (int i){
            cout << "error happened " << i << endl;
        }
    }
    
    // 结果
    // ~SafeIntPointer
    // error happened 1
    

    可以看到,就算发生了异常,也能够保证析构函数成功执行!这里的例子是这个资源只有一个人使用,我不用了就将它释放掉。但还有种情况,一份资源被很多人共同使用,要等到所有人都不再使用的时候才能释放掉,对于这种问题,就需要对上面的SafeIntPointer增加一个引用计数,如下:

    class SafeIntPointer {
    public:
        explicit SafeIntPointer(int v) : m_value(new int(v)), m_used(new int(1)) { }
        ~SafeIntPointer() {
            cout << "~SafeIntPointer" << endl;
            (*m_used) --; // 引用计数减1
            if(*m_used <= 0){
                delete m_used;
                delete m_value;
                cout << "real delete resources" << endl;
            }
        }
        
        SafeIntPointer(const SafeIntPointer& other) {
            m_used = other.m_used;
            m_value = other.m_value;
            (*m_used)++; // 引用计数加1
        }
        SafeIntPointer& operator= (const SafeIntPointer& other) {
            if (this == &other) // 避免自我赋值!!
               return *this;
    
            m_used = other.m_used;
            m_value = other.m_value;
            (*m_used)++; // 引用计数加1
            return *this;
        }
    
        int get() { return *m_value; }
        int getRefCount() {
            return *m_used;
        }
    
    private:
        int* m_used; // 引用计数
        int* m_value;
    };
    
    int main() {
        SafeIntPointer a(5);
        cout << "ref count = " << a.getRefCount() << endl;
        SafeIntPointer b = a;
        cout << "ref count = " << a.getRefCount() << endl;
        SafeIntPointer c = b;
        cout << "ref count = " << a.getRefCount() << endl;
    }
    
    /*
    ref count = 1
    ref count = 2
    ref count = 3
    ~SafeIntPointer
    ~SafeIntPointer
    ~SafeIntPointer
    real delete resources
    */
    

    可以看到每一次赋值,引用计数都加一,最后每次析构一次后引用计数减一,知道引用计数为 0,才真正释放资源。要写出一个正确的管理资源的包装类还是蛮难的,比如上面那个例子就不是线程安全的,只能属于一个玩具,在实际工程中简直没法用。

    所以 C++11 中引入了智能指针(Smart Pointer),它利用了一种叫做 RAII(资源获取即初始化)的技术将普通的指针封装为一个栈对象。当栈对象的生存周期结束后,会在析构函数中释放掉申请的内存,从而防止内存泄漏。这使得智能指针实质是一个对象,行为表现的却像一个指针。

    智能指针主要分为shared_ptrunique_ptrweak_ptr三种,使用时需要引用头文件<memory>。C++98 中还有auto_ptr,基本被淘汰了,不推荐使用。而 C++11 中shared_ptrweak_ptr都是参考boost库实现的。

    三、shared_ptr共享的智能指针

    3.1 shared_ptr的初始化

    最安全的分配和使用动态内存的方法是调用一个名为 make_shared 的标准库函数。 此函数在动态内存中分配一个对象并初始化它,返回指向此对象的 shared_ptr。与智能指针一样,make_shared 也定义在头文件 memory 中。

    // 指向一个值为42的int的shared_ptr
    shared_ptr<int> p3 = make_shared<int>(42);
    
    // p4 指向一个值为"9999999999"的string
    shared_ptr<string> p4 = make_shared<string>(10,'9');
    
    // p5指向一个值初始化的int
    shared_ptr<int> p5 = make_shared<int>();
    

    我们还可以用 new 返回的指针来初始化智能指针,不过接受指针参数的智能指针构造函数是 explicit 的。因此,我们不能将一个内置指针隐式转换为一个智能指针,必须使用直接初始化形式来初始化一个智能指针:

    shared_ptr<int> pi = new int (1024); // 错误:必须使用直接初始化形式
    shared_ptr<int> p2(new int(1024));	// 正确:使用了直接初始化形式
    

    出于相同的原因,一个返回 shared_ptr 的函数不能在其返回语句中隐式转换一个普通指针:

    shared_ptr<int> clone(int p)
    {
        return new int(p); // 错误:隐式转换为 shared_ptr<int>
    }
    

    3.2 shared_ptr的基本使用

    std::shared_ptr的基本使用很简单,看几个例子就明白了:

    #include <memory>
    #include <iostream>
    
    class Test
    {
    public:
        Test()
        {
            std::cout << "Test()" << std::endl;
        }
        ~Test()
        {
            std::cout << "~Test()" << std::endl;
        }
    };
    
    int main()
    {
        std::shared_ptr<Test> p1 = std::make_shared<Test>();
        std::cout << "1 ref:" << p1.use_count() << std::endl;
        {
            std::shared_ptr<Test> p2 = p1;
            std::cout << "2 ref:" << p1.use_count() << std::endl;
        }
        std::cout << "3 ref:" << p1.use_count() << std::endl;
        return 0;
    }
    

    输出如下:

    Test()
    1 ref:1
    2 ref:2
    3 ref:1
    ~Test()
    

    针对代码解读如下:

    • std::make_shared里面调用了 new 操作符分配内存;
    • 第二个p1.use_count()之所以显示为 2,是因为增加了引用对象 p2,而随着大括号的结束,p2 的作用域结束,所以 p1 的引用计数变回 1,而随着 main 函数的结束,p1 的作用域结束,此时检测到计数为 1,那就会在销毁 p1 的同时,调用 p1 的析构函数 delete 掉之前分配的内存空间;

    3.3 shared_ptr常用操作

    下面列出了shared_ptr独有的操作:

    make_shared<T>(args) // 返回一个shared_ptr,指向一个动态分配的类型为T的对象。使用args初始化此对象
    shared_ptr<T> p(q) // p是shared_ptr q的拷贝;此操作会递增q中的引用计数。q中的指针必须能转换成T*
    p = q // p和q都是shared_ptr,所保存的指针必须能相互转换。此操作会递减p中的引用计数,递增q中的引用计数。若p中的引用计数变为0,则将其管理的原内存释放
    p.unique() // 若p.use_count()为1,返回true;否则返回false
    p.use_count() // 返回与p共享对象的智能指针数量;可能很慢,主要用于调试
    

    下面介绍一些改变shared_ptr的其他方法:

    p.reset () //若p是唯一指向其对象的shared_ptr,reset会释放此对象。
    p.reset(q) //若传递了可选的参数内置指针q,会令P指向q,否则会将P置为空。
    p.reset(q, d) //若还传递了参数d,将会调用d而不是delete 来释放q
    

    参考:

    《C++ Primer 第5版》

    c++11&14-智能指针专题

    c++11]智能指针学习笔记


  • 相关阅读:
    LeetCode 453 Minimum Moves to Equal Array Elements
    LeetCode 112 Path Sum
    LeetCode 437 Path Sum III
    LeetCode 263 Ugly Number
    Solutions and Summay for Linked List Naive and Easy Questions
    AWS–Sysops notes
    Linked List
    All About Linked List
    datatable fix error–Invalid JSON response
    [转]反编译c#的相关问题
  • 原文地址:https://www.cnblogs.com/linuxAndMcu/p/14576049.html
Copyright © 2011-2022 走看看