第 16 節

異常處理

0瀏覽次數0訪問次數--跳出率--平均停留

異常處理

異常處理概述

在 C++ 中,程序運行過程中可能會出現各種錯誤,例如:

  • 文件打開失敗
  • 串口打開失敗
  • 參數不合法
  • 數組越界
  • 內存申請失敗
  • 配置文件格式錯誤

如果每個函數都只靠返回值判斷錯誤,代碼會變得比較複雜。

C++ 提供了 異常處理 機制,用來把“正常邏輯”和“錯誤處理邏輯”分開。

異常處理主要使用三個關鍵字:

  • try
  • catch
  • throw

作用:

  • 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::exception
  • std::runtime_error
  • std::logic_error
  • std::invalid_argument
  • std::out_of_range
  • std::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 表示訪問越界。

vectorat() 函數在越界時會拋出異常。

示例:

#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++ 中,應該優先使用:

  • string
  • vector
  • unique_ptr
  • shared_ptr
  • fstream
  • 自定義類的構造和析構

來管理資源,減少手動 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 工程開發來說,重點掌握:

  1. 會寫基本的 try / catch / throw
  2. 會使用 std::runtime_error
  3. 會用 catch (const std::exception& e) 統一捕獲標準異常
  4. 知道異常適合初始化失敗,不適合高頻控制循環
  5. 理解 RAII 可以避免異常導致資源泄漏
音乐页