C++:智能指针

05-09 1400阅读

C++:智能指针

    • 内存泄漏
    • RAII
    • 智能指针
      • auto_ptr
      • unique_ptr
      • shared_ptr
        • 循环引用
        • weak_ptr
        • deleter
          • shared_ptr
          • unique_ptr

            内存泄漏

            内存泄漏是指程序在动态分配内存后,忘记或无法释放已经不再使用的内存,从而导致系统内存资源被逐渐耗尽的问题。这种情况下,即使程序本身并没有出现逻辑错误,也会因为内存泄漏而导致程序运行时间越来越长,甚至最终崩溃。

            案例分析:

            下面是一个典型的 C++ 内存泄漏案例:

            while (true)
            {
                int* p = new int(10);
                // 这里忘记释放内存,导致内存泄漏
            }
            

            在这个程序中,我们在一个无限循环中不断分配新的内存块。然而,我们忘记在循环体内释放这些内存块,导致程序运行时内存占用会越来越大,直到最终耗尽系统内存,程序崩溃。

            这种情况下,即使程序本身没有逻辑错误,也会因为内存泄漏而导致严重的问题。

            内存泄漏的危害:

            1. 系统内存资源被逐渐耗尽:内存泄漏会导致程序运行时内存占用越来越大,直到最终耗尽系统内存。这会严重影响程序的性能和稳定性。
            2. 程序可能会崩溃:内存耗尽后,程序会尝试访问非法内存地址,从而导致程序崩溃。
            3. 资源浪费:即使程序没有崩溃,内存泄漏也会导致大量内存资源被无谓地占用,造成资源浪费。

            内存泄漏是 C++ 程序中一个非常常见的问题,如果不能及时发现并修复,会对程序的性能、稳定性和安全性造成严重影响。

            RAII机制就是一种自动管理资源的机制,其可以帮助程序员自动释放资源,来避免内存泄漏,C++中,智能指针就是基于RAII产生的。


            RAII

            RAII (Resource Acquisition Is Initialization) 是 C++ 中一种非常重要的内存管理机制,它可以帮助我们有效地管理资源,避免内存泄漏等问题。

            RAII 的核心思想是:

            1. 将资源的分配和释放绑定在对象的生命周期上。
            2. 在对象构造时获取资源,在对象析构时释放资源。

            以下示例就是一个基本的RAII:

            template 
            class smartPtr
            {
            public:
                smartPtr(T* ptr)
                    :_ptr(ptr)
                {}
                ~smartPtr()
                {
                    delete _ptr;
                }
            private:
                T* _ptr;
            };
            int main()
            {
                smartPtr ptr1 = new int(5);
                smartPtr ptr2 = new double(3.14);
                smartPtr ptr3 = new vector(10);
                return 0;
            }
            

            在上述示例中,smartPtr 类负责管理资源的获取和释放。在 main 函数中,我们创建了三个 smartPtr 对象 ptr1、 ptr2、 ptr3。

            当这些对象进入作用域时,构造函数会被调用,将smatrPtr的_ptr成员初始化为对应动态内存的指针。

            当三个对象离开作用域时,析构函数会被自动调用,自动delete _ptr释放资源。

            通过 RAII 机制,我们可以确保资源一定会被正确释放,避免了手动释放资源时忘记的问题。这种做法也使得代码更加简洁易读,并且可以实现异常安全性。

            这样做的好处是:

            1. 确保资源一定会被正确释放,避免了手动释放资源时忘记的问题。
            2. 资源的获取和释放过程被封装在对象的构造和析构函数中,使得代码更加简洁易读。
            3. 可以利用 RAII 机制实现异常安全性,即使程序抛出异常,资源也能被正确释放。

            而以上案例,就是C++智能指针最根本的原理,C++一共提供了四种智能指针:auto_ptr、unique_ptr、shared_ptr、weak_ptr。


            智能指针

            讲解这四种智能指针之前,我们先看看我刚刚案例中的smartPtr存在的问题:

            template 
            class smartPtr
            {
            public:
                smartPtr(T* ptr)
                    :_ptr(ptr)
                {}
                ~smartPtr()
                {
                    delete _ptr;
                }
            private:
                T* _ptr;
            };
            int main()
            {
                smartPtr ptr1 = new int(5);
                smartPtr ptr2 = ptr1;
                return 0;
            }
            

            在main函数中,先构造了ptr1,指向一个int类型的动态内存。随后拿ptr1拷贝构造出了ptr2,此时ptr1和ptr2都指向这个int的动态内存。那么此时就要面临一个问题:当ptr1和ptr2出了作用域,那么两个对象都会调用析构函数,导致同一块内存被delete两次!这会直接导致进程崩溃,C++的智能指针需要解决这个问题。

            另外的,智能指针本质上是一个类,不自带*,->等操作,所以要对操作符进行重载:

            T operator*()
            {
                return *_ptr;
            }
            T* operator->()
            {
                return _ptr;
            }
            

            *,->等操作本质都是在对_ptr做操作,至于为什么要这样操作,可见博客[C++:迭代器的封装思想]。


            接下来我们正式讲解C++自带的各种智能指针:

            C++的智能指针,包含在头文件中

            讲解以下四个智能指针时,默认包含了头文件。


            auto_ptr

            C++:智能指针

            auto_ptr是C++标准中,最早出现的智能指针,属于C++98。

            构造函数:

            auto_ptr自带一个通过指针来进行初始化的构造函数,但是其被explicit修饰,这说明不允许进行类型转换:

            auto_ptr ptr1(new int(5));//正确
            auto_ptr ptr2 = new int(10);//错误
            

            auto_ptr只允许通过第一种方式,直接构造,第二种方式的本质是类型转换,从int*转为auto_ptr,只要有对应类型的构造函数那么C++就可以支持这个类型转换,但是如果构造函数被explicit修饰,这个类型转换功能就会被禁止,而auto_ptr就被禁止了。

            其实,C++的四种智能指针,都不允许通过原生指针的类型转化来构造,也就是四种智能指针都只能通过小括号来初始化。

            不过拷贝构造是允许的:

            auto_ptr ptr1(new int(5));
            auto_ptr ptr3(ptr1);
            auto_ptr ptr2 = ptr1;
            

            后两种拷贝方式,都是正确的,因为拷贝构造没有被explicit修饰。

            提到拷贝,我们刚讲到RAII中,如果多个类指向同一块空间,会导致资源重复释放的问题,那么auto_ptr是如何解决的呢?

            当auto_ptr发生拷贝,原先的auto_ptr会变成空指针

            为了方便观察,我们需要得知指针指向的地址:auto_ptr可以get接口看到内部存储的地址。

            示例:

            auto_ptr ptr1(new int(5));
            auto_ptr ptr2 = ptr1;
            cout 1, 2, 3, 4, 5, 6, 7, 8, 9, 10});
            cout 1, 2, 3, 4, 5, 6, 7, 8, 9, 10});
            cout 1, 2, 3, 4, 5, 6, 7, 8, 9, 10});
            cout 1, 2, 3, 4, 5, 6, 7, 8, 9, 10});
            cout 
            public:
                ListNode(int val)
                    : _prev(nullptr)
                    , _next(nullptr)
                    , _val(val)
                {}
                ListNode* _prev;
                ListNode* _next;
                int _val;
            };
            int main()
            {
                ListNode* l1 = new ListNode(5);
                ListNode* l2 = new ListNode(10);
                l1-_next = l2;
                l2-_prev = l1;
                delete l1;
                delete l2;
                
                return 0;
            }
            
            public:
                ListNode(int val)
                    : _prev(nullptr)
                    , _next(nullptr)
                    , _val(val)
                {}
                shared_ptr
                shared_ptr
            public:
                ListNode(int val)
                    : _prev(nullptr)
                    , _next(nullptr)
                    , _val(val)
                {}
                ListNode* _prev;
                ListNode* _next;
                int _val;
            };
            
            p但是现在存在一个问题:/p pre class="brush:python;toolbar:false"l1-_next = l2; l2->_prev = l1;

            这两天语句是错误的,因为l2的类型是shared_ptr,而l1->_next的类型是ListNode*,shared_ptr的类型转换是被禁止的,所以只能通过get接口:

            l1->_next = l2.get();
            l2->_prev = l1.get();
            

            这样未免太不简洁,此时就需要我们的最后一个智能指针weak_ptr出场了。


            weak_ptr

            C++:智能指针

            weak_ptr是一种不参与资源管理的智能指针,其只存在三种构造函数:

            1. 无参默认构造,此时weak_ptr初始化为空指针
            2. 拷贝构造,拷贝其它weak_ptr
            3. 通过shared_ptr初始化,此时shared_ptr和weak_ptr指向同一块内存

            当shared_ptr和weak_ptr指向同一块内存的时候,weak_ptr不会增加引用计数!

            weak_ptr离开作用域的时候,不会释放自己指向的资源,其只负责访问资源。特点在于可以通过shared_ptr初始化。

            因此刚刚的循环引用可以被优化为:

            class ListNode
            {
            public:
                ListNode(int val)
                    : _val(val)
                {}
                weak_ptr _prev;
                weak_ptr _next;
                int _val;
            };
            

            要注意的是,weak_ptr不支持原生指针初始化,哪怕是nullptr也不可以,因此在ListNode的初始化列表中,删掉了_next和_prev的初始化。

            现在以下代码就完全合法了:

            shared_ptr l1(new ListNode(5));
            shared_ptr l2(new ListNode(10));
            l1->_next = l2;
            l2->_prev = l1;
            

            weak_ptr可以通过shared_ptr初始化,因此可以直接将shared_ptr赋值给weak_ptr,又由于weak_ptr不参与计数,最后只要l1,l2离开作用域,空间就会被正常释放。


            deleter

            对于智能指针,有时候需要用特殊的方式来对资源进行释放,比如文件指针:

            shared_ptr fp(fopen("test.txt", "w"));
            

            对于指针fp,不能简单地delete pf,而是通过fclose(pf),此时我们就要用到自定义删除器deleter了。

            对于shared_ptr和unique_ptr,两者的deleter语法不太相同,此处分开讲解:

            shared_ptr

            shared_ptr的语法为:

            shared_ptr p(new T, deleter_function);
            

            其中, deleter_function是一个满足删除器要求的可调用对象,包括函数指针,仿函数,lambda三种。

            比如通过lambda来完成文件的fclose:

            shared_ptr fp(fopen("test.txt", "w"), [](FILE* ptr) { fclose(ptr); });
            

            通过仿函数:

            struct deleteFile
            {
                void operator()(FILE* ptr)
                {
                    fclose(ptr);
                }
            };
            int main()
            {
                shared_ptr fp(fopen("test.txt", "w"), deleteFile());
                return 0;
            }
            

            也就是说,对于shared_ptr,只需要把删除器的可调用对象,直接作为第二个参数传入即可。


            unique_ptr

            unique_ptr的删除器语法比较别扭,要求在模板参数中传入可调用对象的类型。

            unique_ptr p(new T, 可调用对象);
            

            同样的,可调用对象支持函数指针,仿函数,lambda三种。

            以刚刚的关闭文件为例:

            1. 使用函数指针:
            void deleteFunc(FILE* ptr)
            {
                fclose(ptr);
            }
            int main()
            {
                unique_ptr fp2(fopen("test.txt", "w"), deleteFunc);
                return 0;
            }
            

            该函数指针的类型为void(*)(FILE*),作为unique_ptr的第二个模板参数。

            1. 使用仿函数:
            struct deleteFile
            {
                void operator()(FILE* ptr)
                {
                    fclose(ptr);
                }
            };
            int main()
            {
                unique_ptr fp(fopen("test.txt", "w"), deleteFile());
                return 0;
            }
            

            仿函数的类型是deleteFile,即类名,作为unique_ptr的第二个模板参数。

            1. 使用lambda表达式:
            auto expression = [](FILE* ptr) { fclose(ptr); };
            unique_ptr fp(fopen("test.txt", "w"), expression);
            

            这里, expression是一个lambda表达式,由于lambda的类型是随机的,只能通过decltype(expression)来检测类型,作为unique_ptr的第二个模板参数。


VPS购买请点击我

文章版权声明:除非注明,否则均为主机测评原创文章,转载或复制请以超链接形式并注明出处。

目录[+]