C++ 错误处理完全指南

一、错误分类概述

C++ 错误按阶段分为三大类:

阶段 错误类型 发现时机 典型表现
编译期 语法错误、类型错误、模板错误 编译时 编译器输出错误信息
链接期 未定义引用、重复定义 链接时 undefined reference
运行期 内存错误、异常、逻辑错误 程序运行时 崩溃、死循环、错误结果

核心原则:编译期错误是语法问题,链接期错误是组织问题,运行期错误是逻辑问题。


二、编译期错误详解

2.1 语法错误

编译器无法理解代码结构。

典型案例1:中英文标点混用

#include<iostream>
using namespace std;  // 中文分号

int main() {
    cout << "Hello";   // 中文分号
    return 0;
}

报错信息:

error: stray '\243' in program
error: expected ';' before 'return'

解决方案:

  • 始终使用英文输入法
  • IDE 中开启"显示空白符"
  • 使用 clang-format 格式化工具

典型案例2:缺少分号

int a = 10          // 缺少分号
int b = 20;

struct Node {
    int val;
    Node* next
}                   // 结构体定义后缺少分号

报错信息:

error: expected ';' before 'int'
error: expected ';' after struct definition

典型案例3:括号不匹配

if (a > 0 {         // 缺少右括号
    cout << a;
}

报错信息:

error: expected ')' before '{' token

2.2 类型错误

类型系统是 C++ 的核心特性,也是错误高发区。

典型案例1:类型不兼容

char c = "A";              // 双引号是字符串
string s = 'hello';        // 单引号是字符
int* p = 10;               // 指针不能直接赋整数值
double d = 5 / 2;          // 整数除法得 2,不是 2.5

报错信息:

error: invalid conversion from 'const char*' to 'char'
error: invalid conversion from 'int' to 'int*'

正确写法:

char c = 'A';              // 单引号
string s = "hello";        // 双引号
int* p = nullptr;          // 空指针
double d = 5.0 / 2;        // 浮点数除法

典型案例2:const 正确性

const int MAX = 100;
int* p = &MAX;             // 丢弃 const 限定符

const char* str = "hello";
char* p = str;             // 丢弃 const 限定符

报错信息:

error: invalid conversion from 'const int*' to 'int*'

2.3 声明与作用域错误

典型案例1:未声明的标识符

int main() {
    cout << x << endl;     // x 未声明
    return 0;
}

报错信息:

error: 'x' was not declared in this scope

解决方案:

#include <iostream>
using namespace std;

int main() {
    int x = 10;
    cout << x << endl;
    return 0;
}

典型案例2:重复定义

头文件问题:

// utils.h
int global_var = 100;      // 在多个 .cpp 中包含会重复定义

正确做法:

// utils.h
extern int global_var;     // 仅声明
// utils.cpp
int global_var = 100;      // 在一个 cpp 中定义

解决方案:

  • 使用 #pragma once 或头文件守卫
  • 全局变量使用 extern
  • 常量使用 constexpr 替代 #define

2.4 模板错误

模板错误信息通常很长,但核心问题往往简单。

典型案例1:模板嵌套的 >> 问题

priority_queue<int, vector<int>, greater<int>> q;  
// C++98/03 中 >> 会被识别为右移运算符

报错信息:

error: '>>' should be '> >' within a nested template argument list

解决方案:

// C++03 需要加空格
priority_queue<int, vector<int>, greater<int> > q;
// C++11 及以上可直接使用

典型案例2:依赖名称需要 typename

template<typename T>
void func() {
    T::iterator it;        // iterator 是依赖名称
}

报错信息:

error: need 'typename' before 'T::iterator' because 'T' is a dependent scope

解决方案:

template<typename T>
void func() {
    typename T::iterator it;  // 加 typename
}

2.5 编译错误速查表

错误信息关键词 问题定位 快速检查
stray 非法字符 检查是否有中文标点
expected ';' 缺少分号 检查上一行末尾
expected ')' 括号不匹配 检查括号配对
was not declared 未声明 检查拼写、作用域、头文件
invalid conversion 类型不匹配 检查类型一致性
no matching function 函数重载不匹配 检查参数类型和个数
redefinition 重复定义 检查头文件守卫

三、链接错误

编译通过但链接失败,通常是多文件组织问题。

3.1 未定义引用

最常见也最容易让人困惑的错误。

// math.h
int add(int a, int b);    // 声明

// math.cpp(忘记实现)
// int add(int a, int b) { return a + b; }

// main.cpp
#include "math.h"
int main() {
    int result = add(3, 5);  // 链接错误
    return 0;
}

报错信息:

undefined reference to `add(int, int)'
collect2: error: ld returned 1 exit status

解决方案:

  1. 实现所有声明的函数
  2. 编译时包含所有源文件:g++ main.cpp math.cpp -o program
  3. 检查静态库顺序(依赖库放后面)

3.2 虚函数表错误

class Base {
public:
    virtual void func();   // 声明虚函数
};

class Derived : public Base {
    // 忘记实现 func
};

报错信息:

undefined reference to `vtable for Base'
undefined reference to `Base::func()'

解决方案:实现所有虚函数,纯虚函数加上 = 0


四、运行时错误

4.1 内存错误

最危险的错误类型,可能导致安全漏洞。

段错误典型案例

int* p = nullptr;
*p = 10;                 // 解引用空指针

int arr[5];
arr[10] = 100;           // 数组越界

int* p = new int(10);
delete p;
*p = 20;                 // 使用已释放的内存

void recursive(int n) {
    recursive(n + 1);    // 无限递归导致栈溢出
}

排查工具:

# AddressSanitizer
g++ -fsanitize=address -g main.cpp
./a.out

# Valgrind
valgrind --leak-check=full ./a.out

# GDB
gdb ./a.out
run
bt  # 查看调用栈

内存泄漏

while (true) {
    int* p = new int[1000];  // 永不释放
}

检测工具:Valgrind、AddressSanitizer、智能指针。

4.2 标准库异常

异常类型 触发场景 预防措施
std::out_of_range vector::at() 越界 检查索引范围
std::bad_alloc 内存分配失败 检查内存使用
std::bad_cast dynamic_cast 失败 检查类型转换
std::invalid_argument 参数无效 参数校验
std::runtime_error 通用运行时错误 查看错误信息

4.3 逻辑错误

编译运行都正常,但结果不对。

// 整数除法
double ratio = 1 / 2;          // 得到 0.0
double ratio = 1.0 / 2;        // 得到 0.5

// 赋值与比较混淆
if (a = 5) { /* 永远为真 */ }  // 赋值
if (a == 5) { /* 正确比较 */ }

// 短路求值误用
if (ptr->data == 5 && ptr != nullptr) { }  // 可能崩溃
if (ptr != nullptr && ptr->data == 5) { }  // 正确

// 循环边界错误
for (int i = 0; i <= n; i++) { }  // 多循环一次
for (int i = 0; i < n; i++) { }   // 正确

五、调试工具链

5.1 编译器警告选项

# 开发阶段必须开启
g++ -Wall -Wextra -Werror -pedantic main.cpp

# 各选项含义
-Wall       # 常用警告
-Wextra     # 额外警告
-Werror     # 警告视为错误
-pedantic   # 严格 ISO C++ 标准
-g          # 调试信息
-O0         # 关闭优化

5.2 调试工具对比

工具 适用场景 优点 缺点
GDB 通用调试 功能强大 命令行学习曲线陡峭
Visual Studio Debugger Windows 开发 可视化 仅限 Windows
AddressSanitizer 内存错误检测 速度快 需要重新编译
Valgrind 内存泄漏检测 无需重新编译 运行速度慢
IDE 调试器 日常开发 可视化断点 依赖于 IDE

5.3 调试实践

// 使用断言
#include <cassert>
void process(int* p) {
    assert(p != nullptr);
}

// 调试日志
#define DEBUG 1
#ifdef DEBUG
    #define LOG(x) cout << "[DEBUG] " << x << endl
#else
    #define LOG(x)
#endif

// 异常处理
try {
    // 可能出错的代码
} catch (const std::exception& e) {
    cerr << "Error: " << e.what() << endl;
}