C++之智能指针深入学习:从入门到精通!

news2024/9/23 15:24:05

简介

本文详细介绍了 C++ 中的 RAII 与智能指针等技术的基本概念与常用技巧。

资源管理技术:RAII

下面是对 RAII 的一个简单的介绍:

在这里插入图片描述

简而言之就是:RAII(Resource Acquisition Is Initialization)是一种由 C++ 之父 Bjarne Stroustrup 提出的利用栈对象生命周期管理程序资源(包括内存、文件句柄、锁等)的技术。

使用 RAII 时,一般在资源获得的同时构造对象,在对象生存期间,资源一直保持有效;对象析构时,资源被释放。

关键:要保证资源的释放顺序与获取顺序严格相反。

接下来我们使用代码来实现一下 RAII 技术:

#include <iostream> 

using namespace std;

template<typename T>
class RAII {
private:
	//将需要管理的资源封装在类内部,作为类的私有成员变量
	T* _data;
public:
	//在构造函数中初始化资源
	RAII(T* data):_data(data) {
		cout << " RAII(T*) " << endl;
	}
	
	//在析构函数中释放资源
	~RAII() {
		cout << "~RAII()" << endl;
		if (_data) {
			delete _data;
			_data = nullptr;
		}
	}
	
	//提供若干访问资源的方法
	T* operator->() {
		return _data;
	}

	T& operator*() {
		return *_data;
	}

	T* get() {
		return _data;
	}

	void reset(T* data) {
		if (_data) {
			delete _data;
			_data = nullptr;
		}
		_data = data;
	}
	
	//不允许复制或者赋值
	RAII(const RAII& rhs) = delete;
	RAII& operator=(const RAII& rhs) = delete;
};

int main() {

	//利用栈对象的生命周期实现资源的管理
	RAII<int> raii(new int(10));

	//不允许复制或者赋值
	//RAII<int> raii_1 = raii; 报错
	return 0;
}
	

运行结果如下:

在这里插入图片描述

从上面的代码中可以看见 int 指针资源已经由我们的 RAII 类对象 raii 所管理了起来,资源创建和回收都由该对象完成。

raii 本身是一个对象,但是它的使用和一个指针是一样的(别忘了我们还实现了箭头运算符和解引用运算符哟,也就是说如果 T 类型是不是上面的 int 而是某种自定义类型的话,我们是可以 raii->function() 的,这不就跟一个指针很相似了吗?),而这就是某一种智能指针的雏形啦!

智能指针

所谓智能指针(Smart Point)是指指向动态分配(堆)对象指针的类,这种技术在面对异常的时候格外有用,因为它们能够确保正确的销毁动态分配的对象。

C++ 中目前提供了四种类型的智能指针:

在这里插入图片描述

智能指针第一种:auto_ptr

Cpp_Reference 文档中的介绍:

在这里插入图片描述

具体的可以自己去这个网站查看,接下来我们直接上代码,在实战中学习:

#include <iostream> 
#include <memory>

using namespace std;

void test() {
	
	//相对于智能指针,这种原生的指针被称为裸指针
	int* pInt = new int(10);
	//这里我们使用了一个 pInt 指针来对智能指针 auto_ptr 进行了初始化
	auto_ptr<int> ap(pInt);
	
	//比较裸指针和智能指针二者所指向的地址是否相同
	cout << "pInt 的地址为:" << pInt << endl;
	//使用 get 函数可以返回所存储指针所指向的地址
	cout << "ap.get()的地址为:" << ap.get() << endl;
	cout << "解引用*ap的值为:" << *ap << endl;
}

int main() {
	test();
	return 0;
}
	

运行结果如下:

在这里插入图片描述

将上述代码与我们第一小节中讲的 RAII 代码对比后不难发现,二者十分相似,无非是类名的不同罢了。

那么 auto_ptr 可以实现复制或者赋值吗?我们来试一下:

void test() {
	
	//相对于智能指针,这种原生的指针被称为裸指针
	int* pInt = new int(10);
	//这里我们使用了一个 pInt 指针来对智能指针 auto_ptr 进行了初始化
	auto_ptr<int> ap(pInt);
	
	//比较裸指针和智能指针二者所指向的地址是否相同
	cout << "pInt 的地址为:" << pInt << endl;
	//使用 get 函数可以返回所存储指针所指向的地址
	cout << "ap.get()的地址为:" << ap.get() << endl;
	cout << "解引用*ap的值为:" << *ap << endl;

	cout << endl;
	//编译通过,说明可以进行复制
	auto_ptr<int> ap2(ap);
	cout << "解引用*ap2 = " << *ap2 << endl;
	cout << "解引用*ap = " << *ap << endl;
}

运行结果如下:

在这里插入图片描述

可以发现,在打印 ap2 的时候就出现了段错误,这是怎么回事呢?

此时如果我们将上面的代码中的这一行注释掉:

cout << "解引用*ap = " << *ap << endl;

再次编译运行会发现是正常的,不会有段错误:

在这里插入图片描述

说明问题就出在这一行,说明我们对 ap 进行解引用的这个行为引发了问题。

不妨大胆猜测,类比我们之前的 RAII 代码,我们可以推测出如果 RAII 中的 _data 为空的话,那么解引用的行为就会出错了:

T& operator*() {
		return *_data;
	}

那么就有可能是在执行 auto_ptr 复制的时候导致的 ap 为空,也就是在执行下面这行代码的时候导致的 ap 为空:

auto_ptr<int> ap2(ap);

那么我们就有理由怀疑是 auto_ptr 类的拷贝构造函数有鬼,因此不妨我们来看一下其源码是如何实现的:

在这里插入图片描述

可以看到上图中的几行的基本思路和我们之前讲的都差不多,但是我们要看的拷贝构造函数的实现,因此我们找到拷贝构造函数:

在这里插入图片描述

再对比一下我们刚刚测试出错的那一行复制的代码:

auto_ptr<int> ap2(ap);

可以分析在执行复制操作时会调用上图的拷贝构造函数,ap 传进来之后就是形参 _a,而 ap2 则是 _M_ptr。然后这个函数的作用是使用 _a.release() 函数的返回值对 _M_ptr 进行了赋值。

那么 release() 函数做了什么事情?我们点进去看一下:

在这里插入图片描述

首先要明确是我们的 ap 调用的 release() 方法,然后我们看它做了些什么操作:它申请了一个相同类型的指针 _tmp 来指向 _M_ptr,然后将 _M_ptr 给置为了空指针然后返回了一个 _tmp 这个变量。

因此我们可以得出结论:在执行拷贝构造函数的过程中,ap 的指针所指向的内容已经转给了 ap2,然后 ap 的指针就被置为了空指针。因此此时如果我们再去对 ap 进行解引用操作的话则势必就会产生错误。

而赋值运算符函数也正好做了这么一件事情:

在这里插入图片描述

可以看见其也对函数形参 _a 调用了 release() 方法。

其它部分的源码也比较好理解,就粘在下面随便看看啦:

在这里插入图片描述

因此小结一下,auto_ptr 的赋值复制函数表面上执行的是拷贝操作,但是底层已经发生了指针资源所有权的转移,并将 ap 的数据成员置空了,因此我们说第一种智能指针 auto_ptr 是存在缺陷的。

事实上这个智能指针确实也已经被新标准所抛弃了,不再建议使用。

因此我们更推荐使用别的智能指针。

智能指针第二种:unique_ptr

unique_ptr 是一个独享所有权的智能指针,它提供了一种严格语义上的所有权,包括:

1、拥有它所指向的对象。

2、无法进行复制、赋值等操作。

3、保存指向某个对象的指针,当它本身被删除释放的时候,会使用给定的删除器释放它所指向的对象。

4、具有移动语义(std::move()),可作为容器元素。

上代码:

void test() {
	
	//相对于智能指针,这种原生的指针被称为裸指针
	int* pInt = new int(10);
	//这里我们使用了一个 pInt 指针来对智能指针 unique_ptr 进行了初始化
	unique_ptr<int> up(pInt);
	
	//比较裸指针和智能指针二者所指向的地址是否相同
	cout << "pInt 的地址为:" << pInt << endl;
	//使用 get 函数可以返回所存储指针所指向的地址
	cout << "up.get()的地址为:" << up.get() << endl;
	cout << "解引用*up的值为:" << *up << endl;

	cout << endl;
	//编译不通过,说明不可以进行复制
	//unique_ptr<int> up2(up);
	//cout << "解引用*up2 = " << *up2 << endl;
	cout << "解引用*up = " << *up << endl;
}

运行结果如下:

在这里插入图片描述

可以看出,在语法层面 unique_ptr 不允许复制或者赋值。

之前我们说,unique_ptr 具有移动语义,那么说明这个智能指针的类肯定实现的有移动构造函数和移动赋值运算符函数,同时我们还说其可以作为容器元素,什么意思?意思就是容器的参数里面是可以传 unique_ptr 这种类型的元素的。

我们可以来代码实战看一下:

void test() {
	
	//相对于智能指针,这种原生的指针被称为裸指针
	int* pInt = new int(10);
	//这里我们使用了一个 pInt 指针来对智能指针 unique_ptr 进行了初始化
	unique_ptr<int> up(pInt);
	
	//比较裸指针和智能指针二者所指向的地址是否相同
	cout << "pInt 的地址为:" << pInt << endl;
	//使用 get 函数可以返回所存储指针所指向的地址
	cout << "up.get()的地址为:" << up.get() << endl;
	cout << "解引用*up的值为:" << *up << endl;

	cout << endl;
	//编译不通过,说明不可以进行复制
	//unique_ptr<int> up2(up);
	//cout << "解引用*up2 = " << *up2 << endl;
	cout << "解引用*up = " << *up << endl;

	//unique_ptr 作为容器参数
	unique_ptr<int> up4(new int(190));
	vector<unique_ptr<int>> vec;
	vec.push_back(up4);
}

运行结果如下:

在这里插入图片描述

在这里插入图片描述

什么原因?当我们执行这行代码时:

vec.push_back(up4);

肯定是需要将 up4 拷贝一份拷贝到 vector 里面去的,那么这个时候就会调用 unique_ptr 中的拷贝构造函数,但是明显的拷贝构造函数在 unique_ptr 中已经被删除了,是不允许进行拷贝构造的。那么此时当 unique_ptr 类型的元素要想作为容器元素的时候就无法传进去了。

但是别忘了,unique_ptr 具有移动语义,因此我们可以传一个以右值形式的 up4 进入容器充当容器的元素。

所以只需要这样改一下程序即可:

vec.push_back(move(up4));
//或者直接传入一个右值
vec.push_back(unique_ptr<int>(new int(10)));

此时程序即可正常运行。

可 unique_ptr 也还是有缺陷,比如如果我们就是想使用 up(左值)对 up2 进行复制完成初始化操作呢?很明显 unique_ptr 是做不到的:

//编译不通过,说明不可以进行复制
//unique_ptr<int> up2(up);

此时就引出了我们的第三种智能指针了。

智能指针第三种:shared_ptr

shared_ptr 是一个引用计数的智能指针,用于共享对象的所有权:

1、引进了一个计数器 shared_count,用来表示当前有多少个智能指针对象共享指针指向的内存块。

2、析构函数中不是直接释放指针对应的内存块,如果 shared_count 大于 0 则不释放内存只是将引用计数减 1,只有计数等于 0 的时候才释放内存。

3、复制构造与赋值操作符只是提供一般意义上的复制功能(浅拷贝),并且将引用计数加 1 。

来看代码:

void test() {
	
	//相对于智能指针,这种原生的指针被称为裸指针
	int* pInt = new int(10);
	//这里我们使用了一个 pInt 指针来对智能指针 shared_ptr 进行了初始化
	shared_ptr<int> sp(pInt);
	
	//比较裸指针和智能指针二者所指向的地址是否相同
	cout << "pInt 的地址为:" << pInt << endl;
	//使用 get 函数可以返回所存储指针所指向的地址
	cout << "sp.get()的地址为:" << sp.get() << endl;
	cout << "解引用*sp的值为:" << *sp << endl;
	cout << "sp.use_count = " << sp.use_count() << endl;

	cout << endl;
	//编译通过,说明可以进行复制
	shared_ptr<int> sp2(sp);
	cout << "解引用*sp2 = " << *sp2 << endl;
	cout << "sp2.get()的地址为:" << sp2.get() << endl;
	cout << "解引用*sp = " << *sp << endl;
	cout << "复制一份之后的sp.use_count = " << sp.use_count() << endl;
	cout << "复制一份之后的sp2.use_count = " << sp2.use_count() << endl;

	//shared_ptr 作为容器参数,可以发现其也具有移动语义
	shared_ptr<int> sp4(new int(190));
	vector<shared_ptr<int>> vec;
	vec.push_back(move(sp4));
	vec.push_back(shared_ptr<int>(new int(20)));
	//另外 shared_ptr 作为左值参数传入容器中也没有问题
	vec.push_back(sp4);
}

运行结果如下:

在这里插入图片描述

都比较好理解吧,我就不解释了,但是 shared_ptr 同样存在缺陷,就是循环引用的问题。

为了演示这个问题,我们写两个简单的自定义类型:

#include <iostream> 
#include <memory>
#include <vector>

using namespace std;

//前向声明一下
class Child;

class Parent {
public:
	Parent() {
		cout << "Parent()" << endl;
	}
	~Parent() {
		cout << "~Parent()" << endl;
	}

	//Parent拥有一个shared_ptr类型的智能指针变量
	//并且这个智能指针变量所指向的类型是一个自定义类型
	shared_ptr<Child> pParent;
};

class Child {
public:
	Child() {
		cout << "Child()" << endl;
	}
	~Child() {
		cout << "~Child()" << endl;
	}

	//Child拥有一个shared_ptr类型的智能指针变量
	//并且这个智能指针变量所指向的类型是一个自定义类型
	shared_ptr<Parent> pChild;
};

int main() {
	//让智能指针 parentPtr 指向一块 Parent 类型的堆空间
	shared_ptr<Parent> parentPtr(new Parent());
	//让智能指针 childPtr 指向一块 Child 类型的堆空间
	shared_ptr<Child> childPtr(new Child());

	cout << "parentPtr.use_count() = " << parentPtr.use_count() << endl;
	cout << "childPtr.use_count() = " << childPtr.use_count() << endl;

	return 0;
}

运行结果如下:

在这里插入图片描述

应该比较好理解,可以看见是很正常的,那么我们接下来的代码就会有问题了:

int main() {
	//让智能指针 parentPtr 指向一块 Parent 类型的堆空间
	shared_ptr<Parent> parentPtr(new Parent());
	//让智能指针 childPtr 指向一块 Child 类型的堆空间
	shared_ptr<Child> childPtr(new Child());

	cout << "parentPtr.use_count() = " << parentPtr.use_count() << endl;
	cout << "childPtr.use_count() = " << childPtr.use_count() << endl;

	//上面的代码都是正常的,下面的就会有问题了

	parentPtr->pParent = childPtr;
	childPtr->pChild = parentPtr;

	cout << "parentPtr.use_count() = " << parentPtr.use_count() << endl;
	cout << "childPtr.use_count() = " << childPtr.use_count() << endl;

	return 0;
}

此时再运行:

在这里插入图片描述

奇怪的事情发生了,析构函数没被执行,说明内存泄漏了。

那么为什么没有得到释放呢?

原因就是我们新添加的那两行代码:

//下面这一行使得 childPtr 的引用计数先为2
parentPtr->pParent = childPtr;
//下面这一行使得 parentPtr 的引用计数后为2
childPtr->pChild = parentPtr;

这是为什么会有两个引用计数的原因,然后代码继续往下执行,当执行完打印操作时 main 函数结束要进行资源销毁了,当 childPtr 进行销毁时它的引用计数是2,销毁之后引用计数就变为了 1,但是下面这行代码中所 new 的 Child() 内存则无法销毁:

//让智能指针 childPtr 指向一块 Child 类型的堆空间
shared_ptr<Child> childPtr(new Child());

因为引用计数为 1 呢还,所以我们在输出当中只看到了构造函数的调用(因为我们使用了 new Child())却没有看到析构函数的调用(因为能访问这块 new Child() 空间的 parentPtr 指针已经被销毁了没人管这块内存了,即 shared_ptr<Parent> pChild 因为计数器还不为 0 因此没被销毁还存在,那么析构函数就没法被调用),即产生了内存泄漏。

画图来理解一下,首先是一开始的情况,在我们执行了下面两行代码之后:

//让智能指针 parentPtr 指向一块 Parent 类型的堆空间
shared_ptr<Parent> parentPtr(new Parent());
//让智能指针 childPtr 指向一块 Child 类型的堆空间
shared_ptr<Child> childPtr(new Child());

此时的图示情况如下:

在这里插入图片描述

在执行了下面两行代码之后:

//下面这一行使得 childPtr 的引用计数先为2
parentPtr->pParent = childPtr;
//下面这一行使得 parentPtr 的引用计数后为2
childPtr->pChild = parentPtr;

情况就变成了:

在这里插入图片描述

然后在 main 函数执行完成后,要进行资源的销毁,也就是要对 parentPtr 和 childPtr 这两个对象进行销毁,销毁的顺序也很自然肯定是 childPtr 先被销毁,因为是栈对象嘛,肯定是先进后出哇,所以 childPtr 先被销毁。

此时情况就变成了下图所示:

在这里插入图片描述

可以看见由于 childPtr 对象的销毁,pChild 的引用计数变成了 1 。

而紧接着 parentPtr 也要被销毁了:

在这里插入图片描述

发现 pParent 的引用计数也变为了 1,变完之后发现这两块堆空间的引用计数都为 1,因为引用计数只要不为 0 那就肯定不会被销毁,所以这两块堆空间都无法被销毁,产生内存泄漏。

造成这个问题的原因是 share_ptr 是一种强引用的智能指针,它每次都会使得引用计数加一,因此这也就是 shared_ptr 的缺陷。

怎么解决呢?

很容易想到,反正只要让其中有一方的引用计数最后能变为 0 就可以解决了,怎么做?只要让下面这两行代码中的任意一行在执行赋值操作的时候不让引用计数加加就可以了:

//下面这一行使得 childPtr 的引用计数先为2
parentPtr->pParent = childPtr;
//下面这一行使得 parentPtr 的引用计数后为2
childPtr->pChild = parentPtr;

而这种解决思路就是第四种智能指针的设计思路了。

第四种智能指针:weak_ptr

shared_ptr 是强引用类型的智能指针,而 weak_ptr 则相反是一种弱引用类型的智能指针。

强引用的意思是只要有一个引用存在,对象就不能得到释放。

弱引用的意思是并不增加对象的引用计数,但它知道对象是否存在:如果存在,则提升为 shared_ptr 成功,否则提升失败。

通过 weak_ptr 访问对象的成员的时候,要提升为 shared_ptr 。

通过 weak_ptr 我们就可以解决之前的循环引用问题了,我们要做的就是如之前所说,将 parentPtr 或者 childPtr 给换成 weak_ptr 类型即可:

class Child {
public:
	Child() {
		cout << "Child()" << endl;
	}
	~Child() {
		cout << "~Child()" << endl;
	}

	//将 Child 的智能指针成员变量类型改为 weak_ptr 以解决循环引用问题
	weak_ptr<Parent> pChild;
};

其它代码都不用变,运行结果如下:

在这里插入图片描述

可以发现问题就迎刃而解了。

接下来我们再讲一下 weak_ptr 的基本使用:

#include <iostream> 
#include <memory>
#include <vector>

using namespace std;

void test() {
	//无法创建有参 weak_ptr
	//weak_ptr<int> wp(new int(190)); 报错,没办法这么赋值
	
	//使用 shared_ptr 对 weak_ptr 进行赋值没有问题
	weak_ptr<int> wp;
	shared_ptr<int> sp(new int(1));
	wp = sp;
	//或者下面这样也是可以的
	//weak_ptr<int> wp(sp);

	//查看此时的引用计数是多少
	cout << "sp.use_count() = " << sp.use_count() << endl;
	cout << "wp.use_count() = " << wp.use_count() << endl;

	//wp和sp都指向了同一块堆空间,那这个时候我们怎么去直接通过wp
	//来判断一下它所指向的这块堆空间是否还存在呢?
	//除了上面的 use_count() 函数之外,还可以使用 lock() 和 expired()函数
	/*
	* lcok() 函数会返回一个 shared_ptr<T> 类型,通过这个 shared_ptr 我们就可以访问 wp 托管的资源了
	* 能访问的话那自然这块堆空间就还存在
	*/
	cout << endl;
	shared_ptr<int> sp2 = wp.lock();
	if (sp2) {
		cout << "wp 托管的资源存在" << endl;
		cout << *sp2 << endl;
	}
	else {
		cout << "wp 托管的资源不存在" << endl;
	}

	/*
	* expired() 函数可以显示wp所托管的资源是否过期,如果 use_count=0,那就说明wp托管的资源已经没了
	* 那么函数就返回一个 true,否则就返回 false
	*/
	bool flag = wp.expired();
	if (flag) {
		cout << "wp 托管的资源不存在" << endl;
	}
	else {
		cout << "wp 托管的资源存在" << endl;
		cout << *sp2 << endl;
	}
}

int main() {
	test();
	return 0;
}
	

运行结果如下:

在这里插入图片描述

比较简单,就不再赘述了。

最后小结一下这个 weak_ptr:

1、weak_ptr 不会增加引用计数,属于弱引用。

2、不能直接托管裸指针,只能从 shared_ptr 去进行复制或者赋值,或者从其它的 weak_ptr 复制或者赋值。

3、判断 weak_ptr 托管的资源还存在与否,可以使用 lock 函数或者 expired 函数。

4、一般 weak_ptr 都和 shared_ptr 结合使用。

智能指针的删除器

所谓的智能指针的删除器,其实指的就是指针指针的一个参数,如 unique_ptr :

在这里插入图片描述

上图中的 Deleter 就是所谓的删除器,可以看到删除器其实就是一个模板参数。

这个 Deleter 是怎么写的呢?我们可以查看一下它的值,就是这个 default_delete :

在这里插入图片描述

可以看到是一个类,一共有两个成员函数,一个是构造函数,还有一个是括号运算符函数。

括号运算符函数的形式:

在这里插入图片描述

不难发现,在调用括号运算符函数的时候它会将 T 类型的指针变量传进来(在 unique_ptr 的模板参数中一共有两个参数,一个是 T 还有一个就是 Deleter)。传进来之后会执行下面的操作:

在这里插入图片描述

也就俩操作:delete 或者 delete[] 。

接下来我们来看看为什么需要在 unique_ptr 中能够去把申请的堆空间资源给销毁掉。

来看一段代码:

#include <iostream> 
#include <memory>
#include <string>

using namespace std;

void test() {
	string msg = "hello, world\n";
	unique_ptr<FILE> up(fopen("test.txt", "a+"));
	fwrite(msg.c_str(), 1, msg.size(), up.get());
}

int main() {
	test();
	return 0;
}	

运行结果如下,发现没有写进我们想要的数据:

在这里插入图片描述

原因其实也很简单,文件打开之后往文件中写数据是不会直接写到文件里面的,而是会写到相应的缓冲区中先缓存,而缓冲区中的内容如何写到文件里面去呢?

答案是进行刷新操作,而刷新操作我们一般可以使用 fclose();

因此我们修改一下上面的代码:

#include <iostream> 
#include <memory>
#include <string>

using namespace std;

void test() {
	string msg = "hello, world\n";
	unique_ptr<FILE> up(fopen("test.txt", "a+"));
	fwrite(msg.c_str(), 1, msg.size(), up.get());
	fclose(up.get());
}

int main() {
	test();
	return 0;
}

再次运行(visual studio 中无法呈现这个错误,因此又换了 Linux 中的 g++ 编译器来演示):

在这里插入图片描述

在这里插入图片描述

可以看到虽然数据已经写进去了,但是发生了段错误,说明程序是有问题的。

现在的问题是没有 fclose 写不进去数据,但是有了 fclose 又会引发段错误,怎么解决?在解决之前我们要先明白这个问题是怎么来的:

原因是 unique_ptr 自带的默认的 Deleter 其内部是使用 delete 或者 delete[] 进行指针的释放的,而对于一些特定的指针的类型,如这里的 FILE 文件指针类型是没办法使用 delete 或者 delete[] 进行释放的,必须使用 fclose() 来进行释放:

在这里插入图片描述

虽然错误报的是 double free,但一定要注意直接的原因并不是 “double free”,是因为错误地管理了 FILE* 指针的生命周期。在上面的例子中,unique_ptr<FILE> 被用来管理 FILE* 指针,但我们在使用 fclose 后没有正确地让 unique_ptr 知道 FILE* 已经被关闭(并且可能应该被销毁)。

同时,unique_ptr 默认使用 delete 来释放其管理的资源,而 FILE* 应该使用 fclose 而不是 delete。这就是为什么我们不能直接将 FILE* 放入 unique_ptr 中而不进行任何自定义删除操作。

因此这种情况就需要我们使用本小节所说的 unique_ptr 的第二个模板参数 Deleter 派上用场了,我们来改写这个默认的 Deleter :

在这里插入图片描述

此时编译运行结果如下:

在这里插入图片描述

此时问题就解决了,这就是删除器的作用。

而 shared_ptr 同样具有这样的问题,但有趣的是它的删除器并没有被定义在模板参数中,而是定义在了重载的构造函数里:

在这里插入图片描述

不难看出是传递一个 Deleter 的对象进入这个构造函数然后进行使用的。

因此我们同样可以用 Deleter 解决,只需要注意一下 Deleter 传递的位置即可:

在这里插入图片描述

运行发现正常:

在这里插入图片描述

智能指针常见的误用

误用一:使用不同的智能指针托管同一个裸指针(或者说堆空间)

来看错误代码:

#include <iostream> 
#include <memory>
#include <string>

using namespace std;

class Point {
public:
	Point(int ix = 0, int iy = 0) :_ix(ix), _iy(iy) {
		cout << "Point(int=0,int=0)" << endl;
	}
	void print() const {
		cout << "(" << _ix << ", " << _iy << ")" << endl;
	}
	~Point() {
		cout << "~Point()" << endl;
	}
private:
	int _ix;
	int _iy;
};

void test() {
	//使用不同的智能指针托管同一个裸指针(或者说堆空间)
	Point* pt = new Point(1, 2);
	unique_ptr<Point> up(pt);
	unique_ptr<Point> up2(pt);
}

int main() {
	test();
	return 0;
}
	

可以看到,pt 这样一个裸指针被两个不同的智能指针 up 和 up2 同时进行了托管。pt 指向的是一块 new 出来的 Point 对象的堆空间,那这样的话当 up 、up2 去进行销毁的时候势必会将托管的资源 pt 给回收两次造成 double free 的问题。

误用二:通过 reset() 使用不同的智能指针托管同一块堆空间

错误代码如下:

void test() {
	unique_ptr<Point> up(new Point(1, 2));
	unique_ptr<Point> up2(new Point(3, 4));
	//使用不同的智能指针托管同一块堆空间
	up.reset(up2.get());
}

而 reset() 函数的实现我们在前文中是写过的,如下:

void reset(T* data) {
		if (_data) {
			delete _data;
			_data = nullptr;
		}
		_data = data;
	}

这同样存在 double free 的问题,对于 reset() 函数,其先将 up 的堆空间资源给 delete 掉并置空,然后将 up2 的堆空间地址给赋值给了 up 的指针,现在 up 和 up2 都指向的是同一块堆空间资源 Point(3, 4) ,因此在 test 函数退出进行资源释放的时候就会出现 double free 的问题。

误用三:注意 shared_ptr 的两种使用方式

如果像下面这样写,那就同样会有 double free 的问题:

void test() {
	//使用不同的智能指针托管同一个裸指针(或者说堆空间)
	Point* pt = new Point(1, 2);
	shared_ptr<Point> sp(pt);
	shared_ptr<Point> sp2(pt);
}

但是如果是我们之前的写法则不会:

void test() {
	//使用不同的智能指针托管同一个裸指针(或者说堆空间)
	Point* pt = new Point(1, 2);
	shared_ptr<Point> sp(pt);
	shared_ptr<Point> sp2(sp);
}

另外如同误用二一样,如果是误用二的写法那么 shared_ptr 也会出现 double free 的问题:

void test() {
	shared_ptr<Point> sp(new Point(1, 2));
	shared_ptr<Point> sp2(new Point(3, 4));
	//使用不同的智能指针托管同一块堆空间
	sp.reset(sp2.get());
}

误用四:关于 shared_from_this() 函数的使用

错误代码:

#include <iostream> 
#include <memory>
#include <string>

using namespace std;

class Point {
public:
	Point(int ix = 0, int iy = 0) :_ix(ix), _iy(iy) {
		cout << "Point(int=0,int=0)" << endl;
	}
	void print() const {
		cout << "(" << _ix << ", " << _iy << ")" << endl;
	}
	Point* addPoint(Point* pt) {
		_ix += pt->_ix;
		_iy += pt->_iy;
		return this;
	}
	~Point() {
		cout << "~Point()" << endl;
	}
private:
	int _ix;
	int _iy;
};

void test() {
	shared_ptr<Point> sp(new Point(1, 2));
	cout << "*sp = ";
	sp->print();
	cout << endl;

	cout << endl;

	shared_ptr<Point> sp2(new Point(3, 4));
	cout << "*sp2 = ";
	sp2->print();
	cout << endl;

	cout << endl;
	//产生 double free 的地方
	shared_ptr<Point> sp3(sp->addPoint(sp2.get()));
	cout << "*sp3 = ";
	sp3->print();
	cout << endl;
}

int main() {
	test();
	return 0;
}
	

这同样会产生 double free,为什么?

仔细看这行代码:

shared_ptr<Point> sp3(sp->addPoint(sp2.get()));

还有 addPoint() 函数的实现:

Point* addPoint(Point* pt) {
		_ix += pt->_ix;
		_iy += pt->_iy;
		return this;
	}

不难发现 addPoint 将 sp 的托管的内存资源返回给了 sp3 作为使其初始化的裸指针,意味着 sp 和 sp3 都指向了 Point(1, 2) 这块堆内存资源,那么肯定就会有 double free 的问题啦,只不过乍一看看不出来而已。

解决方案如下,修改我们的 addPoint,让其返回一个 shared_ptr 指针即可:

shared_ptr<Point> addPoint(Point* pt) {
		_ix += pt->_ix;
		_iy += pt->_iy;
		return shared_ptr<Point>(this);
	}

这样做似乎就解决了,但是不然,看上去 sp3 进行初始化的时候确实是用了一个智能指针,但是会发现对于上面的代码而言,这个智能指针在进行 return 的时候会有一个拷贝操作,此时也会存在两个 shared_ptr 智能指针托管同一块内存资源的情况:一个是 sp ,还有一个则是在 return 时进行拷贝操作的临时对象 shared_ptr<Point>(this),因为 this 就是指向内存空间 Point(1, 2) 的裸指针。

这就导致了 double free 的问题。

如果要解决这样的问题,就应该让这个整体返回的就是一个智能指针,并且这个智能指针它不是去托管的裸指针,这样问题就可以得到解决。

在智能指针中,有这样一个函数可以达到这样的效果帮助我们解决这个问题:

在这里插入图片描述

上图中的 shared_from_this 这个函数就可以做到这个事情,还要注意到这个函数属于类 enable_shared_from_this。

如果我们想在 Point 这个类中使用别的类 enable_shared_from_this 中的公有 shared_from_this 函数,那么可以使用继承的方式来得到,因此我们改写 Point 类如下:

class Point: public enable_shared_from_this<Point> {
public:
	Point(int ix = 0, int iy = 0) :_ix(ix), _iy(iy) {
		cout << "Point(int=0,int=0)" << endl;
	}
	void print() const {
		cout << "(" << _ix << ", " << _iy << ")" << endl;
	}

	shared_ptr<Point> addPoint(Point* pt) {
		_ix += pt->_ix;
		_iy += pt->_iy;
		return shared_from_this();
	}

	~Point() {
		cout << "~Point()" << endl;
	}
private:
	int _ix;
	int _iy;
};

此时运行就不会再有 double free 的问题了:

在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1989826.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

C#:为什么在多线程环境中操作委托是线程安全的? c的函数指针=>C#委托进化过程详解

文章目录 函数指针>委托使用委托组合委托为什么在多线程环境中操作委托是线程安全的调用带有返回值的委托调用带引用的委托 总结 函数指针>委托 typedef int (*Cal)(int, int); //定义函数指针 int Sum(int a, int b) {return a b; } int main() {int a 9, b 2, c …

【网络】网络的发展历程及其相关概念

1.什么是网络 计算机网络是指将一群具有独立功能的计算机通过通信设备以及传输媒体被互联起来的&#xff0c;在通信软件的支持下&#xff0c;实现计算机间资源共享、信息交换或协同工作的系统。计算机网络是计算机技术与通信技术紧密结合的产物&#xff0c;两者的迅速发展渗透形…

【数据结构】线性表(线性表的定义和基本操作)

计算机考研408-数据结构笔记本之——第二章 线性表 2.1 线性表的定义和基本操作 1 线性表的定义(数据结构三要素——逻辑结构&#xff09; 线性表是具有相同数据类型的n(n≥0)个数据元素的有限序列. 其中n为表长&#xff0c;当n0 时线性表是一个空表。 若用L命名线性表&…

Java小白入门到实战应用教程-异常处理

Java小白入门到实战应用教程-异常处理 前言 我们这一章节进入到异常处理知识点的学习。异常是指程序在运行时遇到的一种特殊情况&#xff0c;它能打断了正常的程序执行流程。 而异常处理是一项至关重要的技术&#xff0c;它使得程序能够优雅地处理运行时错误&#xff0c;避免…

【Go】通过反射解析对象tag信息,实现简易ORM

反射是运行时&#xff0c;需要在运行时解析类型信息&#xff0c;编译期无法优化这些操作&#xff0c;因此比编译时已知类型信息的直接调用效率要低。 package mainimport ("fmt""reflect""strings" )type Person struct {Name string json:&quo…

T9打卡学习笔记

&#x1f368; 本文为&#x1f517;365天深度学习训练营 中的学习记录博客&#x1f356; 原作者&#xff1a;K同学啊 import tensorflow as tfgpus tf.config.list_physical_devices("GPU")if gpus:tf.config.experimental.set_memory_growth(gpus[0], True) #设置…

慢SQL优化的30个思路方案整理

文章目录 &#xff08;1&#xff09;索引优化&#xff08;2&#xff09;查询重构&#xff08;3&#xff09;减少数据扫描量&#xff08;4&#xff09;利用缓存&#xff08;5&#xff09;分区表&#xff08;6&#xff09;优化排序和分组&#xff08;7&#xff09;业务查询条件限…

力扣面试150 基本计算器 双栈模拟

Problem: 224. 基本计算器 &#x1f468;‍&#x1f3eb; 参考题解 Code class Solution {public int calculate(String s) {// 存放所有的数字&#xff0c;用于计算LinkedList<Integer> nums new LinkedList<>();// 为了防止第一个数为负数&#xff0c;先往 nu…

创建stm32f103c8t6基本工程

创建stm32f103c8t6基本工程 (1)桌面空白处,鼠标右键新建文件夹,重命名为工程名字 (2)打开keil5 (3)点击Project-> New uvision project (4)找到我们桌面的刚才新建的文件夹,文件名 , 起自己的工程名字的,不要用空格 , 然后点击保存 (5)选择如下芯片, 然后确定 (6)然后就会弹…

linux的ceph

ceph ceph是一个开源的&#xff0c;用c语言编写的分布式的存储系统。存储文件数据。 分布式由多台物理磁盘组成一个集群&#xff0c;在这个基础之上实现高可用&#xff0c;扩展。 ceph是一个统一的存储系统&#xff0c;同时提供块设备存储&#xff0c;文件系统存储和对象存储…

C++学习笔记05-补充知识点(问题-解答自查版)

前言 以下问题以Q&A形式记录&#xff0c;基本上都是笔者在初学一轮后&#xff0c;掌握不牢或者频繁忘记的点 Q&A的形式有助于学习过程中时刻关注自己的输入与输出关系&#xff0c;也适合做查漏补缺和复盘。 本文对读者可以用作自查&#xff0c;答案在后面&#xff0…

55 华三模拟器Server2 操作

华三模拟器Server2 操作 # /etc/config/dhcp uci set dhcp.eth2dhcp uci set dhcp.eth2.interfaceeth2 uci set dhcp.eth2.start100 uci set dhcp.eth2.limit150 uci set dhcp.eth2.leasetime12h # /etc/config/network uci set network.eth2interface uci set network.eth2.pr…

可爱萌《奥咕和秘密森林》,电脑单机游戏免费分享

《奥咕和秘密森林》是一款2D冒险游戏&#xff0c;游戏中玩家将与奥咕宝宝一起探索一个奇妙的世界。这款游戏的特点包括手绘角色和多种谜题&#xff0c;玩家可以在游戏中与激萌的小动物成为朋友&#xff0c;打败异界怪物&#xff0c;揭开未知世界的秘密。 游戏特色 探索世界&am…

宁德时代社招SHL入职测评:语言理解数字推理测评及综合测评真题、高分攻略、答题技巧

宁德时代的社招入职测评主要采用SHL的Verify系统&#xff0c;测评内容包括语言理解、数字推理、逻辑推理等部分。具体来说&#xff0c;语言理解部分包括阅读理解、逻辑填空和语句排序等题型&#xff0c;要求在限定时间内完成一定数量的题目 。数字推理部分则包括数字序列、数学…

JavaScript 数组排序

JavaScript 提供了多种对数组进行排序的方法&#xff0c;其中最常见和直接的是使用数组的 .sort() 方法。.sort() 方法可以对数组的元素进行排序&#xff0c;并返回排序后的数组。然而&#xff0c;.sort() 方法默认将数组元素转换为字符串&#xff0c;并按照字符串的 Unicode 编…

【Python】数据类型之字典(上)

字典是有序、键不重复且元素只能是键值对的可变的一个容器。 data{"k1":1,"k2":25} data中“k1”和“k2”是键&#xff0c;而1,25是值。“k1”:1,"k2":25是键值对。 1&#xff09;&#xff09;容器&#xff1a;存储多个元素。 2&#xff09;…

2024年港澳台联考高校新一波录取分数线来啦

导读 在前面几次中&#xff0c;我们和大家分享了一些2024年港澳台联考高校最新的录取分数线。今天我们继续来看一批新的录取分数线吧&#xff01;景于行分享的数据基本上都是经过可靠验证的&#xff0c;大家可以放心参考。 上海大学 上海大学和深圳大学是近些年来&#xff0c;依…

haproxy的安装和服务信息

为什么要使用haproxy&#xff1f; 因为LSV无后端检测&#xff0c;当webserver有一台状态异常&#xff0c;则运作异常&#xff1b;所以用haproxy来解决。 haproxy是一款具备高并发(万级以上)、高性能的TCP和HTTP负载均衡器&#xff0c;它支持基于cookie的持久性&#xff0c;自动…

力扣-1两数之和2两数相加-2024/8/3

1、两数之和 解法一 暴力法&#xff08;2个for循环&#xff09; class Solution:def twoSum(self, nums: List[int], target: int) -> List[int]:for ii in range(len(nums)):for jj in range(ii1, len(nums)):if nums[ii]nums[jj] target:return [ii,jj]解法二 哈希表法…

具有并发功能的网页以及一点链表相关内容

最近学习内容&#xff0c;前几天做了个小项目&#xff0c;通过tcp与html构建具有并发功能的商城 具有以下功能&#xff1a; 1 登陆进入查询页面 2 搜索商品信息概述 3 查看商品详细信息 4 记录访客信息 5 注册新用户 主页如下 主页程序 程序的设计&#xff1a;将现实中大…