闲话C++智能指针

2021-11-24  本文已影响0人  Kevifunau

提到指针, 大家就会想到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 又能拷贝又能移动。使用起来非常丝滑。

智能指针作为返回值

// 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; //  无效内存
}
上一篇下一篇

猜你喜欢

热点阅读