異常處理
異常處理
異常處理概述
在 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 可以避免異常導致資源泄漏