C++ 异常处理与调试面试题

1. C++的异常处理机制是什么?

答案:

  • 基本语法
try {
    // 可能抛出异常的代码
    throw runtime_error("Error occurred");
} catch (const exception& e) {
    // 处理异常
    cout << e.what() << endl;
} catch (...) {
    // 捕获所有异常
}
  • 异常类层次exception(基类)logic_error:逻辑错误invalid_argumentout_of_rangelength_errorruntime_error:运行时错误overflow_errorunderflow_errorrange_errorbad_alloc:内存分配失败bad_cast:类型转换失败
  • 异常传播沿调用栈向上传播直到被捕获或到达main未捕获异常调用terminate()
  • 栈展开(Stack Unwinding)异常抛出后,自动调用局部对象的析构函数保证资源正确释放RAII的重要性

2. 什么时候应该使用异常?

答案:

  • 适合使用异常错误无法在当前层级处理构造函数失败(无返回值)运算符重载失败深层调用栈的错误传播异常情况,非正常流程
  • 不适合使用异常正常的控制流程性能关键代码可预期的错误(用返回值)析构函数中(可能导致程序终止)
  • 异常 vs 返回值
// 返回值方式
int divide(int a, int b, int& result) {
    if (b == 0) return -1;  // 错误码
    result = a / b;
    return 0;  // 成功
}

// 异常方式
int divide(int a, int b) {
    if (b == 0) throw invalid_argument("Division by zero");
    return a / b;
}
  • 异常的优势错误处理与正常逻辑分离强制处理错误自动资源清理
  • 异常的劣势性能开销代码流程不直观可能被忽略

3. noexcept的作用是什么?

答案:

  • 定义声明函数不抛出异常C++11引入,替代throw()
  • 语法
void func() noexcept;  // 不抛异常
void func() noexcept(true);  // 不抛异常
void func() noexcept(false);  // 可能抛异常

// 条件noexcept
template<typename T>
void swap(T& a, T& b) noexcept(noexcept(T(std::move(a)))) {
    T temp(std::move(a));
    a = std::move(b);
    b = std::move(temp);
}
  • 作用编译器优化移动构造函数应该标记noexcept容器操作更高效(如vector扩容)
  • 违反noexcept如果noexcept函数抛异常调用std::terminate()程序终止
  • 使用建议移动构造和移动赋值标记noexcept析构函数默认noexceptswap函数标记noexcept确保不会抛异常再使用

4. 如何自定义异常类?

答案:

  • 继承std::exception
class MyException : public std::exception {
    string message;
public:
    MyException(const string& msg) : message(msg) {}
    
    const char* what() const noexcept override {
        return message.c_str();
    }
};
  • 使用示例
void process(int value) {
    if (value < 0) {
        throw MyException("Value must be non-negative");
    }
}

try {
    process(-1);
} catch (const MyException& e) {
    cout << "Error: " << e.what() << endl;
}
  • 最佳实践继承标准异常类重写what()方法what()标记为noexcept提供有意义的错误信息

5. 异常安全性的三个级别是什么?

答案:

  • 基本保证(Basic Guarantee)异常发生后,对象仍处于有效状态不泄漏资源可能改变对象状态
  • 强保证(Strong Guarantee)操作成功或完全回滚异常发生后,状态不变类似事务的原子性
  • 不抛异常保证(No-throw Guarantee)操作不会抛出异常使用noexcept标记析构函数、swap等
  • 实现强保证的技巧
class Widget {
    vector<int> data;
public:
    void update(const vector<int>& newData) {
        vector<int> temp = newData;  // 可能抛异常
        data.swap(temp);  // 不抛异常
    }
};
  • copy-and-swap惯用法
Widget& operator=(const Widget& other) {
    Widget temp(other);  // 拷贝
    swap(temp);  // 交换,不抛异常
    return *this;
}

6. 如何调试C++程序?

答案:

  • 调试器(GDB/LLDB/Visual Studio)设置断点单步执行查看变量调用栈条件断点
  • 打印调试
#define DEBUG
#ifdef DEBUG
    #define LOG(x) cout << #x << " = " << (x) << endl
#else
    #define LOG(x)
#endif

LOG(variable);
  • 断言
#include <cassert>
assert(ptr != nullptr);
assert(size > 0 && "Size must be positive");
  • 静态断言(C++11)
static_assert(sizeof(int) == 4, "int must be 4 bytes");
  • 内存调试工具Valgrind:内存泄漏检测AddressSanitizer:内存错误检测MemorySanitizer:未初始化内存ThreadSanitizer:数据竞争

7. 常见的内存错误有哪些?

答案:

  • 内存泄漏分配的内存未释放使用智能指针避免
  • 野指针指向已释放的内存使用后置nullptr
  • 重复释放同一内存释放多次导致未定义行为
  • 数组越界访问数组范围外的元素使用vector和at()
  • 未初始化内存使用未初始化的变量导致不确定行为
  • 悬空引用引用已销毁的对象注意对象生命周期
  • 检测方法
// 使用智能指针
unique_ptr<int> p(new int(10));

// 使用容器
vector<int> vec(10);
vec.at(5) = 10;  // 边界检查

// 初始化
int x = 0;
int* ptr = nullptr;

8. 如何进行性能分析?

答案:

  • 性能分析工具gprof:GNU性能分析器perf:Linux性能工具Valgrind Callgrind:调用图分析Visual Studio Profiler
  • 时间测量
#include <chrono>

auto start = chrono::high_resolution_clock::now();
// 执行代码
auto end = chrono::high_resolution_clock::now();
auto duration = chrono::duration_cast<chrono::milliseconds>(end - start);
cout << "Time: " << duration.count() << "ms" << endl;
  • 性能优化技巧避免不必要的拷贝(使用引用、移动)使用合适的数据结构减少内存分配缓存友好的数据布局编译器优化选项(-O2、-O3)
  • 微基准测试
// 使用Google Benchmark
static void BM_Function(benchmark::State& state) {
    for (auto _ : state) {
        // 测试代码
    }
}
BENCHMARK(BM_Function);

9. 如何处理资源泄漏?

答案:

  • RAII原则构造函数获取资源析构函数释放资源利用对象生命周期管理资源
  • 智能指针
// 自动管理内存
unique_ptr<int> p(new int(10));
shared_ptr<FILE> fp(fopen("file.txt", "r"), fclose);
  • 自定义资源管理类
class FileHandle {
    FILE* fp;
public:
    FileHandle(const char* filename) {
        fp = fopen(filename, "r");
        if (!fp) throw runtime_error("Cannot open file");
    }
    
    ~FileHandle() {
        if (fp) fclose(fp);
    }
    
    // 禁止拷贝
    FileHandle(const FileHandle&) = delete;
    FileHandle& operator=(const FileHandle&) = delete;
};
  • 标准库资源管理fstream:文件lock_guard:互斥锁unique_ptr:动态内存

10. 如何编写可测试的代码?

答案:

  • 单元测试框架Google TestCatch2Boost.Test
  • 测试示例
#include <gtest/gtest.h>

int add(int a, int b) {
    return a + b;
}

TEST(AddTest, PositiveNumbers) {
    EXPECT_EQ(add(1, 2), 3);
    EXPECT_EQ(add(10, 20), 30);
}

TEST(AddTest, NegativeNumbers) {
    EXPECT_EQ(add(-1, -2), -3);
}
  • 可测试代码的特点函数职责单一依赖注入避免全局状态接口与实现分离
  • 依赖注入示例
// 不好:依赖具体实现
class Service {
    Database db;
public:
    void process() {
        db.query();
    }
};

// 好:依赖抽象
class Service {
    IDatabase& db;
public:
    Service(IDatabase& database) : db(database) {}
    void process() {
        db.query();
    }
};
  • 测试驱动开发(TDD)先写测试再写实现重构代码保证代码质量
C++面试总结 文章被收录于专栏

本专栏系统梳理C++面试高频考点,从基础语法、内存管理、STL与设计模式,到操作系统与项目实战,结合真实面试题深度解析,帮助开发者高效查漏补缺,提升技术理解与面试通过率,打造扎实的C++工程能力。

全部评论

相关推荐

评论
点赞
收藏
分享

创作者周榜

更多
牛客网
牛客网在线编程
牛客网题解
牛客企业服务