17个C++编程常见错误及其解决方案
- 引言
- 1. 空指针解引用
- 2. 多线程竞争条件
- 3. 死锁
- 4. 缓冲区溢出
- 5. 悬挂指针
- 6. 未捕获的异常
- 7. 浮点数精度丢失
- 8. 无符号整数溢出
- 9. 隐式类型转换
- 10. 未正确关闭文件
- 11. 无符号整数循环条件错误
- 12. 错误的类型转换
- 13. 循环体内的副作用
- 14. 字符串字面量和字符数组混淆
- 15. 不恰当的数组边界检查
- 16. 动态内存分配和释放不匹配
- 17. 全局对象的时序和作用域问题
引言
想必不少程序员都有类似的经历:辛苦敲完项目代码,内心满是对作品品质的自信,然而当静态扫描工具登场时,却揭示出诸多隐藏的警告问题。为了让自己的编程之路更加顺畅,也为了持续精进技艺,我想借此机会汇总分享那些常被我们无意间忽视却又导致警告的编程小细节,以此作为对未来的自我警示和提升。
1. 空指针解引用
错误示例:
int* ptr = nullptr; std::cout << *ptr; // 解引用空指针,可能导致段错误
解决方法: 在访问指针之前,务必检查其是否为空。
if (ptr != nullptr) { std::cout << *ptr; }
2. 多线程竞争条件
错误示例: 多个线程同时读写同一数据,未加锁保护。
int shared_var = 0; void thread_func() { for (int i = 0; i < 1000000; ++i) { shared_var++; // 多线程并发执行此操作可能导致结果不准确 } } int main() { std::thread t1(thread_func); std::thread t2(thread_func); t1.join(); t2.join(); std::cout << shared_var; // 预期输出2000000,但实际上可能不是 }
解决方法: 使用互斥量(mutex)或其他同步机制保护共享资源。
std::mutex mtx; int shared_var = 0; void thread_func() { for (int i = 0; i < 1000000; ++i) { std::lock_guard<std::mutex> lock(mtx); shared_var++; } }
3. 死锁
错误示例: 两个线程分别持有对方需要的锁,互相等待导致死锁。
std::mutex m1, m2; bool flag1 = false, flag2 = false; void func1() { std::unique_lock<std::mutex> lck1(m1); std::unique_lock<std::mutex> lck2(m2, std::defer_lock); while (!flag2) { lck2.lock(); // 若func2已获得m1,这里将导致死锁 // ... } } void func2() { std::unique_lock<std::mutex> lck2(m2); std::unique_lock<std::mutex> lck1(m1, std::defer_lock); while (!flag1) { lck1.lock(); // 若func1已获得m2,这里同样导致死锁 // ... } }
解决方法: 遵循锁的获取顺序一致性原则,或者使用更高级的并发原语避免死锁。
4. 缓冲区溢出
错误示例: 数组越界写入。
char str[10]; strcpy(str, "This is a very long string."); // 可能造成缓冲区溢出
解决方法: 使用安全的字符串处理函数,如strncpy或C++11之后的std::string。
5. 悬挂指针
错误示例: 指向动态分配内存的指针在释放内存后仍被继续使用。
int* p = new int(5); delete p; *p = 10; // 悬挂指针,可能导致段错误
解决方法: 释放内存后将指针置为nullptr,表明它不再指向有效的内存。
6. 未捕获的异常
错误示例: 函数内部抛出异常但未被捕获。
void mayThrowException() { throw std::runtime_error("An error occurred."); } int main() { mayThrowException(); // 如果没有捕获,程序会终止 return 0; }
解决方法: 在可能抛出异常的地方添加try-catch
块,并妥善处理异常。
7. 浮点数精度丢失
错误示例: 依赖于精确的浮点数计算。
double a = 0.1; double b = 0.2; if (a + b == 0.3) { // 这里可能为假,因为浮点数运算存在精度误差 // ... }
解决方法: 尽量避免直接比较浮点数相等,而是设定一个合理的误差范围。
8. 无符号整数溢出
错误示例: 对无符号整数执行减法,当结果小于零时可能会导致意外的大数值。
unsigned int a = 0; unsigned int b = 1; std::cout << a - b; // 输出的结果将是UINT_MAX
解决方法: 理解并谨慎使用无符号整数,尤其是涉及负数操作时。
9. 隐式类型转换
错误示例: 不同类型的表达式混合运算导致隐式类型转换,产生非预期结果。
long long num1 = LLONG_MAX; int num2 = INT_MAX; long long result = num1 + num2; // num2提升为long long后导致溢出
解决方法: 尽量避免隐式类型转换,明确指定类型转换以防止潜在问题。
10. 未正确关闭文件
错误示例: 打开文件后在程序结束前忘记关闭,可能导致数据丢失或文件句柄耗尽。
std::ofstream file("output.txt"); file << "Some content"; // 忘记调用file.close()
解决方法: 始终确保在适当的时间关闭文件,可以使用RAII(Resource Acquisition Is Initialization)技术,例如智能指针或C++11引入的std::ofstream的析构函数会自动关闭文件。
11. 无符号整数循环条件错误
错误示例: 在循环中使用无符号整数作为递减计数器,当期望循环结束时计数器为0,但由于无符号整数的特性导致无法正确终止循环。
unsigned int counter = 5; while (counter >= 0) { // 由于counter是无符号整数,当它递减至0时不会变为负数 // 循环体执行 --counter; } // 本应在counter为0时退出循环,但实际上会进入死循环
解决方法: 确保正确设置循环条件,针对无符号整数的特性,应当避免在计数器达到其自然结束点时依赖于负数条件。可以使用固定的循环次数或另一个合适的终止条件来替代。
unsigned int limit = 5; for (unsigned int counter = 0; counter < limit; ++counter) { // 循环体执行 } // 当counter到达limit时,循环自然结束
12. 错误的类型转换
错误示例: 强制类型转换可能掩盖潜在的逻辑错误,特别是在不同类型之间赋值或比较时。
double d = 3.14; int i = d; // 损失精度 if (d == 3) { // 可能永远不成立,因为浮点数与整数比较会有精度损失 // ... }
解决方法: 除非必要,否则尽量避免强制类型转换,尤其是在比较和赋值操作中,确保正确处理类型之间的转换。
13. 循环体内的副作用
错误示例: 在循环体内修改迭代变量,导致意料之外的循环行为。
for (std::vector<int>::iterator it = vec.begin(); it != vec.end(); ++it) { if (*it == target) { it = vec.erase(it); // 直接删除当前元素可能导致未遍历完剩余元素 } }
解决方法: 在循环体内避免对用于迭代的对象进行修改,若必须删除或移动元素,可选择复制迭代器或使用其它合适的数据结构操作方法。
14. 字符串字面量和字符数组混淆
错误示例: 初始化字符数组时,误用字符串字面量,导致未正确终止的字符串。
char name[8] = "John Doe"; // 缺少终止符'\0',可能会导致读取额外的内存数据
解决方法: 确保字符数组的大小足够容纳字符串字面量加上终止符'\0',或者使用C++的std::string类以避免此类问题。
char name[9] = "John Doe"; // 确保有足够的空间存放'\0' // 或者 std::string nameStr = "John Doe"; // 使用std::string类,无需手动管理终止符
15. 不恰当的数组边界检查
错误示例: 访问数组时未检查索引有效性,可能导致数组越界。
int arr[5] = {1, 2, 3, 4, 5}; std::cout << arr[5]; // 数组越界,可能导致未定义行为
解决方法: 在访问数组之前,始终确保索引的有效性,防止数组越界。
int arr[5] = {1, 2, 3, 4, 5}; int index = 5; if (index >= 0 && index < sizeof(arr) / sizeof(arr[0])) { std::cout << arr[index]; // 正确进行边界检查 } else { std::cout << "Index out of bounds.\n"; }
16. 动态内存分配和释放不匹配
错误示例: 使用不同的分配和释放函数,导致内存泄漏或程序崩溃。
void* memory = malloc(sizeof(int)*10); free(memory); // 在C++代码中混用了malloc和free
解决方法: 在C++中,建议使用new和delete操作符进行动态内存分配和释放,以确保匹配:
int* memory = new int[10]; delete[] memory; // 使用delete[]释放动态分配的数组
并且,遵循RAII原则,优先考虑使用智能指针(如std::unique_ptr
或std::shared_ptr
)来自动管理内存,避免手动分配和释放内存带来的问题。
std::unique_ptr<int[]> memory(new int[10]); // 自动释放内存
另一个需要注意的是,对于单个对象的动态内存分配,应当使用new
和delete
而非new[]
和delete[]
:
int* singleMemory = new int; delete singleMemory; // 正确释放单个对象的内存
17. 全局对象的时序和作用域问题
错误示例: 在C/C++程序中,全局对象的初始化顺序由编译器界定,非显式指定,可能会导致依赖全局对象的组件遭遇初始化时序问题,影响对象状态一致性及程序稳定性。
// cpp1 class Database {}; // 数据库类 Database globalDb; // 全局数据库实例 // cpp2 class Service { public: Service(Database& db) { std::cout << "Service initialized." << std::endl; } }; Service globalService(globalDb); // 依赖全局数据库的服务实例
Service
类依赖于Database
实例。尽管直觉上globalDb
应先于globalService
初始化。但依据C++标准,全局对象的初始化顺序未严格规定,尤其在不同编译器或复杂项目中,可能导致Service
使用未完全初始化的Database
对象,引发未预期行为。
解决方法:
- 避免全局依赖:尽量设计成局部或通过参数传递依赖,减少系统范围的耦合。
- 利用单例模式:确保依赖以可控顺序初始化,尤其适用于需全局访问但需管理初始化时机的场景。
- 静态局部变量:在函数内部使用静态局部变量初始化依赖,这样可以在首次使用时按需初始化,且顺序更为确定。
- 显式初始化函数:编写一个启动或配置函数来手动控制所有组件的初始化顺序。