引言

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

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐