870920 Menu

SwingCoder之C++备忘录·19

编码与核查

 写类之前,是否已将本模块的问题域分析透彻?功能或模块是否已划分清楚?是否做了详尽的设计?是否有现成的设计模式可套用?是否已大概确定需要几个类和类之间的关系?是否已画出类图?

 确定要写一个类,意味着已经明确该类是什么,能做什么,可能有什么,它本身需要些什么。务必先用文字描述清楚,仔细斟酌后再动手。

 确定成员函数之前,要以文字或注释的形式仔细描述出每个函数的功能,所需的数据来源,返回结果,内部流程,控制结构,防御性措施等等。尽量用伪代码详尽描述,稍后依照伪代码编程。

 根据类的功能,检查成员函数是否能够满足所需,是否涵盖所有可能的行为,是否重复或遗漏,是否可以声明重载版本,是否便于为该类编写单元测试。

 确定类名和函数名,检查所用的名称是否规范合理、易读易记,是否容易理解,是否有可能引起歧义。

 如需同时返回多个相关的值,则不妨使用这种函数:返回void类型,形参中有多个非const类型的引用或指针(在函数体中修改这些形参)。这种类型的函数,务必要在注释中说明清楚。

 确定成员函数后,回过头来再对照并检查类的功能与作用,考虑该功能或概念是否有必要单独成类,是否职责不清,是否包含了应属于其它类的功能。如职责不清或职责过多,则意味着类的设计有问题,或者需要额外再定义新的类或结构体。如需定义新类,考虑是否可以定义为本类的嵌套类,将新类定义并实现于本编译单元的cpp文件中是否更好,是否确实有必要将新类声明为自己的友元类。

 考虑并确定构造函数的参数,构造函数是否需要重载,类是否需要拷构及赋值运算符重载,是否需要其它运算符重载,单参构造函数是否使用了explicit关键字限制转换构造。

 函数不修改其实参,则函数声明中将形参声明为const,函数的返回值不希望被修改,则函数的返回类型前使用const,函数不修改本类的数据成员,则在函数签名最后使用const。

 成员函数不访问数据成员,则将其声明为static静态函数。语句短小者,声明的同时即可定义。

 根据类的功能与成员函数的需要,确定本类所需的数据成员、数据成员的类型(栈对象、引用或指针,普通成员还是静态成员)。并非所有的数据成员均需声明为private,检查是否可以将某个数据成员声明为public。如本类有可能派生子类,则确定将哪些数据成员声明为protected,或干脆预留给子类声明。

 如本类作为派生类的基类,检查是否已将析构函数和子类有可能重写的函数声明为virtual,是否有必要将本类写成抽象基类,如是,哪个或哪些虚成员函数可以或必须声明为纯虚函数。如果不写为抽象类,同时又不太可能或不想直接使用本类,则考虑是否有必要将构造函数写在protected中。

 再次检查类、成员函数、数据成员的名称是否规范合理,是否容易引起歧义或模糊不清。功能相关、形参相似的函数,其参数顺序与语义是否一致。bool型形参是否有必要替代为更直观的枚举元素。

 开始编码,将伪代码转为C++语句。目前无力完成或不急于完成的部分给出TODO注释。

 某个类或某个模块写完后,及时编写单元测试,力争涵盖所有的成员函数和所有可能的情况。

 确定编译单元。可将多个紧密相关的类集中在一个编译单元中,而无需每个类均一头一源。

 重复以上步骤。直至完成本编译单元中所有类的编码。

 再次核查类的功能描述,函数描述与实现代码,修改完善注释。

 编程过程中,要注意随时修改伪代码并完善注释。整个项目的开发、维护及更新的过程中,每次增删改代码,必须先考虑好,而后写出伪代码或更改注释,最后再修改代码。

 把相关语句组织在一起,而不是无关的语句相互交织。

 不要在h头文件或#include之前使用using指令,而是在cpp文件的#include之后使用。

 尽量少用全局数据(全局变量、全局对象、全局函数等)。如使用,最好将其置于名字空间中。

 多用断言,少抛异常。多用算法,少用循环。

 尽可能减少循环中的运算,相同的运算可在进入循环之前先算好。

 虚函数无法inline。内联是编译时替换,而非运行期绑定。

 优先使用具有退出语句的while循环,该结构更接近人类思考迭代性控制流程的本能方式。

 “无限循环”用for (;;) 而不用while (true)。原因:每次循环无需判断,减少一条指令,不占用寄存器。

 编写嵌套循环的秘诀:由内而外,先写最内层,将内层循环视为一条“实体”,而后再写外层循环。

 嵌套循环,将次数多的循环放在内层。

 查表法:配合模运算,使用下标索引从数组中获取所需的数据,而不是通过语句计算得出。

 经验和想当然对性能优化并没有太大帮助,一切要以实测为准。特别是不同的编译器和操作系统。

 第一次优化通常不会是最好的,即使进行了效果明显的优化,也不要停下来,要进一步扩大战果。

 多利用IDE中的这两个功能:性能分析,查看汇编(VS中查看汇编的方法,参见2.4.6)。

 最好的优化首先是问题域的详尽分析,其次是架构与设计,而后是选择算法,最后才是调整代码。

 尽量使用复合赋值表达式,比如:a += 20; 而不是:a = a + 20;

 常规运算,无论整型,还是浮点型,能用加减,不用乘除;能用乘,不用除。

 类中声明数据成员的顺序:先大后小,倍数空间。杜绝内存中出现“空洞”。

 编译器对递归函数有较大的优化。如果参数不多或不大,那么尽量使用递归。

 创建对象时尽量使用初始化表达式(构造或拷构式),而非=赋值。能匿名构造(比如函数实参),就无需提前声明并初始化。

 类对象作为数据成员时,能在构造函数初始化列表中初始化的,就不要在函数体中初始化。

 原始类型的数据成员,可在构造函数的函数体中采用“=”赋值表达式来初始化。

 如果一个成员函数需要多个参数,并且调用频繁,则将这些参数封装为一个结构体,该结构体的每个数据成员均代表该函数所需的参数之一,而后以结构体对象作为该函数的参数。

 struct结构体属于POD数据(Plain Old Data)。所谓POD,指内存布局与C数据完全一致的数据类型。有虚函数或数据成员有虚函数的对象不属于POD。非POD数据,不能使用memcpy()之类的低级内存操作函数。如果在代码中使用C函数,则只能通过POD数据来进行通信。此时,结构体不能是某个基类派生出来的,也不能有虚函数。

 导入纯C代码后,要关闭编译器的RTII和异常处理,以提高性能,使之真正达到C的效率。

 混合使用C++和C,还需注意4点:① 确保编译器产生二者兼容的obj文件。② 两种语言下均使用的函数声明为extern “C”。③ 尽可能使用C++写main()函数。④ 始终使用delete释放new分配的内存,用free()释放mallo()分配的内存。

 如有可能,用Lambda匿名函数或仿函数(函数对象)替代函数指针。

 返回值或对象的函数,不能返回引用或指针。较好的做法是直接返回该对象的带参构造函数或拷构函数,而不是函数内先创建一个对象,执行代码或赋值,最后返回该对象。不必多此一举。

 代码通过编译并可以运行,绝不代表它就一定是正确的。代码通过编译、能够运行并且正确无误,绝不代表它是合乎逻辑、便于维护修改和用户体验良好的。

 尽量减少头文件的依赖性(除非是同一模块,一起编译),能前向声明(使用了某个类的指针或引用),就不要#include。需要#include某个头文件的情况只有两种:需要知道该类的确切大小(比如使用了该类的栈对象),需要调用该类的成员。

 除非有意为之或确有把握,否则,任何情况下都要避免编译器的隐式类型转换。

 某个函数或类需使用枚举时,尽量将函数参数声明为该枚举的类型,而不是const int。

 修改容器之后,不要再保存或使用指向容器内元素的指针、引用或迭代器。

 提到性能,不得不提数据结构、算法与算法复杂度。软件开发领域常用大O表示法描述算法的复杂度与执行性能。这种表示法体现的是相对性能(相对时间),而非绝对性能(绝对时间),并且仅适用于依赖于来源项(要计算的元素个数)的算法,绝大多数算法都依赖于来源项,而非一次性的随机处理。O(1)表示常数复杂度,O(n)表示线性复杂度,典型的算法、大O表示与对应的复杂度见下表:

算法复杂度 大O表示法 典型算法 备注
常数时间 O(1) 数组的下标索引 执行时间与来源项的多少无关
对数时间 O(log n) 二分法查找 执行时间是来源项数量的对数(2为底)
线性时间 O(n) 在未排序的链表中查找元素 执行时间与来源项的数量成正比
线性对数 O(n * log n) 归并排序 执行时间为来源项的数量乘以其对数
二次方 O(n2) 冒泡排序 执行时间为来源项的数量的平方
n次方 O(nn) 非优化的八皇后算法 执行时间为来源项的数量的n次方

表 1 9 典型算法及对应的算法复杂度

 与算法复杂度对应的是程序(代码)复杂度:抛开宏观方面的规划与设计,但就模块化和结构化的代码单元而言,控制结构的使用与嵌套对程序复杂度的影响最大。程序复杂度的一个衡量标准是:为了理解当前代码,必须在同一时间记住的“智力实体”的数量,即:理解程序所需花费的精力和时间。代码复杂度的计算公式之一:通过“决策点”的数量来判断一段代码(某个函数或代码块)的复杂度。这段代码中,从1开始计数,每出现一次if, else if, else, switch的case, for, while, &&, ||, 函数调用, 声明对象等语句、控制结构和运算符就加1。累计出决策点数量后,分析如下:
 小于6:较好的复杂度。
 7-12:须设法降低复杂度。
 大于13:有必要将这段代码的某些部分或流程拆分到一到多个新函数中(拆分出来的新函数往往以辅助函数的形式声明在类的private区中)。

 将代码复杂度降至最低,是编写高质量程序的关键。

 查数法目测表达式中的括号是否匹配:如果一条语句中使用了多个表达式或多个函数调用,特别是函数实参又是函数的情况下,经常会导致括号不匹配的问题,一个快捷的检查方法是:将编辑器中的脱字符(插字光标)移到句首,重复按键盘上的右光标键,从左向右,从零开始计数,每遇到一个左括号就加1,每遇到一个右括号就减1。数到句末,结果为0,则括号精确匹配,结果为正数则意味着左括号多了,结果为负数则意味着右括号多了。该办法同样适合于以代码块为单位的花括号。