高质量C++/C 编程指南
作者:林锐
前言
一、编程老手与高手的误区
进度表
使用说明书
注释
流程图
参考手册
文档
用户更明白用户需要什么
团队开发的理念
第一次就正确运行
上午9:00 到下午5:00 之间工作
软件设计的基础知识
提高质量与生产率是软件工程要解决的核心问题
写函数strcpy 的代码:
这么一个小不点的函数,他从三个方面考查:
(1)编程风格;
(2)出错处理;
(3)算法复杂度分析(用于提高性能)
二、本书导读
二、本书导读
本书第一章至第六章主要论述C++/C 编程风格。难度不高,但是细节比较多。别小
看了,提高质量就是要从这些点点滴滴做起。
第七章至第十一章是专题论述,技术难度比较高,看书时要积极思考。特别是第七
章“内存管理”,读了并不表示懂了,懂了并不表示就能正确使用。
建议大家阅读本书的参考文献,那些都是经典名著。
试问有多少软件开发人员对正确性、健壮性、可靠性、效率、易用性、可读性(可理解性)、可扩展性、
可复用性、兼容性、可移植性等质量属性了如指掌?并且能在实践中运用自如?。
如果你的编程质量已经过关了,不要就此满足。如果你想成为优秀的软件开发人员,
建议你阅读并按照CMMI 规范做事,让自己的综合水平上升一个台阶。
第1 章文件结构
版权和版本的声明位于头文件和定义文件的开头。例:
/*
* Copyright (c) 2001,上海贝尔有限公司网络应用事业部
* All rights reserved.
*
* 文件名称: filename.h
* 文件标识: 见配置管理计划书
* 摘要: 简要描述本文件的内容
*
* 当前版本: 1.1
* 作者: 输入作者(或修改者)名字
* 完成日期: 2001年7月20日
*
* 取代版本:1.0
* 原作者: 输入原作者(或修改者)名字
* 完成日期: 2001年5月10日
*/
建议将成员函数的定义与声明分开,不论该函数体有多么小。
使用声明相当于输入密码时的确认。
1.5 目录结构
如果一个软件的头文件数目比较多(如超过十个),通常应将头文件和定义文件分别保存于不同的目录,以便于维护。例如可将头文件保存于include 目录,将定义文件保存于source 目录(可以是多级目录)。
第2 章程序的版式
【规则2-1-1】在每个类声明之后、每个函数定义结束之后都要加空行。
【规则2-1-2】在一个函数体内,逻揖上密切相关的语句之间不加空行,其它地方应加空行分隔。
【规则2-2-1】一行代码只做一件事情,如只定义一个变量,或只写一条语句。这样的代码容易阅读,并且方便于写注释。
【规则2-2-2】if、for、while、do 等语句自占一行,执行语句不得紧跟其后。不论执行语句有多少都要加{}。这样可以防止书写失误。
【建议2-2-1】尽可能在定义变量的同时初始化该变量(就近原则)
2.3 代码行内的空格
if、for、while 等关键字之后应留一个空格再跟左括号‘(’,以突出关键字。
【规则2-3-4】‘,’之后要留空格,如Function(x,y, z)。如果‘;’不是一行的结束
符号,其后要留空格,如for(initialization; condition; update)。
if ((a>=b) &&(c<=d)) // 良好的风格。注意,这种情况之下<=两边就不用加空格了!!!
【规则2-6-1】应当将修饰符* 和& 紧靠变量名
例如:
char *name; //name是一个指针,指向的类型是char
int *x, y; // 此处y 不会被误解为指针
将public 类型的函数写在前面,而将private 类型的数据写在后面,采用这种版式的程序员主张类的设计“以行为为中心”,重点关注的是类应该提供什么样的接口(或服务)。
我建议读者采用“以行为为中心”的书写方式,即首先考虑类应该提供什么样的函
数。这是很多人的经验--“这样做不仅让自己在设计类时思路清晰,而且方便别人阅
读。因为用户最关心的是接口,谁愿意先看到一堆私有数据成员!”
第3 章命名规则
【规则3-1-2】标识符的长度应当符合“min-length&& max-information”原则。
【规则3-1-3】命名规则尽量与所采用的操作系统或开发工具的风格保持一致。
【规则3-1-6】变量的名字应当使用“名词”或者“形容词+名词”。
例如:
float value;
float oldValue;
float newValue;
【规则3-1-7】全局函数的名字应当使用“动词”或者“动词+名词”(动宾词组)。类的成员函数应当只使用“动词”,被省略掉的名词就是对象本身。
例如:
DrawBox(); // 全局函数
box->Draw(); // 类的成员函数
【规则3-2-1】类名和函数名用大写字母开头的单词组合而成。
class LeafNode; // 类名
void SetValue(int value); // 函数名
规则3-2-2】变量和参数用小写字母开头的单词组合而成。
例如:
BOOL flag;
int drawMode;
【规则3-2-7】为了防止某一软件库中的一些标识符和其它软件库中的冲突,可以为
各种标识符加上能反映软件性质的前缀。例如三维图形标准OpenGL 的所有库函数均以gl 开头,所有常量(或宏定义)均以GL 开头。如我们公司就有好多函数前面都加LIG
第4 章表达式和基本语句
标准比较:
if (flag) // 表示flag 为真
if (!flag) // 表示flag 为假。
其他的方式都属于不良风格。
4.3.4 指针变量与零值比较
标准if 语句如下:
if (p == NULL) // p 与NULL 显式比较,强调p 是指针变量
if (p != NULL)
不要写成
if (p == 0) // 容易让人误解p 是整型变量
if (p != 0)
if (p) // 容易让人误解p 是布尔变量
if (!p)
第5 章常量
【规则5-3-1】需要对外公开的常量放在头文件中,不需要对外公开的常量放在定义
文件的头部。为便于管理,可以把不同模块的常量集中存放在一个公共的头文件中。(得仔细考虑和权衡)
2001 Page 34 of 101
const float RADIUS = 100;
const float DIAMETER = RADIUS * 2;
5.4 类中的常量
怎样才能建立在整个类中都恒定的常量呢?别指望const 数据成员了,应该用类中
的枚举常量来实现。例如
class A
{
enum { SIZE1 = 1, SIZE2 = 2}; // 枚举常量
int array1[SIZE1];
int array2[SIZE2];
};
枚举常量不会占用对象的存储空间。例
sizeof(A)= 12
注意,const会占用空间
class A
{
const int m_i;
public:
A(int i):m_i(i){}
};
例:
sizeof(A)= 4
第6 章函数设计
【规则6-1-1】参数的书写要完整,不要贪图省事只写参数的类型而省略参数名字。如果函数没有参数,则用void 填充。(同意,至少DWORDGetLastError(void);都是这样的)
例如:
float GetValue(void); // 良好的风格
float GetValue(); // 不良的风格
【规则6-1-2】参数命名要恰当,顺序要合理。
一般地,应将目的参数放在前面,源参数放在后面。
【规则6-2-3】不要将正常值和错误标志混在一起返回。正常值用输出参数获得,而错误标志用return 语句返回。
【建议6-2-1】有时候函数原本不需要返回值,但为了增加灵活性如支持链式表达,可以附加返回值。
例如字符串拷贝函数strcpy 的原型:
char *strcpy(char *strDest,const char *strSrc);
strcpy 函数将strSrc 拷贝至输出参数strDest 中,同时函数的返回值又是strDest。
这样做并非多此一举,可以获得如下灵活性:
char str[20];
int length = strlen( strcpy(str, “Hello World”) );
【规则6-3-1】在函数体的“入口处”,对参数的有效性进行检查。
很多程序错误是由非法参数引起的,我们应该充分理解并正确使用“断言”(assert)
来防止此类错误。
【规则6-5-1】使用断言捕捉不应该发生的非法情况。不要混淆非法情况与错误情况之间的区别,后者是必然存在的并且是一定要作出处理的。
第7 章内存管理
【规则7-2-5】用free 或delete 释放了内存之后,立即将指针设置为NULL,防止产生“野指针”(可以采纳)
7.9 内存耗尽怎么办?
(1)判断指针是否为NULL,如果是则马上用return语句终止本函数。
(2)判断指针是否为NULL,如果是则马上用exit(1)终止整个程序的运行。
(3)为new 和malloc 设置异常处理函数。
我的经验教训是:
(1)越是怕指针,就越要使用指针。不会正确使用指针,肯定算不上是合格的程序员。
(2)必须养成“使用调试器逐步跟踪程序”的习惯,只有这样才能发现问题的本质。
第9 章 类的构造函数、析构函数与赋值函数
对于任意一个类A,如果不想编写上述函数,C++编译器将自动为A 产生四个缺省的函数,如
A(void); // 缺省的无参数构造函数
A(const A &a); // 缺省的拷贝构造函数
~A(void); // 缺省的析构函数
A & operate =(const A &a); // 缺省的赋值函数
纠正:以上观点不是太正确,这些函数只是在需要的时候才产生,看看汇编就知道了。
(2)“缺省的拷贝构造函数”和“缺省的赋值函数”均采用“位拷贝”而非“值拷贝”的方式来实现,倘若类中含有指针变量,这两个函数注定将出错。
// 赋值函数
String & String::operate =(constString &other)
{
// (1) 检查自赋值
if(this == &other)
return *this;
// (2) 释放原有的内存资源
delete [] m_data;
// (3)分配新的内存资源,并复制内容
int length = strlen(other.m_data);
m_data = new char[length+1];
strcpy(m_data, other.m_data);
// (4)返回本对象的引用
return *this;
}
b = a;
c = b;
a = c;
(1)第一步,检查自赋值。你可能会认为多此一举,难道有人会愚蠢到写出a = a 这
样的自赋值语句!的确不会。但是间接的自赋值仍有可能出现,例如
// 地址自赋值
b = &a;
…
a = *b;
也许有人会说:“即使出现自赋值,我也可以不理睬,大不了化点时间让对象复制自己而已,反正不会出错!”
他真的说错了。看看第二步的delete,自杀后还能复制自己吗?所以,如果发现自
赋值,应该马上终止函数。注意不要将检查自赋值的if 语句
if(this == &other)
错写成为
if( *this == other)
第10 章类的继承与组合
注意,当前面向对象技术的应用热点是COM 和CORBA,这些内容超出了C++教材的范畴,请阅读COM 和CORBA 相关论著。
(感觉COM挺不好使的,找时间了解一下CORBA究竟是啥玩意)
第11 章其它编程经验
【建议11-3-11】尽量使用标准库函数,不要“发明”已经存在的库函数。(同意)
【建议11-3-14】如果可能的话,使用PC-Lint、LogiScope 等工具进行代码审查。(目前没做起来,但我感觉我目前的BUG一般不在这些因粗心地方)