C++ 异常处理:搞定程序崩溃与异常安全的核心指南

目录

​编辑

引言

第一部分:异常处理基础

1.1 什么是异常?

1.2 基本语法

1.3 异常的传播

第二部分:标准异常与自定义异常

2.1 标准异常

2.2 自定义异常

第三部分:异常安全与RAII

3.1 异常安全保证

3.2 RAII与异常安全

3.3 自定义RAII类

第四部分:异常的性能与注意事项

4.1 性能开销

4.2 注意事项

第五部分:异常处理的最佳实践

5.1 异常 vs 返回值

5.2 异常安全的函数设计

5.3 集中异常处理

第六部分:高级主题

6.1 异常与模板

6.2 异常与多线程

6.3 异常与异步

6.4 自定义异常层次

第七部分:调试与工具

7.1 GDB

7.2 ASan

7.3 日志

第八部分:C++标准演进

常见错误与教训

结语


 

class 卑微码农:
    def __init__(self):
        self.技能 = ['能读懂十年前祖传代码', '擅长用Ctrl+C/V搭建世界', '信奉"能跑就别动"的玄学']
        self.发量 = 100  # 初始发量
        self.咖啡因耐受度 = '极限'
        
    def 修Bug(self, bug):
        try:
            # 试图用玄学解决问题
            if bug.严重程度 == '离谱':
                print("这一定是环境问题!")
            else:
                print("让我看看是谁又没写注释...哦,是我自己。")
        except Exception as e:
            # 如果try块都救不了,那就...
            print("重启一下试试?")
            self.发量 -= 1  # 每解决一个bug,头发-1
 
 
# 实例化一个我
我 = 卑微码农()

引言

异常处理是C++里绕不过的一环。C++不像Java强制要求处理异常,但用好了能让程序更健壮,用不好就是一堆未定义行为(UB)。我这些年从手动检查返回值到拥抱try-catch,再到异常安全的代码设计,算是趟出了一条路。异常处理的核心是“出错时优雅退出”,而不是让程序炸掉。好了,废话不多说,开始正题!

第一部分:异常处理基础

1.1 什么是异常?

异常是程序运行时发生的意外情况,比如文件打不开、内存分配失败、除零错误。C++用异常机制让你捕获和处理这些问题,而不是直接崩溃。

C++的异常处理基于三关键字:trycatchthrow

  • try:包裹可能抛异常的代码。
  • catch:捕获并处理异常。
  • throw:抛出异常对象。

异常可以是任何类型(int、string、自定义类),但建议用std::exception或其派生类,因为标准且好用。

为什么用异常? 比起返回值检查,异常能跳出调用栈,直接到能处理的地方,代码更简洁。早年我用返回值检查文件操作,结果代码嵌套七八层,维护简直噩梦。

1.2 基本语法

最简单的异常处理长这样:

#include <iostream>
using namespace std;

int main() {
    try {
        throw 42; // 抛个整数异常
    } catch(int e) {
        cout << "Caught int: " << e << endl;
    }
    return 0;
}

输出:Caught int: 42

try里抛异常,catch捕获对应类型。如果不捕获,程序调用std::terminate(),直接退出。

我第一次用异常是在读文件时,忘了catch,程序崩了。后来才明白,异常要么处理,要么传上去。

1.3 异常的传播

异常会沿着调用栈向上找catch,没找到就terminate。可以用多级catch捕获不同类型。

示例:多级catch。

#include <iostream>
#include <string>
using namespace std;

void func() {
    throw string("Error in func!");
}

int main() {
    try {
        func();
    } catch(int e) {
        cout << "Caught int: " << e << endl;
    } catch(const string& s) {
        cout << "Caught string: " << s << endl;
    } catch(...) { // 捕获所有
        cout << "Caught unknown" << endl;
    }
    return 0;
}

输出:Caught string: Error in func!

经验:总是放个catch(...)兜底,避免漏掉异常。我在服务器项目里加了这个,救了不少崩溃。

第二部分:标准异常与自定义异常

2.1 标准异常

C++标准库提供了std::exception(在<stdexcept>),常用派生类:

  • std::logic_error:逻辑错误,如invalid_argument、out_of_range。
  • std::runtime_error:运行时错误,如overflow_error、bad_alloc。

示例:用标准异常。

#include <iostream>
#include <stdexcept>
using namespace std;

double divide(double a, double b) {
    if(b == 0) throw runtime_error("Divide by zero!");
    return a / b;
}

int main() {
    try {
        cout << divide(10, 0) << endl;
    } catch(const runtime_error& e) {
        cout << "Error: " << e.what() << endl;
    }
    return 0;
}

输出:Error: Divide by zero!

标准异常的what()返回错误描述。bad_alloc常用于new失败。

我用runtime_error处理网络超时,logic_error检查参数。

2.2 自定义异常

复杂项目需要自定义异常,继承std::exception。

示例:自定义文件异常。

#include <iostream>
#include <stdexcept>
using namespace std;

class FileError : public runtime_error {
public:
    FileError(const string& msg) : runtime_error(msg) {}
};

void openFile(const string& filename) {
    if(filename.empty()) throw FileError("Empty filename");
    // 模拟文件操作
    throw FileError("File not found: " + filename);
}

int main() {
    try {
        openFile("");
    } catch(const FileError& e) {
        cout << "File error: " << e.what() << endl;
    } catch(const exception& e) {
        cout << "Standard error: " << e.what() << endl;
    } catch(...) {
        cout << "Unknown error" << endl;
    }
    return 0;
}

输出:File error: Empty filename

自定义异常清晰,我在数据库模块用过,区分不同错误类型。

第三部分:异常安全与RAII

3.1 异常安全保证

异常可能导致资源泄漏,如new了内存但没delete。异常安全有三级别:

  • 基本保证:异常后程序状态合法,可能丢数据。
  • 强保证:异常后状态不变(回滚)。
  • 不抛异常:函数不抛异常(noexcept)。

示例:资源泄漏问题。

void badFunc() {
    int* p = new int[100];
    throw runtime_error("Oops"); // p没delete
    delete[] p;
}

修复用RAII(资源获取即初始化)。

3.2 RAII与异常安全

RAII用对象生命周期管理资源,如智能指针、文件句柄。异常发生,析构自动清理。

示例:用unique_ptr。

#include <iostream>
#include <memory>
using namespace std;

void safeFunc() {
    unique_ptr<int[]> p(new int[100]);
    throw runtime_error("Safe throw"); // p自动delete
}

int main() {
    try {
        safeFunc();
    } catch(const exception& e) {
        cout << e.what() << endl;
    }
    return 0;
}

输出:Safe throw

unique_ptr自动释放。我在多线程服务器用RAII管理锁,异常也不漏。

3.3 自定义RAII类

封装资源,如文件。

示例

#include <iostream>
#include <fstream>
using namespace std;

class FileHandle {
    FILE* fp;
public:
    FileHandle(const char* name) : fp(fopen(name, "r")) {
        if(!fp) throw runtime_error("Open failed");
    }
    ~FileHandle() {
        if(fp) fclose(fp);
        cout << "File closed" << endl;
    }
    // 禁用拷贝
    FileHandle(const FileHandle&) = delete;
    FileHandle& operator=(const FileHandle&) = delete;
};

int main() {
    try {
        FileHandle fh("test.txt");
        throw runtime_error("Error after open");
    } catch(const exception& e) {
        cout << e.what() << endl;
    }
    return 0;
}

输出(假设文件不存在):Error: Open failed 或(文件存在):Error after open File closed

RAII让我少写一堆清理代码。

第四部分:异常的性能与注意事项

4.1 性能开销

异常的抛出和捕获比返回值检查慢,因为涉及栈展开、对象拷贝。

测试

#include <iostream>
#include <chrono>
using namespace std;

void throwFunc(int n) {
    if(n < 0) throw runtime_error("Negative");
}

int returnFunc(int n) {
    return n < 0 ? -1 : 0;
}

int main() {
    auto start = chrono::high_resolution_clock::now();
    for(int i=0; i<1000000; ++i) {
        try {
            throwFunc(i);
        } catch(...) {}
    }
    auto end = chrono::high_resolution_clock::now();
    cout << "Exception: " << chrono::duration_cast<chrono::milliseconds>(end - start).count() << "ms" << endl;

    start = chrono::high_resolution_clock::now();
    for(int i=0; i<1000000; ++i) {
        returnFunc(i);
    }
    end = chrono::high_resolution_clock::now();
    cout << "Return: " << chrono::duration_cast<chrono::milliseconds>(end - start).count() << "ms" << endl;

    return 0;
}

输出(因机器而异):Exception: 200ms Return: 10ms

异常适合“异常”情况,别用作控制流。

我曾用异常做循环退出,结果性能崩了,改回if-else。

4.2 注意事项

  • 异常规格(C++11前):throw(Type)限制抛出类型,C++11弃用,改用noexcept。
  • noexcept:声明不抛异常,优化性能。
void noThrow() noexcept {
    // 编译器优化
}
  • 栈展开:异常抛出析构局部对象,小心析构再抛异常。
  • 异常对象拷贝:抛对象可能拷贝,建议throw by value, catch by const ref。
  • 多线程:线程抛异常只能本线程catch。

我在线程池用noexcept函数,减少开销。

第五部分:异常处理的最佳实践

5.1 异常 vs 返回值

  • 异常:罕见错误,跨多层调用,如文件丢失。
  • 返回值:常见情况,如用户输入错误。

示例:混合使用。

#include <iostream>
#include <stdexcept>
using namespace std;

int parseNumber(const string& s) {
    try {
        return stoi(s); // 抛invalid_argument
    } catch(const invalid_argument&) {
        return -1; // 转为返回值
    }
}

int main() {
    cout << parseNumber("123") << endl; // 123
    cout << parseNumber("abc") << endl; // -1
    return 0;
}

5.2 异常安全的函数设计

  • 强保证:用RAII,临时对象。
  • 基本保证:检查状态后操作。

示例:异常安全的swap。

#include <iostream>
#include <memory>
using namespace std;

class SafeArray {
    unique_ptr<int[]> data;
    size_t size;
public:
    SafeArray(size_t n) : data(new int[n]), size(n) {}
    void swap(SafeArray& other) noexcept {
        data.swap(other.data);
        std::swap(size, other.size);
    }
};

int main() {
    SafeArray a(5), b(10);
    a.swap(b); // 异常安全
    return 0;
}

5.3 集中异常处理

顶层catch,记录日志。

示例

#include <iostream>
#include <fstream>
using namespace std;

void process() {
    throw runtime_error("Processing failed");
}

int main() {
    ofstream log("error.log");
    try {
        process();
    } catch(const exception& e) {
        log << "Error: " << e.what() << endl;
        cout << "Logged error" << endl;
    }
    return 0;
}

输出:Logged error

我在服务器用这种方式统一记录。

第六部分:高级主题

6.1 异常与模板

模板函数可能抛未知异常,用noexcept(false)。

示例

template<typename T>
T process(T x) noexcept(false) {
    if(x < 0) throw runtime_error("Negative");
    return x;
}

6.2 异常与多线程

线程抛异常需本线程catch,主线程无法捕获。

示例

#include <iostream>
#include <thread>
using namespace std;

void worker() {
    try {
        throw runtime_error("Thread error");
    } catch(const exception& e) {
        cout << "Caught in thread: " << e.what() << endl;
    }
}

int main() {
    thread t(worker);
    t.join();
    return 0;
}

输出:Caught in thread: Thread error

C++20 jthread自动join,简化。

6.3 异常与异步

std::future捕获异常。

示例

#include <iostream>
#include <future>
using namespace std;

int asyncTask() {
    throw runtime_error("Async error");
}

int main() {
    auto fut = async(launch::async, asyncTask);
    try {
        fut.get();
    } catch(const exception& e) {
        cout << e.what() << endl;
    }
    return 0;
}

输出:Async error

6.4 自定义异常层次

复杂系统用异常继承体系。

示例

class DatabaseError : public runtime_error {
public:
    DatabaseError(const string& msg) : runtime_error(msg) {}
};

class ConnectionError : public DatabaseError {
public:
    ConnectionError(const string& msg) : DatabaseError(msg) {}
};

class QueryError : public DatabaseError {
public:
    QueryError(const string& msg) : DatabaseError(msg) {}
};

分层处理不同错误。

第七部分:调试与工具

7.1 GDB

用gdb捕获异常点,break std::exception::what。

7.2 ASan

AddressSanitizer查内存问题,可能暴露异常引发的泄漏。

7.3 日志

记录异常栈,C++20有source_location。

示例

#include <iostream>
#include <source_location>
using namespace std;

void logError(const string& msg, const source_location loc = source_location::current()) {
    cout << loc.function_name() << ":" << loc.line() << " - " << msg << endl;
}

int main() {
    try {
        throw runtime_error("Test error");
    } catch(const exception& e) {
        logError(e.what());
    }
    return 0;
}

输出类似:main:12 - Test error

第八部分:C++标准演进

  • C++98:基本try-catch,std::exception。
  • C++11:noexcept,exception_ptr。
  • C++17:std::uncaught_exceptions()。
  • C++20:source_location。

我从C++98到20,异常越来越好用。

常见错误与教训

  • 忘catch,程序terminate。
  • catch(...)不记录,查不出原因。
  • 析构抛异常,触发terminate。
  • 异常对象拷贝开销大,throw by value。
  • 多线程异常未捕获。

教训:多用RAII,少裸new,顶层catch。

结语

C++异常处理是健壮程序的基石。我从踩坑到熟练,靠的是多练和总结。异常不是万能药,但用对地方能救命。建议新手从标准异常练起,老手关注异常安全和性能。欢迎评论分享你的坑!

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值