C++必修:异常

news2024/9/27 2:33:45

✨✨ 欢迎大家来到贝蒂大讲堂✨✨

🎈🎈养成好习惯,先赞后看哦~🎈🎈

所属专栏:C++学习
贝蒂的主页:Betty’s blog

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

一般而言,在程序方式异常时,C语言会采用以下两种错误处理的方式:

  1. 终止程序,如assert,缺陷:用户难以接受。如发生内存错误,除0错误时就会终止程序。
  2. 返回错误码,缺陷:需要程序员自己去查找对应的错误。如系统的很多库的接口函数都是通
    过把错误码放到errno中,表示错误。

实际中C语言基本都是使用返回错误码的方式处理错误,部分情况下使用终止程序处理非常严重的错误。

一、使用终止程序的方式
当发生严重的内存分配错误时,可以使用终止程序的方式来处理。

#include <stdio.h>
#include <stdlib.h>

int main() 
{
    int *ptr = (int*)malloc(sizeof(int));
    if (ptr == NULL) 
    {
        printf("内存分配失败,程序终止。\n");
        exit(EXIT_FAILURE);
    }
    // 使用 ptr 的逻辑
    free(ptr);
    return 0;
}

在这个例子中,如果malloc函数无法分配足够的内存,程序将打印错误信息并使用exit函数终止程序,这种情况适用于严重的错误,因为继续执行程序可能会导致不可预测的结果。
二、使用返回错误码的方式
假设我们有一个函数用于打开文件并读取内容。如果文件无法打开,函数将返回一个错误码表示错误情况。

#include <stdio.h>
#include <stdlib.h>
#define FILE_OPEN_ERROR -1
#define READ_SUCCESS 0
int readFileContent(const char *filename) 
{
    FILE *file = fopen(filename, "r");
    if (file == NULL) 
    {
        return FILE_OPEN_ERROR;
    }

    // 读取文件内容的逻辑
    fclose(file);
    return READ_SUCCESS;
}
int main() 
{
    int result = readFileContent("nonexistent.txt");
    if (result == FILE_OPEN_ERROR) 
    {
        printf("无法打开文件。\n");
    } else 
    {
        printf("文件打开成功并处理完毕。\n");
    }
    return 0;
}

在这个例子中,readFileContent函数尝试打开一个文件,如果打开失败则返回错误码FILE_OPEN_ERROR,在main函数中,程序员需要检查这个错误码来确定是否发生了错误。

2. C++异常的概念

异常是一种处理错误的方式,当一个函数发现自己无法处理的错误时就可以抛出异常,让函数的直接或间接的调用者处理这个错误。 而关于异常有三个关键字:throwcatchtry。其作用分别为:

throw: 当问题出现时,程序会抛出一个异常。这是通过使用throw关键字来完成的。
catch: 在您想要处理问题的地方,通过异常处理程序捕获异常catch 关键字用于捕获异常,可以有多个catch进行捕获。
try: try块中的代码标识将被激活的特定异常,它后面通常跟着一个或多个catch

如果有一个块抛出一个异常,捕获异常的方法会使用trycatch关键字。try块中放置可能抛出异常的代码,try块中的代码被称为保护代码。使用try/catch语句的语法如下所示:

try
{
// 保护的标识代码
}catch( ExceptionName e1 )
{
// catch 块
}catch( ExceptionName e2 )
{
// catch 块
}catch( ExceptionName eN )
{
// catch 块
}

比如我们写一个简单的除法错误,我们为了防止除零错误就可以抛出异常:

double div()
{
    double a, b;
    cin >> a >> b;
    if (b == 0)
    {
        throw "除零错误";//抛出异常
    }
    return a / b;
}
int main()
{
    try
    {
        cout << div() << endl;
    }
    catch (const char* a)//捕获异常
    {
        cout << a << endl;
    }
    catch (...)//未知异常
    {
        cout << "unkonwn exception" << endl;
    }
    return 0;
}

image.png

3. 异常的用法

3.1 抛出与捕获的基本规则

一般而言,异常的抛出和捕获遵循以下原则:

  1. 异常是通过抛出对象引发的,抛出对象的类型决定激活哪个catch处理代码。若抛出的异常对象未被捕获或没有匹配类型的捕获,程序会终止报错。
  2. 被选中的处理代码(catch块)是在调用链中与该对象类型匹配且离抛出异常位置最近的那个。
  3. 抛出异常对象后,会生成一个异常对象的拷贝。因为抛出的异常对象可能是临时对象,所以这个拷贝的临时对象会在被catch以后销毁,类似于函数的传值返回。
  4. catch(...)可以捕获任意类型的异常,但捕获后无法得知异常错误具体内容。

此外,实际中异常的抛出和捕获的匹配原则有一个例外情况:**可以抛出派生类对象,使用基类进行捕获,**这在实际中非常有用。
并且当异常被抛出后,还会遵循在函数调用链中异常栈展开的匹配原则::

  1. 首先检查throw本身是否在try块内部。如果在,则查找匹配的catch语句。若有匹配的,就跳到catch的地方进行处理。
  2. 若当前函数栈没有匹配的catch,则退出当前函数栈,继续在上一个调用函数栈中进行查找匹配的catch
  3. 当找到匹配的catch子句并处理以后,会沿着catch子句后面继续执行,而不会跳回到原来抛异常的地方。
  4. 如果到达main函数的栈,依旧没有找到匹配的catch,则终止程序。

例如,在下面的代码中,main函数中调用了func3func3中调用了func2func2中调用了func1,在func1中抛出了一个string类型的异常对象:

void func1()
{
	throw string("这是一个异常");
	cout << "hello betty!" << endl;
}
void func2()
{
	func1();
	cout << "hello betty!" << endl;
}
void func3()
{
	func2();
	cout << "hello betty!" << endl;
}
int main()
{
	try
	{
		func3();
	}
	catch (const string& s)
	{
		cout << "错误描述:" << s << endl;
	}
	catch (...)
	{
		cout << "未知异常" << endl;
	}
	return 0;
}

image.png
func1中的异常被抛出后:

  1. 首先会检查throw本身是否在try块内部。这里由于throw不在try块内部,所以会退出func1所在的函数栈。
  2. 接着继续在上一个调用函数栈中进行查找,即func2所在的函数栈。由于func2中也没有匹配的catch,因此会继续查找。
  3. 继续在上一个调用函数栈中进行查找,也就是func3所在的函数栈。func3中同样没有匹配的catch
  4. 于是就会在main所在的函数栈中进行查找,最终在main函数栈中找到了匹配的catch
  5. 这时就会跳到main函数中对应的catch块中执行对应的代码块,执行完后继续执行该代码块后续的代码。所以也并不会打印出hello betty

调用如下图:

3.2 异常的重新抛出

有可能单个的catch不能完全处理一个异常,在进行一些校正处理以后,希望再交给更外层的调用链函数来处理,catch则可以通过重新抛出将异常传递给更上层的函数进行处理。
首先我们来看看这段代码:

void func1()
{
	//抛出异常
	throw string("这是一个异常");
}
void func2()
{
	int* arr = new int[10];
	func1();
	//...
	cout << "delete[] arr" << endl;
	delete[] arr;//释放内存
}
int main()
{
	try
	{
		func2();
	}
	catch (const string& s)
	{
		cout << s << endl;
	}
	catch (...)
	{
		cout << "未知异常" << endl;
	}
	return 0;
}

image.png
因为fun1被抛异常,而func2中并没有对应的捕获, 只在主函数main中被捕获,这就会导致一个问题:func2动态申请了内存,但fun2中后续语句没有被执行,就会造成内存泄漏的问题。
解决方法非常简单,就是在fun2先捕获,释放内存,再重新抛出。

void func1()
{
	//抛出异常
	throw string("这是一个异常");
}
void func2()
{
	int* arr = new int[10];
	try
	{
		func1();
	}
	catch (...)
	{
		cout << "delete[] arr" << endl;
		delete[] arr;//释放内存
		throw;//重新抛出
	}
	//...
	cout << "delete[] arr" << endl;
	delete[] arr;//释放内存
}
int main()
{
	try
	{
		func2();
	}
	catch (const string& s)
	{
		cout << s << endl;
	}
	catch (...)
	{
		cout << "未知异常" << endl;
	}
	return 0;
}

image.png

3.3 异常安全

为了保证异常并不会引起其他安全问题,我们一般要遵循以下几个建议:

  • 构造函数完成对象的构造和初始化,最好不要在构造函数中抛出异常,否则可能导致对象不完整或没有完全初始化。
  • 析构函数主要完成资源的清理,最好不要在析构函数内抛出异常,否则可能导致资源泄漏(内存泄漏、句柄未关闭等)
  • C++中异常经常会导致资源泄漏的问题,比如在newdelete中抛出了异常,导致内存泄漏,在lockunlock之间抛出了异常导致死锁,一般C++经常使用RAII来解决以上问题。

3.4 异常规范

为了方便函数使用者知晓函数的抛出异常的类型,以及个数,C++标准规定:可以在函数的后面接throw(类型),列出这个函数可能抛掷的所有异常类型。

  1. 函数的后面接throw()noexcept(C++11),表示函数不抛异常。
  2. 若无异常接口声明,则此函数可以抛掷任何类型的异常。
// 这里表示这个函数会抛出A/B/C/D中的某种类型的异常
void fun() throw(A,B,C,D);
// 这里表示这个函数只会抛出bad_alloc的异常
void* operator new (std::size_t size) throw (std::bad_alloc);
// 这里表示这个函数不会抛出异常
void* operator delete (std::size_t size, void* ptr) throw();
// C++11 中新增的noexcept,表示不会抛异常
thread() noexcept;
thread (thread&& x) noexcept;

4. 自定义异常体系

实际使用中很多公司都会自定义自己的异常体系进行规范的异常管理,因为一个项目中如果大家随意抛异常,那么外层的调用者就需要捕获所有类型不同的异常,所以实际中都会定义一套继承的规范体系。这样大家抛出的都是继承的派生类对象,捕获一个基类就可以了。

以下就是服务器开发中通常使用的异常继承体系:

// 服务器开发中通常使用的异常继承体系
class Exception
{
public:
	Exception(const string& errmsg, int id)
		:_errmsg(errmsg)
		, _id(id)
	{}
	virtual string what() const
	{
		return _errmsg;
	}
protected:
	string _errmsg;
	int _id;
};
class SqlException : public Exception
{
public:
	SqlException(const string& errmsg, int id, const string& sql)
		:Exception(errmsg, id)
		, _sql(sql)
	{}
	virtual string what() const
	{
		string str = "SqlException:";
		str += _errmsg;
		str += "->";
		str += _sql;
		return str;
	}
protected:
	const string _sql;
};
class CacheException : public Exception
{
public:
	CacheException(const string& errmsg, int id)
		:Exception(errmsg, id)
	{}
	virtual string what() const
	{
		string str = "CacheException:";
		str += _errmsg;
		return str;
	}
};
class HttpServerException : public Exception
{
public:
	HttpServerException(const string& errmsg, int id, const string& type)
		:Exception(errmsg, id)
		, _type(type)
	{}
	virtual string what() const
	{
		string str = "HttpServerException:";
		str += _type;
		str += ":";
		str += _errmsg;
		return str;
	}
protected:
	const string _type;
};
void SQLMgr()
{
	srand(time(0));
	if (rand() % 7 == 0)
	{
		throw SqlException("权限不足", 100, "select * from name = '张三'");
	}
	//throw "xxxxxx";
}
void CacheMgr()
{
	srand(time(0));
	if (rand() % 5 == 0)
	{
		throw CacheException("权限不足", 100);
	}
	else if (rand() % 6 == 0)
	{
		throw CacheException("数据不存在", 101);
	}
	SQLMgr();
}
void HttpServer()
{
	// ...
	srand(time(0));
	if (rand() % 3 == 0)
	{
		throw HttpServerException("请求资源不存在", 100, "get");
	}
	else if (rand() % 4 == 0)
	{
		throw HttpServerException("权限不足", 101, "post");
	}
	CacheMgr();
}
int main()
{
	while (1)
	{
		this_thread::sleep_for(chrono::seconds(1));
		try {
			HttpServer();
		}
		catch (const Exception& e) // 这里捕获父类对象就可以
		{
			// 多态
			cout << e.what() << endl;
		}
		catch (...)
		{
			cout << "Unkown Exception" << endl;
		}
	}
	return 0;
}

一般而言基类与派生类的成员变量要设为protected,因为如果是private,继承不可见。并且将what函数定义为虚函数,方便重写形成多态。

5. C++标准库的异常体系

C++ 提供了一系列标准的异常,定义在 中,我们可以在程序中使用这些标准的异常。它们是以父子类层次结构组织起来的,如下所示:
8c99c1e1883f450db3821117e3e0c22a.png
以下是对上述异常的解释:

异常描述
std::exception该异常是所有标准 C++异常的父类。
std::bad_alloc该异常可以通过 new 抛出。
std::bad_cast该异常可以通过 dynamic_cast 抛出。
std::bad_exception这在处理 C++程序中无法预期的异常时非常有用。
std::bad_typeid该异常可以通过 typeid 抛出。
std::logic_error理论上可以通过读取代码来检测到的异常。
std::domain_error当使用了一个无效的数学域时,会抛出该异常。
std::invalid_argument当使用了无效的参数时,会抛出该异常。
std::length_error当创建了太长的 std::string 时,会抛出该异常。
std::out_of_range该异常可以通过方法抛出,例如 std::vector 和 std::bitset<>::operator。
std::runtime_error理论上不可以通过读取代码来检测到的异常。
std::overflow_error当发生数学上溢时,会抛出该异常。
std::range_error当尝试存储超出范围的值时,会抛出该异常。
std::underflow_error当发生数学下溢时,会抛出该异常。

注意:实际中我们可以去继承exception类实现自己的异常类。但是实际中很多公司像上面一样自己定义一套异常继承体系。因为C++标准库设计也有缺陷。

#include<vector>
int main()
{
	try 
	{
		vector<int> v(10, 5);
		// 这里如果系统内存不够也会抛异常
		v.reserve(1000000000);
		// 这里越界会抛异常
		v.at(10) = 100;
	}
	catch (const exception& e) // 这里捕获父类对象就可以
	{
		cout << e.what() << endl;
	}
	catch (...)
	{
		cout << "Unkown Exception" << endl;
	}
	return 0;
}

image.png

6. 异常的优缺点

优点:

  1. 错误信息清晰准确:异常对象定义好后,可以清晰准确地展示出错误的各种信息,甚至可以包含堆栈调用等信息,有助于更好地定位程序的bug
  2. 避免层层返回错误码:返回错误码的传统方式在函数调用链中,深层的函数返回错误后,需要层层返回错误码,最终最外层才能拿到错误。而异常可以直接从错误发生的地方抛出,被合适的处理代码捕获。
  3. 与第三方库和测试框架配合好:很多第三方库(如 boost、gtest、gmock 等)和测试框架都使用异常,使用异常能更好地发挥这些库的作用,也能更好地进行白盒测试。
  4. 部分函数更适合用异常处理:比如T& operator这样的函数,如果出现问题(如 pos 越界),只能使用异常或者终止程序处理,没办法通过返回值表示错误。

缺点:

  1. 执行流混乱:异常会导致程序的执行流乱跳,并且非常混乱,这会使得跟踪调试以及分析程序时比较困难。
  2. 有性能开销:异常会有一些性能的开销,不过在现代硬件速度很快的情况下,这个影响基本可以忽略不计。
  3. 易导致资源管理问题:C++没有垃圾回收机制,资源需要自己管理。有了异常非常容易导致内存泄露、死锁等异常安全问题,需要使用RAII来处理资源的管理问题,学习成本比较高。
  4. 标准库异常体系不完善:C++标准库的异常体系定义得不够好,导致大家各自定义自己的异常体系,非常混乱。
  5. 异常使用不规范问题:异常尽量规范使用,否则后果不堪设想,随意抛异常会让外层捕获的用户苦不堪言。
  6. 接口声明不强制:异常接口声明不是强制的,对于没有声明异常类型的函数,无法预知该函数是否会抛出异常。

总体而言,虽然异常有一些缺点,但利大于弊,所以在工程中我们还是鼓励使用异常的,并且面向对象的语言基本都使用异常处理错误,这也可以看出这是大势所趋。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2088802.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

空间计量 | 似不相关回归SUR

通常情况下&#xff0c;研究X对于Y的影响时&#xff0c;Y只能为一个&#xff0c;如果有多个则重复进行多次&#xff0c;即每次都只考虑单一方程估计&#xff0c;如果有多个Y时&#xff0c;将多个Y同时进行联合估计有可能会提高估计效率&#xff0c;即模型的拟合能力更加接近于实…

计算机毕业设计选题推荐-博客平台-博客系统-Java/Python项目实战

✨作者主页&#xff1a;IT毕设梦工厂✨ 个人简介&#xff1a;曾从事计算机专业培训教学&#xff0c;擅长Java、Python、微信小程序、Golang、安卓Android等项目实战。接项目定制开发、代码讲解、答辩教学、文档编写、降重等。 ☑文末获取源码☑ 精彩专栏推荐⬇⬇⬇ Java项目 Py…

嵌入式软件--数电基础 DAY 6

一、前情复习 1.存储电路的复习 我们刚开始接触的第一个存储电路是SR锁存器。但是SR锁存器存在诸多不足&#xff0c;比如我们不希望出现无意义的状态&#xff0c;于是我们有了D锁存器&#xff0c;再输入电路中加入非门电路&#xff0c;避免了无意义状态&#xff0c;但同时失去…

波导模式分析1 高度形变圆波导

摘要:略 简介&#xff1a;略 &#xff08;主要学习分析方法&#xff09; 在本文中&#xff0c;提出了一种宽带圆TE01模式转换器的设计方法。为了在宽频带内抑制寄生模式的生成&#xff0c;引入了高度变形的圆波导。对高度变形圆波导中的本征模的特性进行了理论分析。分析显示&…

【 html+css 绚丽Loading 】000026 五行吞灵盘

前言&#xff1a;哈喽&#xff0c;大家好&#xff0c;今天给大家分享htmlcss 绚丽Loading&#xff01;并提供具体代码帮助大家深入理解&#xff0c;彻底掌握&#xff01;创作不易&#xff0c;如果能帮助到大家或者给大家一些灵感和启发&#xff0c;欢迎收藏关注哦 &#x1f495…

harbor部署+docker-compose

harbor部署 harbor介绍 Harbor 是一个开源的企业级容器镜像仓库&#xff0c;用于存储和分发 Docker 镜像及其他容器格式的镜像。 Harbor 提供了一个集中化的存储库&#xff0c;方便团队成员共享和管理容器镜像。你可以将构建好的镜像推送到 Harbor 仓库中&#xff0c;其他成…

CC1链_全网最菜的分析思路

文章目录 1 你必须知道的点1.1 反序列化利用链的起点是readObject()方法1.2 回顾反射执行系统命令1.3 相关类の功能简单介绍1.3.1 InvokerTransformer类1.3.2 ChainedTransformer类1.3.3 ConstantTransformer类1.3.4 总结一下上述3个类调用transform()方法的不同 2 CC1链的环境…

嘴上说“摆烂”的90后,考了最多的PMP证书......

现在&#xff0c;“摆烂”一词经常被大家挂在嘴边&#xff0c;但真正能够安心摆烂的人却没多少&#xff0c;特别是承担着社会主要劳动力的90后们。 大部分90后都是嘴上说着摆烂&#xff0c;但该卷的时候还是得卷&#xff0c;特别是在考证这件事上&#xff01; 一、PMP考生年龄…

缓存配置错误导致授权绕过

一个电子商务网站它有 2 个资产target.com admin.target.com target.com是面向用户的门户&#xff0c;用户可以去那里购买物品。admin.target.com基本上是卖家的管理门户&#xff0c;卖家可以在其中列出他们的物品&#xff0c;跟踪订单、客户信息等。 我通常使用 Autorize …

国产光耦合器的应用优势

国产光耦合器在近年来的技术发展中表现出了显著的应用优势&#xff0c;尤其是在电子和电力系统中。光耦合器作为一种广泛应用的电子元件&#xff0c;主要用于实现信号隔离、噪声抑制和电压转换等功能。随着国产品牌的不断崛起&#xff0c;国产光耦合器在性能、性价比以及供应链…

基于yolov8的8种人脸表情检测系统python源码+onnx模型+评估指标曲线+精美GUI界面

【算法介绍】 基于YOLOv8的人脸表情检测系统是一个结合了先进目标检测算法&#xff08;YOLOv8&#xff09;与深度学习技术的项目&#xff0c;旨在实时或离线地识别并分类人脸表情&#xff08;如快乐、悲伤、愤怒、惊讶、恐惧、厌恶、中立等&#xff09;。以下是一个简短的介绍…

四通道非洲猪瘟检测仪

四通道非洲猪瘟检测仪具有以下功能优势&#xff1a; 高效性能&#xff1a;四通道设计使得可以同时检测多个样本&#xff0c;大大提高了检测效率。这对于大规模养猪场或集中屠宰场来说尤为重要&#xff0c;可以快速筛查出可能感染非洲猪瘟的猪只。 高准确性&#xff1a;四通道检…

纯原生-如何在不破解情况下使用Android监听支付宝微信收款消息

具体思路&#xff1a; 首先支付宝微信收款均有到账通知&#xff0c;这是其app自带属性&#xff0c;也是为了提醒用户&#xff1b; 然后再规则范围内如何合理利用&#xff0c;在这里我们不说使用xposed这些工具&#xff0c;仅使用手机原生功能如何来做&#xff1b; 思路: 1、新建…

Python进阶06-Web服务器

零、文章目录 Python进阶06-Web服务器 1、HTTP协议 HTTP协议相关请参考HTTP协议详解网络相关内容请参考计算机网络详解 &#xff08;1&#xff09;HTTP协议 HTTP 协议的全称是(HyperText Transfer Protocol)&#xff0c;翻译过来就是超文本传输协议。超文本是超级文本的缩…

图为科技闪耀双展,AI之星智领未来

图为科技同期闪耀2024AGIC深圳(国际)通用人工智能大会与深圳国际电子展&#xff0c;晋升AI领域新星。 人工智能爆炸时代&#xff0c;每一场科技盛会的召开都预示着行业的新风向与无限可能。 作为边缘计算与人工智能领域的佼佼者&#xff0c;图为科技携带前沿技术与创新产品&a…

UI自动化测试 —— 下拉选择框弹出框滚动条操作实践!

前言 UI测试&#xff0c;也称为用户界面测试&#xff0c;是一种测试类型&#xff0c;旨在检查应用程序的界面是否工作正常&#xff0c;以及是否存在任何妨碍用户行为且不符合书面规格的BUG。UI自动化测试则是利用自动化工具来执行这些测试&#xff0c;以提高测试效率和准确性.…

【ubuntu使用笔记】使用timeshift备份ubuntu系统

使用timeshift备份ubuntu系统 安装timeshift sudo apt install timeshift建立备份点 sudo timeshift --create --comments "fist" --tags D查看备份点 sudo timeshift --list参考 三种Ubuntu系统全盘备份与恢复方法—tar、timeshift、systemback

linux访问github网速太慢 the remote end hung up unexpectedly问题

linux访问github网速太慢 the remote end hung up unexpectedly问题 pip install githttps://github.com/zhanghang1989/PyTorch-Encoding/时遇到fatal: the remote end hung up unexpectedly 原因 linux访问github网速太慢 措施&#xff1a; 确定ip&#xff1a; https://link.…

CRM 客户管理系统哪个好用?本篇盘点给你答案!

本文将盘点15款CRM客户管理系统&#xff0c;为企业选型提供参考 。 CRM 客户管理系统哪个好用&#xff1f;这是众多企业在发展过程中常常思考的问题。 CRM 客户管理系统就如同企业的得力助手&#xff0c;能把企业的客户资源管理得井井有条。对于企业来说&#xff0c;如果没有一…

【Mybatis】Mybatis-Plus 高级

1、关于插件 1.1、插件机制 MyBatis 允许你在已映射语句执⾏过程中的某⼀点进⾏拦截调⽤。默认情况下&#xff0c;MyBatis 允许使⽤插件来拦截的⽅法调⽤包括&#xff1a; Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed)Par…