C++11 智能指针:优化资源管理,规避内存泄漏的利器

  C++学习笔记:

C++ 进阶之路__Zwy@的博客-优快云博客

各位于晏,亦菲们,请点赞关注!

我的个人主页:

_Zwy@-优快云博客

一、智能指针简介

1、 什么是智能指针?

在c++中智能指针是为了防止我们的程序中出现内存泄漏而设计出来的一个类模板,用于管理我们在程序中动态分配的内存,它的行为与常规的指针类似,但提供了自动内存管理的功能,能够有效避免内存泄漏、悬空指针等问题。

2、 为什么要有智能指针 ?

我们知道智能指针主要是用来管理资源,避免内存泄漏等问题的出现,要了解为什么要设计智能指针,我们首先要了解什么是内存泄漏。

<1>、什么是内存泄漏?

内存泄漏是指程序在动态分配内存后,失去了对这块内存的控制,导致这块内存无法被释放,一直占用系统内存空间的情况。在 C语言或者C++ 中,当我们使用 malloc或 new等函数动态分配内存后,如果没有使用freedelete释放资源,就可能会发生内存泄漏。如下代码就是发生了内存泄漏的程序,使用new动态申请的内存并没有释放。

void Test()
{
	int* arr = new int;
	//...
}
<2>、内存泄露的危害
int main()
{
	// 在内存中申请⼀个1G未释放,这个程序多次运⾏也不会有什么影响
	// 因为程序很快就结束,进程结束各种资源也就回收了
	char* ptr = new char[1024 * 1024 * 1024];
	cout << (void*)ptr << endl;
	return 0;
}
普通程序运行⼀会就结束了出现内存泄漏问题也不大,进程正常结束,页表的映射
关系解除,物理内存也可以释放。但是对于长期运行的程序出现内存泄漏,影响很大,如操作系统、后台服务器、长时间运行的客户端等等,不断出现内存泄漏会导致可用内存不断变少,各种功能响应越来越慢,最终卡死,导致程序崩溃,带来不可估量的损失。

二、智能指针剖析

1、c++标准库中的智能指针

c++标准库中的智能指针都在<memory>这个头文件下,主要有 auto_ptr 、unique_ptr、 shared_ptr、weak_ptr。

<1>auto_ptr

auto_ptr是 c++98 设计出来的智能指针,它的缺点是拷贝时把被拷贝对象的资源的管理权转让给拷贝对象,这是很糟糕的设计,会导致被拷贝对象悬空,造成访问报错,我们强烈建议不使用auto_ptr,在c++11之后的标准库中auto_ptr 被弃用。

class Date
{
public:
	Date()
	{
		cout << "Date()" << endl;
	}
	void print()
	{
		cout << _year << "-" << _month << "-" << _day << endl;
	}
	~Date()
	{
		cout << "~Date()" << endl;
	}
	Date(const Date& d1)
	{
		_year = d1._year;
		_month = d1._month;
		_day = d1._day;
		cout << "Date(const Date& d1)" << endl;
	}
	Date(Date&& d2)
	{
		std::swap(_year, d2._year);
		std::swap(_month, d2._month);
		std::swap(_day, d2._day);
		cout << "Date(Date&& d2)" << endl;
	}
private:
	int _year = 2024;
	int _month = 11;
	int _day = 11;
};
void Test_auto_ptr()
{
	Date* d = new Date ;
	auto_ptr<Date> p1(d);
	auto_ptr<Date> p2(p1);
}

我们用Date对象d构造了auto_ptr<Date> p1,当用p1拷贝构造p2时,p1会将所指向资源的管理权转移给p2,这时p1自己会置空,如果继续访问就会报错

<2>unique_ptr

unique_ptr 是c++11设计出来的智能指针,它的特点是不支持拷贝,也不支持赋值,如果有不需要的拷贝的场景,我们建议使用unique_ptr。

void Test_unique_ptr()
{
	Date* d = new Date;
	unique_ptr<Date> p1(d);
	//unique_ptr<Date> p2(p1); //不支持拷贝,编译报错
}

我们这里利用Date 对象d构造 unique_ptr<Date> p1,当我们利用p1拷贝构造 p2时,会编译报错,因为unique_ptr 的拷贝构造函数在底层已经被delete禁用了 ,不支持拷贝

同样赋值重载也不支持,在底层被delete禁用。

void Test_unique_ptr()
{
	string* s1 = new string("helloworld");
	unique_ptr<string> p1(s1);
	//unique_ptr<string> p2 = p1; //不支持赋值,会编译报错

}

但是unique_ptr 支持使用move 移动,调用底层的移动构造。

void Test_unique_ptr()
{
	Date* d = new Date;
	unique_ptr<Date> p1(d);
	unique_ptr<Date> p2(move(p1));
}

但是要注意的是,被move后的p1的属性是右值,调用移动构造,会将p1的资源转移,构造完p2后,p1也会置空。

<3> shared_ptr

shared_ptr 也是c++11设计的智能指针,它的特点是共享,支持拷贝,也支持移动,如果需要拷贝的场景,我们推荐使用shared_ptr。

void Test_shared_ptr()
{
	Date* d = new Date;
	shared_ptr <Date> p1(d);
	shared_ptr<Date> p2(p1);
}

本质上用p1拷贝构造p2后,p1和p2管理的是同一片资源,下面我们可以看到p1和p2所指向的 ptr

的地址是相同的,即p1,p2共同管理ptr的资源,它的底层使用引用计数实现。

2、从底层模拟实现shared_ptr

底层使用引用计数将指向资源的对象个数维护起来,当有新的shared_ptr对象指向这块资源时,底层的引用计数就会+1,有指向资源的对象销毁或者更改指向时,引用计数就会-1,直到最后一个对象销毁或者更改指向,引用计数减为0时,会自动调用shared_ptr的析构函数,释放所指向的资源,完成对资源的管理和释放。

template<class T>
class shared_ptr
{
public:
	//这里采用的是默认delete的方式释放资源
	shared_ptr(T* ptr)
		:_ptr(ptr)
		//初始化时起始的引用计数置为1
		, _pcount(new int(1)) 
	{}

	//这里的模板参数D 是用户可以自定义释放资源的方式构造del,来保证申请资源
	// 与释放资源的方式保持一致,防止出现不匹配的问题
	template<class D>
	shared_ptr(T* ptr, D del)
		: _ptr(ptr)
		, _pcount(new int(1))
		, _del(del)
	{}

	//析构时 使用用户传入的方式来释放_ptr所指向的资源,否则使用默认方式
	~shared_ptr()
	{
		if (--(*_pcount) == 0)
		{
			_del(_ptr);
			delete _pcount;
		}
	}

	
	shared_ptr(const shared_ptr& sp)
		:_ptr(sp._ptr)
		, _pcount(sp._pcount)
		, _del(sp._del)
	{
		//调用拷贝构造初始化另一个shared_ptr对象时,引用计数++
		++(*_pcount);
	}

	shared_ptr<T>& operator=(const shared_ptr& sp)
	{
		//判断是否为自身赋值,避免引用计数+1
		if (_ptr != sp._ptr)
		{
			if (--(*_pcount) == 0)
			{
				_del(_ptr);
				delete _pcount;
			}
			_ptr = sp._ptr;
			_pcount = sp._pcount;
			_del = sp._del;
		//如果不是自身赋值 ,那么引用计数也要++,因为多了一个指向资源的对象
			++(*_pcount);
		}
		return *this;
	}

	//模拟常规指针的行为  重载operator-> 返回指向资源的对象的指针
	T* operator->()
	{
		return _ptr;
	}

	//模拟常规指针的行为  重载operator* 返回指向资源的对象的引用
	T& operator*()
	{
		return *(_ptr);
	}

	//返回引用计数 即维护资源的shared_ptr的个数
	int use_count()
	{
		return *_pcount;
	}
private:
	T* _ptr;      //指向资源的对象的指针
	int* _pcount; //底层的引用计数
	              //使用function包装释放资源的成员变量 这里的缺省值默认是以delete方式释放_ptr
	function<void(T*)> _del = [](T* ptr) { delete ptr; };
};

这里我们就写了一个简易的shared_ptr,完成了对动态申请的资源的管理和释放。但是实际上c++标准库中的shared_ptr远比这个复杂的多,有兴趣的同学可以去翻看c++标准库的源码,研究一下。

3、shared_ptr的致命缺陷 — 循环引用问题

在大部分情况下,shared_ptr管理资源非常合适,支持RAII,也支持拷贝,可以完成对动态申请的资源的管理,但是在循环引用的场景下就会失效,无法正确释放资源,会导致内存泄漏,下面我们来具体认识一下循环引用,并且学会用weak_ptr来解决这个问题。

struct ListNode
{
	int _data;
	std::shared_ptr<ListNode> _next;
	std::shared_ptr<ListNode> _prev;
	~ListNode()
	{
		cout << "~ListNode()" << endl;
	}
};
void Test_shared_ptr_c()
{
	// 循环引⽤ -- 内存泄露
	std::shared_ptr<ListNode> n1(new ListNode);
	std::shared_ptr<ListNode> n2(new ListNode);
	cout << n1.use_count() << endl;
	cout << n2.use_count() << endl;
	n1->_next = n2;
	n2->_prev = n1;
	cout << n1.use_count() << endl;
	cout << n2.use_count() << endl;
}

如上代码中,我们定义了一个ListNode 的类,成员变量包括int _data,shared_ptr<ListNode>对象_next和_prev。而在下面的函数中,分别new两个ListNode 对象来构造 n1 和 n2,此时引用计数都为1,当我们将n1的_next指向n2,n2的_prev指向n1,那么引用计数都为 2,就会造成循环引用,下面我们具体分析循环引用。

看图我们可以得到,shared_ptr n1和shared_ptr n2生命周期结束析构时,引用计数都-1,但是此时引用计数都从2减为1,并不为0,并没有释放资源,因为还有左边节点中_next和右边节点中的_prev互相指向,此时左边节点要释放就要先使右边节点中_prev析构,引用计数减为0,那么右边节点中_prev要析构就要使n2节点被释放n2节点要释放就要让左边节点中的_next析构,引用计数减为0,左边节点中_next要析构就要使n1节点被释放此时又回到n1节点要释放的问题,至此构成循环引用,两个节点互相依赖,无法释放,造成内存泄漏

4、使用weak_ptr解决循环引用问题

<1>、weak_ptr 简介

weak_ptr不⽀持RAII,也不支持访问资源,所以我们看⽂档发现weak_ptr构造时不⽀持绑定到资 源,只支持绑定到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访问资源是安全的。

<2>weak_ptr 的简单模拟实现

注意::本质上weak_ptr是shared_ptr所管理对象的一种弱引用,我们这里只是实现了一个极为简单的weak_ptr ,底层的具体细节还请参考标准库。

template<class T>
class weak_ptr
{
public:
	weak_ptr()
	{}
	//weak_ptr 不支持直接指向资源,但是可以绑定在shared_ptr上面
	weak_ptr(const shared_ptr<T>& sp)
		:_ptr(sp.get())//shared_ptr 的get函数返回指向资源的对象的指针
	{}

	//weak_ptr也支持用shared_ptr 赋值
	weak_ptr<T>& operator=(const shared_ptr<T>& sp)
	{
		_ptr = sp.get();
		return *this;
	}
private:
		T* _ptr = nullptr; //底层没有引用计数,不管理资源
};
<3> weak_ptr的使用

struct ListNode
{
	int _data;
    //weak_ptr 不增加引用计数 
	std::weak_ptr<ListNode> _next;
	std::weak_ptr<ListNode> _prev;
	~ListNode()
	{
		cout << "~ListNode()" << endl;
	}
};
void Test_shared_ptr_c()
{
	
	std::shared_ptr<ListNode> n1(new ListNode);
	std::shared_ptr<ListNode> n2(new ListNode);
     //n1和n2 引用计数都为1
	cout << n1.use_count() << endl;
	cout << n2.use_count() << endl;
    //互相指向后 引用计数还是为1 
	n1->_next = n2;
	n2->_prev = n1;
	cout << n1.use_count() << endl;
	cout << n2.use_count() << endl;
}
int main()
{
	Test_shared_ptr_c();
}

我们可以看到绑定前后 n1 和 n 2的引用计数都为1,程序结束后n1和n2管理的资源也正常释放,ListNode的析构函数 调用两次,n1和n2析构后,节点中的weak_ptr会检查所绑定shared_ptr的引用计数,如果为0,会自动调用析构函数释放。到这里循环引用就被解决了。

5、shared_ptr的线程安全问题

由于作者还没有对线程有过系统的学习,这里只是简单的分析下shared_ptr的线程安全,后面追更一篇文章具体来谈有关线程安全的问题!

<1>引用计数的线程安全

原子操作保证:shared_ptr的引用计数通常是原子操作,这意味着在多个线程同时对shared_ptr进行拷贝构造、赋值、析构等操作时,引用计数的增减是线程安全的。例如,当一个线程将shared_ptr赋值给另一个shared_ptr时,引用计数会自动增加,而这个增加操作是原子的,不会出现多个线程同时操作导致计数错误的情况。

<2> 对象的线程安全

对象本身非线程安全shared_ptr所管理的对象本身的线程安全性与shared_ptr无关。如果多个线程同时访问和修改shared_ptr所指向的对象,而该对象本身不是线程安全的,就可能导致数据竞争和未定义行为。例如,如果一个shared_ptr指向一个非线程安全的std::vector,多个线程同时对该vector进行插入和删除操作,就会导致数据不一致和程序崩溃

解决方案为了保证对象的线程安全,可以使用互斥锁、读写锁等同步机制来保护对象的访问。例如,可以在访问shared_ptr所指向的对象之前先获取一个互斥锁,确保同一时间只有一个线程能够访问该对象,从而避免数据竞争

<3>、shared_ptr线程安全的总结

shared_ptr在多线程环境下的引用计数操作是线程安全的,但所指向对象的线程安全性需要由程序员自己来保证。在使用shared_ptr时,需要注意对象的访问和修改是否在多个线程中同时进行,以及在析构对象时是否存在资源竞争等问题.

三、C++11和Boost中智能指针的关系

1、Boost概述

Boost 库由 C++ 标准委员会库工作组成员发起,其中许多人是 C++ 标准库的开发者。其目标是作为标准库的后备,提供那些未被纳入标准库的有用功能,同时也是对 C++ 标准库的补充和扩展,使 C++ 开发者能够更加高效地编写高质量的代码。c++11以及之后的很多新语法都是从boost库中来的,例如智能指针,unordered_map,以及c++的多线程编程。

2、Boost的贡献

智能指针:如boost::shared_ptrboost::weak_ptrboost::scoped_ptr等,提供了安全、高效的动态内存管理机制,通过引用计数等方式自动管理对象的生命周期,避免了内存泄漏和悬空指针等问题。

容器类:除了 C++ 标准库中的容器,Boost 还提供了一些更高级的容器,如boost::arrayboost::unordered_map等,为开发者提供了更多的数据结构选择,以满足不同的应用需求。

多线程编程:boost::thread库为 C++ 开发者提供了跨平台的多线程编程支持,包括线程的创建、同步、互斥等功能,使得开发者能够更方便地编写多线程应用程序,充分利用多核处理器的性能优势。

函数对象和高阶函数:boost::functionboost::bind等库允许开发者将函数作为一等公民进行处理,实现函数的封装、组合和延迟调用,提高了代码的灵活性和可复用性。

数值计算:Boost 提供了丰富的数值计算库,如boost::math包含了各种数学函数和特殊函数的实现,boost::random用于生成随机数,满足了不同领域的数值计算需求。

文件系统操作:boost::filesystem库提供了一个可移植的文件系统操作接口,使得开发者能够方便地进行文件和目录的创建、删除、遍历等操作,提高了文件系统相关操作的效率和可移植性

总的来说,boost为c++的发展以及提供了许多有用的特性和工具,帮助代码编写者极大的提高了效率。

四、结语

感谢各位大佬的观看,创作不易,欢迎各位大佬在评论区指点交流,日后还会更新其他内容,麻烦各位大佬点赞关注支持下!!!

评论 54
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

一整颗红豆

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值