C++智能指针 —— 朝花与未来邂逅,昔日的誓言永不忘却

"无论什么,最终都会消逝,不同的,只有早晚罢了。"

目录

1 智能指针的使用场景分析

2 RAII和智能指针的设计思路

3 C++标准库智能指针的使用

4 智能指针的原理

5 shared_ptr和weak_ptr

  5.1 shared_ptr的循环引用问题

  5.2 weak_ptr

6 C++11和boost中智能指针的关系

7 内存泄漏

  7.1 什么是内存泄漏?内存泄漏的危害?

  7.2 如何避免内存泄漏?


1 智能指针的使用场景分析

double Divide(int a, int b)
{
	int* array1 = new int[10]{0};
	try
	{
		if (b == 0)
		{
			throw("Divide by zero condition!");
		}
		else
		{
			delete[] array1;
			return a / b;
		}
	}
	catch (...)
	{
		delete[] array1;
		throw;
	}
}//通过上述的Divide这个代码我们可以知道,如果发生除0的错误的话就会抛出异常,我们在抛出异常之前,编译器它在这里会先创建一个10个int类型的数组空间,由于我们这里在抛出异常之前开创的资源,因此在抛出之后我们必须要在Divide这个栈中就直接将刚刚所抛出的那个异常给捕获下来,先将array1这块资源释放之后,再将刚刚捕获的那个异常重新抛出即可。

       通过上面的程序中我们可以看到,new了以后,而且也delete了(上述代码中的第12行代码),但是因为抛异常有点早,程序执行不到delete的位置,所以就导致内存泄露了(这里是假设那个异常没有在Divide栈中被捕获),为了防止这种现象的发生,所以我们需要在new以后捕获异常,捕获到异常后释放资源,再把异常重新抛出,但是如果我们在写代码的过程中忘了释放资源的话该怎么办呢?......,再加上上述的过程处理起来也比较麻烦。智能指针放到这样的场景里面的话就会让这个问题变得简单多了。

2 RAII和智能指针的设计思路

       1>.RAII是Resource Acquisition Is Initialization的缩写,他是一种管理资源的类的设计思想,本质上是一种利用类类型的对象生命周期来管理获取到的动态资源,避免资源泄露,这里的资源可以是内存、文件指针、网络连接、互斥锁等等。RAII在获取资源时把资源委托给一个类类型的对象,接着控制对资源的访问,资源在类类型的对象的生命周期内始终保持有效,最后在类类型的对象析构的时候释放资源,这样保障了资源的正常释放,避免资源泄露的问题。

       2>.智能指针类除了满足RAII的设计思路,还要方便资源的访问,所以智能指针类还会像迭代器类一样,重载operator* / operator-> / operator[ ]等运算符,方便访问资源。智能指针访问资源的方式和指针访问资源的方式是基本相同的。

3 C++标准库智能指针的使用

       1>.C++标准库中的智能指针都在<memory>这个头文件下面,我们包含<memory>这个头文件之后就可以使用了,智能指针有好几种,除了weak_ptr以外其余的智能指针它们都符合RAII和像指针一样访问的行为,原理上而言主要是解决智能指针拷贝时的思路不同。

       2>.auto_ptr是C++98时设计出来的智能指针,他的特点是拷贝时把被拷贝对象掌管的那个资源的管理权转移给拷贝对象,这是一个非常糟糕的设计,因为它会使被拷贝对象处于悬空状态,有访问报错的风险,C++11设计出新的智能指针后,强烈建议不要使用auto_ptr。其他的智能指针出来之前很多公司也是明令禁止使用这个智能指针的。

       3>.unique_ptr是C++11设计出来的智能指针,他的名字翻译出来叫唯一指针,他的特点是不支持拷贝,只支持移动。如果不需要拷贝的场景我们这里就非常建议使用它。

       4>.shared_ptr是C++11设计出来的智能指针,他的名字我们翻译过来之后是共享指针,他的特点是支持拷贝,也支持移动。如果有需要拷贝的场景就非常建议使用它。

       5>.weak_ptr是C++11设计出来的智能指针,它的名字翻译出来是弱指针,它完全不同于上面的智能指针,它不支持RAII,也就意味着不能用它直接管理资源,weak_ptr的产生本质是要解决share_ptr的一个循环引用导致内存泄漏的问题。具体的细节我们后面会细讲。

struct Date
{
	int _year;
	int _month;
	int _day;
	Date(int year = 1, int month = 1, int day = 1)
		:_year(year)
		,_month(month)
		,_day(day)
	{ }
};
int main()
{
	//auto_ptr这个智能指针通常是不会有人使用的,而且也不常见,因此我们这里就不花费多余的时间去讲解它的使用了。
	unique_ptr<Date> up1(new Date(2024, 1, 1));//通过我们前面所学的知识可知,类型为unique_ptr类型的指针不支持拷贝,只支持移动。
	//unique_ptr<Date> up2(up1);//不支持拷贝。
	unique_ptr<Date> up3(move(up1));//支持移动,但是我们这里需要注意一下,就是移动之后up1实际上也是悬空了,所以这里使用移动需要谨慎一点。
	shared_ptr<Date> sp1(new Date(2025, 1, 1));
	shared_ptr<Date> sp2(sp1);//支持拷贝。
	cout << sp1->_year << " " << sp1->_month << " " << sp1->_day << endl;//2025 1 1;
	cout << sp2->_year << " " << sp2->_month << " " << sp2->_day << endl;//2025 1 1;
	//通过编译结果来看的话,这里确实是支持拷贝的。
	sp2->_day++;
	sp1->_month++;
	cout << sp1->_year << " " << sp1->_month << " " << sp1->_day << endl;//2025 2 2;
	cout << sp2->_year << " " << sp2->_month << " " << sp2->_day << endl;//2025 1 1;
	//sp1和sp2共同享有同一块资源空间。
	sp1 = move(sp2);//支持移动,但是移动后sp2也悬空了,使用时谨慎。
	return 0;
}

       6>.智能指针析构时默认是进⾏delete释放资源,这也就意味着如果不是new出来的资源,交给智能指针管理,析构时就会崩溃。智能指针⽀持在构造时给⼀个删除器,所谓删除器本质就是⼀个可调⽤对象,这个可调⽤对象中实现你想要的释放资源的⽅式,当构造智能指针时,给了定制的删除器,在智能指针析构时就会调⽤删除器去释放资源。因为new[]经常使⽤,所以为了简洁⼀点,unique_ptr和shared_ptr都特化了⼀份[]的版本,使⽤时 unique_ptr<Date[]> up1(new Date[5]);shared_ptr<Date[]> sp1(new Date[5]); 就可以管理new []的资源。

       7>.template <class T, class... Args> shared_ptr<T> make_shared (Args&&... args);
       8>.shared_ptr 除了⽀持⽤指向资源的指针构造,还⽀持 make_shared ⽤初始化资源对象的值
直接构造。
       9>.shared_ptr 和 unique_ptr 都⽀持了operator bool的类型转换,如果智能指针对象是⼀个空对象没有管理资源,则返回false,否则返回true,意味着我们可以直接把智能指针对象给if判断是否为空。
       10>.shared_ptr 和 unique_ptr 都得构造函数都使⽤explicit 修饰,防⽌普通指针隐式类型转换
成智能指针对象。

template<class T>
void DeleteArrayFunc(T* ptr)
{
	delete[] ptr;
}//接下来我们要去讲解关于删除器的一些使用方法的知识,通过前面的学习我们可知删除器本质上就是一个可调用对象,因此我们这里写一个函数指针,接下来就通过函数指针去做讲解,仿函数当然也可以作为函数其去使用,由于某种原因,这里用一个函数指针就可以了。
int main()
{
	//unique_ptr<Date> up1(new Date[5]);
	//shared_ptr<Date> up1(new Date[5]);//这样写编译器就崩溃了。
	//特化一个[]版本,只要传过去的是T[]类型的变量,编译器就会在这里走特化出来的[]版本。
	unique_ptr<Date[]> up2(new Date[5]);
	shared_ptr<Date[]> up1(new Date[5]);//这样写就不会崩溃了,释放资源时用delete[]去释放。
	//以上的处理方式是其中的一种,但是我们大家在工作中更常用的是用删除器去解决,在讲解删除器的使用方法之前我们需要先来了解一些知识:
	//unique_ptr和shared_ptr支持删除器的方式有所不同,unique_ptr是在类模板参数中支持的,shared_ptr是在构造函数参数中所支持的,由于不同的支持方式就会导致使用方法不同。
	//函数指针做删除器
	unique_ptr<Date, void(*)(Date*)> up2(new Date[5], DeleteArrayFunc<Date>);
	//unique_ptr是在类模板的参数中支持的,因此就要求我们这里必须在参数列表中就将删除器的类型传过去。
	shared_ptr<Date> up2(new Date[5], DeleteArrayFunc<Date>);
	//shared_ptr是在构造函数模板参数中支持的,因此我们可以在构造对象时利用构造函数参数将删除器给传过去。
	//这样的话,就又会产生一个问题,unique_ptr传删除器时,必须要将删除器的类型传过去,而shared_ptr则不需要传类型过去,在构造函数中将删除器类型的对象传过去即可,编译器在这里会自动地推导出删除器地类型。
	//lambda表达式做删除器
	auto delArr = [](Date* ptr) {delete[] ptr; };//定义一个lambda表达式delArr。
	unique_ptr<Date, decltype(delArr)> up3(new Date[5], delArr);//通过我们前面的讲解可以得知unique_ptr要想去使用我们自己传过去的删除器的话,就必须在类模板的参数列表中将删除器的类型传过去,可是对于lambda表达式来说,我们是无法得到它的类型的,这里就选哟用到一个函数decltype,它可以帮助我们去得到一个变量的具体类型。
	shared_ptr<Date> up3(new Date[5], delArr);//对于shared_ptr而言,并没有unique_ptr那么麻烦,只用将对象传过去就可以了。这样的话,就又诞生了一个问题,就是删除器是在析构函数中用的,但是我们这里是在构造函数中将其传过去的,若想让其跨函数使用,就必须让删除器变成创业变量才可以,至于如何成为成员变量,我们在后续的使用中会讲解到,这里就先不讲了。
	shared_ptr<Date> sp4 = make_shared<Date>(2025, 2, 28);//我们前面在构造shared_ptr类类型的对象时,用的是一个指针构造,make_shared支持用初始化对象的值直接构造出来。
	if (sp4)//if(sp4.operator bool())
	{
		cout << "not nullptr" << endl;
	}
	if (!sp4)//if(!sp4.operator bool())
	{
		cout << "is nullptr" << endl;
	}
	//shared_ptr<Date> sp5=new Date(20025,2,28);
	//unique_ptr<Date> sp5=new Date(20025,3,1);//编译器会报错,不支持普通指针隐式类型转换成智能指针。
	return 0;
}

4 智能指针的原理

       1>.auto_ptr和unique_ptr这两个智能指针的实现⽐较简单,⼤家了解⼀下原理即可。auto_ptr的思路是拷⻉时转移资源管理权给被拷⻉对象,这种思路是不被认可的,也不建议使⽤unique_ptr的思路是不⽀持拷⻉。
       2>.⼤家重点要看看shared_ptr是如何设计的,尤其是引⽤计数的设计,主要这⾥⼀份资源就需要⼀个引⽤计数,所以引⽤计数才⽤静态成员的⽅式是⽆法实现的,要使⽤堆上动态开辟的方式,构造智能指针对象时来⼀份资源,就要new⼀个引⽤计数出来。多个shared_ptr指向资源时就++引⽤计数,shared_ptr对象析构时就--引⽤计数,引⽤计数减到0时代表当前析构的shared_ptr是最后⼀个管理资源的对象,则析构资源。

//接下来,我们大家一起来看一看shared_ptr的模拟实现:
template<class T>
class shared_ptr
{
public:
	explicit shared_ptr(T* ptr=nullptr)
		:_ptr(ptr)
		,_pcount(new int(1))
	{ }
	template<class D>//有删除器的构造函数版本
	shared_ptr(T* ptr, D del)
		:_ptr(ptr)
		, _pcount(new int(1))
		, _del(del)
	{ }
	shared_ptr(const shared_ptr<T>& sp)//shared_ptr支持拷贝
		:_ptr(sp._ptr)
		, _pcount(sp._pcount)
		, _del(_del)
	{
		(*_pcount)++;//这里是浅拷贝操作,说明这块资源又有一个shared_ptr类类型指向了,因此引用计数要++。
	}
	~shared_ptr()//析构函数
	{
		if (--(*_pcount) == 0)//既然是析构,那么就说明指向这块资源的shared_ptr类类型的对象少了一个,因此--是必不可少的,不仅如此,因为--之后为0,就说明--之前,指向这块资源就只剩一个shared_ptr类类型的对象了。
		{
			delete _pcount;
			_del(ptr);//_del是删除器。
		}
	}
	shared_ptr<T>& operator=(const shared_ptr<T>& sp)
	{
		//我们这里先说一下赋值的基本思路:赋值是两个已经存在的shared_ptr类类型的对象之间的操作,(我们这里默认进行赋值操作的两个对象分别都有自己所指向的那份资源,也就是不存在赋值的双方有一方为空的情况),既然是赋值,那么首先我们的那个被赋值的shared_ptr类类型的对象所指向的那块资源就要少一个shared_ptr类类型对象的指向,让被赋值的那个对象重新指向赋值的资源。不仅如此,我们这里还需要去预防一下自己给自己赋值的情况,不仅是为了节省时间,增加效率,还要防止被赋值的那个对象在被赋值之前所指向的那个资源所对应的引用计数为1,若按照我们的思路去走的话,先--导致被赋值对象所指向资源释放了。由于是自己给自己赋值,因此赋值的那个对象所指向的资源也被释放掉了,这样会达不到我们想要的效果。
		if (_ptr != sp._ptr)//判断是否是自己给自己赋值
		{
			if (--(*_pcount == 0))//被赋值的那个对象在被赋值之前所指向的那块资源需相对应地"析构"一下。
			{
				_del(ptr);
				delete _pcount;
			}
			//开始赋值
			_pcount = sp._pcount;
			_ptr = sp._ptr;
			_del = sp._del;
			++(*_pcount);
		}
		return *this;
	}
	//...其余一些成员函数。
private:
	int* _pcount;
	T* ptr;
	//通过我们前面的解析,我们可以得知shared_ptr的内部不止有以上两个成员变量,应该还要有一个成员变量,而这个成员变量就是删除器,通过我们上述的解析可知,删除器是在第二个构造函数中才会有的类型为D,这样的话删除器就只能在第二个构造函数中使用,并且我们无法直接在这里用D定义一个删除器成员变量,但是这样的话我们也可以使用包装器去定义一个删除器成员变量。
	function<void(T*)> _del = [](T* ptr) {delete ptr; };//删除器的返回值为空(void),删除器的参数是一个指向资源的指针,给一个缺省值是为了防止我们在不传删除器的情况下使用删除器。
};//我们这里实现的shared_ptr都是以十分简洁的方式去实现的,只能满足一些最为基本的功能,源码中的实现往往要复杂的多的多的多。
//由于我们后面会用到weak_ptr,因此我们这里也来简单模拟实现一下weak_ptr,在模拟实现之前我们先来简单了解一下,之所以要实现weak_ptr,其实是为了解决shared_ptr中所存在的一个问题(后面会讲到),它在指向一个资源时不会使该资源所对应的那个引用计数增加,也就是相当于它只负责资源的管理。
template<class T>
class weak_ptr
{
public:
	weak_ptr()
	{ }
	weak_ptr(const shared_ptr<T>& sp)
		:_ptr(sp.get())//get()函数是获得sp所指向那块资源地地址。
	{ }
	weak_ptr<T>& operator=(const shared_ptr<T>& sp)
	{
		_ptr = sp.get();
		return *this;
	}
private:
	T* _ptr = nullptr;
};//这只是我们的简化版本,真正的比上述要复杂的多。

5 shared_ptr和weak_ptr

  5.1 shared_ptr的循环引用问题

       1>.shared_ptr⼤多数情况下管理资源⾮常合适,⽀持RAII,也⽀持拷⻉。但是在循环引⽤的场景下会导致资源没得到释放内存泄漏,所以我们要认识循环引⽤的场景和资源没释放的原因,并且学会使⽤weak_ptr解决这种问题。

       2>.如下图所述的场景:

struct listNode
{
    int _data;
    shared_ptr<listNode> _next;
    shared_ptr<listNode> _prev;
    //...
};

         1). 右边的节点什么时候释放呢,左边节点中的_next管着呢,_next析构后,右边的节点就释放了。
         2). _next什么时候析构呢,_next是左边节点的的成员,左边节点释放,_next就析构了。

         3). 左边节点什么时候释放呢,左边节点由右边节点中的_prev管着呢,_prev析构后,左边的节点就释放了。
         4). _prev什么时候析构呢,_prev是右边节点的成员,右边节点释放,_prev就析构了。

       3>.⾄此逻辑上成功形成回旋镖似的循环引⽤,谁都不会释放就形成了循环引⽤,导致内存泄漏。
       4>.把ListNode结构体中的_next和_prev改成weak_ptr,weak_ptr绑定到shared_ptr时不会增加它的引⽤计数,_next和_prev不参与资源释放管理逻辑,就成功打破了循环引⽤,解决了这⾥的问题。

  5.2 weak_ptr

       1>.weak_ptr不⽀持RAII,也不⽀持访问资源,所以我们看⽂档发现weak_ptr构造时不⽀持绑定到资源,只⽀持绑定到shared_ptr,绑定到shared_ptr时,不增加shared_ptr的引⽤计数,那么就可以解决上述的循环引⽤问题。
       2>.weak_ptr也没有重载operator*和operator->等,因为他不参与资源管理,那么如果他绑定的shared_ptr已经释放了资源,那么他去访问资源就是很危险的。weak_ptr⽀持expired检查指向的
资源是否过期,use_count也可获取shared_ptr的引⽤计数,weak_ptr想访问资源时,可以调用lock返回⼀个管理资源的shared_ptr,如果资源已经被释放,返回的shared_ptr是⼀个空对象,如
果资源没有释放,则通过返回的shared_ptr访问资源是安全的。

int main()
{
    shared_ptr<string> sp1(new string("111111"));
    weak_ptr<string> wp1=sp1;
    shared_ptr<string> sp2(sp1);
    cout<<wp1.expired()<<endl;
    cout<<wp1.use_count<<endl;//2
    sp1=make_shared_ptr<string>("222222");
    sp2=make_shared_ptr<string>("333333");
    cout<<wp1.expired()<<endl;
    cout<<wp1.use_count<<endl;//0
    auto sp3=wp1.lock();
    cout<<wp1.use_count()<<endl;//1
    return 0;
}

6 C++11和boost中智能指针的关系

       1>.Boost库是为C++语⾔标准库提供扩展的⼀些C++程序库的总称,Boost社区建⽴的初衷之⼀就是为C++的标准化⼯作提供可供参考的实现,Boost社区的发起⼈Dawes本⼈就是C++标准委员会的成员之⼀。在Boost库的开发中,Boost社区也在这个⽅向上取得了丰硕的成果,C++11及之后的新语法和库有很多都是从Boost中来的。
       2>.C++ 98 中产⽣了第⼀个智能指针auto_ptr。
       3>.C++ boost给出了更实⽤的scoped_ptr/scoped_array和shared_ptr/shared_array和weak_ptr等。
       4>.C++ TR1,引⼊了shared_ptr等,不过注意的是TR1并不是标准版。
       5>.C++ 11,引⼊了unique_ptr和shared_ptr和weak_ptr。需要注意的是unique_ptr对应boost的scoped_ptr。并且这些智能指针的实现原理是参考boost中的实现的。

7 内存泄漏

  7.1 什么是内存泄漏?内存泄漏的危害?

       1>.什么是内存泄漏:内存泄漏指因为疏忽或错误造成程序未能释放已经不再使⽤的内存,⼀般是忘记释放或者发⽣异常释放程序未能执⾏导致的。内存泄漏并不是指内存在物理上的消失,⽽是应⽤程序分配某段内存后,因为设计错误,失去了对该段内存的控制,因⽽造成了内存的浪费。
       2>.内存泄漏的危害:普通程序运⾏⼀会就结束了出现内存泄漏问题也不⼤,进程正常结束,⻚表的映射关系解除,物理内存也可以释放。⻓期运⾏的程序出现内存泄漏,影响很⼤,如操作系统、后台服务、⻓时间运⾏的客⼾端等等,不断出现内存泄漏会导致可⽤内存不断变少,各种功能响应越来越慢,最终卡死。

  7.2 如何避免内存泄漏?

       1>.⼯程前期良好的设计规范,养成良好的编码规范,申请的内存空间记着匹配的去释放。ps:这个理想状态。但是如果碰上异常时,就算注意释放了,还是可能会出问题。需要下⼀条智能指针来管理才有保证。
       2>.尽量使⽤智能指针来管理资源,如果⾃⼰场景⽐较特殊,采⽤RAII思想⾃⼰造个轮⼦管理。
       3>.定期使⽤内存泄漏⼯具检测,尤其是每次项⽬快上线前,不过有些⼯具不够靠谱,或者是收费。
       4>.总结⼀下:内存泄漏⾮常常⻅,解决⽅案分为两种:1、事前预防型。如智能指针等。2、事后查错型。如泄漏检测⼯具。

       OK,今天我们就先讲到这里了,那么,我们下一篇再见,谢谢大家的支持!

评论 178
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值