美文网首页
闲话C++智能指针

闲话C++智能指针

作者: Kevifunau | 来源:发表于2021-11-24 18:41 被阅读0次

    提到指针, 大家就会想到memory leak。 C/C++ 使用堆内存都需要程序员手动搭配使用 malloc/new 申请, free/delete
    释放。

    void func()
    {
        string* str = new string("example");  // 这里如果new 失败, 不会有问题, 编译器保证
        foo(str);  // 可能抛异常
        delete str; // 可能人为忘记
    }
    

    1. RAII

    如上两个原因, 如何解决呢? C++ 首先想到用 RAII的思想, 将 new出来的内存地址用对象进行封装。 即: 利用对象的构造函数对 new返回的内存地址进行存储和封装使用,使用} 自动在对象的析构函数中 执行 delete

    // smart_ptr.h
    class smart_ptr {
        public:
        // RAII
        smart_ptr(string* ptr) : ptr_(ptr) {}
        ~smart_ptr() {delete ptr_;}
    
        private:
            string * ptr_;
    };
    // main.cpp
    int main()
    {
        smart_ptr ptr{new string("example")}; // -> smart_ptr(string* ptr) : ptr_(ptr) {}
        return 1;
    } // ->  ~smart_ptr() {delete ptr_;}
    

    如上, 就解决了 memory leak 的问题, 但是RAII还是无法避免 指针的如下两个问题

    // smart_ptr.h
    class smart_ptr {
        public:
        // RAII
        smart_ptr(string* ptr) : ptr_(ptr) {}
        ~smart_ptr() {delete ptr_;}
        // 仿指针行为
        string& operator*() {return *ptr_;};
        string* operator->() {return ptr_;};
        private:
            string * ptr_;
    };
    // main.cpp
    void func1(smart_ptr ptr)
    {
        cout << *ptr << endl;
    } // 1. -> ~smart_ptr() {delete ptr_;}
    int main()
    {
        smart_ptr ptr{new string("example")};
        smart_ptr ptr2 = ptr;
        func1(ptr);
        cout << *ptr2 << endl; // dangling pointer 
        return 1;
    } // 2. -> ~smart_ptr() {delete ptr_;}
    

    如上, 如果该指针对象发生 拷贝行为, 那么RAII机制的} 势必会多次释放该指针, 即 double free; 其次如果该指针释放后仍被使用,就会造成访问无效内存。即 dangling pointer 迷途指针或者叫悬挂指针。
    解决上述两个问题, 我们可以直接把 拷贝行为 禁用。

    // smart_ptr.h
    class smart_ptr {
        public:
        // RAII
        smart_ptr(string* ptr) : ptr_(ptr) {}
        ~smart_ptr() {delete ptr_;}
        // 仿指针行为
        string& operator*() {return *ptr_;};
        string* operator->() {return ptr_;};
        // 禁用拷贝行为
        smart_ptr(smart_ptr& ptr) = delete;
        smart_ptr& operator=(smart_ptr& ptr) = delete;
        private:
            string * ptr_;
    };
    // main.cpp
    int main()
    {
        smart_ptr ptr{new string("example")};
        smart_ptr ptr2 = ptr; // 编译失败  use of deleted function 'smart_ptr::smart_ptr(smart_ptr&)'
        func1(ptr); // 编译失败 use of deleted function 'smart_ptr::smart_ptr(smart_ptr&)'
        cout << *ptr2 << endl; 
        return 1;
    }
    

    unique_ptr

    显然这方法属于 ”解决提出问题的人 而并非解决问题“,拷贝 根本上是 资源的所有权发生变化, 那资源的释放权自然就会从一份变成多份。 我们这里需要改造用户的拷贝行为.这里就可以分为2个场景, 该资源是单一使用还是共享使用。
    如果是单一所有, 那么我们可以移动资源.

    // smart_ptr.cpp
    class smart_ptr {
        public:
        // RAII
        smart_ptr(string* ptr) : ptr_(ptr) {}
        ~smart_ptr() {delete ptr_;}
        // 仿指针行为
        string& operator*() {return *ptr_;};
        string* operator->() {return ptr_;};
        // 禁用拷贝行为
        smart_ptr(smart_ptr& ptr) = delete;
        smart_ptr& operator=(smart_ptr& ptr) = delete;
        // 资源自动移动
        smart_ptr(smart_ptr&& ptr) {
            ptr_ = ptr.release();
        }
        smart_ptr& operator=(smart_ptr rhs) // 拷贝和移动都兼容
        {
            rhs.swap(*this);
            return *this;
        }
        private:
            string * ptr_;
            string* release()
            { // 新弄一个指针地址, 删除老指针地址
                string* newptr = ptr_;
                ptr_ = nullptr;
                return newptr;
            }
            void swap(smart_ptr& rhs)
            { // 交换指针地址内容
                using  std::swap;
                swap(ptr_, rhs.ptr_);
            }
    
    };
    
    
    //main.cpp
    int main()
    {
        smart_ptr ptr{new string("example")};
        smart_ptr ptr2{move(ptr)}; // 移动构造函数
        cout << *ptr2 << endl;  
    
        smart_ptr ptr3{nullptr};
        ptr3 = move(ptr2); // 移动赋值
        cout << *ptr3 << endl; 
    
        return 1;
    }
    

    以上就是 std::unique_ptr的 简易实现啦。

    shared_ptr

    那如果该资源是共享资源呢? 这就涉及到RC技术了reference count 引用计数。这是已经常见的垃圾回收技术。
    要实现RC, 首先就要有个单例的对象存储该资源的count. 这样多份资源在共享该资源是, count 保证只有一份。
    因此 我们要对

        private:
            string * ptr_;
            share_count* share_count_;    // 增加共享计数
    

    进行单例对象改造。
    首先 1. 构造和析构函数要适配share_count, 即 构造的时候如果该指针没有share_count 需要new 一个, 如果析构时share_count_为1, 那需要释放资源。

        // RAII
        smart_ptr(string* ptr) : ptr_(ptr) {
            share_count_ = new share_count();
            share_count_->add_count();
        }
        ~smart_ptr() {
            if (share_count_->reduce_count()) {
                delete ptr_;
                delete share_count_;
            }
        }
    

    其次 2. 修改拷贝。实现对share_count 的增减。

    // 拷贝
        smart_ptr(smart_ptr& rhs) {
            ptr_ = rhs.ptr_;
            share_count_ = rhs.share_count_;
            share_count_->add_count();
        };
        smart_ptr& operator=(smart_ptr& rhs) {
            // 用拷贝构造把 右值构造好 然后swap 给 左值, 返回左值。
            // 原本的this对象自己会析构。
            smart_ptr(rhs).swap(*this);
            return *this;
        };
    

    移动构造和赋值同unique_ptr的实现,这里就不赘述啦。 移动不需要动share_count 因为并没有新增资源使用者,最终 share_ptr简易实现如下:

    // smart_ptr
    
    class share_count {
       public:
       share_count() : count_(0) {}
       bool reduce_count()
       {
           return --count_ == 0;
       }
    
       void add_count()
       {
           count_++;
       }
       int use_count() {return count_;};
       private:
       int count_;
    };
    
    class smart_ptr {
        public:
        // RAII
        smart_ptr(string* ptr) : ptr_(ptr) {
            share_count_ = new share_count();
            share_count_->add_count();
        }
        ~smart_ptr() {
            if (share_count_->reduce_count()) {
                delete ptr_;
                delete share_count_;
            }
        }
        // 仿指针行为
        string& operator*() {return *ptr_;};
        string* operator->() {return ptr_;};
    
        // 拷贝
        smart_ptr(smart_ptr& rhs) {
            ptr_ = rhs.ptr_;
            share_count_ = rhs.share_count_;
            share_count_->add_count();
        };
        smart_ptr& operator=(smart_ptr& rhs) {
            // 用拷贝构造把 右值构造好 然后swap 给 左值, 返回左值。
            // 原本的this对象自己会析构。
            smart_ptr(rhs).swap(*this);
            return *this;
        };
    
        int use_count() {return share_count_->use_count();};
        private:
        string* ptr_; 
        share_count* share_count_;       
    
        string* release()
        {
            string *ptr = ptr_;
            ptr_ = nullptr;
            return ptr;
        }
        void swap(smart_ptr& rhs)
        {
            using std::swap;
            swap(ptr_, rhs.ptr_);
            swap(share_count_, rhs.share_count_);
        }
    };
    // main.cpp
    int main()
    {
        smart_ptr ptr1{new string("example")};
        cout << ptr1.use_count() << endl; // use_count = 1
        smart_ptr ptr2(ptr1);
        cout << ptr1.use_count() << endl; //  // use_count = 2
        {
            auto ptr3 = ptr1;
            cout << ptr1.use_count() << endl; //  // use_count = 3
        }
        cout << ptr1.use_count() << endl;  //  // use_count = 2
        {
            smart_ptr ptr5{nullptr};
            ptr5 = ptr1;
            cout << ptr1.use_count() << endl; // use_count = 3
        }
        cout << ptr1.use_count() << endl; // use_count = 2
        return 1;
    }
    

    以上就是shared_ptr的简易实现啦。

    std::unique_ptr

    了解原理,最终还是服务于更好的使用标准库。标准库实现自然要基于模板, 只需要将实际类型替换成 T, 那先前代码替换string为T后如下

    template <typename T>
    class smart_ptr {
        public:
            smart_ptr(T* ptr) : ptr_(ptr) {} 
            T* Get() const { return ptr_;}
            ~smart_ptr() { delete ptr_;};
            // * / ->
            T& operator*() { return *ptr_; }
            T* operator->() { return ptr_; }
    
            // 指针禁止拷贝
            smart_ptr(const smart_ptr &) = delete;
            smart_ptr& operator=(const smart_ptr &) = delete;
            // 使用移动拷贝构造, 如果要传值, 可以传右值
            smart_ptr(smart_ptr&& other) {
                ptr_ = other.release();
            }
            smart_ptr& operator=(smart_ptr rhs) {
                rhs.swap(*this);
                return *this;
            }
        private:
            T* ptr_;
            T* release()
            {
                T *ptr = ptr_;
                ptr_ = nullptr;
                return ptr;
            }
            void swap(smart_ptr & rhs) {
                using std::swap;
                swap(ptr_, rhs.ptr_);
            }
    };
    // main.cpp
    int main()
    {
        smart_ptr<string> up1(new string("string"));
        cout << *up1 << endl;
        unique_ptr<string> stdup1(new string("string"));
        cout << *stdup1 << endl;
    
        // smart_ptr<string> up2(up1);  use of deleted function 'smart_ptr<T>::smart_ptr(const smart_ptr<T>&) [with T = std::__cxx11::basic_string<char>]'
        // unique_ptr<string> stdup2(stdup1); use of deleted function 'std::unique_ptr<_Tp, _Dp>::unique_ptr(const std::unique_ptr<_Tp, _Dp>&) [with _Tp = std::__cxx11::basic_string<char>; _Dp = std::default_delete<std::__cxx11::basic_string<char> >]'
    
        // smart_ptr<string> up3 = up1; use of deleted function 'smart_ptr<T>::smart_ptr(const smart_ptr<T>&) [with T = std::__cxx11::basic_string<char>]'
        // unique_ptr<string> stdup3 = stdup1; use of deleted function 'std::unique_ptr<_Tp, _Dp>::unique_ptr(const std::unique_ptr<_Tp, _Dp>&) [with _Tp = std::__cxx11::basic_string<char>; _Dp = std::default_delete<std::__cxx11::basic_string<char> >]'
        return 1;
    }
    

    如上, 标准库也是直接简单粗暴的将拷贝赋值和拷贝构造给delete了。unique_ptr 实现简单, 使用也简单, 可以说他就做三件事。

    void print_addr(unique_ptr<string>&& ptr)
    {
        cout << &*ptr << endl;  // // 0x1b1b40
    }
    
    int main()
    {
        // 推荐用法 
        // 1. 定义
        auto up = make_unique<string>("example");
        cout << &*up << endl; // 0x1b1b40
        // 2. 移动构造
        unique_ptr<string> up2 = std::move(up);
         cout << &*up2 << endl; // 0x1b1b40
        // 3. 传参
        print_addr(move(up2)); // 0x1b1b40
        return 1;
    }
    

    了解原理, 记住make_uniqueunique_ptrstd::move(), 这三个关键词,unique_ptr就掌握啦。

    std::share_ptr

    同理, share_ptr 对比unique_ptr , 原理上只是多了可拷贝功能(使用了RC)。

    int main()
    {
        // 1. 定义
        auto sp = make_shared<string>("example");
        // 2. 拷贝赋值
        shared_ptr<string> sp2 = sp;
        cout << sp.use_count(); // 2
        // 3. 拷贝构造
        shared_ptr<string> sp3(sp);
        cout << sp.use_count(); // 3
        // 4. 移动赋值
        shared_ptr<string> sp4 = std::move(sp);
        cout << sp4.use_count(); // 3
        // 5. 移动构造, 转移所有权, 不新增使用者
        shared_ptr<string> sp5(move(sp4));
        cout << sp5.use_count() << endl; // 3
        // 6. 新增一个使用者
        print_addr(sp5); // 4
        // 7. 移动所有权, 未新增使用者
        print_addr(move(sp5)); // 3
    }
    

    如上share_ptr 又能拷贝又能移动。使用起来非常丝滑。

    智能指针作为返回值

    • 编译器对于这种用called func 返回智能指针, 对临时的unique_ptr, 他自己会确保ownership 传递给P. 编译器不会傻乎乎的先释放在重新构造一个新的给调用者的, 具体可以搜搜RVO。
    // unique_ptr<string> BuildString()
    smart_ptr<string> BuildString()
    {
        // return make_unique<string>("example");
        return smart_ptr<string>(new string("example"));
    }; // 并不会调用析构函数释放 string("example")
     
    int main()
    {
        // 1. 定义
        // unique_ptr<string> sp = BuildString();
        smart_ptr<string> sp = BuildString();
        cout << *sp << endl;
    }
    

    当然必须传值不可以返回引用或者右值。

    // unique_ptr<string>& BuildString() // 编译不过
    // unique_ptr<string>&& BuildString() // BUG
    unique_ptr<string> BuildString()
    {
        return make_unique<string>("example");
    }; // 并不会调用析构函数释放 string("example")
     
    int main()
    {
        // 1. 定义
        unique_ptr<string> sp = BuildString();
        cout << *sp << endl;
    }
    

    shared_ptr也同理, 因为函数结束的}势必会减少引用计数或者直接释放该资源, 因此智能按值往外传。

    // shared_ptr<string>& BuildString() // 编译不过
    // shared_ptr<string>&& BuildString() // BUG
    string* BuildString() 
    // shared_ptr<string> BuildString()
    {
        return make_shared<string>("example").get();
    }; // 并不会调用析构函数释放 string("example")
     
    int main()
    {
        // 1. 定义
        string* sp = BuildString(); // BUG
        cout << *sp << endl; //  无效内存
    }
    

    相关文章

      网友评论

          本文标题:闲话C++智能指针

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