异常和智能指针

发布于:2022-10-12 ⋅ 阅读:(148) ⋅ 点赞:(0)

1.C语言传统的处理错误的方式

传统的错误处理机制:

  1. 终止程序,如assert,缺陷:用户难以接受。如发生内存错误,除0错误时就会终止程序。
  2. 返回错误码,缺陷:需要程序员自己去查找对应的错误。如系统的很多库的接口函数都是通过把错误码放到errno中,表示错误,这种比较麻烦要自己查错误码
    但是,C语言基本都是使用返回错误码的方式处理错误,部分情况下使用终止程序处理非常严重的错误也就是我们常常用的assert断言

2.C++异常概念

异常是一种处理错误的方式,当一个函数发现自己无法处理的错误时就可以抛出异常,让函数的直接或间接的调用者处理这个错误

throw关键字: 当问题出现时,程序会抛出一个异常。这是通过使用 throw 关键字来完成的

catch关键字: 在你想要处理问题的地方,通过异常处理程序捕获异常.catch 关键字用于捕获异常,可以有多个catch进行捕获

try关键字: try 块中的代码标识将被激活的特定异常,它后面通常跟着一个或多个 catch 块

1.异常的抛出和匹配原则

  1. 首先检查throw本身是否在try块内部,如果是再查找匹配的catch语句。如果有匹配的,则调到catch的地方进行处理。
  2. 没有匹配的catch则退出当前函数栈,继续在调用函数的栈中进行查找匹配catch。
  3. 如果到达main函数的栈,依旧没有匹配的,则终止程序。上述这个沿着调用链查找匹配的catch子句的过程称为栈展开。所以实际中我们最后都要加一个catch(…)捕获任意类型的异常,否则当有异常没捕获,程序就会直接终止。
  4. 找到匹配的catch子句并处理以后,会继续沿着catch子句后面继续执行。

以下面代码为例我输入了 1 0 这个时候除零错误并找到了throw,这个时候代码是直接跳到了对应到的catch语句里面执行,并不会出现return到Func函数并打印,也就是说异常信息是在catch里面的代码来打印的而不是返回异常信息给Func函数打印

如果这个时候我是正常的输入没有除零错误,没有返回异常,那么他就会正常执行,也就是说他会跳过catch,不执行catch

注意catch里面参数要和throw返回的异常相同,下面代码是异常返回的是char类型,我用const char*来做参数没有问题,但是我把他改成了int那么就会报错,不过当然了,这个报错肯定是触发了除零错误的时候,捕获异常,catch打印的时候才会报错,像生成解决方案ctrl + b(快捷键)和没触发异常的时候就不会报错

而catch(…)这个作用是报了一个你不知道异常,那你catch(…)就会自动捕获异常,不会报错,只会报异常,就是说程序可以继续跑,但是你不知道哪里抛异常了

#include<iostream>
using namespace std;


double Division(int a, int b)
{
	// 当b == 0时抛出异常
	if (b == 0)
		throw "Division by zero condition!";
	else
		return ((double)a / (double)b);
}

void Func()
{
	int len, time;
	cin >> len >> time;
	cout << Division(len, time) << endl;
}

int main()
{
	try 
	{
		Func();
	}
	catch (const char* errmsg)
	{
		cout << errmsg << endl;
	}
	catch (...) // 捕获没有匹配的任意类型异常
	{
		cout << "未知异常" << endl;
	}
	return 0;
}

除了上面的抛异常,还有一种抛派生类的异常,就是捕获父类的来抛子类
也就是说除了const exception& ba还可以std::bad_alloc& ba,因为bad_alloc是exception的父类

#include<iostream>
using namespace std;
#include <time.h>
#include <string>
#include <windows.h>

int main()
{
	try
	{
		size_t i = 0;
		while (1)
		{
			int* myarray = new int[1024*100];
			cout << myarray <<"->"<<i++<<endl;
		}
	}
	catch (const exception& ba)
	{
		std::cerr << "bad_alloc caught: " << ba.what() << "\n";
	}
	return 0;
}

2.c++异常的优点

  1. 异常对象定义好了,相比错误码的方式可以清晰准确的展示出错误的各种信息,甚至可以包
    含堆栈调用的信息,这样可以帮助更好的定位程序的bug。
  2. 返回错误码的传统方式有个很大的问题就是,在函数调用链中,深层的函数返回了错误,那
    么我们得层层返回错误,最外层才能拿到错误。
  3. 很多的第三方库都包含异常,比如boost、gtest、gmock等等常用的库,那么我们使用它们
    也需要使用异常。
  4. 部分函数使用异常更好处理,比如构造函数没有返回值,不方便使用错误码方式处理。比如
    T& operator这样的函数,如果pos越界了只能使用异常或者终止程序处理,没办法通过返回值表示错误

3.c++异常缺点

  1. 异常会导致程序的执行流乱跳,并且非常的混乱,并且是运行时出错抛异常就会乱跳。这会导致我们跟踪调试时以及分析程序时,比较困难。
  2. 异常会有一些性能的开销。当然在现代硬件速度很快的情况下,这个影响基本忽略不计。
  3. C++没有垃圾回收机制,资源需要自己管理。有了异常非常容易导致内存泄漏、死锁等异常安全问题。这个需要使用RAII来处理资源的管理问题。学习成本较高。
  4. C++标准库的异常体系定义得不好,导致大家各自定义各自的异常体系,非常的混乱(没有强制规定使用库的)。
  5. 异常尽量规范使用,否则后果不堪设想,随意抛异常,外层捕获的用户苦不堪言。所以异常规范有两点:一、抛出异常类型都继承自一个基类。二、函数是否抛异常、抛什么异常,都使用 func() throw();的方式规范化。

总结:异常总体而言,利大于弊,所以工程中我们还是鼓励使用异常的。另外OO的语言基本都是
用异常处理错误,这也可以看出这是大势所趋

2.智能指针

1.为什么要用智能指针

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

	return a / b;
}

void func()
{
	int* p1 = new int[10]; // 这里亦可能会抛异常
	int* p2 = new int[10]; // 这里亦可能会抛异常

	try
	{
		div();
	}
	catch (...)
	{
		delete[] p1;
		delete[] p2;
		throw;
	}

	delete[] p1;
	delete[] p2;
}

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

	return 0;
}

看上面代码,如果p1出异常没有问题,因为他会直接返回最近的catch,但是如果p2出异常了,那么他会到最近的catch但是p1并没有被释放,这个时候就有内存泄漏了,其实下面还好,因为只有一个p2,可以用try catch来捕获p2,这样也不会出现问题,但是如果有p3,p4等等,那就很麻烦了,但是这个也不是完全没问题,因为你不知道哪个报异常了,所以你很有可能把成功申请的内存也释放了,那么这个时候就到智能指针出场了

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

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

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

从上面的RAII讲法,都明白了,所谓的智能指针其实就是借类里面的构造函数和析构函数来实现自动构造和自动析构,但是哦!要注意RAII不是智能指针,而是智能指针实现指导思想

namespace li
{
	template<class T>
	class SmartPtr
	{
	public:
		SmartPtr(T* ptr)
			:_ptr(ptr)
		{}

		~SmartPtr()
		{
			cout << "delete" << _ptr << endl;
			delete _ptr;
		}
	private:
		T* _ptr;
	};
}
void func()
{
	li::SmartPtr<int> sp1(new int);
	li::SmartPtr<int> sp2(new int);
	li::SmartPtr<int> sp3(new int);
	li::SmartPtr<int> sp4(new int);

	cout << div() << endl;
}

以上面代码为例,不管抛不抛异常,出了他们的作用都会自动析构,如果p1异常,这个时候构造都没构造成功,就直接跳到了catch的位置了,如果p2异常了,说是说抛异常了,跳到了catch的位置,但是栈帧还在,只是不会执行下面的代码了,不会创建变量,但是出了作用还是会调用析构函数,把他们给析构了

但是智能指针,有指针二个字,所以肯定是像指针一样的东西,所以上面写的智能指针还要完善,比如重载运算符,还有就是pair这个函数也是可以传过去的

改造后的代码

namespace li
{
	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;
		}

		T* Get()
		{
			return _ptr;
		}


	private:
		T* _ptr;
	};
}
void func()
{
	li::SmartPtr<int> sp1(new int);
	li::SmartPtr<int> sp2(new int);
	li::SmartPtr<int> sp3(new int);
	li::SmartPtr<int> sp4(new int);
	li::SmartPtr<pair<string,int>> sp5(new pair<string, int>("sort", 1));

	sp5->second++;


	cout << div() << endl;
}

3.智能指针的问题

智能指针的问题,其实就是拷贝拷贝的问题啦

因为它们是浅拷贝,指的是同一个空间,这样会造成析构二次

int main()
{
	li::SmartPtr<int> sp1(new int);

	li::SmartPtr<int> sp2(sp1);
	return 0;
}

在这里插入图片描述

4.c++98的解决办法auto_ptr

但是我们在了解auto_ptr之前我们先来了解这里到底是想要浅拷贝还是想要深拷贝

智能指针,因为是想要模拟指针的问题,所以他是想要浅拷贝,他是想要二个指针管理同一个地方,帮你托管资源,他是希望二个指针来一起管理同一个空间,但是资源是托管过的,我要释放资源,他分不清,是直接交给你管理,还是拷贝构造过来的,所以二个都会造成析构

这个时候我们先来看看c++98的解决方法auto_ptr,这个东西也是被人吐糟很久的东西

为什么会这样,因为他做了一个很神奇的东西管理权转移
在这里插入图片描述

如果看上面图片还看不出问题再看下面的操作

int main()
{

	std::auto_ptr<int> sp1(new int);
	std::auto_ptr<int> sp2(sp1);

	*sp1 = 10;
	return 0;
}

运行下代码
在这里插入图片描述
可以看到直接报错了,因为把p1的管理权转移给了p2,所以这个时候使用p1就会直接报错这就是管理权转移

5.auto_ptr模拟实现

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

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


		// sp2(sp1)
		auto_ptr(auto_ptr<T>& sp)
			:_ptr(sp._ptr)
		{
			sp._ptr = nullptr;
		}

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

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

		T* get()
		{
			return _ptr;
		}


	private:
		T* _ptr;
	};

其实看到auto_ptr其实就多了一个构造函数

		// sp2(sp1)
		auto_ptr(auto_ptr<T>& sp)
			:_ptr(sp._ptr)
		{
			sp._ptr = nullptr;
		}

这段代码sp等于sp1,_ptr等sp2,把sp1给sp2,然后再把sp1置空

像什么赋值构造原理也是一样的,因为这个东西太坑了,所以没有必要讲auto_ptr了

6.unique_ptr模拟实现

unique_ptr这个智能指针的原理就是防拷贝,不让你拷贝,拷贝编译就报错

	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;
		}

		T* get()
		{
			return _ptr;
		}

	private:

		unique_ptr(const unique_ptr<T>& sp);
		T* _ptr;
	};

这是c++98的实现方式
1.只声明不实现
2.这个要注意了,因为你是只声明的,所以可能有老6自己实现,所以要把他声明到私有的

但是因为这样比较麻烦所以,c++11应用了个新的东西

		unique_ptr(const unique_ptr<T>& sp) = delete;

在这里插入图片描述

可以看到这个报错再说你在用应用已删除的函数

但是注意上面的还有一个问题赋值拷贝问题没有解决

		unique_ptr(const unique_ptr<T>& sp) = delete;
		unique_ptr<T>& operator=(const unique_ptr<T>& sp) = delete;

7.shared_ptr模拟实现

shared_ptr的核心原理就是引用计数,记录几个对象管理这块资源,析构的时候–计数,最后一个析构的对象释放资源

	template<class T>
	class shared_ptr
	{
	public:

		shared_ptr(T* ptr)
			:_ptr(ptr)
			, _pCount(new int(1))
		{}

		~shared_ptr()
		{
			if (--(*_pCount) == 0 && _ptr)
			{
				cout << "delete" << _ptr << endl;
				delete _ptr;
				_ptr = nullptr;

				delete _pCount;
				_pCount = nullptr;
			}
		}

		shared_ptr(const shared_ptr<T>& sp)
			:_ptr(sp._ptr)
			, _pCount(sp._pCount)
		{
			(*_pCount)++;
		}



		// 像指针一样
		T& operator*()
		{
			return *_ptr;
		}

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

		T* get()
		{
			return _ptr;
		}

	private:
		T* _ptr;
		int* _pCount;
	};
	li::shared_ptr<int> sp1(new int);
	li::shared_ptr<int> sp2(sp1);

运行上面代码,他调用他的构造函数,他有个count来进行记时。当你构造的时候或者拷贝构造就count++,析构函数就是进行过一次析构就–count,当count为零的时候这个时候就可以释放,但是这里有个细节要注意,count这个是我们开的空间,析构的时候记得把他也释放掉,虽然数据量不过也就几个字节,但是时间长了就会很恐怖

但是上面代码有个赋值拷贝还没写,看下面图片这个赋值拷贝是系统默认生成的,因为我们暂时没写,可以看到代码是把sp3赋值给sp1,赋值过去后sp3和sp1的count应该是为二的才对,因为它们互相管理同一个指针,而sp2的记时还是2,sp1已经没和sp2共同管理这个指针了,应该减减的,所以说该加加的没加加,该减减的没减减
在这里插入图片描述

	template<class T>
	class shared_ptr
	{
	public:
		void Release()
		{
			if (--(*_pCount) == 0 && _ptr)
			{
				cout << "delete" << _ptr << endl;
				delete _ptr;
				_ptr = nullptr;

				delete _pCount;
				_pCount = nullptr;
			}
		}

		// RAII思想
		shared_ptr(T* ptr)
			:_ptr(ptr)
			, _pCount(new int(1))
		{}

		~shared_ptr()
		{
			Release();
		}

		shared_ptr(const shared_ptr<T>& sp)
			:_ptr(sp._ptr)
			, _pCount(sp._pCount)
		{
			(*_pCount)++;
		}

		

		// sp1 = sp3
		shared_ptr<T>& operator=(const shared_ptr<T>& sp)
		{
			//if (this != &sp)
			if (_ptr != sp._ptr)
			{
				Release();

				_ptr = sp._ptr;
				_pCount = sp._pCount;
				++(*_pCount);
			}

			return *this;
		}


		// 像指针一样
		T& operator*()
		{
			return *_ptr;
		}

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

		T* get()
		{
			return _ptr;
		}

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

这个赋值拷贝,要注意可能自己拷贝自己,所以要进行判断智能指针指的不是同一个地方才可以进行拷贝,要不然自己赋值自己就会给自己count++这样就不对了,赋值拷贝过去,还要注意原本指的空间,不指那个空间了,那个那个空间就要count–;

8.shared_ptr的问题循环引用

struct ListNode
{

	li::shared_ptr<ListNode> _next = nullptr;
    li::shared_ptr<ListNode> _prev = nullptr;

	int _val = 0;

	~ListNode()
	{
		cout << "~ListNode()" << endl;
	}
};

int main()
{
	// 循环引用
	li::shared_ptr<ListNode> p1(new ListNode);
	li::shared_ptr<ListNode> p2(new ListNode);

	p1->_next = p2;
	p2->_prev = p1;


	return 0;
}

看上面代码这是一个简单使用智能指针的一个过程但是会出现一个问题循环引用,我们来看下执行结果看看,循环引用带来了什么后果

在这里插入图片描述
可以看到问题了,没有调用析构函数,这个时候就有内存泄漏了,prev管着左边的节点,next管着右边的节点,当它们的节点释放完成才能把它们的成员函数给释放完成。
那么问题就来了,左节点释放,next析构,next依赖右节点,因为指着的是右节点的空间,右节点释放,prev析构,但是右节点的prev指的是左节点,他要等next先释放才释放自己,这样就形成了一个套娃,比如二个让人握手,没人愿意最先松开手,都在等对面先松开手

9.weak_ptr

造成这种问题的原因是它们的记时都加加了
在这里插入图片描述
看到上面的代码,use_count()代表的是它的个数,而weak_ptr,就是不让它们的count进行加加来和shared_ptr来进行辅助管理

struct ListNode
{

	std::weak_ptr<ListNode> _next;
	std::weak_ptr<ListNode> _prev;

	int _val = 0;

	~ListNode()
	{
		cout << "~ListNode()" << endl;
	}
};

int main()
{
	// 循环引用
	std::shared_ptr<ListNode> p1(new ListNode);
	std::shared_ptr<ListNode> p2(new ListNode);

	cout << p1.use_count() << endl;
	cout << p2.use_count() << endl;

	p1->_next = p2;
	p2->_prev = p1;


	cout << p1.use_count() << endl;
	cout << p2.use_count() << endl;

	return 0;
}

运行结果

在这里插入图片描述
这样子内存泄漏的问题就解决了

10.weak_ptr模拟实现

	// 不参与指向资源的释放管理
	template<class T>
	class weak_ptr
	{
	public:
		weak_ptr()
			:_ptr(nullptr)
		{}

		weak_ptr(const shared_ptr<T>& sp)
			:_ptr(sp.get())
		{}

		weak_ptr<T>& operator=(const shared_ptr<T>& sp)
		{
			if (_ptr != sp.get())
			{
				_ptr = sp.get();
			}

			return *this;
		}

		// 像指针一样
		T& operator*()
		{
			return *_ptr;
		}

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

	public:
		T* _ptr;
	};

这个模拟实现有个要注意的
在这里插入图片描述
这个报错的意思是一个const的对象调用一个不是const的对象,这个时候是权限变大,所以不支持,所以要把shared_

本文含有隐藏内容,请 开通VIP 后查看

网站公告

今日签到

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