一、内存相关错误
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 | 使用智能指针,置空指针 |
| 迭代器失效 | 段错误,未定义行为 | 调试器 | 检查迭代器有效性,使用算法 |
| 栈溢出 | 段错误 | 调试器,限制栈大小 | 避免深度递归,使用堆分配 |
十一、最佳实践总结
- 始终使用智能指针管理动态内存
- 使用标准容器代替原始数组
- 启用所有编译器警告并视为错误
- 使用静态分析工具作为构建流程的一部分
- 编写单元测试,特别是对于边界条件
- 使用RAII管理所有资源(内存、文件、锁等)
- 避免裸循环,使用算法和范围for
- 检查所有用户输入和外部数据
- 使用异常进行错误处理,而不是错误码
- 记录足够的调试信息,但不要在发布版本中
- 定期进行代码审查,特别是并发代码
- 理解并避免未定义行为
记住:运行时错误最好的防御是预防。良好的编码习惯、充分的测试和适当的工具使用可以避免大多数运行时问题。