【实战经验】17个C++编程常见错误及其解决方案

本文涉及的产品
服务治理 MSE Sentinel/OpenSergo,Agent数量 不受限
云原生网关 MSE Higress,422元/月
注册配置 MSE Nacos/ZooKeeper,118元/月
简介: 想必不少程序员都有类似的经历:辛苦敲完项目代码,内心满是对作品品质的自信,然而当静态扫描工具登场时,却揭示出诸多隐藏的警告问题。为了让自己的编程之路更加顺畅,也为了持续精进技艺,我想借此机会汇总分享那些常被我们无意间忽视却又导致警告的编程小细节,以此作为对未来的自我警示和提升。

17个C++编程常见错误及其解决方案

引言

  想必不少程序员都有类似的经历:辛苦敲完项目代码,内心满是对作品品质的自信,然而当静态扫描工具登场时,却揭示出诸多隐藏的警告问题。为了让自己的编程之路更加顺畅,也为了持续精进技艺,我想借此机会汇总分享那些常被我们无意间忽视却又导致警告的编程小细节,以此作为对未来的自我警示和提升。

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_ptrstd::shared_ptr)来自动管理内存,避免手动分配和释放内存带来的问题。

std::unique_ptr<int[]> memory(new int[10]);  // 自动释放内存

另一个需要注意的是,对于单个对象的动态内存分配,应当使用newdelete而非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对象,引发未预期行为。

解决方法:

  • 避免全局依赖:尽量设计成局部或通过参数传递依赖,减少系统范围的耦合。
  • 利用单例模式:确保依赖以可控顺序初始化,尤其适用于需全局访问但需管理初始化时机的场景。
  • 静态局部变量:在函数内部使用静态局部变量初始化依赖,这样可以在首次使用时按需初始化,且顺序更为确定。
  • 显式初始化函数:编写一个启动或配置函数来手动控制所有组件的初始化顺序。
相关文章
|
2月前
|
存储 C++ UED
【实战指南】4步实现C++插件化编程,轻松实现功能定制与扩展
本文介绍了如何通过四步实现C++插件化编程,实现功能定制与扩展。主要内容包括引言、概述、需求分析、设计方案、详细设计、验证和总结。通过动态加载功能模块,实现软件的高度灵活性和可扩展性,支持快速定制和市场变化响应。具体步骤涉及配置文件构建、模块编译、动态库入口实现和主程序加载。验证部分展示了模块加载成功的日志和配置信息。总结中强调了插件化编程的优势及其在多个方面的应用。
305 63
|
1月前
|
自然语言处理 编译器 Linux
告别头文件,编译效率提升 42%!C++ Modules 实战解析 | 干货推荐
本文中,阿里云智能集团开发工程师李泽政以 Alinux 为操作环境,讲解模块相比传统头文件有哪些优势,并通过若干个例子,学习如何组织一个 C++ 模块工程并使用模块封装第三方库或是改造现有的项目。
|
2月前
|
存储 搜索推荐 C++
【C++篇】深度剖析C++ STL:玩转 list 容器,解锁高效编程的秘密武器2
【C++篇】深度剖析C++ STL:玩转 list 容器,解锁高效编程的秘密武器
55 2
【C++篇】深度剖析C++ STL:玩转 list 容器,解锁高效编程的秘密武器2
|
2月前
|
Rust 资源调度 安全
为什么使用 Rust over C++ 进行 IoT 解决方案开发
为什么使用 Rust over C++ 进行 IoT 解决方案开发
76 7
|
2月前
|
安全 程序员 编译器
【C++篇】继承之韵:解构编程奥义,领略面向对象的至高法则
【C++篇】继承之韵:解构编程奥义,领略面向对象的至高法则
87 11
|
2月前
|
存储 C++ 容器
【C++篇】深度剖析C++ STL:玩转 list 容器,解锁高效编程的秘密武器1
【C++篇】深度剖析C++ STL:玩转 list 容器,解锁高效编程的秘密武器
55 5
|
2月前
|
编译器 C语言 C++
C++入门6——模板(泛型编程、函数模板、类模板)
C++入门6——模板(泛型编程、函数模板、类模板)
47 0
C++入门6——模板(泛型编程、函数模板、类模板)
|
2月前
|
算法 编译器 C++
【C++篇】领略模板编程的进阶之美:参数巧思与编译的智慧
【C++篇】领略模板编程的进阶之美:参数巧思与编译的智慧
84 2
|
2月前
|
缓存 Linux 编译器
【C++】CentOS环境搭建-安装log4cplus日志组件包及报错解决方案
通过上述步骤,您应该能够在CentOS环境中成功安装并使用log4cplus日志组件。面对任何安装或使用过程中出现的问题,仔细检查错误信息,对照提供的解决方案进行调整,通常都能找到合适的解决之道。log4cplus的强大功能将为您的项目提供灵活、高效的日志管理方案,助力软件开发与维护。
64 0
|
14天前
|
存储 编译器 C语言
【c++丨STL】string类的使用
本文介绍了C++中`string`类的基本概念及其主要接口。`string`类在C++标准库中扮演着重要角色,它提供了比C语言中字符串处理函数更丰富、安全和便捷的功能。文章详细讲解了`string`类的构造函数、赋值运算符、容量管理接口、元素访问及遍历方法、字符串修改操作、字符串运算接口、常量成员和非成员函数等内容。通过实例演示了如何使用这些接口进行字符串的创建、修改、查找和比较等操作,帮助读者更好地理解和掌握`string`类的应用。
25 2

热门文章

最新文章