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

本文涉及的产品
Serverless 应用引擎 SAE,800核*时 1600GiB*时
云原生网关 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对象,引发未预期行为。

解决方法:

  • 避免全局依赖:尽量设计成局部或通过参数传递依赖,减少系统范围的耦合。
  • 利用单例模式:确保依赖以可控顺序初始化,尤其适用于需全局访问但需管理初始化时机的场景。
  • 静态局部变量:在函数内部使用静态局部变量初始化依赖,这样可以在首次使用时按需初始化,且顺序更为确定。
  • 显式初始化函数:编写一个启动或配置函数来手动控制所有组件的初始化顺序。
相关文章
|
6天前
|
编解码 Java 程序员
写代码还有专业的编程显示器?
写代码已经十个年头了, 一直都是习惯直接用一台Mac电脑写代码 偶尔接一个显示器, 但是可能因为公司配的显示器不怎么样, 还要接转接头 搞得桌面杂乱无章,分辨率也低,感觉屏幕还是Mac自带的看着舒服
|
8天前
|
存储 缓存 关系型数据库
MySQL事务日志-Redo Log工作原理分析
事务的隔离性和原子性分别通过锁和事务日志实现,而持久性则依赖于事务日志中的`Redo Log`。在MySQL中,`Redo Log`确保已提交事务的数据能持久保存,即使系统崩溃也能通过重做日志恢复数据。其工作原理是记录数据在内存中的更改,待事务提交时写入磁盘。此外,`Redo Log`采用简单的物理日志格式和高效的顺序IO,确保快速提交。通过不同的落盘策略,可在性能和安全性之间做出权衡。
1562 10
|
1月前
|
弹性计算 人工智能 架构师
阿里云携手Altair共拓云上工业仿真新机遇
2024年9月12日,「2024 Altair 技术大会杭州站」成功召开,阿里云弹性计算产品运营与生态负责人何川,与Altair中国技术总监赵阳在会上联合发布了最新的“云上CAE一体机”。
阿里云携手Altair共拓云上工业仿真新机遇
|
11天前
|
人工智能 Rust Java
10月更文挑战赛火热启动,坚持热爱坚持创作!
开发者社区10月更文挑战,寻找热爱技术内容创作的你,欢迎来创作!
737 27
|
8天前
|
存储 SQL 关系型数据库
彻底搞懂InnoDB的MVCC多版本并发控制
本文详细介绍了InnoDB存储引擎中的两种并发控制方法:MVCC(多版本并发控制)和LBCC(基于锁的并发控制)。MVCC通过记录版本信息和使用快照读取机制,实现了高并发下的读写操作,而LBCC则通过加锁机制控制并发访问。文章深入探讨了MVCC的工作原理,包括插入、删除、修改流程及查询过程中的快照读取机制。通过多个案例演示了不同隔离级别下MVCC的具体表现,并解释了事务ID的分配和管理方式。最后,对比了四种隔离级别的性能特点,帮助读者理解如何根据具体需求选择合适的隔离级别以优化数据库性能。
225 3
|
14天前
|
Linux 虚拟化 开发者
一键将CentOs的yum源更换为国内阿里yum源
一键将CentOs的yum源更换为国内阿里yum源
779 5
|
2天前
|
Python
【10月更文挑战第10天】「Mac上学Python 19」小学奥数篇5 - 圆和矩形的面积计算
本篇将通过 Python 和 Cangjie 双语解决简单的几何问题:计算圆的面积和矩形的面积。通过这道题,学生将掌握如何使用公式解决几何问题,并学会用编程实现数学公式。
108 60
|
1天前
|
人工智能
云端问道12期-构建基于Elasticsearch的企业级AI搜索应用陪跑班获奖名单公布啦!
云端问道12期-构建基于Elasticsearch的企业级AI搜索应用陪跑班获奖名单公布啦!
115 1
|
3天前
|
Java 开发者
【编程进阶知识】《Java 文件复制魔法:FileReader/FileWriter 的奇妙之旅》
本文深入探讨了如何使用 Java 中的 FileReader 和 FileWriter 进行文件复制操作,包括按字符和字符数组复制。通过详细讲解、代码示例和流程图,帮助读者掌握这一重要技能,提升 Java 编程能力。适合初学者和进阶开发者阅读。
104 61
|
14天前
|
JSON 自然语言处理 数据管理
阿里云百炼产品月刊【2024年9月】
阿里云百炼产品月刊【2024年9月】,涵盖本月产品和功能发布、活动,应用实践等内容,帮助您快速了解阿里云百炼产品的最新动态。
阿里云百炼产品月刊【2024年9月】