C++中的智能指针
1.为什么要使用智能指针?
先来看一段代码,分析这段代码有没有内存上的错误?
#include <iostream>
using namespace std;
void _MergeSort(int* a, int left, int right, int* tmp)
{
if (left >= right) return;
int mid = left + ((right - left) >> 1);
// [left, mid]
// [mid+1, right]
_MergeSort(a, left, mid, tmp);
_MergeSort(a, mid + 1, right, tmp);
int begin1 = left, end1 = mid;
int begin2 = mid + 1, end2 = right;
int index = left;
while (begin1 <= end1 && begin2 <= end2) {
if (a[begin1] < a[begin2])
tmp[index++] = a[begin1++];
else
tmp[index++] = a[begin2++];
}
while (begin1 <= end1)
tmp[index++] = a[begin1++];
while (begin2 <= end2)
tmp[index++] = a[begin2++];
memcpy(a + left, tmp + left, sizeof(int)*(right - left + 1));
}
void MergeSort(int* a, int n){
int* tmp = (int*)malloc(sizeof(int)*n);
_MergeSort(a, 0, n - 1, tmp);
// free(tmp);
}
int main()
{
int a[5] = { 4, 5, 2, 3, 1 };
MergeSort(a, 5);
return 0;
}
上面的问题分析出来我们发现有以下两个问题:
- 我们malloc开辟出来的内存空间,在程序结束前没有进行释放,存在内存泄漏的问题;
- 如果在malloc和free之间如果存在抛异常,那么还是有内存泄漏,这种问题就叫异常安全。
类似于malloc这样的问题,还有比如我们使用new来开辟内存空间,在程序结束之前也必须使用delete来释放。但当一个程序中频繁的使用内存空间,你能保证自己每一个手动开辟的内存块都释放了吗?在delete之前,倘若程序因为异常跳转后,你还能保证内存被释放吗?为了解决这种因为遗忘或者异常处理而导致内存泄漏的问题,我们就要用智能指针。
2.智能指针的使用及其原理
2.1RAII
RAII(Resource Acquisition Is Initialization)是一种利用对象生命周期来控制程序资源(如内存、文件句柄、网络连接、互斥量等等)的简单技术。
在对象构造时获取资源,接着控制对资源的访问使之在对象的生命周期内始终保持有效,最后在对象析构的时候释放资源。 借此,我们实际上把管理一份资源的责任托管给了一个对象。 这种做法有两大好处:
- 不需要显式地释放资源。
- 采用这种方式,对象所需的资源在其生命期内始终保持有效。
使用RAII思想设计的SmartPtr类:
// 使用RAII思想设计的SmartPtr类
template<class T>
class SmartPtr
{
public:
//构造对象时获取资源
SmartPtr(T* ptr = nullptr)
: _ptr(ptr){
}
//对象析构时释放资源
~SmartPtr(){
if(_ptr)
delete _ptr;
}
private:
T* _ptr;
};
void MergeSort(int* a, int n)
{
int* tmp = (int*)malloc(sizeof(int)*n);
SmartPtr<int> sp(tmp);
}
2.2智能指针的使用及原理
上述代码中的SmartPtr还不能称为智能指针,因为它还不具有指针的行为。指针可以解引用,也可以通过->去访问所指内存空间中的内容。因此AutoPtr类中还需要将*、->重载下,才能让其像指针一样去使用。
class SmartPtr
{
public:
SmartPtr(T* ptr = nullptr)
: _ptr(ptr){
}
~SmartPtr(){
if(_ptr)
delete _ptr;
}
w
T& operator*() {return *_ptr;}
T* operator->() {return _ptr;}
private:
T* _ptr;
};
具体使用:
#include<iostream>
using namespace std;
template<class T>
class SmartPtr
{
public:
SmartPtr(T* ptr = nullptr)
: _ptr(ptr){
}
~SmartPtr(){
if (_ptr)
delete _ptr;
}
//重载* ->
T& operator*(){
return *_ptr;
}
T* operator->(){
return _ptr;
}
private:
T * _ptr;
};
struct Date
{
int _year;
int _month;
int _day;
};
int main()
{
SmartPtr<int> sp1(new int);
*sp1 = 10;
cout << *sp1 << endl;
SmartPtr<Date> sparray(new Date);
// 需要注意的是这里应该是sparray.operator->()->_year = 2018;
// 本来应该是sparray->->_year这里语法上为了可读性,省略了一个->
sparray->_year = 2019;
sparray->_month = 6;
sparray->_day = 1;
cout << sparray->_year<<" "<<sparray->_month <<" "<< sparray->_day << endl;
return 0;
}

构造对象时直接用new来申请内存空间,程序结束时不用手动释放,类中的构造函数为我们释放内存空间。智能指针是一种预防型的内存泄漏的解决方案。 智能指针在C++没有垃圾回收器环境下,可以很好的解决异常安全等带来的内存泄漏问题
智能指针的原理:
- RAII特性
- 重载operator*和opertaor->,具有像指针一样的行为
在智能指针的发展历程中,C++不断推出了各种各样的智能指针。但在赋值/拷贝过程,但他们都有缺陷,直到今日,shared_ptr才算一种成熟的智能指针。
3.C++中常见的智能指针
3.1auto_ptr
C++98版本的库中就提供了auto_ptr的智能指针,它可谓是智能指针中的元老。auto_ptr是独占指针,它的出现是能够自动析构动态分配的内存,避免内存泄漏,但是auto_ptr有很多弊端,下面会通过示例和讲解一一将弊端和用法展现出来。
3.1.1 auto_ptr的实现原理
auto_ptr的实现原理: 管理权转移的思想。
//auto_ptr的实现原理: 管理权转移的思想
#include <memory>
#include<iostream>
using namespace std;
template<class T>
class AutoPtr
{
public:
AutoPtr(T* ptr = NULL)
: _ptr(ptr){
}
~AutoPtr(){
if (_ptr)
delete _ptr;
}
// Auto_ptr原理:一旦发生拷贝,就将ap中资源转移到当前对象中,然后ap与其所管理资源断开联系
// 解决了一块空间被多个对象使用而造成程序奔溃问题
AutoPtr(AutoPtr<T>& ap)
: _ptr(ap._ptr){
ap._ptr = NULL;
}
AutoPtr<T>& operator=(AutoPtr<T>& ap){
// 检测是否为自己给自己赋值
if (this != &ap){
// 释放当前对象中资源
if (_ptr)
delete _ptr;
// 转移ap中资源到当前对象中
_ptr = ap._ptr;
ap._ptr = NULL;
}
return *this;
}
T& operator*(){
return *_ptr;
}
T * operator->(){
return _ptr;
}
private:
T* _ptr;
}
3.1.2auto_ptr的缺陷
- auto_ptr不能初始化为指向非动态内存(delete 表达式会被应用在不是动态分配的指针上这将导致未定义的程序行为)。对象通过初始化只能指向由new创建的动态内存,它是这块内存的拥有者。auto_ptr的用途:管理对象的生命周期,不造成内存泄漏。
// 直接构造智能指针
auto_ptr<int> ap(new int(1));
// 将已存在的指向动态内存的普通指针作为参数来构造
int* ptr = new int(1);
auto_ptr<int> ap1(ptr);
- auto_ptr智能指针的拷贝/赋值,会发生所有权的转移。拷贝/赋值后把ap对象的指针赋空了,导致ap对象悬空。
#include <memory>
#include<iostream>
using namespace std;
template<class T>
class AutoPtr
{
public:
AutoPtr(T* ptr = NULL)
: _ptr(ptr){
}
~AutoPtr(){
if (_ptr)
delete _ptr;
}
AutoPtr(AutoPtr<T>& ap)
: _ptr(ap._ptr){
ap._ptr = NULL;
}
AutoPtr<T>& operator=(AutoPtr<T>& ap){
if (this != &ap){
if (_ptr)
delete _ptr;
_ptr = ap._ptr;
ap._ptr = NULL;
}
return *this;
}
T& operator*(){
return *_ptr;
}
T * operator->(){
return _ptr;
}
private:
T* _ptr;
};
class Date{
public:
Date(){
cout << "Date()" << endl;
}
~Date(){
cout << "~Date()" << endl;
}
int _year;
int _month;
int _day;
};
int main()
{
AutoPtr<Date> ap(new Date);
AutoPtr<Date> ap1(ap);
AutoPtr<Date> ap2(ap);
// 拷贝后把ap对象的指针赋空了,导致ap对象悬空,通过ap对象访问资源时就会出现问题。
ap->_year = 2018;
cout << &ap << endl;
cout << &ap1 << endl;
cout << &ap2 << endl;
return 0;
}
上述代码中ap1拷贝ap,拷贝后ap对象的指针赋空了,导致ap对象悬空,通过ap对象访问资源时就会出现问题。

调用内存窗口观察:

再次验证:当一个auto_ptr被拷贝或者赋值后,其对象就被悬空了,指向NULL
- 一个指针变量指向的空间不能由两个auto_ptr对象管理,不然会析构两次,使程序崩溃。由于auto_ptr对象析构时会删除它所拥有的指针,所以使用时避免多个auto_ptr对象管理同一个指针。
class Date
{
public:
Date(){
cout << "Date()" << endl;
}
~Date(){
cout << "~Date()" << endl;
}
int _year;
int _month;
int _day;
};
int main()
{
Date* ptr = new Date;
AutoPtr<Date> ap(ptr);
AutoPtr<Date> ap2(ptr);
return 0;
}
ptr所指的内存空间被析构两次,导致程序崩溃。


- auto_ptr的拷贝构造,将源指针的管理权交给目标指针,会使得源指针悬空,解引用会出现很多问题。


- auto_ptr不能用来管理数组指针:auto_ptr的内部实现中,析构函数中的是使用delete而不是delete[]。
int *ptr = new int[10];
auto_ptr<int> ap(ptr);
使用auto_ptr的方式,在ap析构时,执行delete,仅仅释放了数组第一个元素的空间,仍然会造成内存泄漏,所有使用auto_ptr管理数组不合理的。
- 不要将auto_ptr作为函数参数按值传递。
因为在函数调用过程中在函数的作用域中会产生一个局部的临时auto_ptr对象来接收传入的 auto_ptr(拷贝构造),这样,传入的实参auto_ptr对其指针的所有权转移到了临时auto_ptr对象上,临时auto_ptr在函数退出时析构,所以当函数调用结束,原实参所指向的对象已经被删除了。
void func(auto_ptr<int> ap){
cout << *ap << endl;
}
auto_ptr<int> ap(new int(1));
func(ap); //值传递后,ap的所有权转移,ap失效,将不再拥有对象
cout << *ap << endl;//错误,函数调用结束后,ap已经不再拥有任何对象了
- 非要将auto_ptr作为参数传递时,一定要使用const &类型。
使用const 引用传递则可以阻止在函数体内对auto_ptr对象的所有权转移。如果不得不使用auto_ptr对象作为函数参数时,尽量使用const引用传递参数。
- auto_ptr支持所拥有的指针类型之间隐式类型转换。
//实现从auto_ptr的隐式转换,derived*可以转换成base*类型
class base{};
class derived: public base{};
auto_ptr<base> apbase = auto_ptr<derived>(new derived);
- auto_ptr对象不能作为STL容器元素
3.2unique_ptr
C++11中推出了更靠谱的unique_ptr,unique_ptr的设计思路非常的粗暴:防拷贝,也就是不让拷贝和赋值。 使用=delete阻止拷贝类对象,将拷贝构造/赋值函数定义成已删除的函数,任何调用它的行为都将产生编译期错误,这是C++11标准的内容。C++的做法是将这两个函数设为私有,且只声明不实现。
//unique_ptr的实现原理
template<class T>
class UniquePtr
{
public:
UniquePtr(T * ptr = nullptr)
: _ptr(ptr){
}
~UniquePtr(){
if(_ptr)
delete _ptr;
}
T& operator*(){
return *_ptr;
}
T* operator->(){
return _ptr;
}
private:
// C++98防拷贝的方式:只声明不实现+声明成私有
UniquePtr(UniquePtr<T> const &);
UniquePtr & operator=(UniquePtr<T> const &);
// C++11防拷贝的方式:=delete
UniquePtr(UniquePtr<T> const &) = delete;
UniquePtr & operator=(UniquePtr<T> const &) = delete;
private:
T * _ptr;
};
这种智能指针比起auto_ptr要好且实现简单。但是遇到要拷贝构造 / 赋值的情景就会哑火,所以只能算中规中矩。
3.3shared_ptr
C++11中开始提供更靠谱的并且支持拷贝的shared_ptr,shared_ptr完善了前两种的不足,既不会直接剥夺原对象对内存的控制权,也允许进行拷贝构造和赋值,这都源自于他引入了一个新的标志—引用计数。
shared_ptr的原理:是通过引用计数的方式来实现多个shared_ptr对象之间共享资源。
- shared_ptr在其内部,给每个资源都维护了一份计数,用来记录该份资源被几个对象共享。
- 在对象被销毁时(也就是析构函数调用),就说明自己不使用该资源了,对象的引用计数减一。
- 如果引用计数是0,就说明自己是最后一个使用该资源的对象,必须释放该资源;
- 如果不是0,就说明除了自己还有其他对象在使用该份资源,不能释放该资源,否则其他对象就成野指针了。
//shared_ptr的原理:引用计数
template <class T>
class SharedPtr
{
public:
SharedPtr(T* ptr = nullptr)
: _ptr(ptr)
, _pRefCount(new int(1)){
}
~SharedPtr(){
if (--(*_pRefCount) == 0) {
delete _ptr;
delete _pRefCount;
}
}
SharedPtr(const SharedPtr<T>& sp)
: _ptr(sp._ptr)
, _pRefCount(sp._pRefCount){
(*_pRefCount)++;
}
// sp1 = sp2
SharedPtr<T>& operator=(const SharedPtr<T>& sp){
if (this != &sp) {
if (--(*_pRefCount) == 0){
delete _ptr;
delete _pRefCount;
}
// 释放管理的旧资源
// 共享管理新对象的资源,并增加引用计数
_ptr = sp._ptr;
_pRefCount = sp._pRefCount;
(*_pRefCount)++;
}
return *this;
}
T& operator*(){
return *_ptr;
}
T* operator->(){
return _ptr;
}
int UseCount(){
return *_pRefCount;
}
T* Get(){
return _ptr;
}
private:
int* _pRefCount; // 引用计数
T* _ptr; // 指向管理资源的指针
};
实现:
#include<memory>
#include<iostream>
#include <thread>
#include <mutex>
using namespace std;
template <class T>
class SharedPtr
{
public:
SharedPtr(T* ptr = nullptr)
: _ptr(ptr)
, _pRefCount(new int(1)){
}
~SharedPtr(){
if (--(*_pRefCount) == 0) {
delete _ptr;
delete _pRefCount;
}
}
SharedPtr(const SharedPtr<T>& sp)
: _ptr(sp._ptr)
, _pRefCount(sp._pRefCount){
(*_pRefCount)++;
}
// sp1 = sp2
SharedPtr<T>& operator=(const SharedPtr<T>& sp){
if (this != &sp) {
if (--(*_pRefCount) == 0){
delete _ptr;
delete _pRefCount;
}
// 释放管理的旧资源
// 共享管理新对象的资源,并增加引用计数
_ptr = sp._ptr;
_pRefCount = sp._pRefCount;
(*_pRefCount)++;
}
return *this;
}
T& operator*(){
return *_ptr;
}
T* operator->(){
return _ptr;
}
int UseCount(){
return *_pRefCount;
}
T* Get(){
return _ptr;
}
private:
int* _pRefCount; // 引用计数
T* _ptr; // 指向管理资源的指针
};
int main()
{
SharedPtr<int> sp1(new int(10));
SharedPtr<int> sp2(sp1);
*sp2 = 20;
cout <<"sp1.UseCount():" <<sp1.UseCount() << endl;
cout <<"sp2.UseCount():" <<sp2.UseCount() << endl;
cout << endl;
SharedPtr<int> sp3(new int(10));
sp2 = sp3;
cout <<"sp1.UseCount():" <<sp1.UseCount() << endl;
cout <<"sp2.UseCount():" <<sp2.UseCount() << endl;
cout <<"sp3.UseCount():" <<sp3.UseCount() << endl;
cout << endl;
sp1 = sp3;
cout <<"sp1.UseCount(): "<<sp1.UseCount() << endl;
cout <<"sp1.UseCount(): "<<sp2.UseCount() << endl;
cout <<"sp1.UseCount(): "<<sp3.UseCount() << endl;
return 0;
}

sp1和sp2共享同一份资源,所以其计数为2。注意这里我们用sp1拷贝构造了sp2,所以他们两个指向同一个内存,然后通过*sp2=20修改了sp2中的资源,看上去我们只修改了sp2中的资源,但这里我们要清楚sp2和sp1公用同一份内存空间。通过地址修改sp2中的资源相应的sp1中的资源也被修改了。他们两个仍然共享同一份资源。

再接着我们用sp3给sp2赋值,此时sp3和sp2共享同一份资源,sp1单独享用一份资源。所以sp1计数为1,sp2和sp3计数为2。

再接着sp3给sp1赋值,此时三个对象共享一份资源。

3.3.1shared_ptr中的循环引用的问题
了解循环引用:
#include<memory>
#include<iostream>
using namespace std;
struct ListNode
{
int _data;
shared_ptr<ListNode> _prev;
shared_ptr<ListNode> _next;
~ListNode()
{
cout << "~ListNode" << endl;
}
};
int main()
{
shared_ptr<ListNode> node1(new ListNode);
shared_ptr<ListNode> node2(new ListNode);
cout << "node1:" << node1.use_count() << endl;
cout << "node2:" << node2.use_count() << endl;
node1->_next = node2;
node2->_prev = node1;
cout << "引用循环后 :" << endl;
cout << "node1:"<< node1.use_count() << endl;
cout << "node2:"<< node2.use_count() << endl;
return 0;
}
node1和node2两个智能指针对象指向两个节点,引用计数变成1,我们不需要手动delete。 node1的_next指向node2,node2的_prev指向node1,引用计数变成2。 node1和node2析构,引用计数减到1,但是_next还指向下一个节点,_prev还指向上一个节点。 也就是说_next析构了,node2就释放了; _prev析构了,node1就释放了。 但是_next属于node的成员,node1释放了,_next才会析构,而node1由_prev管理,_prev属于node2成员,所以这就叫循环引用,谁也不会释放。

C++库为了解决这个问题,专门定义了一个叫做weak_ptr的东西,专门用于辅助shared_ptr来解决引用计数的问题。那他是怎么解决这么问题的呢?当shared_ptr内部要监视其他的shared_ptr对象时,类型就采用weak_ptr。这种weak_ptr在指向被监视的shared_ptr后,并不会使被监视的引用计数增加,且当被监视的对象析构后就自动失效。
//解决方案:在引用计数的场景下,把节点中的_prev和_next改成weak_ptr
//原理:node1->_next = node2;和node2->_prev = node1;weak_ptr的_next和_prev不会增加node1和node2的引用计数。
#include<memory>
#include<iostream>
using namespace std;
struct ListNode
{
int _data;
shared_ptr<ListNode> _prev;
shared_ptr<ListNode> _next;
~ListNode()
{
cout << "~ListNode" << endl;
}
};
int main()
{
weak_ptr<ListNode> node1(new ListNode);
weak_ptr<ListNode> node2(new ListNode);
cout << "node1:" << node1.use_count() << endl;
cout << "node2:" << node2.use_count() << endl;
node1->_next = node2;
node2->_prev = node1;
cout << "引用循环后 :" << endl;
cout << "node1:"<< node1.use_count() << endl;
cout << "node2:"<< node2.use_count() << endl;
return 0;
}
这里的cur和next在析构的时候 , 不用引用计数减一 , 直接删除结点

4.总结
- 尽量不要使用auto_ptr,因为他的缺陷导致我们拷贝构造/赋值的时候有很大的麻烦。
- 在不需要拷贝构造/赋值的时候,可以使用unique_ptr。
- 有拷贝构造/赋值的情况,推荐使用shared_ptr.
- 类内有访问其他shared_ptr对象时,指针类型设为weak_ptr,可以不改其他shared_ptr对象的引用计数。
- 代码中尽量不用delete关键字,因为我们的内存的管理与释放全权交给对象处理。
本文介绍了C++中智能指针的重要性,解释了RAII原则,详细讨论了auto_ptr、unique_ptr和shared_ptr的使用、原理及优缺点,特别是shared_ptr中的循环引用问题和解决方案。
3886

被折叠的 条评论
为什么被折叠?



