异常处理
异常处理
异常处理概述
在 C++ 中,程序运行过程中可能会出现各种错误,例如:
- 文件打开失败
- 串口打开失败
- 参数不合法
- 数组越界
- 内存申请失败
- 配置文件格式错误
如果每个函数都只靠返回值判断错误,代码会变得比较复杂。
C++ 提供了 异常处理 机制,用来把“正常逻辑”和“错误处理逻辑”分开。
异常处理主要使用三个关键字:
trycatchthrow
作用:
try:放置可能出现异常的代码throw:抛出异常catch:捕获异常并处理
语法:
try
{
// 可能出现异常的代码
}
catch (异常类型 变量名)
{
// 异常处理代码
}
示例:
#include <iostream>
using namespace std;
int divide(int a, int b)
{
if (b == 0)
{
throw "除数不能为0";
}
return a / b;
}
int main()
{
try
{
int ret = divide(10, 0);
cout << "ret = " << ret << endl;
}
catch (const char* msg)
{
cout << "捕获到异常:" << msg << endl;
}
return 0;
}
运行结果:
捕获到异常:除数不能为0
程序运行后,divide(10, 0) 会抛出异常,后面的正常返回不会继续执行,程序会跳转到 catch 中处理异常。
throw 抛出异常
throw 用来主动抛出异常。
语法:
throw 异常对象;
异常对象可以是普通类型,也可以是标准库异常对象。
示例:
#include <iostream>
using namespace std;
void checkAge(int age)
{
if (age < 0)
{
throw "年龄不能小于0";
}
cout << "年龄为:" << age << endl;
}
int main()
{
try
{
checkAge(-10);
}
catch (const char* msg)
{
cout << "错误信息:" << msg << endl;
}
return 0;
}
运行结果:
错误信息:年龄不能小于0
当 age < 0 时,函数会通过 throw 抛出异常,程序跳转到 catch 中执行。
catch 捕获异常
catch 用来捕获异常。
如果抛出的异常类型和 catch 中的类型一致,就会进入对应的 catch 代码块。
示例:
#include <iostream>
using namespace std;
void test()
{
throw 10;
}
int main()
{
try
{
test();
}
catch (int e)
{
cout << "捕获到 int 类型异常:" << e << endl;
}
return 0;
}
运行结果:
捕获到 int 类型异常:10
throw 10; 抛出的是 int 类型异常,所以会被 catch (int e) 捕获。
多个 catch 捕获不同类型异常
一个 try 后面可以跟多个 catch,用于处理不同类型的异常。
示例:
#include <iostream>
using namespace std;
void test(int type)
{
if (type == 1)
{
throw 10;
}
else if (type == 2)
{
throw 3.14;
}
else if (type == 3)
{
throw "字符串异常";
}
}
int main()
{
try
{
test(3);
}
catch (int e)
{
cout << "捕获到 int 异常:" << e << endl;
}
catch (double e)
{
cout << "捕获到 double 异常:" << e << endl;
}
catch (const char* e)
{
cout << "捕获到字符串异常:" << e << endl;
}
return 0;
}
运行结果:
捕获到字符串异常:字符串异常
根据 throw 抛出的类型不同,会进入不同的 catch 代码块。
捕获所有异常
如果不关心异常的具体类型,可以使用 catch (...) 捕获所有异常。
语法:
catch (...)
{
// 捕获所有异常
}
示例:
#include <iostream>
using namespace std;
void test()
{
throw 3.14;
}
int main()
{
try
{
test();
}
catch (...)
{
cout << "捕获到未知类型异常" << endl;
}
return 0;
}
运行结果:
捕获到未知类型异常
无论抛出的异常是什么类型,catch (...) 都可以捕获。
注意:
catch (...) 一般放在最后。
try
{
test();
}
catch (int e)
{
cout << "int异常" << endl;
}
catch (...)
{
cout << "其他异常" << endl;
}
标准异常类
在实际 C++ 工程中,不推荐随便抛出字符串,更多时候会使用标准库提供的异常类。
常用异常类:
std::exceptionstd::runtime_errorstd::logic_errorstd::invalid_argumentstd::out_of_rangestd::bad_alloc
使用标准异常类需要包含头文件:
#include <stdexcept>
runtime_error
runtime_error 表示运行时错误,例如文件打开失败、串口打开失败、网络连接失败等。
示例:
#include <iostream>
#include <stdexcept>
using namespace std;
void openSerialPort(const string& device)
{
if (device.empty())
{
throw runtime_error("串口设备名不能为空");
}
cout << "打开串口:" << device << endl;
}
int main()
{
try
{
openSerialPort("");
}
catch (const runtime_error& e)
{
cout << "运行时错误:" << e.what() << endl;
}
return 0;
}
运行结果:
运行时错误:串口设备名不能为空
e.what() 可以获取异常中的错误信息。
invalid_argument
invalid_argument 表示参数不合法。
示例:
#include <iostream>
#include <stdexcept>
using namespace std;
void setSpeed(double speed)
{
if (speed < 0)
{
throw invalid_argument("速度不能为负数");
}
cout << "设置速度:" << speed << endl;
}
int main()
{
try
{
setSpeed(-1.5);
}
catch (const invalid_argument& e)
{
cout << "参数错误:" << e.what() << endl;
}
return 0;
}
运行结果:
参数错误:速度不能为负数
当传入的速度小于 0 时,会抛出参数错误异常。
out_of_range
out_of_range 表示访问越界。
vector 的 at() 函数在越界时会抛出异常。
示例:
#include <iostream>
#include <vector>
#include <stdexcept>
using namespace std;
int main()
{
vector<int> nums = {10, 20, 30};
try
{
cout << nums.at(5) << endl;
}
catch (const out_of_range& e)
{
cout << "访问越界:" << e.what() << endl;
}
return 0;
}
运行结果:
访问越界:vector::_M_range_check: __n (which is 5) >= this->size() (which is 3)
nums.at(5) 超出了 vector 的范围,所以会抛出 out_of_range 异常。
注意:
nums[5]; // 越界时不一定抛异常,可能直接产生未定义行为
nums.at(5); // 越界时会抛出 out_of_range 异常
捕获 std::exception
很多标准异常类都继承自 std::exception。
因此实际工程中,经常使用下面这种写法:
catch (const std::exception& e)
{
cout << e.what() << endl;
}
示例:
#include <iostream>
#include <stdexcept>
using namespace std;
void test()
{
throw runtime_error("程序运行出现错误");
}
int main()
{
try
{
test();
}
catch (const exception& e)
{
cout << "捕获到标准异常:" << e.what() << endl;
}
return 0;
}
运行结果:
捕获到标准异常:程序运行出现错误
runtime_error 属于标准异常类型,可以被 const exception& 捕获。
catch 捕获顺序
如果同时捕获父类异常和子类异常,应该先捕获子类,再捕获父类。
错误写法:
try
{
throw runtime_error("运行时错误");
}
catch (const exception& e)
{
cout << "exception:" << e.what() << endl;
}
catch (const runtime_error& e)
{
cout << "runtime_error:" << e.what() << endl;
}
上面的写法中,runtime_error 会先被 exception 捕获,后面的 runtime_error 分支就没有意义了。
推荐写法:
#include <iostream>
#include <stdexcept>
using namespace std;
int main()
{
try
{
throw runtime_error("运行时错误");
}
catch (const runtime_error& e)
{
cout << "runtime_error:" << e.what() << endl;
}
catch (const exception& e)
{
cout << "exception:" << e.what() << endl;
}
return 0;
}
运行结果:
runtime_error:运行时错误
总结:
子类异常写在前面,父类异常写在后面。
异常在函数之间传递
异常可以跨函数传递。
如果当前函数没有处理异常,异常会继续向调用它的上层函数传递。
示例:
#include <iostream>
#include <stdexcept>
using namespace std;
void func3()
{
throw runtime_error("func3 中出现错误");
}
void func2()
{
func3();
}
void func1()
{
func2();
}
int main()
{
try
{
func1();
}
catch (const exception& e)
{
cout << "main 中捕获异常:" << e.what() << endl;
}
return 0;
}
运行结果:
main 中捕获异常:func3 中出现错误
func3() 抛出异常后,func2() 和 func1() 都没有处理,最后异常传递到 main() 中被捕获。
重新抛出异常
有时候当前函数捕获异常后,只做一部分处理,然后继续把异常交给上层处理。
可以使用:
throw;
重新抛出当前异常。
示例:
#include <iostream>
#include <stdexcept>
using namespace std;
void readConfig()
{
try
{
throw runtime_error("配置文件读取失败");
}
catch (const exception& e)
{
cout << "readConfig 中记录错误:" << e.what() << endl;
// 重新抛出异常
throw;
}
}
int main()
{
try
{
readConfig();
}
catch (const exception& e)
{
cout << "main 中最终处理:" << e.what() << endl;
}
return 0;
}
运行结果:
readConfig 中记录错误:配置文件读取失败
main 中最终处理:配置文件读取失败
异常先在 readConfig() 中被捕获一次,然后通过 throw; 重新抛出,最后在 main() 中再次被捕获。
自定义异常类
标准异常类型已经能解决大多数问题。
如果希望区分更加具体的错误,也可以自定义异常类。
自定义异常类通常继承自 std::runtime_error。
示例:
#include <iostream>
#include <stdexcept>
using namespace std;
class SerialException : public runtime_error
{
public:
SerialException(const string& msg)
: runtime_error(msg)
{
}
};
void openSerial(const string& device)
{
if (device.empty())
{
throw SerialException("串口设备名为空");
}
cout << "打开串口:" << device << endl;
}
int main()
{
try
{
openSerial("");
}
catch (const SerialException& e)
{
cout << "串口异常:" << e.what() << endl;
}
catch (const exception& e)
{
cout << "其他异常:" << e.what() << endl;
}
return 0;
}
运行结果:
串口异常:串口设备名为空
当串口设备名为空时,会抛出 SerialException 类型异常。
注意:
普通学习阶段不需要大量自定义异常类,标准异常类已经够用。
异常与构造函数
构造函数没有返回值。
如果对象初始化失败,可以在构造函数中抛出异常。
示例:
#include <iostream>
#include <stdexcept>
using namespace std;
class SerialPort
{
public:
SerialPort(const string& device)
{
if (device.empty())
{
throw runtime_error("串口初始化失败:设备名为空");
}
m_Device = device;
cout << "串口初始化成功:" << m_Device << endl;
}
private:
string m_Device;
};
int main()
{
try
{
SerialPort port("");
}
catch (const exception& e)
{
cout << "创建对象失败:" << e.what() << endl;
}
return 0;
}
运行结果:
创建对象失败:串口初始化失败:设备名为空
如果构造函数抛出异常,对象创建失败,程序会跳转到 catch 中处理异常。
应用场景:
例如:
- 打开文件失败
- 打开串口失败
- 读取配置失败
- 初始化网络连接失败
这些情况可以考虑在构造函数中抛出异常。
异常与析构函数
析构函数中一般不要抛出异常。
因为对象销毁时如果再次抛出异常,可能导致程序直接终止。
错误示例:
class Test
{
public:
~Test()
{
// 不推荐在析构函数中抛异常
// throw runtime_error("析构失败");
}
};
推荐做法:
析构函数中如果释放资源失败,通常只记录错误,不继续抛出异常。
#include <iostream>
using namespace std;
class File
{
public:
~File()
{
// 析构函数中尽量不要 throw
cout << "释放文件资源" << endl;
}
};
总结:
析构函数中不要随便抛异常。
异常与 RAII
RAII 是现代 C++ 中非常重要的思想。
RAII 的核心思想是:
资源在对象构造时获取,在对象析构时释放。
常见资源包括:
- 堆内存
- 文件
- 串口
- 网络连接
- 锁
- 句柄
异常发生时,已经创建成功的局部对象会自动调用析构函数。
普通指针的问题
示例:
#include <iostream>
#include <stdexcept>
using namespace std;
void test()
{
int* p = new int[100];
throw runtime_error("中途出现异常");
delete[] p;
}
int main()
{
try
{
test();
}
catch (const exception& e)
{
cout << e.what() << endl;
}
return 0;
}
运行结果:
中途出现异常
throw 之后,delete[] p; 不会执行,可能造成内存泄漏。
使用 vector 避免资源泄漏
示例:
#include <iostream>
#include <vector>
#include <stdexcept>
using namespace std;
void test()
{
vector<int> nums(100);
throw runtime_error("中途出现异常");
cout << nums.size() << endl;
}
int main()
{
try
{
test();
}
catch (const exception& e)
{
cout << e.what() << endl;
}
return 0;
}
运行结果:
中途出现异常
即使发生异常,vector 对象也会自动析构,内部内存会自动释放。
使用智能指针管理资源
#include <iostream>
#include <memory>
#include <stdexcept>
using namespace std;
void test()
{
unique_ptr<int[]> p(new int[100]);
throw runtime_error("中途出现异常");
}
int main()
{
try
{
test();
}
catch (const exception& e)
{
cout << e.what() << endl;
}
return 0;
}
运行结果:
中途出现异常
unique_ptr 会自动释放内存,不需要手动 delete[]。
总结:
异常处理要和 RAII 一起理解。
现代 C++ 中,应该优先使用:
stringvectorunique_ptrshared_ptrfstream- 自定义类的构造和析构
来管理资源,减少手动 new/delete。
异常和返回值错误码
异常不是所有地方都适合使用。
适合使用异常的情况
异常适合处理“不应该频繁发生”的严重错误。
例如:
- 配置文件不存在
- 串口打开失败
- 参数明显错误
- 初始化失败
- 程序无法继续运行
示例:
#include <iostream>
#include <stdexcept>
using namespace std;
void initSystem(const string& configPath)
{
if (configPath.empty())
{
throw runtime_error("配置文件路径为空");
}
cout << "系统初始化成功" << endl;
}
int main()
{
try
{
initSystem("");
}
catch (const exception& e)
{
cout << "初始化失败:" << e.what() << endl;
return 1;
}
return 0;
}
运行结果:
初始化失败:配置文件路径为空
程序随后以状态码 1 结束。
适合使用返回值的情况
对于经常发生、可以恢复的小错误,可以用返回值。
例如:
- 串口偶尔读取失败
- 网络偶尔超时
- 传感器某一帧数据无效
- 电机反馈偶尔丢包
示例:
#include <iostream>
using namespace std;
bool readEncoder()
{
// 模拟读取失败
return false;
}
int main()
{
if (!readEncoder())
{
cout << "编码器读取失败,本次循环跳过" << endl;
return 0;
}
cout << "编码器读取成功" << endl;
return 0;
}
运行结果:
编码器读取失败,本次循环跳过
总结:
- 初始化阶段的大错误,可以用异常
- 运行循环中的常见错误,优先用返回值或状态码
ROS2 / 机器人项目中的异常使用思路
在 ROS2 或机器人项目中,异常处理要谨慎使用。
适合抛异常的位置
例如节点启动阶段:
- 参数读取失败
- 配置文件不存在
- 串口无法打开
- CAN 设备无法打开
- 硬件初始化失败
示例:
#include <iostream>
#include <stdexcept>
using namespace std;
class MotorDriver
{
public:
MotorDriver(const string& device)
{
if (device.empty())
{
throw runtime_error("电机驱动初始化失败:设备名为空");
}
cout << "电机驱动初始化成功:" << device << endl;
}
};
int main()
{
try
{
MotorDriver driver("");
}
catch (const exception& e)
{
cout << "程序启动失败:" << e.what() << endl;
return 1;
}
return 0;
}
运行结果:
程序启动失败:电机驱动初始化失败:设备名为空
程序随后以状态码 1 结束。
不适合频繁抛异常的位置
例如控制循环中:
while (true)
{
// 不推荐每次通信失败都 throw
}
更推荐:
bool readMotorState()
{
// 读取失败返回 false
return false;
}
int main()
{
if (!readMotorState())
{
cout << "读取电机状态失败,跳过本次控制周期" << endl;
}
return 0;
}
运行结果:
读取电机状态失败,跳过本次控制周期
总结:
机器人控制中,周期性代码更关注稳定性和实时性,不要让异常在高频控制循环中到处乱飞。
noexcept 简介
noexcept 表示一个函数承诺不抛出异常。
语法:
void func() noexcept
{
}
示例:
#include <iostream>
using namespace std;
void test() noexcept
{
cout << "这个函数承诺不抛异常" << endl;
}
int main()
{
test();
return 0;
}
运行结果:
这个函数承诺不抛异常
注意:
如果一个 noexcept 函数中真的抛出了异常,程序通常会直接终止。
void test() noexcept
{
// 不应该这样写
// throw runtime_error("错误");
}
普通学习阶段只需要知道:
noexcept表示函数不抛异常- 析构函数、移动构造函数、底层资源释放函数中经常会看到它
- 现阶段不需要深入研究底层机制
异常处理注意事项
尽量使用引用捕获
推荐:
catch (const exception& e)
{
cout << e.what() << endl;
}
不推荐:
catch (exception e)
{
cout << e.what() << endl;
}
原因:
按值捕获会产生拷贝,还可能造成对象切片问题。
不要滥用异常
异常适合处理异常情况,不适合替代普通的 if else。
不推荐:
try
{
if (score < 60)
{
throw "不及格";
}
}
catch (...)
{
cout << "成绩不及格" << endl;
}
普通逻辑直接写:
if (score < 60)
{
cout << "成绩不及格" << endl;
}
不要在析构函数中随便抛异常
析构函数中应该尽量保证安全释放资源。
标准异常优先于字符串异常
推荐:
throw runtime_error("文件打开失败");
不推荐:
throw "文件打开失败";
main 函数中可以统一捕获严重异常
示例:
#include <iostream>
#include <stdexcept>
using namespace std;
int main()
{
try
{
throw runtime_error("程序出现严重错误");
}
catch (const exception& e)
{
cout << "fatal error:" << e.what() << endl;
return 1;
}
return 0;
}
运行结果(程序最后以状态码 1 结束):
fatal error:程序出现严重错误
程序出现严重异常后,在 main 函数中统一处理,避免程序直接崩溃且没有错误信息。
总结
C++ 异常处理的核心内容:
try:尝试执行可能出错的代码throw:抛出异常catch:捕获并处理异常std::exception:标准异常基类std::runtime_error:常用运行时异常e.what():获取异常信息- 异常可以跨函数传递
- 构造函数可以抛异常
- 析构函数不要随便抛异常
- 异常处理要配合 RAII 理解
- 不要滥用异常
对于普通 C++ 学习和 ROS2 工程开发来说,重点掌握:
- 会写基本的
try / catch / throw - 会使用
std::runtime_error - 会用
catch (const std::exception& e)统一捕获标准异常 - 知道异常适合初始化失败,不适合高频控制循环
- 理解 RAII 可以避免异常导致资源泄漏