<高质量C++编程指南>
第1章 文件结构
1.版权和版本的声明
2.头文件的结构
【规则1-2-1】为了防止头文件被重复引用,应当用ifndef/define/endif结构产生预处理块。
【规则1-2-2】用 #include <filename.h> 格式来引用标准库的头文件(编译器将从标准库目录开始搜索)。
【规则1-2-3】用 #include “filename.h” 格式来引用非标准库的头文件(编译器将从用户的工作目录开始搜索)。
【建议1-2-1】头文件中只存放“声明”而不存放“定义”。
【建议1-2-2】不提倡使用全局变量,尽量不要在头文件中出现象extern int value 这类声明。
3.定义文件的结构
4.头文件的作用
5.目录结构
第2章 程序的版式
1.空行
【规则2-1-1】在每个类声明之后、每个函数定义结束之后都要加空行。
【规则2-1-2】在一个函数体内,逻揖上密切相关的语句之间不加空行,其它地方应加空行分隔。
2.代码行
【规则2-2-1】一行代码只做一件事情,如只定义一个变量,或只写一条语句。
【规则2-2-2】if、for、while、do等语句自占一行,执行语句不得紧跟其后。
【建议2-2-1】尽可能在定义变量的同时初始化该变量(就近原则)。
3.代码行内的空格
【规则2-3-1】关键字之后要留空格。
【规则2-3-2】函数名之后不要留空格,紧跟左括号‘(’,以与关键字区别。
【规则2-3-3】‘(’向后紧跟,‘)’、‘,’、‘;’向前紧跟,紧跟处不留空格。
【规则2-3-4】‘,’之后要留空格,如Function(x, y, z)。
【规则2-3-5】赋值操作符、比较操作符、算术操作符、逻辑操作符、位域操作符,如“=”、“+=” “>=”、“<=”、“+”、“*”、“%”、“&&”、“||”、“<<”,“^”等二元操作符的前后应当加空格。
【规则2-3-6】一元操作符如“!”、“~”、“++”、“--”、“&”(地址运算符)等前后不加空格。
【规则2-3-7】象“[]”、“.”、“->”这类操作符前后不加空格。
【建议2-3-1】对于表达式比较长的for语句和if语句,为了紧凑起见可以适当地去掉一些空格,如for (i=0; i<10; i++)和if ((a<=b) && (c<=d))。
4.对齐
【规则2-4-1】程序的分界符‘{’和‘}’应独占一行并且位于同一列,同时与引用它们的语句左对齐。
【规则2-4-2】{ }之内的代码块在‘{’右边数格处左对齐。
5.长行拆分
【规则2-5-1】代码行最大长度宜控制在70至80个字符以内。
【规则2-5-2】长表达式要在低优先级操作符处拆分成新行,操作符放在新行之首(以便突出操作符)。
6.修饰符的位置
【规则2-6-1】应当将修饰符 * 和 & 紧靠变量名。
7.注释
【规则2-7-1】注释是对代码的“提示”,而不是文档。程序中的注释不可喧宾夺主,注释太多了会让人眼花缭乱。注释的花样要少。
【规则2-7-2】如果代码本来就是清楚的,则不必加注释。
【规则2-7-3】边写代码边注释,修改代码同时修改相应的注释,以保证注释与代码的一致性。不再有用的注释要删除。
【规则2-7-4】注释应当准确、易懂,防止注释有二义性。
【规则2-7-5】尽量避免在注释中使用缩写,特别是不常用缩写。
【规则2-7-6】注释的位置应与被描述的代码相邻,可以放在代码的上方或右方,不可放在下方。
【规则2-7-8】当代码比较长,特别是有多重嵌套时,应当在一些段落的结束处加注释,便于阅读。
8.类的版式
类的版式主要有两种方式:
(1)将private类型的数据写在前面,而将public类型的函数写在后面。采用这种版式的程序员主张类的设计“以数据为中心”,重点关注类的内部结构。
(2)将public类型的函数写在前面,而将private类型的数据写在后面。采用这种版式的程序员主张类的设计“以行为为中心”,重点关注的是类应该提供什么样的接口(或服务)。
第3章 命名规则
1.共性规则
【规则3-1-1】标识符应当直观且可以拼读,可望文知意,不必进行“解码”。
【规则3-1-2】标识符的长度应当符合“min-length && max-information”原则
【规则3-1-3】命名规则尽量与所采用的操作系统或开发工具的风格保持一致。
【规则3-1-4】程序中不要出现仅靠大小写区分的相似的标识符。
【规则3-1-5】程序中不要出现标识符完全相同的局部变量和全局变量,尽管两者的作用域不同而不会发生语法错误,但会使人误解。
【规则3-1-6】变量的名字应当使用“名词”或者“形容词+名词”。
【规则3-1-7】全局函数的名字应当使用“动词”或者“动词+名词”(动宾词组)。
【规则3-1-8】用正确的反义词组命名具有互斥意义的变量或相反动作的函数等。
【建议3-1-1】尽量避免名字中出现数字编号,如Value1,Value2等,除非逻辑上的确需要编号。这是为了防止程序员偷懒,不肯为命名动脑筋而导致产生无意义的名字。
2.简单的Windows应用程序命名规则
【规则3-2-1】类名和函数名用大写字母开头的单词组合而成。
【规则3-2-2】变量和参数用小写字母开头的单词组合而成。
【规则3-2-3】常量全用大写的字母,用下划线分割单词。
【规则3-2-4】静态变量加前缀s_(表示static)。
【规则3-2-5】如果不得已需要全局变量,则使全局变量加前缀g_(表示global)。
【规则3-2-6】类的数据成员加前缀m_(表示member),这样可以避免数据成员与成员函数的参数同名。
【规则3-2-7】为了防止某一软件库中的一些标识符和其它软件库中的冲突,可以为各种标识符加上能反映软件性质的前缀。
3.简单的Unix应用程序命名规则
第4章 表达式和基本语句
1.运算符的优先级
【规则4-1-1】如果代码行中的运算符比较多,用括号确定表达式的操作顺序,避免使用默认的优先级。
2.复合表达式
【规则4-2-1】不要编写太复杂的复合表达式。
【规则4-2-2】不要有多用途的复合表达式。
【规则4-2-3】不要把程序中的复合表达式与“真正的数学表达式”混淆。
3.if 语句
【规则4-3-1】不可将布尔变量直接与TRUE、FALSE或者1、0进行比较。
【规则4-3-2】应当将整型变量用“==”或“!=”直接与0比较。
【规则4-3-3】不可将浮点变量用“==”或“!=”与任何数字比较。
【规则4-3-4】应当将指针变量用“==”或“!=”与NULL比较。
4.循环语句的效率
【建议4-4-1】在多重循环中,如果有可能,应当将最长的循环放在最内层,最短的循环放在最外层,以减少CPU跨切循环层的次数。
【建议4-4-2】如果循环体内存在逻辑判断,并且循环次数很大,宜将逻辑判断移到循环体的外面。
5.for 语句的循环控制变量
【规则4-5-1】不可在for 循环体内修改循环变量,防止for 循环失去控制。
【建议4-5-1】建议for语句的循环控制变量的取值采用“半开半闭区间”写法。
6.switch语句
【规则4-6-1】每个case语句的结尾不要忘了加break,否则将导致多个分支重叠(除非有意使多个分支重叠)。
【规则4-6-2】不要忘记最后那个default分支。
7.goto语句
第5章 常量
1.为什么需要常量
如果不使用常量,直接在程序中填写数字或字符串:
程序的可读性(可理解性)变差;
在程序的很多地方输入同样的数字或字符串,难保不发生书写错误;
如果要修改数字或字符串,则会在很多地方改动,既麻烦又容易出错。
【规则5-1-1】 尽量使用含义直观的常量来表示那些将在程序中多次出现的数字或字符串。
2.const 与 #define的比较
const优点:
const常量有数据类型,而宏常量没有数据类型;
有些集成化的调试工具可以对const常量进行调试,但是不能对宏常量进行调试。
【规则5-2-1】在C++ 程序中只使用const常量而不使用宏常量,即const常量完全取代宏常量。
3.常量定义规则
【规则5-3-1】需要对外公开的常量放在头文件中,不需要对外公开的常量放在定义文件的头部。
【规则5-3-2】如果某一常量与其它常量密切相关,应在定义中包含这种关系,而不应给出一些孤立的值。
4.类中的常量
用类中的枚举常量来实现整个类中都恒定的常量。
第6章 函数设计
1.参数的规则
【规则6-1-1】参数的书写要完整,不要贪图省事只写参数的类型而省略参数名字。如果函数没有参数,则用void填充。
【规则6-1-2】参数命名要恰当,顺序要合理。
【规则6-1-3】如果参数是指针,且仅作输入用,则应在类型前加const,以防止该指针在函数体内被意外修改。
【规则6-1-4】如果输入参数以值传递的方式传递对象,则宜改用“const &”方式来传递,这样可以省去临时对象的构造和析构过程,从而提高效率。
【建议6-1-1】避免函数有太多的参数,参数个数尽量控制在5个以内。
【建议6-1-2】尽量不要使用类型和数目不确定的参数。
2.返回值的规则
【规则6-2-1】不要省略返回值的类型。
【规则6-2-2】函数名字与返回值类型在语义上不可冲突。
【规则6-2-3】不要将正常值和错误标志混在一起返回。正常值用输出参数获得,而错误标志用return语句返回。
【建议6-2-1】有时候函数原本不需要返回值,但为了增加灵活性如支持链式表达,可以附加返回值。
【建议6-2-2】如果函数的返回值是一个对象,有些场合用“引用传递”替换“值传递”可以提高效率。而有些场合只能用“值传递”而不能用“引用传递”,否则会出错。
3.函数内部实现的规则
【规则6-3-1】在函数体的“入口处”,对参数的有效性进行检查。
【规则6-3-2】在函数体的“出口处”,对return语句的正确性和效率进行检查。
如果函数返回值是一个对象,要考虑return语句的效率。
return String(s1 + s2);表示“创建一个临时对象并返回它”。编译器直接把临时对象创建并初始化在外部存储单元中,省去了拷贝和析构的化费,提高了效率。
String temp(s1 + s2);return temp;表示“先创建一个局部对象temp并返回它的结果”
4.其它建议
【建议6-4-1】函数的功能要单一,不要设计多用途的函数。
【建议6-4-2】函数体的规模要小,尽量控制在50行代码之内。
【建议6-4-3】尽量避免函数带有“记忆”功能。
【建议6-4-4】不仅要检查输入参数的有效性,还要检查通过其它途径进入函数体内的变量的有效性,例如全局变量、文件句柄等。
【建议6-4-5】用于出错处理的返回值一定要清楚,让使用者不容易忽视或误解错误情况。
5.使用断言
【规则6-5-1】使用断言捕捉不应该发生的非法情况。不要混淆非法情况与错误情况之间的区别,后者是必然存在的并且是一定要作出处理的。
【规则6-5-2】在函数的入口处,使用断言检查参数的有效性(合法性)。
【建议6-5-1】在编写函数时,要进行反复的考查,并且自问:“我打算做哪些假定?”一旦确定了的假定,就要使用断言对假定进行检查。
【建议6-5-2】一般教科书都鼓励程序员们进行防错设计,但要记住这种编程风格可能会隐瞒错误。当进行防错设计时,如果“不可能发生”的事情的确发生了,则要使用断言进行报警。
6.引用与指针的比较
第7章 内存管理
1.内存分配方式
从静态存储区域分配。
在栈上创建。
从堆上分配,亦称动态内存分配。
2.常见的内存错误及其对策
【规则7-2-1】用malloc或new申请内存之后,应该立即检查指针值是否为NULL。防止使用指针值为NULL的内存。
【规则7-2-2】不要忘记为数组和动态内存赋初值。防止将未被初始化的内存作为右值使用。
【规则7-2-3】避免数组或指针的下标越界,特别要当心发生“多1”或者“少1”操作。
【规则7-2-4】动态内存的申请与释放必须配对,防止内存泄漏。
【规则7-2-5】用free或delete释放了内存之后,立即将指针设置为NULL,防止产生“野指针”。
3.指针与数组的对比
数组要么在静态存储区被创建(如全局数组),要么在栈上被创建。
当数组作为函数的参数进行传递时,该数组自动退化为同类型的指针。
4.指针参数是如何传递内存的
如果函数的参数是一个指针,不要指望用该指针去申请动态内存。
编译器总是要为函数的每个参数制作临时副本,指针参数p的副本是 _p,编译器使 _p = p。
5.free和delete把指针怎么啦
6.动态内存会被自动释放吗
指针消亡了,并不表示它所指的内存会被自动释放。
内存被释放了,并不表示指针会消亡或者成了NULL指针。
7.杜绝“野指针”
原因:
指针变量没有被初始化。
指针p被free或者delete之后,没有置为NULL。
指针操作超越了变量的作用范围。
8.有了malloc/free为什么还要new/delete
new/delete不是库函数,是操作符。
运算符new:能完成动态内存分配和初始化工作。
运算符delete:能完成清理与释放内存工作。
9.内存耗尽怎么办?
(1)判断指针是否为NULL,如果是则马上用return语句终止本函数。
(2)判断指针是否为NULL,如果是则马上用exit(1)终止整个程序的运行。
(3)为new和malloc设置异常处理函数
10.malloc/free 的使用要点
11.new/delete 的使用要点
12.一些心得体会
越是怕指针,就越要使用指针。不会正确使用指针,肯定算不上是合格的程序员。
必须养成“使用调试器逐步跟踪程序”的习惯,只有这样才能发现问题的本质。
第8章 C++函数的高级特性
1.函数重载的概念
只能靠参数而不能靠返回值类型的不同来区分重载函数。
如果C++程序要调用已经被编译后的C函数,该怎么办?
假设某个C函数的声明如下:
void foo(int x, int y);
该函数被C编译器编译后在库中的名字为_foo,而C++编译器则会产生像_foo_int_int之类的名字用来支持函数重载和类型安全连接。由于编译后的名字不同,C++程序不能直接调用C函数。C++提供了一个C连接交换指定符号extern“C”来解决这个问题。
当心隐式类型转换导致重载函数产生二义性
2.成员函数的重载、覆盖与隐藏
3.参数的缺省值
【规则8-3-1】参数缺省值只能出现在函数的声明中,而不能出现在定义体中。
【规则8-3-2】如果函数有多个参数,参数只能从后向前挨个儿缺省,否则将导致函数调用语句怪模怪样。
4.运算符重载
运算符 规则
所有的一元运算符 建议重载为成员函数
= () [] -> 只能重载为成员函数
+= -= /= *= &= |= ~= %= >>= <<= 建议重载为成员函数
所有其它运算符 建议重载为全局函数
5.函数内联
慎用内联:
如果函数体内的代码比较长,使用内联将导致内存消耗代价较高。
如果函数体内出现循环,那么执行函数体内代码的时间要比函数调用的开销大。
6.一些心得体会
第9章 类的构造函数、析构函数与赋值函数
1.构造函数与析构函数的起源
2.构造函数的初始化表
构造函数初始化表的使用规则:
如果类存在继承关系,派生类必须在其初始化表里调用基类的构造函数。
类的const常量只能在初始化表里被初始化。
非内部数据类型的成员对象应当采用初始化表方式初始化,以获取更高的效率。
3.构造和析构的次序
成员对象初始化的次序完全不受它们在初始化表中次序的影响,只由成员对象在类中声明的次序决定。
4.示例:类String的构造函数与析构函数
5.不要轻视拷贝构造函数与赋值函数
如果不主动编写拷贝构造函数和赋值函数,编译器将以“位拷贝”的方式自动生成缺省的函数。倘若类中含有指针变量,那么这两个缺省的函数就隐含了错误。
拷贝构造函数是在对象被创建时调用的,而赋值函数只能被已经存在了的对象调用。
6.示例:类String的拷贝构造函数与赋值函数
7.偷懒的办法处理拷贝构造函数与赋值函数
只需将拷贝构造函数和赋值函数声明为私有函数,不用编写代码。
8.如何在派生类中实现类的基本函数
在编写派生类的赋值函数时,注意不要忘记对基类的数据成员重新赋值。
9.一些心得体会
第10章 类的继承与组合
1.继承
【规则10-1-1】如果类A和类B毫不相关,不可以为了使B的功能更多些而让B继承A的功能和属性。
【规则10-1-2】若在逻辑上B是A的“一种”(a kind of ),则允许B继承A的功能和属性。
2.组合
【规则10-2-1】若在逻辑上A是B的“一部分”(a part of),则不允许B从A派生,而是要用A和其它东西组合出B。
第11章 其它编程经验
1.使用const提高函数的健壮性
const可以修饰函数的参数、返回值,甚至函数的定义体。被const修饰的东西都受到强制保护,可以预防意外的变动,能提高程序的健壮性。
对于非内部数据类型的输入参数,应该将“值传递”的方式改为“const引用传递”,目的是提高效率。例如将void Func(A a) 改为void Func(const A &a)。
对于内部数据类型的输入参数,不要将“值传递”的方式改为“const引用传递”。否则既达不到提高效率的目的,又降低了函数的可理解性。例如void Func(int x) 不应该改为void Func(const int &x)。
任何不会修改数据成员的函数都应该声明为const类型。
2.提高程序的效率
【规则11-2-1】不要一味地追求程序的效率,应当在满足正确性、可靠性、健壮性、可读性等质量因素的前提下,设法提高程序的效率。
【规则11-2-2】以提高程序的全局效率为主,提高局部效率为辅。
【规则11-2-3】在优化程序的效率时,应当先找出限制效率的“瓶颈”,不要在无关紧要之处优化。
【规则11-2-4】先优化数据结构和算法,再优化执行代码。
【规则11-2-5】有时候时间效率和空间效率可能对立,此时应当分析哪个更重要,作出适当的折衷。
【规则11-2-6】不要追求紧凑的代码,因为紧凑的代码并不能产生高效的机器码。
3.一些有益的建议
【建议11-3-1】当心那些视觉上不易分辨的操作符发生书写错误。
【建议11-3-2】变量(指针、数组)被创建之后应当及时把它们初始化,以防止把未被初始化的变量当成右值使用。
【建议11-3-3】当心变量的初值、缺省值错误,或者精度不够。
【建议11-3-4】当心数据类型转换发生错误。尽量使用显式的数据类型转换(让人们知道发生了什么事),避免让编译器轻悄悄地进行隐式的数据类型转换。
【建议11-3-5】当心变量发生上溢或下溢,数组的下标越界。
【建议11-3-6】当心忘记编写错误处理程序,当心错误处理程序本身有误。
【建议11-3-7】当心文件I/O有错误。
【建议11-3-8】避免编写技巧性很高代码。
【建议11-3-9】不要设计面面俱到、非常灵活的数据结构。
【建议11-3-10】如果原有的代码质量比较好,尽量复用它。但是不要修补很差劲的代码,应当重新编写。
【建议11-3-11】尽量使用标准库函数,不要“发明”已经存在的库函数。
【建议11-3-12】尽量不要使用与具体硬件或软件环境关系密切的变量。
【建议11-3-13】把编译器的选择项设置为最严格状态。
【建议11-3-14】如果可能的话,使用PC-Lint、LogiScope等工具进行代码审查。