跳到正文

Wiki

異常處理

約 9 分鐘閱讀

本文由簡體中文內容確定性轉換,並受版本化術語表保護。

1.異常處理

1.1.異常處理概述

在 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 中處理異常。


1.2.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 中執行。


1.3.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) 捕獲。


1.4.多個 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 程式碼塊。


1.5.捕獲所有異常

如果不關心異常的具體型別,可以使用 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;
}

1.6.標準異常類

在實際 C++ 工程中,不推薦隨便丟擲字串,更多時候會使用標準庫提供的異常類。

常用異常類:

  • std::exception
  • std::runtime_error
  • std::logic_error
  • std::invalid_argument
  • std::out_of_range
  • std::bad_alloc

使用標準異常類需要包含標頭檔案:

#include <stdexcept>

1.6.1.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() 可以獲取異常中的錯誤資訊。


1.6.2.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 時,會丟擲引數錯誤異常。


1.6.3.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 异常

1.7.捕獲 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& 捕獲。


1.8.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:运行时错误

總結:

子類異常寫在前面,父類異常寫在後面。


1.9.異常在函式之間傳遞

異常可以跨函式傳遞。

如果當前函式沒有處理異常,異常會繼續向呼叫它的上層函式傳遞。

示例:

#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() 中被捕獲。


1.10.重新丟擲異常

有時候當前函式捕獲異常後,只做一部分處理,然後繼續把異常交給上層處理。

可以使用:

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() 中再次被捕獲。


1.11.自定義異常類

標準異常型別已經能解決大多數問題。

如果希望區分更加具體的錯誤,也可以自定義異常類。

自定義異常類通常繼承自 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 型別異常。

注意:

普通學習階段不需要大量自定義異常類,標準異常類已經夠用。


1.12.異常與建構函式

建構函式沒有返回值。

如果物件初始化失敗,可以在建構函式中丟擲異常。

示例:

#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 中處理異常。

應用場景:

例如:

  • 開啟檔案失敗
  • 開啟序列埠失敗
  • 讀取配置失敗
  • 初始化網路連線失敗

這些情況可以考慮在建構函式中丟擲異常。


1.13.異常與解構函式

解構函式中一般不要丟擲異常。

因為物件銷燬時如果再次丟擲異常,可能導致程式直接終止。

錯誤示例:

class Test
{
public:
    ~Test()
    {
        // 不推荐在析构函数中抛异常
        // throw runtime_error("析构失败");
    }
};

推薦做法:

解構函式中如果釋放資源失敗,通常只記錄錯誤,不繼續丟擲異常。

#include <iostream>
using namespace std;

class File
{
public:
    ~File()
    {
        // 析构函数中尽量不要 throw
        cout << "释放文件资源" << endl;
    }
};

總結:

解構函式中不要隨便拋異常。


1.14.異常與 RAII

RAII 是現代 C++ 中非常重要的思想。

RAII 的核心思想是:

資源在物件構造時獲取,在物件析構時釋放。

常見資源包括:

  • 堆記憶體
  • 檔案
  • 序列埠
  • 網路連線
  • 控制代碼

異常發生時,已經建立成功的區域性物件會自動呼叫解構函式。

1.14.1.普通指標的問題

示例:

#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; 不會執行,可能造成記憶體洩漏。

1.14.2.使用 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 物件也會自動析構,內部記憶體會自動釋放。

1.14.3.使用智慧指標管理資源

#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


1.15.異常和返回值錯誤碼

異常不是所有地方都適合使用。

1.15.1.適合使用異常的情況

異常適合處理“不應該頻繁發生”的嚴重錯誤。

例如:

  • 配置檔案不存在
  • 序列埠開啟失敗
  • 引數明顯錯誤
  • 初始化失敗
  • 程式無法繼續執行

示例:

#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 結束。

1.15.2.適合使用返回值的情況

對於經常發生、可以恢復的小錯誤,可以用返回值。

例如:

  • 序列埠偶爾讀取失敗
  • 網路偶爾超時
  • 感測器某一幀資料無效
  • 電機反饋偶爾丟包

示例:

#include <iostream>
using namespace std;

bool readEncoder()
{
    // 模拟读取失败
    return false;
}

int main()
{
    if (!readEncoder())
    {
        cout << "编码器读取失败,本次循环跳过" << endl;
        return 0;
    }

    cout << "编码器读取成功" << endl;

    return 0;
}

執行結果:

编码器读取失败,本次循环跳过

總結:

  • 初始化階段的大錯誤,可以用異常
  • 執行迴圈中的常見錯誤,優先用返回值或狀態碼

1.16.ROS2 / 機器人專案中的異常使用思路

在 ROS2 或機器人專案中,異常處理要謹慎使用。

1.16.1.適合拋異常的位置

例如節點啟動階段:

  • 引數讀取失敗
  • 配置檔案不存在
  • 序列埠無法開啟
  • 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 結束。

1.16.2.不適合頻繁拋異常的位置

例如控制迴圈中:

while (true)
{
    // 不推荐每次通信失败都 throw
}

更推薦:

bool readMotorState()
{
    // 读取失败返回 false
    return false;
}

int main()
{
    if (!readMotorState())
    {
        cout << "读取电机状态失败,跳过本次控制周期" << endl;
    }

    return 0;
}

執行結果:

读取电机状态失败,跳过本次控制周期

總結:

機器人控制中,週期性程式碼更關注穩定性和即時性,不要讓異常在高頻控制迴圈中到處亂飛。


1.17.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 表示函式不拋異常
  • 解構函式、移動建構函式、底層資源釋放函式中經常會看到它
  • 現階段不需要深入研究底層機制

1.18.異常處理注意事項

1.18.1.儘量使用引用捕獲

推薦:

catch (const exception& e)
{
    cout << e.what() << endl;
}

不推薦:

catch (exception e)
{
    cout << e.what() << endl;
}

原因:

按值捕獲會產生複製,還可能造成物件切片問題。


1.18.2.不要濫用異常

異常適合處理異常情況,不適合替代普通的 if else

不推薦:

try
{
    if (score < 60)
    {
        throw "不及格";
    }
}
catch (...)
{
    cout << "成绩不及格" << endl;
}

普通邏輯直接寫:

if (score < 60)
{
    cout << "成绩不及格" << endl;
}

1.18.3.不要在解構函式中隨便拋異常

解構函式中應該儘量保證安全釋放資源。


1.18.4.標準異常優先於字串異常

推薦:

throw runtime_error("文件打开失败");

不推薦:

throw "文件打开失败";

1.18.5.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 函式中統一處理,避免程式直接崩潰且沒有錯誤資訊。


1.19.總結

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 可以避免異常導致資源洩漏