• 【C++】智能指针



    一、什么是智能指针

    智能指针本身是一个类模板, 可以管理不同类型的指针, 它通过RAII的技术, 即利用对象生命周期来控制程序资源. 智能指针的原理也就是具有RAII的特性和重载operator->和operator*, 是其能像指针一样使用.

    为什么会需要智能指针呢? 在C++中, new出来的对象通常是需要我们自己来对其进行释放的, 而忘记释放而导致内存泄漏的问题也并不罕见, 而且在遇到执行流跳跃的情况下, 比如异常的抛出, 可能导致即使我们写了释放语句, 也可能存在执行不到的情况, 因此在此种情况下使用智能指针来管理是一种很好的避免内存泄漏的方法.

    二、类型及其使用语法

    下面介绍C++中的三个智能指针auto_ptr, unique_ptr, shared_ptr.
    它们的头文件以及所在命名空间都是相同的.
    头文件: < memory>
    命名空间: std

    1. auto_ptr (C++17已弃用)

    智能指针都存在一个问题, 那就是智能指针的拷贝和赋值问题, 用一个智能指针去拷贝或赋值给另一个智能指针, 此时会出现一个资源被两个对象管理的情况, 那么在执行释放时会出现资源被释放两次的情况, 比如:

    #include 
    #include 
    using namespace std;
    
    int main()
    {
        auto_ptr<int> ap1(new int(1));
        auto_ptr<int> ap2(ap1);
    
        cout << *ap1 << endl;
    
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    auto_ptr解决拷贝和赋值问题是通过"转移管理权"的方式, 以上代码表示ap1将new int(1)的管理权交给ap2, 而自己什么都不管理了, 通过调试可以看到:

    管理权转移前:
    在这里插入图片描述
    管理权转移后:
    在这里插入图片描述
    而此时又会出现新的问题, 即ap1处于悬空状态, 但如果我们不了解此时ap1悬空了, 再接着对其进行访问时, 就会出错:
    在这里插入图片描述
    而其在C++17已被弃用:
    在这里插入图片描述

    2. unique_ptr

    unique_ptr防拷贝和赋值的手段简单粗暴, 既然拷贝和赋值会出问题, 那么直接禁用拷贝和赋值就可以了. 所以unique_ptr是直接禁用了拷贝构造和赋值运算符重载.

    样例代码:

    #include 
    #include 
    using namespace std;
    
    int main()
    {
        unique_ptr<int> up1(new int(1));
        unique_ptr<int> up2(up1);
        unique_ptr<int> up3 = up1;
    
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    在尝试拷贝和赋值时, 会得到如下错误信息:
    在这里插入图片描述
    通过手册可以看到, 其拷贝和赋值确实被封住了:
    在这里插入图片描述
    在这里插入图片描述
    所以对于没有赋值和拷贝智能指针的需求, 那就直接使用unique_ptr吧.

    3. shared_ptr

    shared_ptr支持拷贝和赋值, 其是通过引用计数的方式来避免对资源多次释放的问题, 比如智能指针a管理了资源a, 那么资源a的引用计数就是1, 如果此时通过智能指针a拷贝或赋值给了智能指针b, 那么资源a的引用计数就是2, 而如果此时智能指针a进行了析构, 并不会直接释放资源a, 而是在析构时对资源a的引用计数减一, 再判断此时资源a的引用计数是否为0, 为0就表示该资源a只有自己管理它, 那么就直接把它释放掉, 而如果此时资源a的引用计数不为0, 表示还有其他的智能指针在管理资源a, 就不会对其进行释放, 那么这样就很好的避免了重复释放资源的情况, 还解决了智能指针之间的拷贝和赋值问题.

    样例代码:

    #include 
    #include 
    using namespace std;
    
    int main()
    {
        shared_ptr<int> sp1(new int(1));
        shared_ptr<int> sp2(sp1);
        shared_ptr<int> sp3 = sp1;
    
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    通过调试可以清楚的看到, 在只有sp1管理资源时, 引用计数为1:
    在这里插入图片描述
    而在进行了拷贝之后, 可以看到, 引用计数变为了3, 而管理的都是同一个资源:
    在这里插入图片描述

    3.1 循环计数问题

    但shared_ptr也并不是就完全的没有问题了, 在某些特殊情况下会出现循环引用的情况, 那么来看看这种循环引用是个什么情况, 假设有这么一个节点:

    struct node
    {
        int _val;
        shared_ptr<node> _prev;
        shared_ptr<node> _next;
    
        ~node()
        {
            cout << "~node()" << endl;
        }
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    如果此时代码是这么写的:

    int main()
    {
        shared_ptr<node> sp1(new node);
        shared_ptr<node> sp2(new node);
    
        cout << sp1.use_count() << endl; //use_count(): 查看引用计数
        cout << sp2.use_count() << endl;
        
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    图例:
    在这里插入图片描述

    这样写不会有什么问题, 引用计数都是1, 会正常调用析构, 而如果这样写:

    int main()
    {
        shared_ptr<node> sp1(new node);
        shared_ptr<node> sp2(new node);
    
        cout << sp1.use_count() << endl;
        cout << sp2.use_count() << endl;
    
        sp1->_next = sp2;
    
        cout << sp1.use_count() << endl;
        cout << sp2.use_count() << endl;
    
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    图例:
    在这里插入图片描述
    此时sp1->_next和sp2都管理同一资源, 在析构时sp2先析构, 那么它会把引用计数减1, 此时一看, 引用计数为1, 表示还有其他的智能指针管理着这一资源, 那么sp2不会对该资源进行释放, 然后到sp1析构, sp1析构时它会把引用计数减1, 而此时该资源的引用计数为0了, 那么sp1会释放该资源, 而该资源释放时对内置类型不做处理, 对自定义类型调用其析构, 那么sp1->_next也会析构, 此时sp1->_next和sp2管理的资源的引用计数为0, 那么在sp1->_next析构时会把其也给释放了, 而sp1也正常析构并释放了管理的资源, 所以此时这种情况也不会出现问题, 再来就是最后一种情况:

    int main()
    {
        shared_ptr<node> sp1(new node);
        shared_ptr<node> sp2(new node);
    
        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;
    
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    图例:
    在这里插入图片描述
    此时问题就来了, sp2先析构, 在析构时, 引用计数减1, 2-1=1, 发现还有1个智能指针 (sp1->_next) 管理着这块资源, 那么sp2就走了, 而到sp1析构时, 引用计数减1, 2-1=1, 发现也还有1个智能指针
    (sp2->_prev) 管理着这块资源, 也直接走了, 此时就出现了如下图所示的情况:
    在这里插入图片描述
    此时sp1和sp2都析构走了, 但是资源并没有被释放, 这就造成了内存泄露了.
    在这里插入图片描述
    可以看到程序运行起来并没有调用析构函数.

    那么解决该问题的办法就是, 将node节点里的shared_ptr换成weak_ptr, 如下:

    struct node
    {
        int _val;
        weak_ptr<node> _prev;
        weak_ptr<node> _next;
    
        ~node()
        {
            cout << "~node()" << endl;
        }
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    weak_ptr的头文件和所在命名空间和shared_ptr一样, 它并不支持RAII, 但能像指针一样使用, 它主要用于辅助shared_ptr解决循环引用的问题, 它能解决该问题的主要原因是, 它指向资源, 但并不参与资源的管理, 即不会增加资源的引用计数, 那么问题就迎刃而解了, 代码跑起来也正常调用了析构:
    在这里插入图片描述
    可以看到引用计数都是1, 证明了weak_ptr确实不会增加引用计数.

    3.2 线程安全问题

    shared_ptr本身对于引用计数的加加或减减是线程安全的, 但对于所管理的资源的数据修改不是线程安全的, 比如:

    #include 
    #include 
    #include 
    using namespace std;
    
    int Test(shared_ptr<int>& sp, int n)
    {
        for (int i = 0; i < n; ++i)
        {
            (*sp)++;
        }
        return n;
    }
    
    int main()
    {
        shared_ptr<int> sp(new int(0));
    
        thread t1(Test, ref(sp), 10000);
        thread t2(Test, ref(sp), 10000);
    
        t1.join();
        t2.join();
    
        cout << *sp << endl;
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27

    上面的代码就是两个线程对同一个智能指针管理的资源进行加加, 一个线程加一万次, 正确的累加结果应该是20000, 而在不加锁的情况下运行结果是这样的:
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    通过运行结果可以看出这不是线程安全的, 而加了锁以后就是线程安全的了:

    #include 
    #include 
    #include 
    #include 
    using namespace std;
    
    int Test(shared_ptr<int>& sp, int n, mutex& mtx)
    {
        for (int i = 0; i < n; ++i)
        {
            mtx.lock();
            (*sp)++;
            mtx.unlock();
        }
        return n;
    }
    
    int main()
    {
        shared_ptr<int> sp(new int(0));
        mutex mtx;
    
        thread t1(Test, ref(sp), 1000000, ref(mtx));
        thread t2(Test, ref(sp), 1000000, ref(mtx));
    
        t1.join();
        t2.join();
    
        cout << *sp << endl;
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31

    需要注意在线程中传递引用参数要用 ref( ) 这个语法规则.

    三、定制删除器

    智能指针管理的资源并不单单就是一个诸如int或者node这样的资源, 可能会是一个node[]数组等, 那么此时析构时会有问题吗? 看看代码:

    struct node
    {
        int _val;
        weak_ptr<node> _prev;
        weak_ptr<node> _next;
    
        ~node()
        {
            cout << "~node()" << endl;
        }
    };
    
    int main(
    )
    {
        shared_ptr<node> sp1(new node[5]);
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    运行结果:
    在这里插入图片描述
    虽然调用了析构, 但报错了. 原因是delete的方式不匹配, 这里应该用delete[ ]来进行释放.

    而此时可以传递一个自定义的删除器 (可调用对象) 来解决该问题:

    template<class T>
    struct del
    {
        void operator()(T* ptr)
        {
            cout << "void operator()(T* ptr)" << endl;
            delete [] ptr;
        }
    };
    
    int main()
    {
        shared_ptr<node> sp1(new node[5], del<node>()); //仿函数
        
        //lambda
        //shared_ptr sp1(new node[5], [](node* ptr){
        //    delete[] ptr;
        //});
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    运行结果:
    在这里插入图片描述
    此时程序正常运行结束, 没问题.

  • 相关阅读:
    Docker容器只有JRE没有JDK使用Jattach导出内存快照
    Llama 2 70B 问答 - 由人工神经网络训练的程序,与使用编程语言和数学算法编写的程序之间有何区别?
    【Hadoop】 软件
    VMware安装CentOS Stream 8以及JDK和Docker
    使用Packet Tracer了解网络模型及Lab3 - 1
    算法-二分查找
    穿越晋商百年-体验非遗文化
    Dapr 长程测试和混沌测试
    【洛谷 P1591】阶乘数码 题解(模拟+高精度)
    计算器中处于不同进制时
  • 原文地址:https://blog.csdn.net/m0_62714628/article/details/137915239