C++缺陷识别于调试
一、C++缺陷概述
-
C++语言编写的程序的缺陷大部分继承于C语言本身的语法使用缺陷,同时有一部分也来源于C++本身语法的缺陷。(但我理解规则是固定的,缺陷来源于你如何使用,而非设计)
-
尽量让缺陷暴露在编译阶段,通过不断完善的编译器来发现缺陷。
-
避免使用编译器的隐式类型转换,建议使用explicit修饰带一个入参的构造函数,并避免使用转换赋值运算符号
-
使用类型作为入参,在某些场景下尽量不要将enum当作整型数使用
-
定义合适的类来表示特定场景下的数据类型,而不是使用double类型,但指挥使用到0~10000范围的数
-
-
大部分的缺陷仍会暴露在运行阶段,如数组或指针越界访问
-
运行时错误分为两种:程序本身的缺陷 + 外部因素导致的错误(如输入错误、无网络、无权限、文件不存在、硬件缺失等)
-
通过编写安全检查相关的代码来自动检查程序本身的缺陷,安全检查捕获到缺陷时,可以做
-
记录错误信息及变量值
-
做适当的处理,如直接退出函数或者配出异常
-
-
安全检查会增加工作量,同时降低执行效率,可通过宏开关控制启用阶段
-
二、具体C++常见缺陷
1. 索引越界
-
数组分为静态数据、动态数据和vector,数组元素均可通过一个无符号整数作为索引进行访问
T StaticArr[20]; T* pDynamicArr = new T[20]; std::vector<T> vecArr;
-
动态数组: std::vector.at(index)函数会进行out_of_range的越界访问检查, 但若大量使用,代码效率会降低
-
建议还是通过[]进行数组元素的访问,但可新定义一个继承于std::vector的vector类型,重写[]操作符函数进行安全检查,并通过宏定义开关
-
静态数组可通过定义array模板类,重写[]操作符,加入安全检查代码
-
多维数据通过定义matrix模板类,重写[]操作符,加入安全检查代码
-
-
不要使用静态或动态数组,可以改用自己实现的array和vector模板类
-
避免使用new T[]分配动态数组内存,建议使用vector模板为多个元素分配内存
-
使用自定义的vector代替std::vector,使用自定义的ayyay代替静态数组,并视场景打开安全检查
-
对于多维数据,使用自定义的matrix,并通过()操作符访问元素,并视场景打开安全检查
2. 指针运算
-
指针允许被修改它本身的值以及指向的值
-
指针运算是数组索引访问内存的另一种语法
-
建议避免使用指针运算,而使用vector模板或者数组索引,因为他们可以重载[],加入安全检查
3. 无效的指针、引用和迭代器
当向vector中插入新的元素时,会重新分配空间并拷贝原来的元素,这将导致原来的元素的地址发生变化,如以下案例
vector<int> vecVal; for(int i = 0; i < 10; ++i) {vecVal.push_back(i); } int* pArr3 = &vecVal[3]; int& Arr3 = vecVal[3]; vector<int>::const_iterator begin = vecVal.begin() cout << "pArr3 val = " << (*pArr3) << ", pArr3 address = " << pArrs << endl; // 插入新的元素 for(int i = 0; i < 100; ++i) {vecVal.push_back(i*10); } cout << "vecVal[3] val = " << (*pArr3) << ", pArr3 address = " <<&vecVal[3]; << endl;
-
vec[3]输出的值不会变化,地址发生了变化
-
pArr3向的地址应该被回收,可能会被其他变量分配,此时若写入值,可能会引起错误
-
用迭代器指向vector中的值,在重新分配空间后,其指向也会发生变化
-
建议在修改vector之前得到的指向其中元素的指针、引用、迭代器在vector由于增加元素而被修改之后就不应该再使用了;
-
其他STL容器或者即将纳入标准的容器类如hash_map、hash_set在被修改(新增元素或删除元素后),其之前的指针、索引,迭代器也不建议使用.
-
当容器在跨线程使用时,特别要注意容器内容的变化与指针、引用、迭代器使用时的时机
-
vector使用[]索引访问时,会计算出当前的新地址,其他容器则必须先获取新容器的元素新地址再使用。
-
再修改了容器之后,务必不要再保存和使用之前指向容器的指针、引用和迭代器.
4. 未初始化的变量
-
内置变量类型没有构造函数,在没有初始化的时候是随机值
-
可通过构建模板类型,将内置类型(int、float等)封装使用,提供构造函数,避免未初始化的场景,可见源码(https://github.com/vladimir-kushnir)中Tnumber<T>的案例
-
内置类型bool未赋初始值时同样可使用封装类型class Bool来提供构造函数
针对内置类型变量忘记初始化的问题,建议
-
不要直接只用int,unsigned,double,bool等内置类型作为类的成员,反之,可使用Int,Unsigned,Double,Bool等封装类型,可避免忘记初始化导致赋值随机值
-
在形参传递时多了一层安全检查;
5. 内存泄露
内存泄露是指在堆上分配了一块内存,并把这块内存的地址赋值给了一个指针,结果这个指针离开了作用域或者指向了另一块堆内存,忘记释放原来那块堆内存,导致原来的那块内存无人可知也无法回收使用的情况。
A和B两个对象分别包含了一个指向对方的指针,此种情况下,释放任何一个对象,另一个对象的内存就会泄露——循环引用
----------------------------------------------------未完待续---------------------------------------------------------------
源码来源:https://github.com/vladimir-kushnir
参考书籍:《C++编程调试秘笈》