C/C++ 错误处理全解:从 errno 到 C++ 异常

C/C++ 错误处理全解:从 errno 到 C++ 异常

前言

写 C/C++ 的时候,错误处理是一个绕不开的话题。C 语言没有内置的异常机制,靠的是返回值、errno、assert 这些老路子;C++ 则提供了 try-catch-throw 这套结构化异常处理。两种语言的设计哲学不同,适用的场景也不一样。这篇文章把两边的错误处理方式都捋一遍,该用哪种、怎么用,看完心里大概就有数了。

关于安全警告 :在 Windows 下使用 Visual Studio 编译时,fopenstrcpystrerror 等函数会被标记为"不安全",建议改用带 _s 后缀的安全版本(如 fopen_s)。这些安全函数会额外检查缓冲区大小,减少缓冲区溢出风险。本文示例均采用安全版本,同时在注释中说明如何通过定义 _CRT_SECURE_NO_WARNINGS 来禁用此警告(适用于兼容旧代码或跨平台项目)。


一、C语言的错误处理方式

C 语言不提供对错误处理的直接支持。作为一种系统编程语言,它靠返回值让你判断函数调用是否成功,同时配合一个全局的错误码变量 errno 来记录具体错误原因。

1.1 errno、perror() 与 strerror_s()

errno 是一个全局变量,定义在 <errno.h> 中。C 标准库的很多函数在执行出错时会设置 errno 的值。开发人员应该在程序初始化时把 errno 设为 0,0 表示没有错误。

C 语言提供了两个函数来显示 errno 对应的文本信息:

  • perror():显示你传给它的字符串,后跟一个冒号、一个空格和当前 errno 值的文本表示
  • strerror_s():安全版本的 strerror,将错误信息写入用户提供的缓冲区,并指定缓冲区大小

来看一个打开不存在的文件的例子(使用安全函数):

c 复制代码
#include <stdio.h>
#include <errno.h>
#include <string.h>

int main()
{
    FILE *pf;
    errno_t err;
    char errbuf[256];

    err = fopen_s(&pf, "unexist.txt", "rb");
    if (err != 0) {
        // 获取当前错误码
        int errnum = errno;
        fprintf(stderr, "Value of errno: %d\n", errnum);
        perror("Error printed by perror");

        // 使用 strerror_s 获取安全错误信息
        if (strerror_s(errbuf, sizeof(errbuf), errnum) == 0) {
            fprintf(stderr, "Error opening file: %s\n", errbuf);
        }
    } else {
        fclose(pf);
    }
    return 0;
}

输出结果:

复制代码
Value of errno: 2
Error printed by perror: No such file or directory
Error opening file: No such file or directory

需要注意 :错误信息应该输出到 stderr 而不是 stdout

跨平台提示fopen_sstrerror_s 是 C11 标准 Annex K 中定义的可选函数,在 Windows 平台完全支持。如果要在 Linux/macOS 下编译,可以定义 _CRT_SECURE_NO_WARNINGS 并继续使用 fopen/strerror,或者自行封装条件编译。

1.2 被零除的处理

C 语言中除以零会引发运行时错误。避免的办法是在做除法之前先检查除数:

c 复制代码
#include <stdio.h>
#include <stdlib.h>

int main()
{
    int dividend = 20;
    int divisor = 0;
    int quotient;

    if (divisor == 0) {
        fprintf(stderr, "Division by zero! Exiting...\n");
        exit(EXIT_FAILURE);
    }
    quotient = dividend / divisor;
    fprintf(stderr, "Value of quotient: %d\n", quotient);
    exit(EXIT_SUCCESS);
}

程序成功退出时返回 EXIT_SUCCESS(定义为 0),出错退出时返回 EXIT_FAILURE(定义为 -1)。

1.3 assert 断言

assert 是 C 语言中用于调试的宏,定义在 <assert.h> 中。它在运行时检查一个条件,如果条件为假,就在 stderr 打印错误信息(包括出问题的表达式、文件名和行号),然后调用 abort() 终止程序。

c 复制代码
#include <stdio.h>
#include <assert.h>

int main()
{
    int a = 10;
    int *p1 = &a;
    assert(p1 != NULL);  // 表达式为真,不执行

    int *p2 = NULL;
    assert(p2 != NULL);  // 表达式为假,程序崩溃
    return 0;
}

assert 只应该用在调试阶段,用来捕获"绝不应该发生"的逻辑错误。如果程序已经确认没问题,可以在 #include <assert.h> 之前定义 NDEBUG 宏来禁用所有 assert

c 复制代码
#define NDEBUG
#include <stdio.h>
#include <assert.h>
// 所有 assert 语句都会被跳过

几个需要留意的点:

  • assert 会降低程序运行效率,不要过度使用
  • assert 导致程序立即终止,可能无法完成资源释放
  • Release 版本通常应该移除或替换 assert

1.4 setjmp() 与 longjmp():C语言中的"异常"

setjmplongjmp 定义在 <setjmp.h> 中,提供了一种非局部跳转的机制------可以跨函数跳转。这就像在 C 语言里模拟 try-catch。

基本用法:

  • setjmp(jmp_buf env):保存当前执行环境到 jmp_buf,第一次调用返回 0
  • longjmp(jmp_buf env, int val):跳转回 setjmp 保存的位置,让 setjmp 返回 val
c 复制代码
#include <stdio.h>
#include <setjmp.h>

jmp_buf env;

void f2()
{
    printf("f2 begins\n");
    longjmp(env, 1);      // 跳转回 setjmp 的位置
    printf("f2 returns\n"); // 这行不会执行
}

void f1()
{
    printf("f1 begins\n");
    f2();
    printf("f1 returns\n");
}

int main()
{
    if (setjmp(env) == 0) {
        printf("setjmp returned 0\n");
    } else {
        printf("Program terminates: longjmp called\n");
        return 0;
    }
    f1();
    printf("Program terminates normally\n");
    return 0;
}

输出:

复制代码
setjmp returned 0
f1 begins
f2 begins
Program terminates: longjmp called

setjmp 第一次调用返回 0,程序继续执行;f2 中调用 longjmp 后,程序直接跳回 setjmp 的位置,这次返回的是 longjmp 的第二个参数 1。

使用 setjmp/longjmp 的注意事项

  1. longjmpenv 参数必须先被 setjmp 初始化
  2. 包含 setjmp 调用的函数不能在 longjmp 之前返回
  3. 这两个函数会让代码难以理解和维护,除非必要否则慎用

二、C++ 的异常处理机制

C++ 提供了结构化的异常处理,核心是三个关键字:trythrowcatch

2.1 基本语法

cpp 复制代码
#include <iostream>
#include <stdexcept>
#include <limits>

void MyFunc(int c)
{
    if (c > std::numeric_limits<char>::max()) {
        throw std::invalid_argument("Argument too large");
    }
}

int main()
{
    try {
        MyFunc(256);  // 触发异常
    } catch (const std::invalid_argument& e) {
        std::cerr << "Caught: " << e.what() << std::endl;
        return -1;
    }
    return 0;
}

流程是这样的:

  • try 块括住可能抛出异常的代码
  • throw 抛出异常对象(可以是任意类型)
  • catch 按类型匹配捕获异常
  • 如果找不到匹配的 catch,调用 std::terminate() 终止程序

建议 :按值抛出、按 const 引用捕获。异常类型最好直接或间接继承自 std::exception

catch(...) 可以捕获所有异常,但应该放在最后,并且只用来记录错误和做清理工作,不要让程序继续运行。

2.2 栈展开(Stack Unwinding)

C++ 抛出异常后,运行时会从 throw 的位置开始,逐层往回查找匹配的 catch 块。在这个过程中,沿途所有栈上的局部对象都会被自动析构。

cpp 复制代码
#include <iostream>
#include <memory>

class A
{
    int num;
public:
    A(int i) : num(i) {
        std::cout << "A constructed, num=" << num << std::endl;
    }
    ~A() {
        std::cout << "A destructed, num=" << num << std::endl;
    }
};

void test1()
{
    A* pa = new A(1);
    throw 1;      // 异常抛出,delete 不会执行
    delete pa;    // 内存泄漏
}

void test2()
{
    std::auto_ptr<A> pa(new A(2));
    pa->show();
    throw 2;      // auto_ptr 会在栈展开时自动析构,释放内存
}

栈展开过程中,裸指针 不会自动释放,可能导致内存泄漏。解决方案是使用 RAII 手法------用智能指针(auto_ptrunique_ptrshared_ptr)管理资源,让析构函数在栈展开时自动释放。

重要 :析构函数中不要抛出异常。如果析构函数在栈展开过程中又抛出异常,而另一个异常已经在传播中,std::terminate 会立即被调用,程序直接终止。C++11 起,析构函数默认带有 noexcept 属性。

2.3 异常规范:noexcept

C++11 引入了 noexcept 关键字,用来声明一个函数不会抛出异常。

cpp 复制代码
void func() noexcept;           // 保证不抛异常
void func() noexcept(true);     // 同上
void func() noexcept(false);    // 可能抛异常

如果 noexcept 函数真的抛出了异常,会直接调用 std::terminate() 终止程序。

什么时候用 noexcept

  • 移动构造函数和移动赋值运算符(能提升性能)
  • 析构函数(默认就是 noexcept
  • 任何你确定不会抛出异常的函数

旧的 throw() 动态异常规范在 C++11 中被弃用,C++17 中已移除。

2.4 自定义异常类

标准库提供了一系列异常类,都继承自 std::exception

异常类 说明
std::exception 所有标准异常的基类
std::bad_alloc 内存分配失败
std::bad_cast 类型转换失败
std::logic_error 逻辑错误(编译时能发现的)
std::invalid_argument 无效参数
std::out_of_range 越界访问
std::runtime_error 运行时错误

如果这些不够用,可以继承 std::exception 自定义异常类。为了安全起见,推荐用 std::string 保存错误信息,避免手工管理 char*

cpp 复制代码
#include <exception>
#include <string>

class MyException : public std::exception
{
private:
    std::string msg;
public:
    explicit MyException(const std::string& m) : msg(m) {}
    const char* what() const noexcept override {
        return msg.c_str();
    }
};

三、C 与 C++ 错误处理对比

维度 C C++
错误表示 整型错误码 / errno 任意类型的异常对象
传播路径 手动逐层返回 自动栈展开
处理位置 必须立即处理 可在任意上层捕获
默认行为 忽略错误继续执行 未捕获则终止程序
性能开销 几乎为零 异常抛出时有较大开销

选型建议

  • C 项目:用返回值 + errno 做常规错误处理,用 assert 做调试断言
  • C++ 项目
    • 用异常处理"可能发生"的运行时错误(I/O 失败、参数校验等)
    • assert 处理"绝不应该发生"的逻辑错误
    • 性能敏感的循环中,考虑用错误码代替异常

四、完整示例:一个简单的银行账户系统

下面这个示例把 C 和 C++ 的错误处理方式都用上了,可以对照着看。所有可能触发安全警告的函数都已替换为安全版本(fopen_sstrerror_s),并检查了返回值。

cpp 复制代码
#include <iostream>
#include <exception>
#include <string>
#include <cstring>
#include <cerrno>

// ==================== C风格错误处理 ====================

// 用 errno 模拟错误
void c_style_divide(int a, int b)
{
    if (b == 0) {
        errno = EINVAL;  // 设置错误码
        return;
    }
    printf("Result: %d\n", a / b);
}

// ==================== C++ 自定义异常类 ====================

class InsufficientFundsException : public std::exception
{
private:
    std::string msg;   // 使用 std::string 避免手工内存管理
public:
    explicit InsufficientFundsException(const std::string& m) : msg(m) {}
    const char* what() const noexcept override {
        return msg.c_str();
    }
};

class NegativeAmountException : public std::exception
{
public:
    const char* what() const noexcept override {
        return "Amount cannot be negative";
    }
};

// ==================== 银行账户类 ====================

class BankAccount
{
private:
    double balance;
    std::string owner;

public:
    BankAccount(const std::string& name, double initial) : owner(name), balance(initial) {}

    void deposit(double amount)
    {
        if (amount < 0) {
            throw NegativeAmountException();
        }
        balance += amount;
        std::cout << "Deposited " << amount << ", new balance: " << balance << std::endl;
    }

    void withdraw(double amount)
    {
        if (amount < 0) {
            throw NegativeAmountException();
        }
        if (amount > balance) {
            throw InsufficientFundsException("Insufficient balance");
        }
        balance -= amount;
        std::cout << "Withdrew " << amount << ", new balance: " << balance << std::endl;
    }

    double getBalance() const { return balance; }
    std::string getOwner() const { return owner; }
};

// ==================== 资源管理示例(RAII,使用安全 fopen_s) ====================

class FileGuard
{
private:
    FILE* fp;
public:
    FileGuard(const char* filename, const char* mode) : fp(nullptr) {
        errno_t err = fopen_s(&fp, filename, mode);
        if (err != 0 || fp == nullptr) {
            // 出错时抛出异常,构造函数不返回
            throw std::runtime_error("Failed to open file");
        }
    }
    ~FileGuard() {
        if (fp) {
            fclose(fp);
            std::cout << "File closed automatically" << std::endl;
        }
    }
    FILE* get() const { return fp; }
};

// ==================== main ====================

int main()
{
    std::cout << "========== C风格错误处理 ==========" << std::endl;

    // C风格:检查返回值 + errno,并使用 strerror_s 安全获取错误信息
    errno = 0;
    c_style_divide(10, 0);
    if (errno != 0) {
        char errbuf[256];
        if (strerror_s(errbuf, sizeof(errbuf), errno) == 0) {
            std::cerr << "C style error: " << errbuf << std::endl;
        } else {
            std::cerr << "C style error: unknown" << std::endl;
        }
    }

    c_style_divide(10, 2);
    if (errno != 0) {
        char errbuf[256];
        if (strerror_s(errbuf, sizeof(errbuf), errno) == 0) {
            std::cerr << "C style error: " << errbuf << std::endl;
        }
    }

    std::cout << "\n========== C++ 异常处理 ==========" << std::endl;

    BankAccount account("张三", 1000.0);

    try {
        account.deposit(500);
        account.withdraw(200);
        account.withdraw(2000);  // 余额不足,抛出异常
    } catch (const InsufficientFundsException& e) {
        std::cerr << "Exception caught: " << e.what() << std::endl;
    } catch (const NegativeAmountException& e) {
        std::cerr << "Exception caught: " << e.what() << std::endl;
    } catch (const std::exception& e) {
        std::cerr << "Unknown exception: " << e.what() << std::endl;
    }

    std::cout << "Final balance: " << account.getBalance() << std::endl;

    std::cout << "\n========== RAII 资源管理 ==========" << std::endl;

    try {
        // 文件在栈上分配,异常发生时自动析构关闭
        FileGuard fg("test.txt", "w");
        fprintf(fg.get(), "Hello, World!\n");
        // 即使这里抛出异常,fg 的析构函数也会被调用
        throw std::runtime_error("Something went wrong");
    } catch (const std::exception& e) {
        std::cerr << "Caught: " << e.what() << std::endl;
    }

    // 注意:如果 FileGuard 构造函数中 fopen_s 失败,它会直接抛异常,
    // 不会产生未关闭的文件句柄

    std::cout << "\n========== 多个 catch 块 ==========" << std::endl;

    try {
        account.withdraw(-100);  // 负数金额
    } catch (const NegativeAmountException& e) {
        std::cerr << "Negative amount: " << e.what() << std::endl;
    } catch (const InsufficientFundsException& e) {
        std::cerr << "Insufficient: " << e.what() << std::endl;
    } catch (...) {
        std::cerr << "Unknown exception caught" << std::endl;
    }

    return 0;
}

运行结果:

复制代码
========== C风格错误处理 ==========
C style error: Invalid argument
Result: 5

========== C++ 异常处理 ==========
Deposited 500, new balance: 1500
Withdrew 200, new balance: 1300
Exception caught: Insufficient balance
Final balance: 1300

========== RAII 资源管理 ==========
File closed automatically
Caught: Something went wrong

========== 多个 catch 块 ==========
Negative amount: Amount cannot be negative

这个示例展示了:

  1. C 风格 :通过 errnostrerror_s() 安全地传递错误信息
  2. 自定义异常类 :继承 std::exception,使用 std::string 存储消息,避免手工内存操作
  3. try-catch-throw:基本异常处理流程
  4. 多个 catch 块 :按类型匹配,catch(...) 兜底
  5. RAIIFileGuard 类在析构时自动释放资源,即使发生异常也能保证资源不泄漏,且使用 fopen_s 保证文件操作安全

总结

  • C 语言的错误处理靠返回值和 errno,简单直接但没有强制约束
  • assert 适合调试阶段捕获逻辑错误
  • setjmp/longjmp 能在 C 中模拟跨函数跳转,但可读性差,慎用
  • C++ 的异常机制让错误处理代码和业务逻辑分离,但需要理解栈展开和异常安全
  • RAII 是 C++ 资源管理的核心,配合异常机制使用效果最好
  • 自定义异常类应该继承 std::exception,提供有意义的错误信息
  • 在 Windows 环境下优先使用 _s 安全函数,或定义 _CRT_SECURE_NO_WARNINGS 来兼容旧代码

没有哪种方式是万能的。根据项目规模、性能要求和团队习惯,选择合适的错误处理策略才是正道。

相关推荐
啊啊啊迈 旋棍8 分钟前
【译】TypeScript 7 测试版已在 Visual Studio 2026 18.6 Insiders 3 中默认启用
ubuntu·typescript·visual studio
ziguo112243 分钟前
深入浅出 C/C++ 数据类型:从入门到踩坑
linux·c语言·c++·windows·visual studio
无垠的广袤1 小时前
【工业树莓派 CM0 Dev Board】扩展板设计
linux·python·嵌入式硬件·pcb设计·模块化·传感器
白色冰激凌1 小时前
[SECS/GEM研究] (三)SECS-I 串口上消息怎么分块和重传
c++·secs/gem
会编程的土豆1 小时前
MySQL 入门:库、表、行、主键是什么
linux·数据库·网络协议·http
光头闪亮亮1 小时前
Fyne ( go跨平台GUI )项目实战-项目开发必备基础知识(中)
android·c++·go
光头闪亮亮2 小时前
Fyne ( go跨平台GUI )项目实战-项目开发必备基础知识(下)
android·c++·go
阿扬ABCD2 小时前
ipmitool的Windows版本编译
windows·驱动开发
_wyt0012 小时前
洛谷 P7912 [CSP-J 2021] 小熊的果篮 题解
c++·队列
△曉風殘月〆2 小时前
如何在Linux中安装Qt开发环境
linux·运维·qt