《高质量C/C++编程》笔记

本文详细介绍了编程过程中应遵循的规范及最佳实践,包括代码格式、变量命名、内存管理、函数设计等方面,帮助开发者写出高质量的代码。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

博士毕业前半年,我曾到微软中国研究院找工作,接受微软公司一位资深软件工程师的面试。他让我写函数 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类

目 录 前 言6 第1 章 文件结构 1.1 版权和版本的声明. 1.2 头文件的结构. 1.3 定义文件的结构. 1.4 头文件的作用. 1.5 目录结构. 第2 章 程序的版式 2.1 空行. 2.2 代码行. 2.3 代码行内的空格. 2.4 对齐. 2.5 长行拆分. 2.6 修饰符的位置. 2.7 注释. 2.8 类的版式. 第3 章 命名规则 3.1 共性规则. 3.2 简单的WINDOWS 应用程序命名规则. 3.3 简单的UNIX 应用程序命名规则 第4 章 表达式和基本语句 4.1 运算符的优先级. 4.2 复合表达式. 4.3 IF 语句 4.4 循环语句的效率. 4.5 FOR 语句的循环控制变量. 4.6 SWITCH 语句. 4.7 GOTO 语句. 第5 章 常量 5.1 为什么需要常量. 5.2 CONST 与 #DEFINE 的比较. 5.3 常量定义规则. 5.4 类中的常量. 第6 章 函数设计 高质量C++/C 编程指南,v 1.0 2001 Page 4 of 101 6.1 参数的规则. 6.2 返回值的规则. 6.3 函数内部实现的规则. 6.4 其它建议. 6.5 使用断言. 6.6 引用与指针的比较. 第7 章 内存管理 7.1 内存分配方式 7.2 常见的内存错误及其对策 7.3 指针与数组的对比 7.4 指针参数是如何传递内存的? 7.5 FREE 和DELETE 把指针怎么啦? 7.6 动态内存会被自动释放吗?. 7.7 杜绝“野指针”. 7.8 有了MALLOC/FREE 为什么还要NEW/DELETE ?. 7.9 内存耗尽怎么办?. 7.10 MALLOC/FREE 的使用要点 7.11 NEW/DELETE 的使用要点. 7.12 一些心得体会 第8 章 C++函数的高级特性 8.1 函数重载的概念. 8.2 成员函数的重载、覆盖与隐藏. 8.3 参数的缺省值. 8.4 运算符重载. 8.5 函数内联. 8.6 一些心得体会. 第9 章 类的构造函数、析构函数与赋值函数 9.1 构造函数与析构函数的起源. 9.2 构造函数的初始化表. 9.3 构造和析构的次序. 9.4 示例:类STRING 的构造函数与析构函数 9.5 不要轻视拷贝构造函数与赋值函数. 9.6 示例:类STRING 的拷贝构造函数与赋值函数 9.7 偷懒的办法处理拷贝构造函数与赋值函数. 9.8 如何在派生类中实现类的基本函数. 9.9 一些心得体会. 第10 章 类的继承与组合. 高质量C++/C 编程指南,v 1.0 2001 Page 5 of 101 10.1 继承 10.2 组合 第11 章 其它编程经验. 11.1 使用CONST 提高函数的健壮性 11.2 提高程序的效率 11.3 一些有益的建议 参考文献 附录A :C++/C 代码审查表. 附录B :C++/C 试题. 附录C :C++/C 试题的答案与评分标准.
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值