博士毕业前半年,我曾到微软中国研究院找工作,接受微软公司一位资深软件工程师的面试。他让我写函数 strcpy 的代码。
太容易了吧?
错!
这么一个小不点的函数,他从三个方面考查:
(1)编程风格;
(2)出错处理;
(3)算法复杂度分析(用于提高性能)
版权和版本的声明位于头文件和定义文件的开头(参见示例 1-1) ,主要内容有:
(1)版权信息。
(2)文件名称,标识符,摘要。
(3)当前版本号,作者/修改者,完成日期。
(4)版本历史信息。
头文件由三部分内容组成:
(1)头文件开头处的版权和版本声明(参见示例 1-1)。
(2)预处理块。
(3)函数和类结构声明等。
定义文件有三部分内容:
(1) 定义文件开头处的版权和版本声明(参见示例 1-1)。
(2) 对一些头文件的引用。
(3) 程序的实现体(包括数据和代码)
【规则 1-2-1】为了防止头文件被重复引用,应当用 ifndef/define/endif 结构产生预处理块。
【建议 1-2-1】头文件中只存放“声明”而不存放“定义”。
【建议 1-2-2】 不提倡使用全局变量, 尽量不要在头文件中出现象 extern int value 这类声明。
头文件的作用:
(1)通过头文件来调用库功能。在很多场合,源代码不便(或不准)向用户公布,只要向用户提供头文件和二进制的库即可。用户只需要按照头文件中的接口声明来调用库功能,而不必关心接口怎么实现的。编译器会从库中提取相应的代码。
(2)头文件能加强类型安全检查。如果某个接口被实现或被使用时,其方式与头文件中的声明不一致,编译器就会指出错误。
【规则 2-1-1】在每个类声明之后、每个函数定义结束之后都要加空行。
【规则 2-1-2】在一个函数体内,逻揖上密切相关的语句之间不加空行,其它地方应加空行分隔。
【规则 2-2-1】一行代码只做一件事情,如只定义一个变量,或只写一条语句。这样的代码容易阅读,并且方便于写注释。
【规则 2-2-2】if、for、while、do 等语句自占一行,执行语句不得紧跟其后。不论执行语句有多少都要加{}。
【建议 2-2-1】尽可能在定义变量的同时初始化该变量(就近原则)。
【规则 2-3-1】关键字之后要留空格。象 const、virtual、inline、case 等关键字之后至少要留一个空格,否则无法辨析关键字。
【规则 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 语句,为了紧凑起见可以适当地去掉一些空格
【规则 2-5-1】代码行最大长度宜控制在 70 至 80 个字符以内。
【规则 2-5-2】 长表达式要在低优先级操作符处拆分成新行, 操作符放在新行之首 (以便突出操作符) 。
【规则 2-6-1】应当将修饰符 * 和 & 紧靠变量名。
例如:
类的版式主要有两种方式:
(1)将 private 类型的数据写在前面,而将 public 类型的函数写在后面,采用这种版式的程序员主张类的设计“以数据为中心” ,重点关注类的内部结构。
(2)将 public 类型的函数写在前面,而将 private 类型的数据写在后面,采用这种版式的程序员主张类的设计“以行为为中心”, 重点关注的是类应该提供什么样的接口(或服务)
我建议读者采用“以行为为中心”的书写方式,即首先考虑类应该提供什么样的函数。因为用户最关心的是接口。
【规则 3-1-3】命名规则尽量与所采用的操作系统或开发工具的风格保持一致。
【规则 3-1-4】程序中不要出现仅靠大小写区分的相似的标识符。
【规则 3-1-5】程序中不要出现标识符完全相同的局部变量和全局变量,尽管两者的作用域不同而不会发生语法错误。
【建议 3-1-1】尽量避免名字中出现数字编号。
BOOL型与0值比较 TRUE 的值究竟是什么并没有统一的标准。
if (flag) // 表示 flag 为真
if (!flag) // 表示 flag 为假
整形变量与0值比较
if (value == 0)
if (value != 0)
浮点变量与零值比较 两个浮点数可以直接比较大小
if ((x>=-EPSINON) && (x<=EPSINON))
指针变量与零值比较
if (p == NULL)
if (p != NULL)
【建议 4-4-1】在多重循环中,如果有可能,应当将最长的循环放在最内层,最短的循环放在最外层,以减少 CPU 跨切循环层的次数。
【建议 4-4-2】如果循环体内存在逻辑判断,并且循环次数很大,宜将逻辑判断移到循环体的外面。
const 与 #define 的比较
const 常量有数据类型,而宏常量没有数据类型。编译器可以对前者进行类型安全检查。而对后者只进行字符替换。有些集成化的调试工具可以对const常量进行调试。
不能在类声明中初始化 const 数据成员,const 数据成员的初始化只能在类构造函数的初始化表中进行
怎样才能建立在整个类中都恒定的常量呢?别指望 const 数据成员了,应该用类中的枚举常量来实现。
【规则 6-1-2】参数命名要恰当,顺序要合理。一般地,应将目的参数放在前面,源参数放在后面。
【建议 6-2-1】有时候函数原本不需要返回值,但为了增加灵活性如支持链式表达,可以附加返回值。 如字符串拷贝函数 strcpy。
对于相加函数,应当用“值传递”的方式返回 String 对象。
【规则 6-3-1】在函数体的“入口处” ,对参数的有效性进行检查。
【规则 6-3-2】在函数体的“出口处” ,对 return 语句的正确性和效率进行检查。
return 语句不可返回指向“栈内存”的“指针”或者“引用”;
【建议 6-5-1】在编写函数时, 要进行反复的考查, 并且自问: “我打算做哪些假定?”一旦确定了的假定,就要使用断言对假定进行检查。
引用与指针的比较
(1)引用被创建的同时必须被初始化(指针则可以在任何时候被初始化)。
(2)不能有 NULL 引用,引用必须与合法的存储单元关联(指针则可以是 NULL)。
(3)一旦引用被初始化,就不能改变引用的关系(指针则可以随时改变所指的对象)。
内存分配方式有三种:
(1) 从静态存储区域分配。内存在程序编译的时候就已经分配好,这块内存在程序的整个运行期间都存在。例如全局变量,static 变量。
(2) 在栈上创建。
(3) 从堆上分配,亦称动态内存分配。
常见的内存错误
内存分配未成功,却使用了它
内存分配虽然成功,但是尚未初始化就引用它
操作越过了内存的边界
忘记了释放内存,造成内存泄露
释放了内存却继续使用它(返回栈上指针、野指针)
【规则 7-2-1】用 malloc 或 new 申请内存之后,应该立即检查指针值是否为 NULL。防止使用指针值为 NULL 的内存。
指针与数组的比较
数组要么在静态存储区被创建 (如全局数组), 要么在栈上被创建。 数组名对应着 (而不是指向)一块内存,其地址与容量在生命期内保持不变,只有数组的内容可以改变。
不能对数组名进行直接复制与比较。
用运算符 sizeof 可以计算出数组的容量(字节数)。
数组作为函数的参数进行传递时,该数组自动退化为同类型的指针。
如果函数的参数是一个指针,不要指望用该指针去申请动态内存。如果非得要用指针参数去申请内存, 那么应该改用 “指向指针的指针”。可以用函数返回值来传递动态内存。这里强调不要用 return 语句返回指向“栈内存”的指针
(1)指针消亡了,并不表示它所指的内存会被自动释放。
(2)内存被释放了,并不表示指针会消亡或者成了 NULL 指针。
对于 32 位以上的应用程序而言,无论怎样使用malloc 与 new,几乎不可能导致“内存耗尽”。因为 32 位操作系统支持“虚存”。内存耗尽后可根据判断申请的指针为NULL来exit(1)退出程序。
malloc 返回值的类型是 void *;malloc 函数本身并不识别要申请的内存是什么类型,它只关心内存的总字节数;在用 delete 释放对象数组时,留意不要丢了符号‘[]’
当心隐式类型转换导致重载函数产生二义性
重载、覆盖(override)与隐藏
重载位于同一类中,参数不完全一样。而覆盖则发生在派生类与基类完全一样的函数定义。隐藏则是由于派生类和基类的函数参数不同或者函数定义完全相同,而基类中没有使用virtual而导致的基类的同名函数被隐藏(与多态即覆盖类似,也是因为用基类指针指向派生类的对象)。
【规则 8-3-1】参数缺省值只能出现在函数的声明中,而不能出现在定义体中。
内联与宏函数的区别
宏函数只是简单的替换,无法操作类的私有数据成员,不能进行类型安全检查,或者进行自动类型转换。
内联是以代码膨胀(复制)为代价,仅仅省去了函数调用的开销。
“缺省的拷贝构造函数”和“缺省的赋值函数”均采用“位拷贝”而非“值拷贝”的方式来实现,倘若类中含有指针变量,这两个函数注定将出错。
派生类必须在其初始化表里调用基类的构造函数。B::B(int x, int y) :A(x) ?好像也可以在构造函数中调用A(x)。非内部数据类型的成员对象应当采用初始化表的方式初始化,以获取更高的效率。
如果给以“指针传递”方式的函数返回值加 const 修饰,那么函数返回值(即指针)的内容不能被修改,该返回值只能被赋给加 const 修饰的同类型指针。 如果函数返回值采用“值传递方式”, 由于函数会把返回值复制到外部临时的存储单元中,加 const 修饰没有任何价值。
任何不会修改数据成员的函数都应该声明为 const 类型。const 成员函数的声明看起来怪怪的:const 关键字只能放在函数声明的尾部。
String类