美文网首页
C++11 智能指针

C++11 智能指针

作者: 拂去尘世尘 | 来源:发表于2023-03-11 14:53 被阅读0次

    开篇

      C/C++开发过程中,动态内存的管理通过new/delete完成。new在动态内存中为对象分配一块空间并返回一个指向该对象的指针;delete指向一个动态独享的指针,销毁对象,并释放与之关联的内存。

    在日常动态内存的使用中,经常会出现以下问题:

    • 申请动态内存后忘记释放,造成内存泄漏,长时间运行会导致内存耗尽;
    • 尚有指针引用动态内存的情况下就释放了它,造成引用非法内存指针,导致程序异常coredump。

    为解决上述问题,C++11引入了智能指针的概念。

    智能指针初识

      智能指针就是RAII(资源获取即初始化)模板类,其将基本类型指针封装为(模板)类对象指针,在离开作用域时调用析构函数,delete指向的内存空间。C++11在头文件<memory>,提供了shared_ptr、unique_ptr、weak_ptr。

    auto_ptr也是一种智能指针,不过已经被unique_ptr取代,本篇不讨论此指针。

    shared_ptr

    shared_ptr采用引用计数的智能指针。如果需要将一个原始指针分配给多个所有者(例如,从容器返回了指针副本又想保留原始指针时),可以使用该指针。 直至所有shared_ptr所有者结束生命周期或放弃所有权,才会delete原始指针。

    创建方式

    1. 构造空shared_ptr指针
    std::shared_ptr<T> p1;             //不传入任何实参
    std::shared_ptr<T> p2(nullptr);    //传入空指针 nullptr
    

    空的 shared_ptr 指针,其初始引用计数为 0,而不是 1。

    1. 明确指向的shared_ptr指针
    std::shared_ptr<T> p3(new T());  // new方式
    std::shared_ptr<T> p3 = std::make_shared<T>(); // make_shared方式
    

    此两种方式创建的p3完全相同,《Effective Modren C++》第21条款推荐优先使用make_shared而非new

    1. 拷贝构造函数和移动构造函数
    //调用拷贝构造函数
    std::shared_ptr<T> p4(p3);//或者 std::shared_ptr<T> p4 = p3;
    
    //调用移动构造函数
    std::shared_ptr<T> p5(std::move(p4)); //或者 std::shared_ptr<T> p5 = std::move(p4);
    

    p3 和 p4 都是shared_ptr 类型的智能指针,因此可以用 p3 来初始化 p4,由于 p3 是左值,因此会调用拷贝构造函数。需要注意的是,如果 p3 为空智能指针,则 p4 也为空智能指针,其引用计数初始值为 0;反之,则表明 p4 和 p3 指向同一块堆内存,同时该堆空间的引用计数会加 1。

    而对于 std::move(p4) 来说,该函数会强制将 p4 转换成对应的右值,因此初始化 p5 调用的是移动构造函数。另外和调用拷贝构造函数不同,用 std::move(p4) 初始化 p5,会使得 p5 拥有了 p4 的堆内存,而 p4 则变成了空智能指针。

    1. 自定义所指堆内存的释放规则

    shared_ptr支持自定义释放规则,即在智能指针生命结束时调用自定义函数。

    // 空智能指针p,在删除共享指针时调用删除函数d
    shared_ptr<T> p(d);
    // 非空智能指针p, 管理原始指针q,在删除共享指针时调用删除函数d
    shared_ptr<T> p(q, d);
    // E.g,可配合lambda表达式
    shared_ptr<FILE> fp(fopen("./tmp.txt","r"), fclose);
    

    在某些场景中,自定义释放规则很有必要。比如,对于申请的动态数组来说,shared_ptr 指针默认的释放规则是不支持释放数组的,只能自定义对应的释放规则,才能正确地释放申请的堆内存。

    shared_ptr常用函数

    成员函数名 功 能
    operator=() 重载赋值号,使得同一类型的 shared_ptr 智能指针可以相互赋值。
    operator*() 重载 * 号,获取当前 shared_ptr 智能指针对象指向的数据。
    operator->() 重载 -> 号,当智能指针指向的数据类型为自定义的结构体时,通过 -> 运算符可以获取其内部的指定成员。
    swap() 交换 2 个相同类型 shared_ptr 智能指针的内容。
    reset() 当函数没有实参时,该函数会使当前 shared_ptr 所指堆内存的引用计数减 1,同时将当前对象重置为一个空指针;当为函数传递一个新申请的堆内存时,则调用该函数的 shared_ptr 对象会获得该存储空间的所有权,并且引用计数的初始值为 1。
    get() 获得 shared_ptr 对象内部包含的普通指针。
    use_count() 返回同当前 shared_ptr 对象(包括它)指向相同的所有 shared_ptr 对象的数量。
    unique() 判断当前 shared_ptr 对象指向的堆内存,是否不再有其它 shared_ptr 对象再指向它。
    operator bool() 判断当前 shared_ptr 对象是否为空智能指针,如果是空指针,返回 false;反之,返回 true。

    使用示例

    static void help_info()
    {
        LOG("usage: \n"
            "a. Regular test.\n"
            "b. Custom class test.\n"
            "c. Custorm delete test.\n"
            "q. exit.\n"
        );
    }
    
    static void regular_use(shared_ptr<string> &str)
    {
        shared_ptr<string> pTmpStr(str);
        LOG("pTmpStr: %s. memory count: %ld.\n", pTmpStr->c_str(), pTmpStr.use_count());
    }
    
    class CTestSharedPtr
    {
    public:
        CTestSharedPtr(string desc) : mDescription(desc) {
            LOG("Enter %s.\n", __FUNCTION__);
        }
    
        ~CTestSharedPtr() {
            LOG("Enter %s.\n", __FUNCTION__);
        }
    
        string GetDesc() {
            return mDescription;
        }
    
    private:
        string mDescription;
    };
    
    int main(int argc, char *argv[])
    {
        char a;
        help_info();
    
        do
        {
            scanf("%c", &a);
            switch (a)
            {
                case 'a':   // shared_ptr 标准类型
                {
                    shared_ptr<string> pStr1(new string("hello world"));
                    LOG("pStr1: %s. memory count: %ld.\n", pStr1->c_str(), pStr1.use_count());
                    regular_use(pStr1);
                    shared_ptr<string> pStr2(pStr1);
                    LOG("pStr2: %s. memory count: %ld.\n", pStr2->c_str(), pStr2.use_count());
                }
                break;
    
                case 'b':   // shared_ptr 自定义类型
                {
                    shared_ptr<CTestSharedPtr> pCTest1 = make_shared<CTestSharedPtr>("ClassTest");
                    LOG("pCTest1: %s. memory count: %ld.\n", pCTest1->GetDesc().c_str(), pCTest1.use_count());
                    shared_ptr<CTestSharedPtr> pCTest2(pCTest1);
                    LOG("pCTest2: %s. memory count: %ld.\n", pCTest2->GetDesc().c_str(), pCTest2.use_count());
                }
                break;
    
                case 'c':   // 自定义shared_ptr删除器
                {
                    char *pArry = nullptr;
    
                    auto fStart = [](char *p) {
                        p = (char *)malloc(sizeof(char) * 6);
                        strncpy(p, "hello", 6);
                        cout << "Enter fStart(). malloc p:" << p << endl;
                        return p;
                    };
    
                    auto fStop = [](char *p) {
                        cout << "Enter fStop(). p:" << p;
                        free(p);
                        p = nullptr;
                        cout << " free." << endl;
                    };
    
                    // 自定义删除器,当pDTest1生命周期结束时,通过delete_test(pDTest1)释放内存,不再调用delete
                    shared_ptr<char> pDTest1(fStart(pArry), fStop);
                    LOG("pDTest1.use_count: %ld. %s\n", pDTest1.use_count(), pDTest1.get());
                }
                break;
    
                default:
                break;
            }
        } while(a != 'q');
    
        return 0;
    }
    

    执行输出

    $ ./exe 
    usage: 
    a. Regular test.
    b. Custom class test.
    c. Custorm delete test.
    q. exit.
    a
    pStr1: hello world. memory count: 1.
    pTmpStr: hello world. memory count: 2.
    pStr2: hello world. memory count: 2.
    b
    Enter CTestSharedPtr.
    pCTest1: ClassTest. memory count: 1.
    pCTest2: ClassTest. memory count: 2.
    Enter ~CTestSharedPtr.
    c
    Enter fStart(). malloc p:hello
    pDTest1.use_count: 1. hello
    Enter fStop(). p:hello free.
    

    unique_ptr

      只允许基础指针的一个所有者。可以移到新所有者,但不会复制或共享。替换已弃用的auto_ptr。必要情况下,可以转化为shared_ptr

    创建方式

    1. 创建空unique_ptr指针
    std::unique_ptr<T> p1();
    std::unique_ptr<T> p2(nullptr);
    
    1. 明确指向的unique_ptr指针
    std::unique_ptr<T> p3(new T);
    

    C++11 标准中并没有为unique_ptr类型指针添加类似的模板函数。C++14提供了make_unique<T>()模板函数用于初始化unique_ptr指针。

    1. 移动构造函数
    std::unique_ptr<T> p4(new T);
    // std::unique_ptr<T> p5(p4);//编译错误,堆内存不共享
    std::unique_ptr<T> p5(std::move(p4));//正确,调用移动构造函数
    

    unique_ptr指针不共享拥有的堆内存,因此C++11标准中的 unique_ptr模板类没有提供拷贝构造函数,只提供了移动构造函数。

    1. 自定义所指堆内存的释放规则
    // 空unique_ptr指针, 删除智能指针时,执行d而非delete
    unique_ptr<T, D> u1(d);
    // 非空unique_ptr指针, 管理指针p; 删除智能指针时,执行d而非delete
    unique_ptr<T, D> u2(p, d);
    

    unique_ptr常用函数

    成员函数名 功 能
    成员函数名 功 能
    operator*() 获取当前 unique_ptr 指针指向的数据。
    operator->() 重载 -> 号,当智能指针指向的数据类型为自定义的结构体时,通过 -> 运算符可以获取其内部的指定成员。
    operator =() 重载了 = 赋值号,从而可以将 nullptr 或者一个右值 unique_ptr 指针直接赋值给当前同类型的 unique_ptr 指针。
    operator 重载了 [] 运算符,当 unique_ptr 指针指向一个数组时,可以直接通过 [] 获取指定下标位置处的数据。
    get() 获取当前 unique_ptr 指针内部包含的普通指针。
    get_deleter() 获取当前 unique_ptr 指针释放堆内存空间所用的规则。
    operator bool() unique_ptr 指针可直接作为 if 语句的判断条件,以判断该指针是否为空,如果为空,则为 false;反之为 true。
    release() 释放当前 unique_ptr 指针对所指堆内存的所有权,但该存储空间并不会被销毁。
    reset(p) 其中 p 表示一个普通指针,如果 p 为 nullptr,则当前 unique_ptr 也变成空指针;反之,则该函数会释放当前 unique_ptr 指针指向的堆内存(如果有),然后获取 p 所指堆内存的所有权(p 为 nullptr)。
    swap(x) 交换当前 unique_ptr 指针和同类型的 x 指针。

    使用示例

    static void help_info()
    {
        LOG("usage: \n"
            "a. reset() test.\n"
            "b. Custom class test.\n"
            "c. Custorm delete test.\n"
        );
    }
    
    class CTestUniquePtr
    {
    public:
        CTestUniquePtr(string desc) : mDescription(desc) {
            LOG("Enter %s.\n", __FUNCTION__);
        }
    
        ~CTestUniquePtr() {
            LOG("Enter %s.\n", __FUNCTION__);
        }
    
        string GetDesc() {
            return mDescription;
        }
    
    private:
        string mDescription;
    };
    
    int main(int argc, char *argv[])
    {
        char a;
        help_info();
    
        do {
            scanf("%c", &a);
    
            switch(a)
            {
                case 'a':
                {
                    unique_ptr<CTestUniquePtr> pUnPtr1(new CTestUniquePtr("unique_ptr"));
                    pUnPtr1.reset();
                    LOG("pUnPtr1 is %d.\n", pUnPtr1 ? 1 : 0);
                }
                break;
    
                case 'b':
                {
                    unique_ptr<CTestUniquePtr> pUnPtr1(new CTestUniquePtr("unique_ptr"));
                    unique_ptr<CTestUniquePtr> pUnPtr2(pUnPtr1.release());
                    LOG("pUnPtr1 is %s. pUnPtr2 is %s.\n", pUnPtr1 ? pUnPtr2->GetDesc().c_str() : "nullptr",
                            pUnPtr2 ? pUnPtr2->GetDesc().c_str() : "nullptr");
                }
                break;
    
                case 'c':
                {
                    std::unique_ptr< int, function<void(int*)> > ptr1(new int[100],
                        [](int*p)->void {
                            cout << "Delete int[]" << endl;
                            delete []p;
                        }
                    );
                    
                    std::unique_ptr< FILE, function<void(FILE*)> > ptr2(fopen("data.txt","w"),
                        [](FILE*p)->void {
                            cout << "Delet FILE" << endl;
                            fclose(p);
                        }
                    );
                }
                break;
    
                default:
                break;
            }
        } while (a != 'q');
    
        return 0;
    }
    

    执行输出

    $./exe 
    usage: 
    a. reset() test.
    b. Custom class test.
    c. Custorm delete test.
    a
    Enter CTestUniquePtr.
    Enter ~CTestUniquePtr.
    pUnPtr1 is 0.
    b
    Enter CTestUniquePtr.
    pUnPtr1 is nullptr. pUnPtr2 is unique_ptr.
    Enter ~CTestUniquePtr.
    c
    Delet FILE
    Delete int[]
    

    weak_ptr

      结合shared_ptr使用的弱智能指针。weak_ptr提供对一个或多个shared_ptr实例拥有的对象的访问,但不参与引用计数。 如果需要观察某个对象但不需要其保持活动状态,可使用该实例。可解决shared_ptr实例间的循环引用导致的内存泄漏问题。

    weak_ptr没有提供常用的指针操作,无法直接访问内存,需要先通过lock方法提升为shared_ptr强智能指针,才能访问资源。

    创建方式

    1. 创建空weak_ptr指针
    std::weak_ptr<T> wp1;
    
    1. 拷贝构造函数
    std::weak_ptr<T> wp2(wp1);
    
    1. 通过shared_ptr构建weak_ptr
    auto = make_shared<T>();
    std::weak_ptr<T> wp2(sp); // 不增加sp内部引用计数
    

    weak_ptr常用函数

    成员函数名 功 能
    operator=() 重载 = 赋值运算符,是的 weak_ptr 指针可以直接被 weak_ptr 或者 shared_ptr 类型指针赋值。
    swap(x) 其中 x 表示一个同类型的 weak_ptr 类型指针,该函数可以互换 2 个同类型 weak_ptr 指针的内容。
    reset() 将当前 weak_ptr 指针置为空指针。
    use_count() 查看指向和当前 weak_ptr 指针相同的 shared_ptr 指针的数量。
    expired() 判断当前 weak_ptr 指针为否过期(指针为空,或者指向的堆内存已经被释放)。
    lock() 如果当前 weak_ptr 已经过期,则该函数会返回一个空的 shared_ptr 指针;反之,该函数返回一个和当前 weak_ptr 指向相同的 shared_ptr 指针。

    使用示例

    {
        shared_ptr<string> pShPtr1 = make_shared<string>("ptr");    // 创建共享指针, 内存引用 +1
        LOG("1. pShPtr1.use_count: %ld.\n", pShPtr1.use_count());
        shared_ptr<string> pShPtr2(pShPtr1);                        // 共享指针拷贝,内存引用 +1
        LOG("2. pShPtr1.use_count: %ld.\n", pShPtr1.use_count());
        weak_ptr<string> pWeakPtr(pShPtr1);                         // 弱引用智能指针,内存引用不增加
        LOG("3. pWeakPtr.use_count: %ld.\n", pWeakPtr.use_count());
        shared_ptr<string> pShPtr3(pWeakPtr.lock());                // 拷贝弱指针返回的共享指针,内存引用 +1
        LOG("4. pShPtr3.use_count: %ld.\n", pShPtr3.use_count());
    }
    

    执行输出

    1. pShPtr1.use_count: 1.
    2. pShPtr1.use_count: 2.
    3. pWeakPtr.use_count: 2.
    4. pShPtr3.use_count: 3.
    

    总结

    • 通过本篇对三种指针的介绍,大致梳理出三者的使用场景:
      独占内存用unique_ptr;
      内存被多个指针引用shared_ptr;
      当作为内存观察者或者解决循环引用时使用weak_ptr

    • C++智能指针的使用注意事项:
      ① 优先使用unique_ptr而非auto_ptr
      shared_ptr不支持动态数组,若默认使用delete来释放管理资源,delete只会调用第一个元素的析构函数,导致内存泄漏;(可通过自定义删除器管理) unique_ptr支持动态数组,默认detele会自动使用delete[]。
      使用unique_ptr可转化为shared_ptr,反之则不行。
      shared_ptr消耗资源比unique_ptr大,若无内存共享需求,优先考虑unique_ptr
      ⑤ 禁止使用静态分配对象指针初始化智能指针,否则智能指针生命周期结束时,会试图删除指向非动态分配对象的指针,导致未定义的行为。
      ⑥ 谨慎使用智能指针的get()release()方法。
      当使用get()方法返回裸指针时,智能指针并没有释放指向对象的所有权,因此避免裸指针的使用导致崩溃。
      unique_ptr.release()返回裸指针并让出内存控制权,需要及时接管或者delete,避免导致内存泄漏。
      ⑦ 禁止使用一个裸指针初始化多个智能指针;禁止手动delete智能指针的裸指针。
      ⑧ 不要把类对象指针(this)作为shared_ptr返回,改用enable_shared_from_this
      ⑨ 通过weak_ptr.lock()方法获取shared_ptr时,必须判断该shared_ptr是否有效。
      ⑩ 优先考虑使用std::make_uniquestd::make_shared而非new(C++11暂未提供std::make_shared)。
      shared_ptr没有保证共享对象的线程安全性。
      ⑫ 循环引用shared_ptr会导致内存泄漏,应替换为weak_ptr

    参考

    相关文章

      网友评论

          本文标题:C++11 智能指针

          本文链接:https://www.haomeiwen.com/subject/rihaiktx.html