引言
在程序运行过程中,错误是不可避免的。内存分配失败、文件不存在、网络连接中断、除数为零......这些运行时错误如果处理不当,轻则导致程序崩溃,重则造成数据丢失甚至系统故障。
C 语言处理错误的方式相对原始,主要依赖返回值和全局变量 errno。这种方式虽然简单,但存在明显缺陷:错误处理代码与正常业务逻辑混杂在一起,降低了代码的可读性和可维护性。
C++ 引入了异常处理机制,提供了一种结构化的错误处理方式,将错误检测与错误处理分离开来,使代码更加清晰、健壮。

第一部分:C 语言的错误处理回顾
一、常见错误处理方式
1. 返回值判断
cpp
#include <stdlib.h>
#include <stdio.h>
// malloc 失败返回 NULL
int *p = (int*)malloc(sizeof(int) * 100);
if (p == NULL) {
printf("内存分配失败\n");
return -1;
}
2. errno 错误号
cpp
#include <errno.h>
#include <string.h>
#include <fcntl.h>
int fd = open("nonexistent.txt", O_RDONLY);
if (fd == -1) {
printf("错误号: %d\n", errno);
printf("错误信息: %s\n", strerror(errno));
perror("open error");
}
3. assert 断言
cpp
#include <assert.h>
void divide(int a, int b) {
assert(b != 0); // 表达式为假时,发送 SIGABRT 信号终止程序
return a / b;
}
二、C 语言方式的局限性
| 问题 | 说明 |
|---|---|
| 错误检测与处理耦合 | 业务逻辑被大量 if 判断污染 |
| 返回值二义性 | 有些函数的返回值既可能是有效数据,也可能是错误码 |
| 错误传播困难 | 深层调用的错误需要逐层返回,容易遗漏 |
| 无法处理构造函数错误 | C 语言的 struct 没有构造函数 |
第二部分:C++ 异常处理基础
一、异常处理三要素
C++ 异常处理由三个关键字组成:
| 关键字 | 作用 |
|---|---|
throw |
抛出异常 |
try |
检测可能抛出异常的代码块 |
catch |
捕获并处理特定类型的异常 |
二、基本语法
cpp
try {
// 可能抛出异常的代码
throw 异常对象;
}
catch (异常类型1 变量名) {
// 处理类型1的异常
}
catch (异常类型2 变量名) {
// 处理类型2的异常
}
catch (...) {
// 处理所有其他异常
}
三、异常抛出与捕获流程

四、基础示例:除零保护
cpp
#include <iostream>
using namespace std;
int divide(int a, int b) {
if (b == 0)
throw "除数为0"; // 抛出 const char* 类型异常
return a / b;
}
int main() {
int a, b;
while (1) {
cout << "a b: ";
cin >> a >> b;
if (a == 0) break;
try {
cout << divide(a, b) << endl;
}
catch (const char* err) { // 捕获 const char* 异常
cout << "error: " << err << endl;
}
}
cout << "--OK--" << endl;
return 0;
}
关键理解:
-
throw可以是任意类型:int、double、const char*、string、类对象 -
catch的类型必须与throw的类型匹配 -
异常发生时,
try块中剩余的代码被跳过
五、多类型异常捕获
cpp
#include <iostream>
#include <string>
using namespace std;
void testerror(int a, int b, int c) {
if (a > 10) throw a; // 抛出 int 类型
if (b > 5) throw 1.25; // 抛出 double 类型
if (c > 2) throw string("c值不能超出2"); // 抛出 string 类型
cout << "a:" << a << ", b:" << b << ", c:" << c << endl;
}
int main() {
int a, b, c;
while (1) {
cout << "a b c: ";
cin >> a >> b >> c;
if (a == 0) break;
try {
testerror(a, b, c);
}
catch (int e) { // 捕获 int 异常
cout << e << " 的值超过了10" << endl;
}
catch (double e) { // 捕获 double 异常
cout << e << " 的值超过了5" << endl;
}
catch (string e) { // 捕获 string 异常
cout << e << endl;
}
}
cout << "Over" << endl;
return 0;
}
第三部分:自定义异常类
一、基本要求
自定义异常类没有强制要求继承哪个类,但建议遵循以下规范:
| 建议 | 说明 |
|---|---|
| 包含错误信息成员 | string errinfo 存储错误描述 |
| 提供获取方法 | what() 或 error() 返回错误信息 |
继承自 exception |
与 C++ 标准库异常体系统一 |
| 支持虚函数 | 便于多态处理 |
二、基础自定义异常类
cpp
#include <iostream>
#include <string>
using namespace std;
class Exception {
private:
string einfo;
public:
Exception(const string& info) : einfo(info) {
cout << "构造异常对象: " << this << endl;
}
Exception(const Exception& other) {
einfo = other.einfo;
cout << "拷贝构造异常对象: " << this
<< " from " << &other << endl;
}
virtual ~Exception() {
cout << "销毁异常对象: " << this << endl;
}
virtual string what() {
return einfo;
}
};
三、派生异常类
cpp
// 越界异常
class OutOfRangeException : public Exception {
public:
OutOfRangeException() : Exception("下标越界") {}
// 重写 what() 提供更详细的信息
string what() override {
return string("严重错误: ") + Exception::what();
}
};
// 使用示例
void check_index(int index, int size) {
if (index < 0 || index >= size) {
throw OutOfRangeException();
}
}
四、异常对象生命周期
cpp
void test1() {
// 方式1:抛出临时对象(直接存入异常栈)
throw Exception("测试异常1");
// 方式2:抛出局部对象(拷贝到异常栈)
Exception e("测试异常1_1");
throw e; // 这里发生拷贝构造
}
int main() {
try {
test1();
}
catch (Exception& e) { // 建议使用引用捕获
cout << e.what() << endl;
}
return 0;
}
关键理解:

建议 :catch 时使用引用,避免不必要的拷贝。
第四部分:C++ 标准异常体系
一、异常类层次结构

二、继承标准异常类
cpp
#include <stdexcept>
#include <string>
#include <cstring>
using namespace std;
class InvalidException : public invalid_argument {
private:
int argV;
public:
InvalidException(int argV, const string& msg)
: argV(argV), invalid_argument(msg) {}
const char* what() const noexcept override {
static char buff[128] = {0};
snprintf(buff, sizeof(buff), "%d %s",
argV, invalid_argument::what());
return buff;
}
};
// 使用
void test(int a) {
if (a < 0 || a > 100)
throw InvalidException(a, "不在取值范围[0, 100]中");
cout << "a: " << a << endl;
}
int main() {
try {
test(20);
test(-1); // 抛出异常
}
catch (invalid_argument& e) { // 捕获派生类
cout << "invalid_argument: " << e.what() << endl;
}
catch (logic_error& e) { // 捕获基类
cout << "logic_error: " << e.what() << endl;
}
catch (exception& e) { // 捕获更基类
cout << "exception: " << e.what() << endl;
}
return 0;
}
三、异常捕获顺序规则

规则 :catch 块按顺序匹配,派生类应放在基类前面,否则基类会先"拦截"所有异常。
第五部分:noexcept 关键字
一、基本语法
cpp
void func() noexcept {
// 此函数承诺不抛出异常
}
二、作用与效果
| 作用 | 说明 |
|---|---|
| 编译器优化 | 编译器知道该函数不会抛出异常,可以生成更优化的代码 |
| 代码文档 | 明确告知调用者该函数是安全的 |
| 强制约束 | 若 noexcept 函数内部抛出异常,程序直接调用 terminate() |
三、noexcept 函数的后果
cpp
#include <iostream>
#include <stdexcept>
using namespace std;
void safe_function() noexcept {
cout << "safe_function() OK" << endl;
// 如果非要抛出异常,程序会 abort
// throw runtime_error("error!"); // 会导致 terminate
}
int main() {
try {
safe_function();
}
catch (exception& e) {
cout << e.what() << endl; // 永远不会执行
}
return 0;
}
如果 noexcept 函数抛出异常:
-
程序调用
std::terminate() -
默认行为是调用
std::abort()终止程序 -
catch块无法捕获该异常
四、建议使用 noexcept 的函数
| 函数类型 | 原因 |
|---|---|
| 构造函数 | 构造失败应使用异常,但成功构造后不应抛出 |
| 析构函数 | C++11 起析构函数默认是 noexcept |
| 简单 getter | 只返回成员变量,不应抛出异常 |
| 移动构造函数 | 通常只是转移资源,不应抛出 |
| swap 函数 | 应保证不抛出 |
cpp
class MyClass {
private:
int value;
public:
MyClass() noexcept : value(0) {} // 构造函数
int getValue() const noexcept { // getter
return value;
}
void setValue(int v) { // setter 可能抛出
if (v < 0)
throw invalid_argument("值不能为负");
value = v;
}
};
第六部分:throw 声明(已废弃)
一、历史演变
cpp
// C++98 动态异常说明(已废弃)
void func1() throw(int, double) { } // 可能抛出 int 或 double
void func2() throw() { } // 不抛出任何异常
// C++11 noexcept(推荐使用)
void func3() noexcept { } // 不抛出异常
void func4() noexcept(false) { } // 可能抛出异常
二、为什么不推荐 throw()
| 问题 | 说明 |
|---|---|
| 运行时检查 | 违反声明时调用 unexpected(),性能差 |
| 模板兼容性差 | 难以推断模板参数的异常类型 |
| C++17 移除 | 只保留 throw() 作为 noexcept 的别名 |
第七部分:异常安全的三个级别
| 级别 | 说明 | 示例 |
|---|---|---|
| 基本保证 | 异常发生时,程序状态仍然有效,无资源泄漏 | 大多数代码 |
| 强保证 | 异常发生时,状态回滚到操作前的状态(原子操作) | 事务操作 |
| 不抛出保证 | 承诺永远不抛出异常 | noexcept 函数 |
cpp
// 基本保证示例
void append_to_file(const string& data) {
ofstream file("data.txt", ios::app);
file << data; // 如果失败,file 析构函数正确关闭
}
// 强保证示例
void update_record(int id, const string& new_data) {
string old_data = get_record(id); // 保存旧数据
try {
set_record(id, new_data);
} catch (...) {
set_record(id, old_data); // 回滚
throw; // 重新抛出
}
}
总结
一、C 与 C++ 错误处理对比
| 对比项 | C 语言 | C++ 异常 |
|---|---|---|
| 错误检测 | 返回值 + if 判断 | try 块 |
| 错误报告 | 返回错误码、设置 errno | throw |
| 错误处理 | 函数内就地处理 | catch 块集中处理 |
| 错误传播 | 逐层返回,容易遗漏 | 自动向上传播(栈展开) |
| 代码清晰度 | 错误处理与业务逻辑混杂 | 正常逻辑与错误处理分离 |
| 资源管理 | 手动清理 | RAII + 栈展开自动清理 |
二、核心知识点速查
| 知识点 | 说明 |
|---|---|
throw |
抛出任意类型的异常 |
try |
包裹可能抛出异常的代码 |
catch(T& e) |
捕获类型为 T 的异常(建议引用) |
catch(...) |
捕获所有异常 |
| 异常栈 | 存储异常对象的安全区域 |
| 栈展开 | 异常发生时,自动销毁栈上的局部对象 |
noexcept |
声明函数不抛出异常 |
exception::what() |
获取异常描述信息 |
| 捕获顺序 | 从派生类到基类 |
三、最佳实践

C++ 异常处理机制是构建健壮程序的重要工具。它让错误检测与错误处理分离,使代码更加清晰;通过栈展开和 RAII 技术,确保异常发生时资源正确释放;通过异常类层次结构,实现灵活的分类处理。
学习建议:
-
先用简单的
throw int和throw string理解异常抛出与捕获流程 -
掌握自定义异常类的编写,特别是继承
std::exception -
理解异常对象的生命周期,养成使用引用捕获的习惯
-
合理使用
noexcept关键字,提升程序性能 -
结合 RAII 技术,编写异常安全的代码