美文网首页
闲话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++智能指针

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

  • C++研发工程师笔试题/面试题(1-10)

    1. (1) 简述智能指针的原理;(2)c++中常用的智能指针有哪些?(3)实现一个简单的智能指针。 简述智能指针...

  • C++ 智能指针

    C++智能指针[https://zhuanlan.zhihu.com/p/54078587] C++11中智能指针...

  • c++智能指针用法

    智能指针是什么 智能指针是c++中有四个智能指针:auto_ptr、shared_ptr、weak_ptr、uni...

  • 阿里巴巴面试题基础篇 C++基础篇(二)

    ● 请你来说一下C++中的智能指针参考回答:C++里面的四个智能指针: auto_ptr, shared_ptr,...

  • Android智能指针分析

    Android智能指针分析总结 什么是智能指针 C++ 指针需要手动释放,否则会造成内存泄露,但是如果项目工程比较...

  • C++智能指针

    引用计数技术及智能指针的简单实现 基础对象类 辅助类 智能指针类 使用测试 参考: C++ 引用计数技术及智能指针...

  • 智能指针share_ptr的若干问题

    一 什么是智能指针 c++的智能指针是利用了c++的RAII机制,这样可以及时的释放资源,且即使代码中触发了异常,...

  • 智能指针学习笔记

    1. 介绍 本文介绍智能指针的使用。智能指针是c++ 中管理资源的一种方式,用智能指针管理资源,不必担心资源泄露,...

  • c++11 智能指针

    智能指针介绍 C++里面的四个智能指针: auto_ptr, unique_ptr,shared_ptr, wea...

网友评论

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

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