C++之智能指针
目录
一、智能指针的使用场景
二、RAII思想
三、C++标准库中的智能指针
关于计数器:
关于删除器:
五、weak_ptr
一、智能指针的使用场景
C++中当我们new一个对象的时候一定要对其进行delete处理,但当new、delete之间抛出异常导致这个栈空间销毁时会导致delete无法正常执行造成内存泄漏。
这时我们可以在抛出异常的同时或者在同栈的捕获地址进行delete处理。这样的步骤即麻烦也不美观,这种情况下使用智能指针就可以解决这个问题。
double Divide(int x, int y)
{if (y == 0)throw string("除零错误");else return (double)x / y;
}void Fun()
{int* arr1= new int[10];double* arr2 = new double[10];int x1, x2; cin >> x1 >> x2;try{Divide(x1, x2);}//异常捕获后会使相关栈展开链上的函数销毁,这时候就需要手动清理内存防止泄漏catch (...){delete[] arr1;cout << "delete[] arr1" << endl;delete[] arr2;cout << "delete[] arr2" << endl;//将被抛出的异常再捕获,交给外面的catch进行进一步处理throw;//万能抛出}//没有抛出异常也要清理内存防止泄漏delete[] arr1;cout << "delete[] arr1" << endl;delete[] arr2;cout << "delete[] arr2" << endl;
}int main()
{try{Fun();}catch (const string& err){cout << err << endl;}catch (const exception& x){cout << x.what() << endl;}catch (...){cout << "unkonw error" << endl;}return 0;
}
二、RAII思想
RAII是Resource Acquisition Is Initialization的缩写,他是⼀种管理资源的类的设计思想,本质是 ⼀种利用对象生命周期来管理获取到的动态资源,避免资源泄漏。
例如我们将上面的arr1和arr2指针用一个类进行封装,将delete放在类的析构中。这时候就算异常抛出捕获导致相关栈空间销毁使得封装类一同销毁,但由于类的销毁会调用析构函数会实现自动调用delete释放arr1和arr2的空间资源防止了内存泄漏。这就是典型的RAII思想,优雅又美观。
RAII思想在内存、文件指针、网络连接、互斥锁等方面都有着广泛使用。
智能指针也是RAII设计思想的体现,但除了用类封装外还需要满足指针相关功能的使用,至少需要重载* -> [ ]这些运算符以满足相关数据访问。
三、C++标准库中的智能指针
C++的智能指针都包含在头文件memory中:https://legacy.cplusplus.com/reference/memory/
我们先了解其中三种智能指针:
1、auto_ptr:auto_ptr - C++ Reference (cplusplus.com)
auto_ptr是C++98时设计出来的智能指针,他的特点是拷贝时把被拷贝对象的资源的管理权转移给拷贝对象,这是⼀个非常糟糕的设计,因为他会导致被拷贝对象悬空,访问报错的问题。简单点说就是被拷贝指针对资源的管理权限移交给auto_ptr指针,这时候使用被拷贝的原指针会导致访问错误(通常崩溃,因为访问了nullptr)。故十分不推荐使用auto_ptr,在有的公司会干脆将其禁用
模拟实现:
namespace auto_pointer
{template<class T>class auto_ptr{public:auto_ptr(T* ptr):_ptr(ptr){ }//拷贝构造会导致原指针悬空auto_ptr(auto_ptr<T>& x):_ptr(x._ptr){//auto智能指针的权限转移给新拷贝得到的对象x._ptr = nullptr;}auto_ptr<T>& operator=(auto_ptr<T>& x){if (&x!=this){//先清除被赋值对象中的资源,//_ptr完成赋值后原资源会丢失,若不清理会导致内存泄漏!if (_ptr)delete _ptr;_ptr = x._ptr;//管理权限转移x._ptr = nullptr;}return *this;}~auto_ptr(){if (_ptr) delete _ptr;}T& operator*(){return *_ptr;}T* operator->(){return _ptr;}private:T* _ptr;};
}
上面代码很明显看到指针悬空的问题(如:x._ptr = nullptr;)
2、unique_ptr:unique_ptr - C++ Reference (cplusplus.com)
unique_ptr是C++11设计出来的智能指针,他的名字翻译出来是唯⼀指针,他的特点的不支持拷 贝,只支持移动。如果不需要拷贝的场景就非常建议使用他
namespace unique_pointer
{template<class T>class unique_ptr{public://参数可能发生普通指针向智能指针类型隐式转化,explicit修饰防止转化explicit unique_ptr(T* ptr):_ptr(ptr){ }~unique_ptr(){if (_ptr)delete _ptr;}//unique_ptr不支持拷贝构造unique_ptr(const unique_ptr<T>& x) = delete;//禁用这种赋值重载是为了防止与拷贝构造杂糅unique_ptr<T>& operator=(const unique_ptr<T>& x) = delete;//右值引用unique_ptr(unique_ptr<T>&& x):_ptr(x._ptr){if (x._ptr)x._ptr = nullptr;}//由于禁用拷贝,只能用移动方式unique_ptr<T>& operator=(unique_ptr<T>&& x){if (this != &x){if (_ptr) delete _ptr;_ptr = x._ptr;if (x._ptr)x._ptr = nullptr;}return *this;}T& operator*(){return *_ptr;}T* operator->(){return _ptr;}private:T* _ptr;};
}
3、shared_ptr:shared_ptr - C++ Reference (cplusplus.com)
shared_ptr是C++11设计出来的智能指针,他的名字翻译出来是共享指针,他的特点是支持拷贝, 也支持移动。如果需要拷贝的场景就需要使用他了。底层是用引用计数的方式实现的。
auto_ptr和unique_ptr两个智能指针相比shared_ptr要简单不少,下面以shared_ptr智能指针为例继续讲解智能指针。
四、shared_ptr智能指针
由于shared_ptr指针支持拷贝,也就导致存在多个shared_ptr指针指向同一块空间的情况。故shared_ptr指针内部还需要设计一个计数器用于统计指向该位置的shared_ptr指针总个数,并且被指向空间销毁必须保证没有智能指针指向该位置(也就是在最后一个shared_ptr指针销毁后才能销毁该空间)。
关于计数器:
很明显,指向同一空间的shared_ptr指针共享同一个计数器。但我们可不能因此设置一个static具有静态属性的计数器,否者会导致明明是指向不同空间的智能指针却共用同一个计数器的错误。
正确的做法是设置一个普通指针类型存储计数,在智能指针指向新空间(未被指向空间)时开辟一块空间专门用于对当前空间的计数。当拷贝时就将用于计数的空间一同拷贝过去。
关于删除器:
智能指针既然都能“托管”原资源空间那么肯定也得具备处理资源释放的能力。智能指针中对相应的类型对象都会提供合适的析构函数或可调用逻辑,这个析构函数或者说是可调用逻辑我们就称之为删除器。
另外,还是因为shared_ptr指针允许拷贝的缘故,在拷贝过程中删除器也需要一同被拷贝过去。否则会出现用delete释放数组空间(应该用delete[ ])之类的错误。
又因为删除器要具备可传递性,单单是析构函数类型无法满足这项要求。这时候就该类型擦除器std::function和匿名函数对象lambda大显身手的时候了。
//默认删除器
std::function<void(T*)> _del = [](T*_ ptr)->void{delete _ptr; };
shared_ptr智能指针简单模拟实现
namespace shared_pointer
{template<class T>class shared_ptr{public://防止普通指针转化为智能指针explicit shared_ptr(T* ptr=nullptr):_ptr(ptr){if (_ptr)_num = new int(1);else _num = nullptr;}//删除器版本template<class D>shared_ptr(T* ptr,D del):_ptr(ptr),_num(new int(1)),_del(del){ }shared_ptr(const shared_ptr<T>& x):_ptr(x._ptr),_num(x._num),_del(x._del){++(*_num);}void release(){if (_num == nullptr) return;if (--(*_num) == 0){_del(_ptr);delete _num;_ptr = nullptr;_num = nullptr;}}~shared_ptr(){release();}shared_ptr<T>& operator=(const shared_ptr<T>& x){if (this != &x){//清理被赋值指针的空间,为赋值做准备release();_ptr = x._ptr;_num = x._num;_del = x._del;++(*_num);}return *this;}T* get()const{return _ptr;}int use_count()const{if (_ num == nullptr) return 0;return *_num;}T& operator*(){assert(_ptr);return *_ptr;}T* operator->(){return _ptr;}private:T* _ptr;int* _num;//默认删除器std::function<void(T*)> _del = [](T* p)->void{delete p; };};
}
上面对shared_ptr的模拟虽然并不如库里的完美,但也实现了相关功能。
shared_ptr智能指针循环引用问题
现在我们给两个shared_ptr指针n1、n2给上_next和_prev(头尾指针)并令其相互指向。
当我们要销毁n1/n2时就会出现这种问题:
不管怎么样,n1/n2的引用计数都会减到1。当要销毁n1时,因为n1被n2管理(n1的prev是n2)所以要先销毁n2.
当要销毁n2时,n2又被n1管理(n1又是n2的prev)所以要先销毁n1。
这样n1、n2相互不停的踢皮球导致了死循环,引发内存泄漏。为了解决shared_ptr中的这个问题,C++特意创建weak_ptr指针来解决这个问题。
_next和_prev改成weak_ptr,weak_ptr绑定到shared_ptr时不会增加它的引用计数,_next和_prev不参与资源释放管理逻辑,就成功打破了循环引用,解决了这里的问题。
五、weak_ptr
weak_ptr是弱指针,它跟前面的三个智能指针都不一样。weak_ptr不支持RAII,也不支持访问资源。它只支持绑定到shared_ptr,绑定到shared_ptr时,不增加shared_ptr的引用计数,那么就可以解决上述的循环引用问题。
weak_ptr也没有重载operator*和operator->等,因为他不参与资源管理,那么如果他绑定的 shared_ptr已经释放了资源,那么他去访问资源就是很危险的。
weak_ptr支持expired检查指向的资源是否过期,use_count也可获取shared_ptr的引用计数,weak_ptr想访问资源时,可以调用lock返回⼀个管理资源的shared_ptr,如果资源已经被释放,返回的shared_ptr是⼀个空对象,如果资源没有释放,则通过返回的shared_ptr访问资源是安全的。
六、shared_ptr线程安全问题
shared_ptr的引用计数对象在堆上,如果多个shared_ptr对象在多个线程中,进行shared_ptr的拷贝析构时会访问修改引用计数,就会存在线程安全问题,所以shared_ptr引用计数是需要加锁或者原子操作保证线程安全的。
shared_ptr指向的对象也是有线程安全的问题的,但是这个对象的线程安全问题不归shared_ptr 管,它也管不了,应该有外层使用shared_ptr的⼈进行线程安全的控制。