软件构建(五)——各种数据类型的使用原则
几乎所有的常见编程语言,都包含了整数、浮点数、字符串、布尔等等数据类型,而通过基本的数据类型又可以复合出各种数据结构。本文总结了常用的数据类型(整数、浮点数、字符、字符串、布尔、枚举、数组等等)以及一些不常见的数据类型(结构体、指针等等)的使用原则。最后总结了关于全局数据的一些风险和使用原则。
1. 常用的数据类型使用原则
1.1 使用数值的原则
- 普遍原则
- 避免使用“神秘数值”,即硬编码数字:这是为了使修改变得可靠容易,并更有可读性
- 如果需要,可以使用硬编码的0和1:通常用于增减量或循环的第一个元素
- 预防除零错误
- 使类型转换变得明显:不要依赖隐式类型转换
- 避免混合类型的比较:同样不要依赖隐式类型转换,确保比较的两个数是同种类型的
- 注意编译器的警告
- 整数
- 检查整数除法:小心整数除和现实除法的差异
- 检查整数溢出和中间结果溢出
- 浮点数
- 避免数量级相差巨大的数之间的加减运算:由于精度问题,对于32位浮点数,小心1000000.00 + 0.1得到的结果和期望不同。如果要把一系列差异巨大的数相加,解决方法是先排序,再从最小值开始加起,这样可以把影响减少到最低限度
- 避免直接进行等量比较:应自己编写equals函数判断两者之差是否小于某阈值
- 处理舍入误差问题:考虑换用更高精度的类型,或者把浮点数放大n倍用整型类型做计算
- 使用语言和函数库对特定数据类型的支持
1.2 字符和字符串
- 避免使用神秘字符和字符串:除了数值所提到的理由之外,还有这些理由:分离字符串到字符串资源文件更容易实现i18n,字符串字面量会占用较多的存储空间
- 了解你的语言和开发环境是如何支持Unicode的
- 在程序生命期中尽早决定国际化/本地化策略
- 如果你知道只需要支持一种文字的语言,考虑使用ISO8859字符集,否则使用Unicode
- 采用某种一致的字符串类型转换策略
- 针对C语言字符串的建议:
- 注意字符串指针和字符数组之间的差异:警惕任何包含字符串和等号的表达式;通过命名规则区分变量是字符数组还是字符串指针
- 把C-style字符串的长度声明为
STRING_LENGTH + 1
:统一约定这条规则,有助于减少脑力消耗以及编程失误 - 用null初始化字符串以避免没有终端的字符串
- 如果内存不是限制性的因素,就用字符数组取代C中的指针
- 用strncpy()取代strcpy()以避免无终端的字符串
1.3 布尔变量
用布尔变量对程序加以文档说明,并用来简化复杂的判断。通过下面的例子来了解布尔变量的正确用法:
// 目的不明确的布尔判断,咋一看根本不知道要判断什么
if ( (elementindex < 0 ) || (MAX_ELEMENTS < elementIndex) || (elementIndex == lastElementIndex) ) { ... }
// 目的明确的布尔判断
bool finished = (elementindex < 0 ) || (MAX_ELEMENTS < elementIndex);
bool repeatedEntry = (elementIndex == lastElementIndex);
if (finished || repeatedEntry) { ... }
1.4 枚举类型
- 用枚举类型来提高可读性和可靠性:如果仅使用具名常量,编译器无法知道是否使用了非法的数值
- 将枚举类型作为布尔变量的替换方案:有时布尔变量无法充分表达它所需要表达的含义(比如出错信息),那么用枚举
- 定义出枚举的第一项和最后一项,以使用于循环边界,把枚举类型的第一个元素留做非法值:如
enum Country {InvalidFirst = 0, First = 1, China = 1, England = 2, Usa = 3, Last = 4}
,但是这样做也可能造成混乱,一定要明确定义项目代码编写标准,并在使用时保持一致,否则就不要用 - 警惕给枚举元素明确赋值而带来的失误:当定义0,1,2,4,8这样的枚举值时,不要去遍历
1.5 数组
- 确认所有的数组下标都没有超出数组的边界
- 考虑用合适的容器(栈、队列、集合、列表等等)来取代数组
- 检查数组的边界点
- 如果数组是多维的,确认下标的使用顺序是正确的:如很容易把
array[i][j]
搞混成array[j][i]
- 提防下标串话:如把
array[i]
写成array[j]
,和上一条一样,如果使用比i和j更有意义的下标名,这种错误就很难发生
2. 不常见的数据类型
2.1 结构体
在Java和C++里面,类有时表现得也像结构体一样(当类完全由公用的数据成员组成,而不包含公用子程序的时候)。通常情况下,你会希望创建类而非结构体,下面列出了一些使用结构体的理由:
- 用结构体来明确数据关系
- 用结构体简化对数据块的操作
- 用结构体来简化参数列表
- 用结构体来减少维护
2.2 指针
指针的使用是现代编程中最容易出错的领域之一,即便你的语言不要求你使用指针,很好地理解指针也会有助于你理解你的编程语言是如何工作的。从概念上看,每一个指针都包含两个部分:内存中的某处位置(实质是一个整数值,常用16进制表示),以及如何解释指针所指的内容(由指针的基类型决定)。
通常,指针错误都产生于指针指向了它不应该指向的位置。因此,更正指针错误的大部分工作量便是找出它的位置。正确地使用指针要求程序员采用一种双向策略,首先要避免造成指针错误,其次在编写代码之后尽快地检测出指针错误来。下面说明如何实现这些目标。
- 把指针操作限制在子程序或者类里面
- 同时声明和定义指针
- 在与指针分配相同的作用域中删除指针
- 在使用指针之前确保指针所指向的内存位置是合理的
- 先检查指针所引用的变量内容再使用它
- 用标记字段来检测损毁的内存:分配内存时多分配4个字节,将前4个字节设为标记字段,返回这4个字节后的内存的指针,到了需要删除该指针的时候,检查这个标记,如果标记的值是正确的,就把它设为NULL,最后删除该指针
- 增加明显的冗余:将某些特定字段重复两次,以此替代标记字段的方案,但是这样会带来很高的成本
- 用额外的指针变量来提高代码清晰度:如不要写绕口的诸如
pointer->next->last->next
- 画一个图理清指针之间的结构关系
- 按照正确的顺序删除链表中的指针
- 分配一片保留的内存后备区域:如果使用动态内存,最好预先分配一片内存后备,防止程序忽然用尽内存
- 在删除或者释放指针之后把它们设为空值
- 在删除变量之前检查非法指针
- 跟踪指针分配情况:维护一份你已经分配的指针的列表
- 编写覆盖子程序,集中实现避免指针问题的策略:如编写
SAFE_NEW
和SAFE_DELETE
宏来统一包装指针的操作 - 采用非指针的技术
还有一些特定的针对C++和C语言的指针使用技巧,此处不赘述。
3. 全局数据
全局数据可以在程序中任意一个位置访问,这一概念有时被延伸到作用域比局部变量更广的变量,例如可以在一个包或一个命名空间内任意位置访问。一般来说,使用全局数据的风险比使用局部数据大,只有在万不得已时才使用,如果要用也应该遵循一些使用原则来降低风险。
3.1 使用全局数据的风险
- 无意间修改了全局数据
- 与全局数据有关的别名问题:当一个全局变量被传递给一个子程序,然后该子程序将它既用作全局变量又用作参数使用的情况下会出现这种问题
- 多线程下风险很大
- 全局数据阻碍代码重用
- 与全局数据有关的非确定的初始化顺序事直
- 全局数据破坏了模块化和智力上的可管理性
3.2 全局数据的使用原则
- 创建一种命名规则来突出全局变量:例如使用g_前缀
- 为全部的全局变量创建一份注释良好的清单
- 不要用全局变量来存放中间结果
- 不要把所有的数据都放在一个大对象中并到处传递,以说明你没有使用全局变量:这纯粹是一种负担,如果要用全局数据,就大胆公开地用
参考文献:电子工业出版社《代码大全(第2版)》第12、13章