C++智能指针原理
简介
智能指针就是对指针进行封装,使其提供特有的功能。
unique_ptr:封装了原始指针使其只能在同一时刻被同一对象拥有,并且在离开作用域时会自动销毁。
shared_ptr: 封装了原始指针,利用引用技术技术,实现多个对象同时共享一个指针,并且在所有对象都离开作用域时释放内存.
weak_ptr : 用来解决shared_ptr带来的循环计数问题,而且weak_ptr中的lock函数保证是线程安全
实现一个简单的unique_ptr
unique_ptr主要功能 :
- 不能够赋值,拷贝,允许移动
- operator* 实现
- operator→ 实现
- default_delete实现
- reset, release, get实现
template <class Tp, class Dp = default_delete<Tp>> class unique_ptr { public: typedef Tp element_type; typedef Delete delete_type; typedef element_type* pointer; typedef element_type& reference; public: unique_ptr(pointer data = nullptr) : data_(data) {} unique_ptr(pointer data, delete_type del) : data_(data), del_(del) {} ~unique_ptr() { clear(); } // 不允许赋值,拷贝 unique_ptr(const unique_ptr&) = delete; unique_ptr& operator=(const unique_ptr&) = delete; // 允许移动 unique_ptr(unique_ptr&& up) : data_(up.relase()), del_(up.del_) {} unique_ptr& operator=(unique_ptr&& up) { if (&up != *this) { reset(up.release()); del_ = up.del_; } return *this; } private: Tp* data_; Dp del_; };
上面的代码实现了第一步,不允许赋值拷贝,允许移动,代码非常简单,unqiue_ptr有两个模板参数,第一个是指针类型,第二个参数就是对应着这个指针的删除器
reset, release和clear实现 :
pointer release() noexcept{ auto res = data_; data_ = nullptr; return res; } void reset(pointer p = pointer()) noexcept { auto tmp = data_; data_ = p; if (tmp) del_(tmp); } void clear() const { del_(data_); data_ = nullptr; }
elease函数的作用就是放弃原生指针的所有权,reset的功能是用一个新指针来替换原来的指针
operator*, operator→实现:
reference operator*() const { return (*data_); } pointer operator->() const noexcept { return data_; }
defalut_delete实现 :
template <typename T> struct default_delete { void operator()(T* ptr) { if (ptr) delete ptr; } }; template <typename T> struct default_delete<T []> { void operator()(T* ptr) { if (ptr) delete []ptr; } }
这两个default_delete利用模板偏特化技术分别实现了删除普通指针以及数组指针, 到这里unique_ptr就已经实现完了,代码并不复杂,很简单,最后来看一个make_unique
tempalte <typename T, typename... Args> unique_ptr<T> make_unique(Args&&... args) { return unique_ptr<T>(new T(std::forward<Args>(args)...)); }
实现一个简单的shared_ptr
shared_ptr主要功能:
- 引用计数
- operator*
- operator→
- 赋值拷贝构造函数
引用计数实现:
template <typename ClassName, typename T> class RefCount { friend ClassName; RefCount(T* p) : pointer_(p), count(1) {} ~RefCount() { delete pointer_; } T* pointer_; size_t count_; };
这个引用计数实现的非常简单,仅仅包含了一个指针和一个计数值,真正的操作放到了Shared_ptr里面了
operator*, operator→, 赋值,拷贝构造函数实现:
template <typename T> class shared_ptr { public: typedef T value_type; typedef T* pointer; typedef T& reference; public: shared_ptr(value_type* ptr) : ptr_(new RefCount<shared_ptr, T>(ptr)) {} shared_ptr(const shared_ptr& sp) : ptr_(sp) { ++ptr_->count_; } shared_ptr& operator=(const shared_ptr& rhs) { if (&rhs == this) { // 由于rhs需要赋值给*this,所以将rhs的计数先+1 ++rhs.ptr_->count_; // 由于*this 需要抛弃掉本身保存的指针,所以将计数-1并判断是否已经是最后一个 if (--ptr_->count_ == 0) delete ptr_; ptr_ = rhs.ptr_; } retur *this; } ~shared_ptr() { if (--ptr_->count_ == 0) delete ptr_; } reference operator*() const { return *ptr_->pointer_;} pointer operator->() const { return &(operator*()); } private: RefCount<shared_ptr, T>* ptr_; };
shared_ptr中最主要的就是拷贝构造和赋值运算符以及析构函数中对count进行的管理,在析构的时候需要将count减1,并判断是否为0,为0就表示当前是最后一个引用这个指针的shared_ptr,需要释放资源,在赋值和拷贝时都需要将计数+1, 在来看一下make_shared.
template <typename T, typename... Args> shared_ptr<T> make_shared(Args&&... args> { return shared_ptr<T>(new T(std::forward<Args>(args)...)); }
shared_ptr 多线程问题
由上面我们实现的代码可以看出,在对count进行操作时不是多线程安全的,在标准库的实现中引用计数是线程安全的,它在底层用的是原子操作,也就是说在多线程情况下它只会释放一次也时线程不安全的,但是在构造,swap,reset操作中不是线程安全的,所以在多线程中共享shared_ptr需要格外的小心,要么加锁来保证安全,或者用weak_ptr来代替shared_ptr
shared_ptr 循环引用问题
#include <iostream> #include <memory> using namespace std; class B; class A { public:// 为了省去一些步骤这里 数据成员也声明为public shared_ptr<B> pb; void doSomthing() { } ~A() { cout << "kill A\n"; } }; class B { public: shared_ptr<A> pa; ~B() { cout <<"kill B\n"; } }; int main(int argc, char** argv) { shared_ptr<A> sa(new A()); shared_ptr<B> sb(new B()); if(sa && sb) { sa->pb=sb; sb->pa=sa; } cout<<"sa use count:"<<sa.use_count()<<endl; return 0; }
上面的代码运行结果为:sa use count:2, 注意此时sa,sb都没有释放,产生了内存泄露问题!!
为什么产生内存泄漏 : 由于A里面有一个B的shared_ptr, 所以在A析构之前B必须析构,但是是B里面又有一个A的shared_ptr, 所以在B析构之前,A必须析构,有木有发现逻辑全乱了,就是这样就导致了循环引用,也就内存泄漏了.
weak_ptr解决shared_ptr循环引用
#include <iostream> #include <memory> using namespace std; class B; class A { public:// 为了省去一些步骤这里 数据成员也声明为public weak_ptr<B> pb; void doSomthing() { shared_ptr<B> pp = pb.lock(); if(pp)//通过lock()方法来判断它所管理的资源是否被释放 { cout<<"sb use count:"<<pp.use_count()<<endl; } } ~A() { cout << "kill A\n"; } }; class B { public: shared_ptr<A> pa; ~B() { cout <<"kill B\n"; } }; int main(int argc, char** argv) { shared_ptr<A> sa(new A()); shared_ptr<B> sb(new B()); if(sa && sb) { sa->pb=sb; sb->pa=sa; } sa->doSomthing(); cout<<"sb use count:"<<sb.use_count()<<endl; return 0; }