第 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 可以避免异常导致资源泄漏
音乐页