C++运行时错误全集与解决方案

5 阅读13分钟

一、内存相关错误

1. 访问违规 (Access Violations)

空指针解引用

int* ptr = nullptr;
*ptr = 42;  // 运行时错误:访问地址0x0

原因:指针未初始化或已释放

解决方案

// 1. 初始化指针
int* ptr = new int(42);
*ptr = 10;

// 2. 使用前检查
if (ptr != nullptr) {
    *ptr = 10;
}

// 3. 使用智能指针
#include <memory>
auto ptr = std::make_unique<int>(42);
*ptr = 10;  // 安全,ptr永远不会为nullptr

// 4. 使用引用(如果可能)
int value = 42;
int& ref = value;  // 引用必须绑定到有效对象
ref = 10;

野指针 (Dangling Pointers)

int* createInt() {
    int value = 42;
    return &value;  // 返回局部变量地址
}

int* dangling = createInt();  // 指针指向已释放的栈内存
std::cout << *dangling;  // 未定义行为

解决方案

// 1. 避免返回局部变量地址
int* createInt() {
    int* value = new int(42);  // 堆分配
    return value;  // 调用者负责delete
}

// 更好的做法:使用智能指针
std::unique_ptr<int> createInt() {
    return std::make_unique<int>(42);
}

// 2. 避免指针失效
std::vector<int> vec = {1, 2, 3};
int* ptr = &vec[0];
vec.push_back(4);  // 可能导致重新分配
// ptr可能失效,因为vector可能重新分配内存
std::cout << *ptr;  // 危险!

// 正确做法:使用迭代器
auto it = vec.begin();
vec.push_back(4);
// 迭代器失效,需要重新获取
it = vec.begin();  // 重新获取

2. 内存泄漏 (Memory Leaks)

void memoryLeak() {
    int* ptr = new int[1000];
    // 忘记delete[] ptr;
    return;  // 内存泄漏!
}

检测工具

# 使用valgrind检测内存泄漏
valgrind --leak-check=full ./program

# 使用AddressSanitizer
g++ -fsanitize=address -g program.cpp -o program

解决方案

// 1. 使用RAII(Resource Acquisition Is Initialization)
class Resource {
    int* data;
public:
    Resource(size_t size) : data(new int[size]) {}
    ~Resource() { delete[] data; }
    
    // 遵循三五法则
    Resource(const Resource&) = delete;
    Resource& operator=(const Resource&) = delete;
    Resource(Resource&& other) noexcept : data(other.data) {
        other.data = nullptr;
    }
    Resource& operator=(Resource&& other) noexcept {
        if (this != &other) {
            delete[] data;
            data = other.data;
            other.data = nullptr;
        }
        return *this;
    }
};

// 2. 使用智能指针
#include <memory>
void noLeak() {
    // 单个对象
    auto ptr = std::make_unique<int>(42);
    
    // 数组
    auto arr = std::make_unique<int[]>(1000);
    
    // 共享所有权
    auto shared = std::make_shared<int>(42);
    
    // 弱引用
    std::weak_ptr<int> weak = shared;
}  // 自动释放内存

// 3. 使用标准容器
#include <vector>
void useVector() {
    std::vector<int> vec(1000);  // 自动管理内存
    vec.push_back(42);
}  // 自动清理

3. 双重释放 (Double Free)

int* ptr = new int(42);
delete ptr;
delete ptr;  // 双重释放!程序崩溃

解决方案

// 1. 删除后置为nullptr
int* ptr = new int(42);
delete ptr;
ptr = nullptr;  // 安全,delete nullptr是空操作
delete ptr;  // 安全

// 2. 使用智能指针
{
    auto ptr = std::make_unique<int>(42);
}  // 自动释放一次

// 3. 遵循单一所有权原则
class UniqueOwner {
    int* data;
public:
    explicit UniqueOwner(int* ptr) : data(ptr) {}
    ~UniqueOwner() { delete data; }
    
    // 禁止拷贝
    UniqueOwner(const UniqueOwner&) = delete;
    UniqueOwner& operator=(const UniqueOwner&) = delete;
    
    // 允许移动
    UniqueOwner(UniqueOwner&& other) noexcept : data(other.data) {
        other.data = nullptr;
    }
};

4. 内存越界 (Buffer Overflow/Underflow)

int arr[5] = {1, 2, 3, 4, 5};
arr[5] = 6;  // 越界写入
int val = arr[-1];  // 越界读取

解决方案

// 1. 使用std::vector或std::array
#include <vector>
#include <array>

std::vector<int> vec = {1, 2, 3, 4, 5};
// vec[5] = 6;  // 运行时检查(调试版本),但可能不抛出异常
// 正确:使用at()进行边界检查
try {
    vec.at(5) = 6;  // 抛出std::out_of_range
} catch (const std::out_of_range& e) {
    std::cerr << "越界访问: " << e.what() << std::endl;
}

// 2. 使用span(C++20)
#include <span>
void process(std::span<int> data) {
    for (size_t i = 0; i < data.size(); ++i) {
        data[i] = i * 2;  // 自动边界检查
    }
}

// 3. 自己实现边界检查
template<typename T, size_t N>
class SafeArray {
    T data[N];
public:
    T& operator[](size_t index) {
        if (index >= N) {
            throw std::out_of_range("数组索引越界");
        }
        return data[index];
    }
};

二、标准库相关运行时错误

1. STL容器错误

迭代器失效

std::vector<int> vec = {1, 2, 3, 4, 5};
auto it = vec.begin();
while (it != vec.end()) {
    if (*it % 2 == 0) {
        vec.erase(it);  // 错误!erase后迭代器失效
    } else {
        ++it;
    }
}

解决方案

// 正确使用erase-remove惯用法
std::vector<int> vec = {1, 2, 3, 4, 5};
vec.erase(std::remove_if(vec.begin(), vec.end(),
    [](int x) { return x % 2 == 0; }),
    vec.end());

// 或者正确使用erase返回值
auto it = vec.begin();
while (it != vec.end()) {
    if (*it % 2 == 0) {
        it = vec.erase(it);  // erase返回下一个有效迭代器
    } else {
        ++it;
    }
}

// map/set的erase
std::map<int, std::string> m = {{1, "a"}, {2, "b"}};
for (auto it = m.begin(); it != m.end(); /* 不在这里自增 */) {
    if (it->first == 1) {
        it = m.erase(it);  // C++11后erase返回下一个迭代器
    } else {
        ++it;
    }
}

引用失效(特别是vector)

std::vector<int> vec = {1, 2, 3};
int& ref = vec[0];
vec.push_back(4);  // 可能导致重新分配
std::cout << ref;  // 危险!引用可能失效

解决方案

// 1. 避免在修改后使用引用
std::vector<int> vec = {1, 2, 3};
int val = vec[0];  // 复制值,不是引用
vec.push_back(4);
std::cout << val;  // 安全

// 2. 预留足够空间
std::vector<int> vec;
vec.reserve(100);  // 预留容量
int& ref = vec.emplace_back(1);
vec.emplace_back(2);
std::cout << ref;  // 安全,只要不超过reserve的容量

// 3. 使用索引而不是引用
size_t index = 0;
vec.push_back(4);
if (index < vec.size()) {
    std::cout << vec[index];  // 安全
}

2. 字符串相关错误

空字符结尾问题

const char* str = "hello";
char buffer[5];
strncpy(buffer, str, 5);  // 没有空间放空字符
std::cout << buffer;  // 可能输出乱码

// 正确做法
char buffer[6];  // 为'\0'留空间
strncpy(buffer, str, 5);
buffer[5] = '\0';  // 手动添加结尾

现代C++解决方案

#include <string>
#include <cstring>

// 使用std::string
std::string s1 = "hello";
std::string s2 = s1;  // 自动处理内存
std::cout << s2;

// 如果需要C风格字符串
const char* cstr = s1.c_str();  // 有效直到s1被修改
size_t len = s1.length();

// 安全的字符数组操作
char buffer[10];
std::snprintf(buffer, sizeof(buffer), "%s", "hello");  // 自动截断

3. 文件操作错误

#include <fstream>
#include <iostream>

int main() {
    std::ifstream file("nonexistent.txt");
    if (!file) {  // 必须检查!
        std::cerr << "无法打开文件" << std::endl;
        return 1;
    }
    
    int value;
    while (file >> value) {  // 检查读取是否成功
        std::cout << value << std::endl;
    }
    
    if (file.bad()) {
        std::cerr << "I/O错误" << std::endl;
    } else if (file.eof()) {
        std::cout << "文件结束" << std::endl;
    } else if (file.fail()) {
        std::cerr << "类型不匹配" << std::endl;
    }
    
    return 0;
}

三、类型与转换运行时错误

1. dynamic_cast失败

class Base { 
public:
    virtual ~Base() = default;  // 必须有虚函数
};
class Derived : public Base {};
class Other {};

Base* basePtr = new Derived();
Derived* derivedPtr = dynamic_cast<Derived*>(basePtr);  // 成功
Other* otherPtr = dynamic_cast<Other*>(basePtr);  // 返回nullptr

Base* notDerived = new Base();
Derived* badCast = dynamic_cast<Derived*>(notDerived);  // 返回nullptr

解决方案

// 检查指针转换
if (Derived* d = dynamic_cast<Derived*>(basePtr)) {
    // 成功
    d->derivedMethod();
} else {
    // 转换失败
    std::cerr << "转换失败" << std::endl;
}

// 引用转换会抛出异常
try {
    Derived& d = dynamic_cast<Derived&>(*basePtr);
    d.derivedMethod();
} catch (const std::bad_cast& e) {
    std::cerr << "转换失败: " << e.what() << std::endl;
}

2. bad_alloc异常

#include <new>
#include <iostream>

try {
    // 尝试分配过大内存
    size_t huge = static_cast<size_t>(-1);
    int* ptr = new int[huge];  // 抛出std::bad_alloc
} catch (const std::bad_alloc& e) {
    std::cerr << "内存分配失败: " << e.what() << std::endl;
    // 处理内存不足的情况
}

解决方案

// 1. 使用std::nothrow
int* ptr = new(std::nothrow) int[100000000];
if (ptr == nullptr) {
    std::cerr << "内存分配失败" << std::endl;
    // 优雅降级
}

// 2. 预先计算内存需求
size_t calculateNeededMemory(size_t count) {
    if (count > SIZE_MAX / sizeof(int)) {
        throw std::bad_array_new_length();
    }
    return count * sizeof(int);
}

// 3. 使用自定义分配器
class PoolAllocator {
    std::vector<char> pool;
    size_t used = 0;
public:
    explicit PoolAllocator(size_t size) : pool(size) {}
    
    void* allocate(size_t size) {
        if (used + size > pool.size()) {
            return nullptr;
        }
        void* ptr = &pool[used];
        used += size;
        return ptr;
    }
};

四、并发与多线程错误

1. 数据竞争 (Data Race)

#include <thread>
#include <iostream>

int counter = 0;

void increment() {
    for (int i = 0; i < 1000000; ++i) {
        ++counter;  // 数据竞争!
    }
}

int main() {
    std::thread t1(increment);
    std::thread t2(increment);
    t1.join();
    t2.join();
    std::cout << "Counter: " << counter << std::endl;  // 可能不是2000000
    return 0;
}

解决方案

#include <atomic>
#include <mutex>

// 方案1:使用原子操作
std::atomic<int> atomic_counter{0};
void atomic_increment() {
    for (int i = 0; i < 1000000; ++i) {
        ++atomic_counter;  // 线程安全
    }
}

// 方案2:使用互斥锁
std::mutex mtx;
int mutex_counter = 0;
void mutex_increment() {
    for (int i = 0; i < 1000000; ++i) {
        std::lock_guard<std::mutex> lock(mtx);
        ++mutex_counter;
    }
}

// 方案3:使用thread_local(如果每个线程需要自己的副本)
thread_local int thread_counter = 0;
void thread_local_increment() {
    for (int i = 0; i < 1000000; ++i) {
        ++thread_counter;
    }
    std::cout << "Thread counter: " << thread_counter << std::endl;
}

2. 死锁 (Deadlock)

#include <mutex>
#include <thread>

std::mutex mtx1, mtx2;

void thread1() {
    std::lock_guard<std::mutex> lock1(mtx1);
    std::this_thread::sleep_for(std::chrono::milliseconds(100));
    std::lock_guard<std::mutex> lock2(mtx2);  // 可能阻塞
    // ...
}

void thread2() {
    std::lock_guard<std::mutex> lock2(mtx2);
    std::this_thread::sleep_for(std::chrono::milliseconds(100));
    std::lock_guard<std::mutex> lock1(mtx1);  // 可能阻塞
    // ...
}

解决方案

// 方案1:总是按相同顺序加锁
void safe_thread1() {
    std::lock_guard<std::mutex> lock1(mtx1);
    std::lock_guard<std::mutex> lock2(mtx2);
    // ...
}

void safe_thread2() {
    std::lock_guard<std::mutex> lock1(mtx1);  // 相同顺序
    std::lock_guard<std::mutex> lock2(mtx2);
    // ...
}

// 方案2:使用std::lock同时锁定多个互斥量
void safer_thread() {
    std::unique_lock<std::mutex> lock1(mtx1, std::defer_lock);
    std::unique_lock<std::mutex> lock2(mtx2, std::defer_lock);
    std::lock(lock1, lock2);  // 同时锁定,避免死锁
    // ...
}

// 方案3:使用std::scoped_lock(C++17)
void modern_thread() {
    std::scoped_lock lock(mtx1, mtx2);  // 自动处理死锁避免
    // ...
}

3. 条件变量使用错误

#include <condition_variable>
#include <mutex>
#include <thread>

std::mutex mtx;
std::condition_variable cv;
bool ready = false;

void consumer() {
    std::unique_lock<std::mutex> lock(mtx);
    cv.wait(lock);  // 可能虚假唤醒
    // 使用ready...
}

void producer() {
    std::this_thread::sleep_for(std::chrono::seconds(1));
    {
        std::lock_guard<std::mutex> lock(mtx);
        ready = true;
    }
    cv.notify_one();
}

解决方案

// 正确使用条件变量
void correct_consumer() {
    std::unique_lock<std::mutex> lock(mtx);
    // 使用谓词防止虚假唤醒
    cv.wait(lock, []{ return ready; });
    // ready一定为true
}

// 使用超时防止永久等待
bool timed_consumer() {
    std::unique_lock<std::mutex> lock(mtx);
    if (cv.wait_for(lock, std::chrono::seconds(5), []{ return ready; })) {
        // 条件满足
        return true;
    } else {
        // 超时
        return false;
    }
}

五、异常安全错误

1. 异常导致资源泄漏

class Resource {
    int* data;
public:
    Resource() : data(new int[1000]) {
        throw std::runtime_error("构造失败");  // 内存泄漏!
    }
    ~Resource() { delete[] data; }
};

解决方案

// 使用RAII确保异常安全
class SafeResource {
    std::unique_ptr<int[]> data;  // 智能指针自动清理
public:
    SafeResource() : data(std::make_unique<int[]>(1000)) {
        throw std::runtime_error("构造失败");  // 无内存泄漏
    }
    // 无需手动析构函数
};

2. 异常不透明

void processFile(const std::string& filename) {
    std::ifstream file(filename);
    if (!file) {
        throw std::runtime_error("无法打开文件: " + filename);
    }
    // 处理文件
    throw std::runtime_error("处理错误");  // 异常类型信息丢失
}

解决方案

// 定义具体的异常类型
class FileError : public std::runtime_error {
public:
    explicit FileError(const std::string& msg, const std::string& filename)
        : std::runtime_error(msg + ": " + filename), filename(filename) {}
    
    const std::string filename;
};

class ProcessError : public std::runtime_error {
public:
    using std::runtime_error::runtime_error;
};

void betterProcessFile(const std::string& filename) {
    std::ifstream file(filename);
    if (!file) {
        throw FileError("无法打开文件", filename);
    }
    // 处理文件
    throw ProcessError("处理错误");
}

// 捕获时可以提供更多上下文
try {
    betterProcessFile("data.txt");
} catch (const FileError& e) {
    std::cerr << "文件错误: " << e.what() << std::endl;
} catch (const ProcessError& e) {
    std::cerr << "处理错误: " << e.what() << std::endl;
}

六、数值计算错误

1. 整数溢出

int max = INT_MAX;
int overflow = max + 1;  // 有符号整数溢出,未定义行为

解决方案

#include <limits>
#include <stdexcept>

// 安全的加法
int safe_add(int a, int b) {
    if (b > 0 && a > std::numeric_limits<int>::max() - b) {
        throw std::overflow_error("加法溢出");
    }
    if (b < 0 && a < std::numeric_limits<int>::min() - b) {
        throw std::underflow_error("减法下溢");
    }
    return a + b;
}

// 使用大整数库
#include <boost/multiprecision/cpp_int.hpp>
boost::multiprecision::cpp_int huge = 1;
for (int i = 0; i < 1000; ++i) {
    huge *= 2;  // 不会溢出
}

2. 浮点数精度问题

double a = 0.1;
double b = 0.2;
double c = 0.3;
bool equal = (a + b == c);  // 可能是false!

解决方案

#include <cmath>
#include <limits>

// 比较浮点数
bool approximatelyEqual(double a, double b, double epsilon = 1e-6) {
    return std::fabs(a - b) <= epsilon;
}

// 相对误差比较
bool essentiallyEqual(double a, double b, double epsilon = 1e-6) {
    return std::fabs(a - b) <= epsilon * std::max(std::fabs(a), std::fabs(b));
}

// 使用固定精度类型
#include <boost/multiprecision/cpp_dec_float.hpp>
boost::multiprecision::cpp_dec_float_50 precise = 0.1;
precise += 0.2;  // 高精度计算

3. 除以零

int divide(int a, int b) {
    return a / b;  // 如果b=0,未定义行为
}

解决方案

// 检查除数
int safe_divide(int a, int b) {
    if (b == 0) {
        throw std::runtime_error("除以零");
    }
    return a / b;
}

// 浮点数除以零
double safe_divide_double(double a, double b) {
    if (b == 0.0) {
        if (a == 0.0) {
            return std::numeric_limits<double>::quiet_NaN();  // 0/0 = NaN
        } else if (a > 0) {
            return std::numeric_limits<double>::infinity();
        } else {
            return -std::numeric_limits<double>::infinity();
        }
    }
    return a / b;
}

七、未定义行为 (Undefined Behavior, UB)

1. 有符号整数溢出

int i = INT_MAX;
i++;  // 有符号溢出,UB

解决方案

// 使用无符号整数
unsigned int u = UINT_MAX;
u++;  // 定义良好,环绕到0

// 使用检查
#include <cfenv>
#include <iostream>

#pragma STDC FENV_ACCESS ON
void test_overflow() {
    std::feclearexcept(FE_ALL_EXCEPT);
    int i = INT_MAX;
    i++;
    if (std::fetestexcept(FE_OVERFLOW)) {
        std::cout << "检测到溢出" << std::endl;
    }
}

2. 违反严格的别名规则

float x = 3.14f;
int* p = reinterpret_cast<int*>(&x);
int y = *p;  // 违反严格别名规则,UB

解决方案

// 使用memcpy
#include <cstring>
float x = 3.14f;
int y;
std::memcpy(&y, &x, sizeof(float));  // 合法

// 使用union(C++中有限制)
union FloatInt {
    float f;
    int i;
};
FloatInt u;
u.f = 3.14f;
int y = u.i;  // 在特定条件下合法

3. 越界访问

int arr[5];
int val = arr[10];  // UB

解决方案

// 使用安全的容器
#include <array>
#include <vector>

std::array<int, 5> arr1;  // 编译时大小检查
std::vector<int> arr2(5);  // 运行时大小

// 自己实现边界检查
template<typename T, size_t N>
class SafeArray {
    T data[N];
public:
    T& operator[](size_t index) {
        if (index >= N) {
            throw std::out_of_range("数组索引越界");
        }
        return data[index];
    }
    
    const T& operator[](size_t index) const {
        if (index >= N) {
            throw std::out_of_range("数组索引越界");
        }
        return data[index];
    }
};

八、调试和诊断工具

1. 编译器工具

# GCC/Clang sanitizers
g++ -fsanitize=address -g program.cpp -o program  # 地址检查
g++ -fsanitize=undefined -g program.cpp -o program  # 未定义行为
g++ -fsanitize=thread -g program.cpp -o program    # 线程检查
g++ -fsanitize=leak -g program.cpp -o program      # 内存泄漏

# 全部启用
g++ -fsanitize=address,undefined,leak -g program.cpp -o program

2. Valgrind

# 内存检查
valgrind --leak-check=full --show-leak-kinds=all ./program

# 缓存分析
valgrind --tool=cachegrind ./program
kcachegrind cachegrind.out.*

# 调用图分析
valgrind --tool=callgrind ./program

3. 调试器

# GDB
gdb ./program
(gdb) break main
(gdb) run
(gdb) backtrace
(gdb) watch variable
(gdb) catch throw  # 捕获异常

# LLDB
lldb ./program
(lldb) breakpoint set --name main
(lldb) run

4. 静态分析工具

# Clang-Tidy
clang-tidy program.cpp --checks='*'

# Cppcheck
cppcheck --enable=all --inconclusive program.cpp

# 包括Boost检查
cppcheck --check-level=exhaustive --enable=all program.cpp

九、防御性编程技巧

1. 契约式设计

#include <cassert>

void process(int* data, size_t size) {
    // 前置条件
    assert(data != nullptr);
    assert(size > 0);
    
    // 不变式
    assert(isValid(data, size));
    
    // 后置条件
    assert(resultIsValid(data, size));
}

// 使用GCC/Clang内置断言
#ifndef NDEBUG
#define MY_ASSERT(expr) \
    if (!(expr)) __builtin_trap()
#else
#define MY_ASSERT(expr) ((void)0)
#endif

2. 日志和断言

#include <iostream>
#include <fstream>

class Logger {
    std::ofstream logfile;
public:
    Logger(const std::string& filename) : logfile(filename) {
        if (!logfile) {
            throw std::runtime_error("无法打开日志文件");
        }
    }
    
    template<typename T>
    Logger& operator<<(const T& msg) {
        logfile << msg;
        std::cout << msg;  // 同时输出到控制台
        return *this;
    }
};

// 条件日志
#ifdef DEBUG
#define DEBUG_LOG(msg) std::cout << "[DEBUG] " << msg << std::endl
#else
#define DEBUG_LOG(msg) ((void)0)
#endif

3. 单元测试

// 使用Google Test
#include <gtest/gtest.h>

TEST(MemoryTest, SmartPointer) {
    auto ptr = std::make_unique<int>(42);
    ASSERT_NE(ptr, nullptr);
    EXPECT_EQ(*ptr, 42);
}

TEST(ExceptionTest, DivisionByZero) {
    EXPECT_THROW(safe_divide(1, 0), std::runtime_error);
    EXPECT_NO_THROW(safe_divide(1, 1));
}

十、常见运行时错误速查表

错误类型典型表现检测工具预防措施
空指针解引用段错误 (SIGSEGV)AddressSanitizer, Valgrind使用智能指针,检查nullptr
内存泄漏内存占用增长Valgrind, LeakSanitizer使用RAII,智能指针
越界访问段错误,数据损坏AddressSanitizer, 边界检查使用vector.at(),自定义检查
数据竞争结果不一致,崩溃ThreadSanitizer使用互斥锁,原子操作
死锁程序挂起调试器,日志固定锁顺序,使用scoped_lock
整数溢出错误结果UBSanitizer检查边界,使用大整数库
异常未捕获程序终止异常处理器捕获所有异常,记录日志
双重释放程序崩溃AddressSanitizer使用智能指针,置空指针
迭代器失效段错误,未定义行为调试器检查迭代器有效性,使用算法
栈溢出段错误调试器,限制栈大小避免深度递归,使用堆分配

十一、最佳实践总结

  1. 始终使用智能指针管理动态内存
  2. 使用标准容器代替原始数组
  3. 启用所有编译器警告并视为错误
  4. 使用静态分析工具作为构建流程的一部分
  5. 编写单元测试,特别是对于边界条件
  6. 使用RAII管理所有资源(内存、文件、锁等)
  7. 避免裸循环,使用算法和范围for
  8. 检查所有用户输入和外部数据
  9. 使用异常进行错误处理,而不是错误码
  10. 记录足够的调试信息,但不要在发布版本中
  11. 定期进行代码审查,特别是并发代码
  12. 理解并避免未定义行为

记住:运行时错误最好的防御是预防。良好的编码习惯、充分的测试和适当的工具使用可以避免大多数运行时问题。