C++11:异常和智能指针

发布于:2023-01-22 ⋅ 阅读:(350) ⋅ 点赞:(0)

"我亦曽踏足山巅,也曾跌落低谷。每个经历都让我受用良多。" 


 (一)抛异常

(1)异常的由来;

C语言传统的处理错误的方式有三种:

①assert:暴力终止程序 发生错误会立即中断程序。

②exit code(错误码):需要程序员根据错误信息 去对应错误原因。

C 标准库中setjmp和longjmp组合:不是很 常用

基于C语言传统对于处理错误的方式,不是很让人舒服。因此C++推出了一套自己报错的方式。

C++异常         

throw:当问题出现时,程序会抛出一个异常. 

try:激活try块 内代码异常,对其进行保护

catch:在您想要处理问题的地方,通过异常处理程序捕获异常(catch可以有多个)

//伪代码
try
{
  //块1
}
catch(T& e)  //T为任意类型
{

}
catch(...)
{

}

 C++11库中自带了 一套异常;


(2) 异常的使用

异常的抛出和匹配原则

1.throw可以抛出任意类型的对象,但catch内的对象类型一定需要匹配!

2.被选中的处理代码调用链中与该对象类型匹配且离抛出异常位置最近的那一个。

3.抛出的异常对象实则是一份临时拷贝,会在catch后自动销毁

4.catch(...) 可以捕获任意类型,但却无法知道异常信息。

5.对于基类子类,catch不一定需要 完全的类型匹配。

函数调用链展开原则;

我们先来阅读下面代码:

void f1()
{
	int i, j;
	cin >> i >> j;
	if (j == 0)
	{
		throw string("除0错误");
	}
}

void f2()
{
	f1();
}

int main()
{
	try
	{
		f2();
	}
	catch (const string& e)
	{
		cout << e << endl;
	}

	return 0;
}

 

 原则:

1.首先检查throw本身是否在try块内部,然后是查找匹配的catch语句。如果有匹配的,则调到catch的地方进行处理。

2.当前函数栈没有catch语句,则退出,继续沿上查找。

3.如果到达main函数的栈,依旧没有匹配的,则终止程序


(3)异常规范与安全;

常见的异常安全

1.最好不要在构造函数中抛出异常,否则可能导致对象不完整或没有完全初始化。

2.最好不要析构函数内抛出异常,因为异常可能会跳过 清理的步骤,导致资源泄漏。

3.C++11引入了 unique_lock(guard_lock)/智能指针... 进行资源管理(后面会说)

我们来看看下面代码:

int Devision(int len,int dol)
{
	if (dol == 0)
	{ 
		throw string("除0操作");
	}

	return len / dol;
}

void Fnc()
{
	int* array = new int[10];
	try
	{
		int len, dol;
		cin >> len >> dol;
		cout << Devision(len, dol) << endl;
	}
	catch (...)  //捕捉任何异常
	{
		cout << "catch :delete[]" << endl;
		delete[] array;
		throw;
	}

	delete[] array;
	array = nullptr;
}

int main()
{
	try
	{
		Fnc();
	}
	catch (const string& e)
	{
		cout << e << endl;
	}
	 
	return 0;
}

 

 异常规范(了解就行)

//函数 可能会抛出四种 类型
void func() throw(A,B,C,D)

//开辟失败
void* operator(std::size_t size) throw(std::bad_alloc)

//不会抛异常
void* operator(std::size_t size) throw()


(4)自定义异常体系

通常做项目的时候,会统一进行抛异常的管理。否则,各个人有各个人抛异常的习惯,这样管理的成本大大增加,效率也变得低下。

 

①基类:

//基类
class MyException
{
public:
	MyException(const int errid,const char* errmsg)
		:_errid(errid),
		_errmsg(errmsg)
	{}

	//获取 errid
	int Geterrid()
	{
		return _errid;
	}

	//错误描述信息
	//catch 的是 一份临时拷贝
	virtual string what()  //帮助子类 进行重写 +virtual
	{
		return _errmsg;
	}

	int _errid;
	string _errmsg;
};

②子类;

//继承 基类
class CacheExcetion :public MyException
{
public:
	CacheExcetion(const int errid,const char* errmsg)
		:MyException(errid,errmsg)  //父类 成员变量交给 父类去构造+初始化
	{}

	//重写-->多态
	virtual string what()const
	{
		string msg = "CacheExcetion:";
		msg += _errmsg;
		return msg;
	}

protected:
	//.....
};

class SqlExcetion :public MyException
{
public:
	SqlExcetion(const int errid,const char* msg,const char* sql)
		:MyException(errid,msg),  //父类的传给父类去构建
		_sql(sql)  //自己的自己构建
	{}

	virtual string what()const 
	{
		string msg = "SqlExcetion:";
		msg += _errmsg;
		msg += "Sql 语句:";
		msg += _sql;
		return msg;
	}
protected:
	string _sql;
};

 

③测试

void f1()
{
	int i;
	cin >> i;
	if (i == 1)
	{
		throw CacheExcetion(0, "数据不存在");
	}
	else
	{
		throw MyException(1, "数据损坏");
	}
}

void f2()
{
	int j;
	cin >> j;
	if (j == 0)
	{
		throw SqlExcetion(0, "数据库查询失败", "far from orrginal");
	}
}

int main()
{
	try
	{
		f1();
		f2();
	}
	catch (const MyException& me)  //只需要统一捕捉基类对象
	{
		cout << me.what() << endl;
	}

	return 0;
}

  


 

(5)异常反思;

 (1)异常优点;

①异常定义得好,相较于传统处理错误的方式,得到的错误信息肯定更准确,细致,更好定位BUG位置

②相对于传统的出错后,会沿着函数调用链层层返回。但异常不用,他会直接调到main位置的catch处。

③当然 异常在处理类成员函数的时候,也占有一定的优势.比如没有返回值的 构造函数,或者operator重载 的越界处理.......

(2)异常缺点;

①异常最惹人脑的地方,在于导致执行流的混乱。增加代码分析难度。

②C++没有自己的回收机制,在用异常的地方,切记处理异常安全提及的问题(RAII)。

③C++标准库的异常体系定义得不好,导致大家各自定义各自的异常体系,非常的混乱      


 

(二)智能指针

为什么要存在智能指针?

在上文提过,C++没有自带的回收系统资源的方法。当面临尤其是异常安全问题时,可能会因为跳转而造成没有及时对 申请的空间进行清理,造成内存泄漏。

(1) 智能指针概念

①什么是RAII技术?

是一种利用对象生命周期来控制程序资源

实际上是把 管理资源的任务,托管给了一个类。由它的生命周期进行管理。

 

智能指针的原理

就是利用一个类 去封装ptr,但是需要去operator 重载,让智能指针像一个 指针一样去使用! 

 

(2)智能指针的应用;

智能指针的种类与特性

C++11(前):

1.auto_ptr:C++98中引入--->本质是将管理权转移,保持一个资源对一个对象管理。同时原对象会被置位空;(不推荐使用)

2.scoped_ptr/shared_ptr/weak_ptr/:C++11之前 boost引入,后来也被纳入C++11。

C++11(后);

3.unique_ptr(scoped_ptr):简单粗暴,干脆禁止拷贝

4.shared_ptr:顾名思义,引用 计数的方式,保证一个对象,多个智能对象进行管理。---无法解决"循环引用"

5.weak_ptr:可以算是shared_ptr的分支,用于解决它遗留的 “循环引用”的问题

 

①auto_ptr: 

     //简单的模拟实现
	template<class T>
	class auto_ptr
	{
	public:
		auto_ptr(T* ptr)
			:_ptr(ptr)
		{}

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

		//像指针一样去使用
		T& operator*()
		{
			return *_ptr; //这里主要*
		}

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

		//拷贝构造 +赋值
		//s1=s2
		auto_ptr(auto_ptr<T>& ap)
			:_ptr(ap._ptr)  //管理权转移
		{
			ap._ptr = nullptr;
		}

		// ap1 = ap2
		auto_ptr<T>& operator=(auto_ptr<T>& ap)
		{
			if(_ptr != ap._ptr)
			{		
				delete _ptr;
				_ptr = ap._ptr;
				ap._ptr = nullptr;
			}
			return *this;
		}

	private:
		T* _ptr; //保存指针
	};

 

 ②unique_ptr

	template<class T>
	class unique_ptr
	{
	public:
		unique_ptr(T* ptr)
			:_ptr(ptr)
		{}

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

		//像指针一样去使用
		T& operator*()
		{
			return *_ptr; //这里主要*
		}

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

		//unique 比较简单粗暴 直接把拷贝构造+赋值重载禁用掉
		unique_ptr(const unique_ptr<T>& p) = delete;
		const unique_ptr<T>&  operator=(unique_ptr<T>& p) = delete;
	private:
		T* _ptr;
	};

 禁止掉了拷贝+赋值重载;

 

 

③shared_ptr:

 shared_ptr的设计更巧,第一步就需要理解,该怎么引用计数方法.

析构: 

 

 第一版;

	template<class T>
	class shared_ptr
	{
	private:
		void Relese()
		{
			if (--(*_pcount) == 0)
			{
				cout << "shared_ptr delete:" << endl;
				delete _ptr;
				delete _pcount;
			}
		}

		void AddRef()
		{
			(*_pcount)++;
		}

	public:
		shared_ptr(T* ptr)
			:_ptr(ptr),
			_pcount(new int(1)) //构造初始化为1
		{}

		~shared_ptr()
		{
			Relese();
		}

		T& operator*()
		{
			return *_ptr; //这里主要*
		}

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

		//拷贝+析构
		shared_ptr(shared_ptr<T>& sp)
			:_ptr(sp._ptr),
			_pcount(sp._pcount)
		{
			//对管理通一块空间的 Add++
			AddRef();  
		}

		//赋值重载
		shared_ptr<T>& operator=(shared_ptr<T>& sp)
		{
			//s1 = s1
			if (_ptr != sp._ptr)
			{
				//s1 = s2
				// s1 = s3
				//先让 s2的进行
				Relese();

				_pcount = sp._pcount;
				_ptr = sp._ptr;

				AddRef();
			}
			return *this;
		}

		//当然还可以 封装其他内容
		int Getcount()const 
		{
			return *_pcount;
		}

		T* Getptr()const 
		{
			return _ptr;
		}

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

 

多线程下的++ --失效讨论(这里会涉及到一些C++11 封装的thread线程库的 问题,这里就先进行示范)。

当多个线程,同时由智能指针管控,

因为++、--不具备原子性,那么不排除 会导致智能指针失效。

 不加锁:

 

加锁:

 

第二版(加锁版):

	template<class T>
	class shared_ptr
	{
	private:
		//对 ++ --非原子性操作 进行加锁
		void Relese()
		{
			//注意锁也要释放 因为也new出来的
			bool flag = false;
			_mtx->lock();
			if (--(*_pcount) == 0)
			{
				cout << "shared_ptr delete:" << endl;
				delete _ptr;
				delete _pcount;
				flag = true;
			}
			_mtx->unlock();

			if (flag == true)
			{
				delete _mtx;
			}
		}

		void AddRef()
		{
		_mtx->lock();
			(*_pcount)++;
			_mtx->unlock();
		}

	public:
		shared_ptr(T* ptr)
			:_ptr(ptr),
			_pcount(new int(1)), //构造初始化为1
			_mtx(new mutex)
		{}

		~shared_ptr()
		{
			Relese();
		}

		T& operator*()
		{
			return *_ptr; //这里主要*
		}

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

		//拷贝+析构
		shared_ptr(const shared_ptr<T>& sp)
			:_ptr(sp._ptr),
			_pcount(sp._pcount),
			_mtx(sp._mtx)
		{
			//对管理通一块空间的 Add++
			AddRef();  
		}

		//赋值重载
		shared_ptr<T>& operator=(shared_ptr<T>& sp)
		{
			//s1 = s1
			if (_ptr != sp._ptr)
			{
				//s1 = s2
				// s1 = s3
				//先让 s2的进行
				Relese();

				_pcount = sp._pcount;
				_ptr = sp._ptr;

				AddRef();
			}
			return *this;
		}

		//当然还可以 封装其他内容
		int use_count()
		{
			return *_pcount;
		}

		T* Getptr()const 
		{
			return _ptr;
		}

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

加锁是否 能保证智能指针指向的对象的安全?

答案是不能的!只能保证自己的安全!

 

④weak_ptr:

设置如下情景;

 

 

出现死锁!

 其实最主要的还是,如果_next 与 _prev不关系引用计数 那就 可以解决了!

	template<class T>
	class weak_ptr
	{
	public:
		weak_ptr()
			:_ptr(nullptr)
		{}

		//这个 weak_ptr主要是 去 承接shared_ptr
		weak_ptr(const shared_ptr<T>& sp)
			:_ptr(sp.Getptr())
		{}

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

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

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

	private:
		T* _ptr;
	};

 


 

(3)定制删除器 

 所谓定制删除器,也就是可以删除任意类型。

可以看到,我们上述shared_ptr指针 在delete的时候,当遇到多空间删除时,并不适用。

库中也给了相应的 模板。

其实无非就是给 shared_ptr 配一个调用对象而已(函数指针+仿函数+lambda表达式。。)

第三版(shared):

	template<class T>
	struct Delete
	{
		void operator()(const T* ptr)
		{
			cout << "Delete" << endl;
			delete ptr;
		}
	};

  	template<class T,class D= Delete<T>> //定制删除器
	class shared_ptr
	{
	private:
		//对 ++ --非原子性操作 进行加锁
		void Relese()
		{
			//注意锁也要释放 因为也new出来的
			bool flag = false;
			_mtx->lock();
			if (--(*_pcount) == 0)
			{
				if (_ptr)
				{
					cout << "shared_ptr delete:" << endl;
					//delete _ptr;
					_del(_ptr);
				}
				delete _pcount;
				flag = true;
			}
			_mtx->unlock();

			if (flag == true)
			{
				delete _mtx;
			}
		}

		void AddRef()
		{
		_mtx->lock();
			(*_pcount)++;
			_mtx->unlock();
		}

	public:
		shared_ptr(T* ptr, D del)
			:_ptr(ptr),
			_pcount(new int(1)), //构造初始化为1
			_mtx(new mutex),
			_del(del)
		{}

		//写出两个构造
		shared_ptr(T* ptr = nullptr)
			:_ptr(ptr),
			_pcount(new int(1)), //构造初始化为1
			_mtx(new mutex)
		{}

		~shared_ptr()
		{
			Relese();
		}

		T& operator*()
		{
			return *_ptr; //这里主要*
		}

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

		//拷贝+析构
		shared_ptr(const shared_ptr<T>& sp)
			:_ptr(sp._ptr),
			_pcount(sp._pcount),
			_mtx(sp._mtx)
		{
			//对管理通一块空间的 Add++
			AddRef();  
		}

		//赋值重载
		shared_ptr<T>& operator=(const shared_ptr<T>& sp)
		{
			//s1 = s1
			if (_ptr != sp._ptr)
			{
				//s1 = s2
				// s1 = s3
				//先让 s2的进行
				Relese();

				_pcount = sp._pcount;
				_ptr = sp._ptr;
				_mtx = sp._mtx;
				AddRef();
			}
			return *this;
		}

		//当然还可以 封装其他内容
		int use_count()
		{
			return *_pcount;
		}

		T* Getptr()const 
		{
			return _ptr;
		}

	private:
		 T* _ptr;
		int* _pcount;
		mutex* _mtx;
		D _del;
	};


 

(三)类型转换

(1)C语言中的类型转换:

①隐式类型转换:能转就转,不能转就编译失败

②强制类型转换:需要用户自己处理

缺点:

转换的可视性比较差,所有的转换形式都是以一种相同形式书写,难以跟踪错误的转换

 

(2)C++中的四种类型转换:

 为什么C++需要四种类型转换?

C两种类型转换固然简单,但也有其对应缺陷;

1.隐式类型转化有些情况下可能会出问题:比如数据精度丢失

2. 显式类型转换将所有情况混合在一起,代码不够清晰

因此C++提出了自己的类型转化风格,注意因为C++要兼容C语言,所以C++中还可以使用C语言的转化风格

①static_cast:

T a=static_cast<T> (_b);        

类似于隐式类型转换,适合相近类型的进行转换!

 ②reinterpret_cast;

FNC f=reinterpret_cast<T> (Cuse);

重新解释一种类型,总得来说很变态!

 

const_cast;

T* p=cast_const<T*>(&s);

删除const属性,便于赋值  

 

dynamic_cast:

Tc* pb2=dynamic_cast<Tc*> (pa);

用于将一个父类对象的指针/引用转换为子类对象的指针或引用(动态转换) 

 

 


总结: 

①异常的匹配规则和展开原则

②异常安全与规范

③自定义类异常

④智能指针的三个问题:1.RAII 2.像指针一样(重载operator) 3.拷贝+赋值重载

⑤三种智能指针类型+特性:auto_ptr unique_ptr shared_ptr(weak_ptr)

⑥C++引入的四种类型转换+使用场景

static_cast/reienpreter_cast/const_cast/dynamic_cast 


本篇就到这里结束了,感谢你的阅读

祝你好运~ 

 


网站公告

今日签到

点亮在社区的每一天
去签到