【C++11】智能指针

1.为什么需要智能指针

cpp 复制代码
int div()
{
	int a, b;
	cin >> a >> b;
	if (b == 0)
		throw invalid_argument("除0错误");

	return a / b;
}
void f()
{
	pair<string, string>* p1 = new pair<string, string>;
	pair<string, string>* p2 = new pair<string, string>;
	//pair<string, string>* p3 = new pair<string, string>;
	//pair<string, string>* p4 = new pair<string, string>;

	div();
	/*try //这里可以采取捕获异常后重新抛出
	{     //继续执行因抛异常而跳过未执行的
		div();//delete语句,简单几个还能接受
	}//万一有多个资源需要delete呢?很麻烦
	catch (...)
	{
		delete p1;
		cout << "delete:" << p1 << endl;
		throw;
	}*/

	delete p1;
	delete p2;
}

int main()
{
	try
	{
		f();
	}
	catch (const exception& e)
	{
		cout << e.what() << endl;
	}

	return 0;
}

该代码存在内存泄漏问题,若在指针p1,p1创建之后抛出异常,会直接跳到main函数中执行catch语句,那么p1和p2就没有释放。在注释掉的代码中可以通过重新抛出异常解决该问题,但涉及到要释放的资源一多,就很麻烦。需要智能指针来解决。

注意:在C语言阶段学习时只要不忘记释放就行,但在C++中异常出现后情况就变复杂了,因为异常会改变程序执行流

2.智能指针的使用及原理

RAll

RAII(Resource Acquisition Is Initialization)是一种利用对象生命周期来控制程序资源(如内存、文件句柄、网络连接、互斥量等等)的简单技术。

在对象构造时获取资源 ,接着控制对资源的访问使之在对象的生命周期内始终保持有效,最后在对象析构的时候释放资源 。借此,我们实际上把管理一份资源的责任托管给了一个对象。这种做法有两大好处:

1.不需要显式地释放资源。

2.采用这种方式,对象所需的资源在其生命期内始终保持有效

智能指针原理:

1.RAll特性 2.重载operator*和opertaor->,具有像指针一样的行为。

cpp 复制代码
template<class T>
class SmartPtr
{
public:
	SmartPtr(T*ptr)
		:_ptr(ptr)
	{}

	~SmartPtr()
	{
		cout << "delete:" << _ptr << endl;
		delete _ptr;
	}

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

	T* operator->()
	{
		return _ptr;
	}
private:
	T* _ptr;
};

void f()
{
	SmartPtr<pair<string, string>> sp1(new pair<string, string>("1111", "22222"));
	SmartPtr<pair<string, string>> sp2(new pair<string, string>);
	SmartPtr<pair<string, string>> sp3(new pair<string, string>);
	SmartPtr<string> sp4(new string("xxxxx"));

	cout << *sp4 << endl;
	cout << sp1->first << endl;
	cout << sp1->second << endl;

	div();
}

int main()
{
	try
	{
		f();
	}
	catch(const exception& e)
	{
		cout << e.what() << endl;
	}
	return 0;
}

定义一个SmartPtr智能指针模板类,定义一个模板参数,使其可以构造任何类型指针,实现构造,析构与重载函数,实现用对象的生命周期来控制资源。

回顾一下有关异常的知识(结合上述代码):

what()是std::exception类的一个成员函数,返回一个描述异常的字符串。e.what()返回了异常的具体信息

catch块捕获所有派生自std::exception的异常类型cons tstd::exception& e表示捕获的异常对象是一个常量引用,避免了不必要的拷贝,并且保证了异常对象不会被修改。

对比catch(...):
捕获所有类型的异常 ,包括标准异常、自定义异常和基本数据类型。适用于无法确定具体异常类型的情况,或者需要对所有异常进行统一处理。确保程序不会因为未捕获的异常而崩溃,但缺乏类型信息,调试友好性较差。

在实际开发中,建议优先使用catch(const exception& e),并在必要时使用catch(...)作为最后的兜底处理。

历史来源

1.C++ 98 中产生了第一个智能指针auto_ptr

  1. C++ boost给出了更实用的scoped_ptr和shared_ptr和weak_ptr

  2. C++ 11,引入了unique_ptr和shared_ptr和weak_ptr。需要注意的是unique_ptr对应boost的scoped_ptr。并且这些智能指针的实现原理是参考boost中的实现的。

boost是一个可移植,提供源代码的C++库,是由C++标准委员会发起的提供扩展的库,是"准"标准库

std::auto_ptr

C++98版本的库中提供了auto_ptr的智能指针。支持RAll原则和像指针一样操作,但在拷贝和赋值的操作中都会转移管理权。

  • 模拟实现
cpp 复制代码
template<class T>
	class auto_ptr
	{
	public:
		auto_ptr(T* ptr)
			:_ptr(ptr)
		{}

		~auto_ptr()
		{
			cout << "delete:" << _ptr << endl;
			delete _ptr;
		}

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

		T* operator->()
		{
			return _ptr;
		}
		//拷贝, 管理权转移
		auto_ptr( auto_ptr<T>& ap)
			:_ptr(ap._ptr)
		{
			ap._ptr = nullptr;
		}

		//赋值, 管理权转移
		auto_ptr<T>& operator=(auto_ptr<T>& ap)
		{
			//检查是否自己给自己赋值
			if (this != &ap)
			{
				//将目标对象原有资源清理
				if(_ptr)
				delete _ptr;
				//赋值
				_ptr = ap._ptr;
				//将原对象置空,管理权转移
				ap._ptr = nullptr;
			}
			return *this;
		}
	private:
		T* _ptr;
	};

class A
{
public:
	A(int a=0)
		:_a(a)
	{
		cout << "A(int a=0) " << endl;
	}

	~A()
	{
		cout << this;
		cout << "~A() " << endl;
	}
//private:
	int _a;
};

auto智能指针的拷贝和赋值都会发生管理权转移,将源对象的指针置空,把所有权交给目标。但这样形成了悬空指针,等作用域结束后再统一调用析构销毁。

std::unique_ptr

简单粗暴的防拷贝,直接禁用

  • 模拟实现
cpp 复制代码
template<class T>
	class unique_ptr
	{
	public:
		unique_ptr(T* ptr)
			:_ptr(ptr)
		{}

		~unique_ptr()
		{
			cout << "delete:" << _ptr << endl;
			delete _ptr;
		}

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

		T* operator->()
		{
			return _ptr;
		}
		//拷贝, 禁用
		unique_ptr(unique_ptr<T>& ap) = delete;
		//赋值, 禁用
		unique_ptr<T>& operator=(unique_ptr<T>& ap) = delete;
		
	private:
		T* _ptr;
	};

C++98可以只声明(私有)不定义来阻止某个函数的实现,C++11直接用delete

std::shared_ptr

shared_ptr的原理:

通过引用计数的方式来实现多个shared_ptr对象之间共享资源

  1. 在对象被销毁时(也就是析构函数调用),就说明自己不使用该资源了,对象的引用计数减一。
  2. 如果引用计数是0,就说明自己是最后一个使用该资源的对象,必须释放该资源;
  3. 如果不是0,就说明除了自己还有其他对象在使用该份资源,不能释放该资源,否则其他对象就成野指针了。

注意:

1.不能创建一个普通计数起变量,这样每个变量都会生成一个计数器;

2.不能创建一个静态变量计数器,这样全局就只有一个计数器

我们目标是每个资源拥有一个独立的计数器

赋值中释放了资源,接下来的访问和赋值为什么没有报错?

编译器对越界的检查无法做到完全准确,只能对一些标志位做简单的抽查,有些地方检查不到,所以程序有问题不一定报错,再进一步访问可能报错或展现错误值

  • 模拟实现
cpp 复制代码
template<class T>
	class shared_ptr
	{
	public:
		shared_ptr(T* ptr=nullptr)
			:_ptr(ptr)
			,_pcount(new int(1))
		{}

		~shared_ptr()
		{
			if (--(*_pcount) == 0)
			{
				cout << "delete:" << _ptr << endl;
				delete _ptr;
				delete _pcount;
			}
		}

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

		T* operator->()
		{
			return _ptr;
		}
		//拷贝
		shared_ptr(const shared_ptr<T>& sp)
			:_ptr(sp._ptr)
			,_pcount(sp._pcount)
		{
			(*_pcount)++;
		}
		//赋值
		shared_ptr<T>& operator=(const shared_ptr<T>& sp)
		{
			//检查是否自我赋值和同一资源间的赋值
			if (_ptr == sp._ptr)
			{
				return *this;
			}
			//引用计数为0时调用析构
			if (--(*_pcount) == 0)
			{
			//释放目标对象
			delete _ptr;
			delete _pcount;

			}
			//进行赋值
			_ptr = sp._ptr;
			_pcount = sp._pcount;
			++(*_pcount);
			return *this;
		}


		int use_count()const
		{
			return *_pcount;
		}

		T* get()const
		{
			return _ptr;
		}

	private:
		T* _ptr;
		int* _pcount;
	};

//创建一个自定义类,方便观察资源共享的关系
struct Node
{
	A _val;
	//Node* prev;这样定义指针在改变链接
	//Node* next;时会造成类型不匹配问题
	ee::shared_ptr<Node>_prev;
	ee::shared_ptr<Node>_next;
};
  • 拷贝构造:

将源对象中资源拷贝给目标对象,让目标对象一同管理该资源,管理者++,引用计数++

每个共享资源只会调用一次析构

  • 赋值重载

1.自我赋值情况是目标对象会先释放资源,再访问用自己本身(地址已经无效是随机值)对它进行赋值会生成随机值;管理同一资源的对象间相互赋值,本质上是没有问题的目标对象先释放,引用计数--,源对象再赋值,引用计数++,还是管理同一资源,引用计数没变,只是效率降低,不建议

2.如果使用this指针与sp的地址对比,只能避免自我赋值的情况;使用指针对比还可以避免管理同一资源的对象间相互赋值,保证效率

3.释放目标对象时引用计数为0就直接释放指针和引用对象的动态数组

4.源对象进行赋值后记得更新引用计数

5.不管失败还是成功都返回调用者this指针

shared_ptr存在的问题

循环引用

双方或多方,互相持有对方的shared_ptr,形成了循环闭合关系,导致它们的引用计数永远不为0,析构函数不会被调用,导致内存泄漏问题

分析:

_prev管着左边的节点,_next管着右边的节点

1.什么时候_prev析构?---右边节点析构时_prev析构

1.右边节点什么时候析构?---_next析构右边节点就析构

1.什么时候_next析构?---左边节点析构时_prev析构

1.左边节点什么时候析构?---_prev析构左边节点就析构

这就导致了循环引用问题,引用计数不为0,无法析构

std::weak_ptr

1.std::weak_ptr 依赖于 std::shared_ptr。它不能直接管理对象的生命周期,而是通过 shared_ptr 来间接引用对象,并不影响其生命周期。

2.构造函数和赋值运算符需要一个 shared_ptr 作为参数。

3.weak_ptr不是RAII智能指针,专门为解决shared_ptr循环引用问题而产生

4.weak_ptr不增加引用计数,可以访问资源,但不参与资源释放的管理

  • 模拟实现
cpp 复制代码
template<class T>
	class weak_ptr
	{
	public:
		weak_ptr()
			:_ptr(nullptr)
		{}

		~weak_ptr()
		{}//不参与资源释放管理

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

		T* operator->()
		{
			return _ptr;
		}
		//拷贝
		weak_ptr(const shared_ptr<T>& sp)
			:_ptr(sp.get())
		{}

		//赋值
		weak_ptr<T>& operator=(const shared_ptr<T>& sp)
		{
			_ptr = sp.get();
			return *this;
		}

	private:
		T* _ptr;
	};
	
//创建一个自定义类,方便观察资源共享的关系
struct Node
{
	A _val;
	//Node* prev;这样定义指针在改变链接
	//Node* next;时会造成类型不匹配问题
	//ee::shared_ptr<Node>_prev;会造成循环引用问题
	//ee::shared_ptr<Node>_next;
	ee::weak_ptr<Node>_prev;
	ee::weak_ptr<Node>_next;
};

引用计数没有增加,解决了循环引用问题,各个资源都正确得到释放

订制删除器

当对象不是new出来的,如果是new出来的一个数组或malloc出来又或是文件操作,如何用智能指针来处理?

可以通过设置仿函数删除器来处理,但在标准库中的做法是在构造函数中增加一个模板参数,传入删除器,删除器是一个可调用对象(如函数指针、函数对象、lambda 表达式等),通过智能指针的生命周期来管理资源

成员变量:

用包装器定义一个成员变量,删除器是什么类型不重要,我们知道删除器的返回值为空,传入参数是模板参数T类型的指针,就可以用包装器来接收删除器。给一个缺省值,默认使用delete的销毁资源方式

构造函数:

两个构造函数构成重载,普通的使用默认delete删除器。显示传入的在构造时接收并将其储存为成员变量

  • 模拟实现
cpp 复制代码
template<class T>
	class shared_ptr
	{
	public:
		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()
		{
			if (--(*_pcount) == 0)
			{
				cout << "delete:" << _ptr << endl;
				//delete _ptr;
				_del(_ptr);
				delete _pcount;
			}
		}

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

		T* operator->()
		{
			return _ptr;
		}
		//拷贝
		shared_ptr(const shared_ptr<T>& sp)
			:_ptr(sp._ptr)
			,_pcount(sp._pcount)
			,_del(sp._del)
		{
			(*_pcount)++;
		}
		//赋值
		shared_ptr<T>& operator=(const shared_ptr<T>& sp)
		{
			//检查是否自我赋值和同一资源间的赋值
			if (_ptr == sp._ptr)
				return *this;
			//引用计数为0时调用析构
			if (--(*_pcount) == 0)
			{
			//释放目标对象
			_del(_ptr);
			delete _pcount;
			}
			//进行赋值
			_ptr = sp._ptr;
			_pcount = sp._pcount;
			_del = sp._del;
			++(*_pcount);
			return *this;
		}

		int use_count()const
		{
			return *_pcount;
		}

		T* get()const
		{
			return _ptr;
		}

	private:
		T* _ptr;
		int* _pcount;
		//给一个删除器缺省值,没传的时候默认用delete
		function<void(T*)>_del = [](T*ptr) {delete ptr; };
	};

//测试
int main()
{
	//ee::shared_ptr<A> sp1(new A[10]);
	ee::shared_ptr<A> sp1(new A[10], DeleteArray<A>());
	ee::shared_ptr<A> sp2((A*)malloc(sizeof(A)), [](A* ptr){ free(ptr); });
	ee::shared_ptr<FILE> sp3(fopen("Test.cpp", "r"), [](FILE* ptr) {
		cout << "fclose:" << ptr << endl;
		fclose(ptr); });

	ee::shared_ptr<A> sp4(new A);//没有传删除器,用默认的
	return 0;
}

1.析构中_del(_ptr);:调用存储在 _del 成员变量中的删除器,传递 _ptr 作为参数。删除器是一个可调用对象

2.自定义实现的shared智能指针的拷贝构造中,要记得构造删除器

3.自定义的赋值函数中释放目标对象的原有资源时也要调用删除器来操作,再被赋值接收源对象的删除器

相关推荐
希望20175 分钟前
go并发编程| channel入门
开发语言·后端·golang
进阶的小木桩8 分钟前
C# 导出word 插入公式问题
开发语言·c#·word
啊阿狸不会拉杆11 分钟前
[特殊字符]《计算机组成原理》第 8 章 - CPU 的结构和功能
java·开发语言·计算机组成原理
疯狂的沙粒21 分钟前
React与Vue的内置指令对比
开发语言·前端·javascript·vue.js
君鼎32 分钟前
剑指offer11_矩阵中的路径
数据结构·c++·算法
黄雪超1 小时前
JVM——SubstrateVM:AOT编译框架
java·开发语言·jvm
编码小笨猪1 小时前
[ Qt ] | Qlabel使用
开发语言·c++·qt
吃个糖糖1 小时前
Halcon联合QT ROI绘制
开发语言·qt
还有几根头发呀1 小时前
double怎么在c/c++中输出保留输出最小精度为一位
c语言·开发语言·c++
天天代码码天天2 小时前
PP-OCRv5 C++封装DLL C#调用源码分享
开发语言·c++·c#·ocr