高质量C++编程指南(林锐)阅读笔记

高质量C++编程指南(林锐)阅读笔记

请注意,本文编写于 1475 天前,最后修改于 1157 天前,其中某些信息可能已经过时。

高质量C++编程指南(林锐)阅读笔记

第 0 章 前言

今天在刷题群里发现有群友在发这个文件。想起C++ 许老师曾经推荐过这本《编程指南》,索性将这本书花了数个小时粗略过完。其中作者的一些心得是能够迁移到其他语言上的,但是也由于这本《编程指南》撰写的时代过于久远,我随后也参照了一些现行的编程风格规范。这本《编程指南》非常全面,也涉及了很多我已经忘记的C++语法,结合了很多作者的心得。在阅读完成后,我认为大家可以先通读这本《编程指南》,然后去查询阿里或者Google的编程风格规范,结合自己的情况做调整,尽量写出可读性更强的代码。

第 1 章 文件结构

每个 C++/C 程序通常分为两个文件。一个文件用于保存程序的声明(declaration), 称为头文件('.h'后缀)。另一个文件用于保存程序的实现(implementation),称为定义(definition) 文件('.c/.cpp'后缀)。

1.1 头文件的结构

头文件由三部分内容组成:

(1)头文件开头处的版权和版本声明(参见示例 1-1)。

(2)预处理块。

(3)函数和类结构声明等。

  • 为了防止头文件被重复引用,应当用 ifndef/define/endif 结构产生预处 理块。
  • #include 格式来引用标准库的头文件(编译器将从 标准库目录开始搜索)。
  • #include “filename.h” 格式来引用非标准库的头文件(编译器将 从用户的工作目录开始搜索)。
  • 头文件中只存放“声明”而不存放“定义”
#ifndef GRAPHICS_H // 防止 graphics.h 被重复引用
#define GRAPHICS_H
#include <math.h> // 引用标准库的头文件
…
#include “myheader.h” // 引用非标准库的头文件
…
void Function1(…); // 全局函数声明
…
class Box // 类结构声明
{
…
};
#endif

1.2 定义文件的结构

定义文件有三部分内容:

(1) 定义文件开头处的版权和版本声明(参见示例 1-1)。

(2) 对一些头文件的引用。

(3) 程序的实现体(包括数据和代码)。

#include “graphics.h” // 引用头文件
…
// 全局函数的实现体
void Function1(…)
{
…
}
// 类成员函数的实现体
void Box::Draw(…)
{
…
}

1.3 目录结构

  • 如果一个软件的头文件数目比较多(如超过十个),通常应将头文件和定义文件分别 保存于不同的目录,以便于维护。
  • 例如可将头文件保存于 include 目录,将定义文件保存于 source 目录(可以是多级 目录)。
  • 如果某些头文件是私有的,它不会被用户的程序直接引用,则没有必要公开其“声 明”。为了加强信息隐藏,这些私有的头文件可以和定义文件存放于同一个目录。

第 2 章 程序的版式

2.1 空行

  • 在每个类声明之后、每个函数定义结束之后都要加空行。
  • 在一个函数体内,逻揖上密切相关的语句之间不加空行,其它地方应 加空行分隔。

2.2 代码行

  • 一行代码只做一件事情,如只定义一个变量,或只写一条语句。这样 的代码容易阅读,并且方便于写注释。
  • if、for、while、do 等语句自占一行,执行语句不得紧跟其后。不论 执行语句有多少都要加{}。
  • 尽可能在定义变量的同时初始化该变量

2.3 代码行内的空格

  • 关键字之后要留空格
  • 函数名之后不要留空格
  • 赋值操作符、比较操作符、算术操作符、逻辑操作符、位域操作符等二元 操作符的前后应当加空格。
  • 表达式比较长的 for 语句和 if 语句,为了紧凑起见可以适当地去 掉一些空格,如 for (i=0; i<10; i++)和 if ((a<=b) && (c<=d))

2.4 对齐

  • 程序的分界符‘{’和‘}’应独占一行并且位于同一列,同时与引用 它们的语句左对齐。

2.5 长行拆分

  • 代码行最大长度宜控制在 70 至 80 个字符以内。

2.6 修饰符的位置

  • 应当将修饰符 * 和 & 紧靠变量名
int *x, y; // 此处 y 不会被误解为指针

2.8 类的格式

主要分为以数据为中心以及以行为中心的格式

  • 以数据为中心:先定义数据成员,再定义函数
  • 以行为中心:先定义函数,在定义数据成员

建议”以行为中心“

第 3 章 命名规则(有待商榷)

  • 标识符应当直观且可以拼读,可望文知意,不必进行“解码”
  • 标识符的长度应当符合“min-length && max-information”原则
  • 变量的名字应当使用“名词”或者“形容词+名词”。
  • 全局函数的名字应当使用“动词”或者“动词+名词”(动宾词组)。 类的成员函数应当只使用“动词”,被省略掉的名词就是对象本身。
  • 类名和函数名用大写字母开头的单词组合而成
  • 变量和参数用小写字母开头的单词组合而成。
  • 常量全用大写的字母,用下划线分割单词
  • 静态变量加前缀 s_(表示 static)。
  • 如果不得已需要全局变量,则使全局变量加前缀 g_
  • 类的数据成员加前缀 m_

第 4 章 表达式与基本语句

4.1 优先级

  • 如果代码行中的运算符比较多,用括号确定表达式的操作顺序,避免 使用默认的优先级。

4.2 if 语句

  • 不可将布尔变量直接与 TRUE、FALSE 或者 1、0 进行比较
  • 应当将整型变量用“==”或“!=”直接与 0 比较。
  • 不可将浮点变量用“==”或“!=”与任何数字比较。

    • 无论是 float 还是 double 类型的变量,都有精度限制。
    • 转化为if ((x>=-EPSINON) && (x<=EPSINON))
  • 应当将指针变量用“==”或“!=”与 NULL 比较。
// BOOL 变量
if (flag) // 表示 flag 为真
if (!flag) // 表示 flag 为假
// 整型变量
if (value == 0)
if (value != 0)
// 浮点变量
if ((x>=-EPSINON) && (x<=EPSINON))
// 指针变量  
if (p == NULL) // p 与 NULL 显式比较,强调 p 是指针变量
if (p != NULL)
  

4.3 循环语句的效率

  • 在多重循环中,如果有可能,应当将最长的循环放在最内层,最短的 循环放在最外层,以减少 CPU 跨切循环层的次数

4.4 for 语句的循环控制变量

不可在 for 循环体内修改循环变量,防止 for 循环失去控制

第 5 章 常量

5.1 const 与 #define 的比较

(1)const 常量有数据类型,而宏常量没有数据类型。编译器可以对前者进行类型安 全检查。而对后者只进行字符替换,没有类型安全检查,并且在字符替换可能会 产生意料不到的错误(边际效应)。

(2) 有些集成化的调试工具可以对 const 常量进行调试,但是不能对宏常量进行调试。

第 6 章 函数设计

6.1 参数的规则

  • 如果参数是指针,且仅作输入用,则应在类型前加 const,以防止该 指针在函数体内被意外修改。

6.2 函数内部实现的规则

  • 在函数体的“入口处”,对参数的有效性进行检查(可以使用”断言“)
  • 在函数体的“出口处”,对 return 语句的正确性和效率进行检查。

6.3 使用断言

  • 使用断言捕捉不应该发生的非法情况。不要混淆非法情况与错误情况 之间的区别,后者是必然存在的并且是一定要作出处理的。
  • 在函数的入口处,使用断言检查参数的有效性(合法性)

6.4 引用与指针的比较

引用的一些规则如下:

(1)引用被创建的同时必须被初始化(指针则可以在任何时候被初始化)。

(2)不能有 NULL 引用,引用必须与合法的存储单元关联(指针则可以是 NULL)。

(3)一旦引用被初始化,就不能改变引用的关系(指针则可以随时改变所指的对象)。

第 7 章 内存管理

640K ought to be enough for everybody — Bill Gates 1981

7.1 内存分配方式

(1) 从静态存储区域分配。内存在程序编译的时候就已经分配好,这块内存在程序的 整个运行期间都存在。例如全局变量,static 变量。

(2) 在栈上创建。在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函 数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集 中,效率很高,但是分配的内存容量有限。

(3) 从堆上分配,亦称动态内存分配。程序在运行的时候用 malloc 或 new 申请任意多 少的内存,程序员自己负责在何时用 free 或 delete 释放内存。动态内存的生存期 由我们决定,使用非常灵活,但问题也最多。

7.2 常见的内存错误及其对策

常见的内存错误
  • 内存分配未成功,却使用了它。
  • 内存分配虽然成功,但是尚未初始化就引用它。
  • 内存分配成功并且已经初始化,但操作越过了内存的边界。
  • 忘记了释放内存,造成内存泄露。
  • 释放了内存却继续使用它。

    • 函数的 return 语句写错了,注意不要返回指向“栈内存”的“指针”或者“引用”, 因为该内存在函数体结束时被自动销毁。
    • 使用 free 或 delete 释放了内存后,没有将指针设置为 NULL。导致产生“野指针”。
内存错误对策
  • 用 malloc 或 new 申请内存之后,应该立即检查指针值是否为 NULL。 防止使用指针值为 NULL 的内存。
  • 不要忘记为数组和动态内存赋初值。防止将未被初始化的内存作为右 值使用。
  • 避免数组或指针的下标越界,特别要当心发生“多 1”或者“少 1” 操作。
  • 动态内存的申请与释放必须配对,防止内存泄漏。
  • 用 free 或 delete 释放了内存之后,立即将指针设置为 NULL,防止产 生“野指针”。

7.3 free 和 delete 把指针怎么啦?

  • 指针 p 被 free 以后其地址仍然不变(非 NULL),只是 该地址对应的内存是垃圾,p 成了“野指针”。如果此时不把 p 设置为 NULL,会让人误 以为 p 是个合法的指针。

7.4 动态内存会被自动释放吗?

(1)指针消亡了,并不表示它所指的内存会被自动释放。

(2)内存被释放了,并不表示指针会消亡或者成了 NULL 指针。

7.5 杜绝“野指针”

7.5.1 什么是”野指针“
  • “野指针”不是 NULL 指针,是指向“垃圾”内存的指针。
7.5.2 “野指针”的成因主要有两种:
  • 指针变量没有被初始化。

    • 所以,指针变量在创建的同时应当被初始化,要么 将指针设置为 NULL,要么让它指向合法的内存。
  • 指针 p 被 free 或者 delete 之后,没有置为 NULL,
  • 指针操作超越了变量的作用范围

7.6 有了 malloc/free 为什么还要 new/delete ?

  • 由于 malloc/free 是库函数而不是运算符,不在编译器控制权限之内,不能够把执行构造函数 和析构函数的任务强加于 malloc/free。
  • 因此 C++语言需要一个能完成动态内存分配和初始化工作的运算符 new,以及一个 能完成清理与释放内存工作的运算符 delete。注意 new/delete 不是库函数。
  • 如果用 free 释放“new 创建的动态对象”,那么该对象因无法执行析构函数而可能 导致程序出错。如果用 delete 释放“malloc 申请的动态内存”,理论上讲程序不会出错, 但是该程序的可读性很差。所以 new/delete 必须配对使用,malloc/free 也一样。

7.7 一些心得体会

  • 越是怕指针,就越要使用指针。不会正确使用指针,肯定算不上是合格的程序员。
  • 必须养成“使用调试器逐步跟踪程序”的习惯,只有这样才能发现问题的本质。
  • C++中尽量使用 new/delete 进行内存的管理

第 8 章 C++函数的高级特性

对比于 C 语言的函数,C++增加了重载(overloaded)、内联(inline)、const 和 virtual 四种新机制。其中重载和内联机制既可用于全局函数也可用于类的成员函数,const 与 virtual 机制仅用于类的成员函数。

8.1 函数重载

8.1.1 为什么要进行重载
  • 在 C++程序中,可以将语义、功能相似的几个函数用同一个名字表示,即函数重载。 这样便于记忆,提高了函数的易用性。
  • C++语言采用重载机制的另一个理由是:类的构造函数需要重载机制。因为 C++规定 构造函数与类同名
8.1.2 重载如何实现
  • 如果同名函数的参数不同(包括类型、顺序不同),那么容易区别出它们是不同的函 数。
  • 如果同名函数仅仅是返回值类型不同,有时可以区分,有时却不能。
8.1.3 重载的易错点
  • 当心隐式类型转换导致重载函数产生二义性

8.2 成员函数的重载、覆盖与隐藏

8.2.1 成员函数被重载的特征:

(1)相同的范围(在同一个类中);

(2)函数名字相同;

(3)参数不同;

(4)virtual 关键字可有可无。

8.2.1 覆盖是指派生类函数覆盖基类函数,特征是:

(1)不同的范围(分别位于派生类与基类);

(2)函数名字相同;

(3)参数相同;

(4)基类函数必须有 virtual 关键字。

8.3 参数的缺省值

  • 参数缺省值只能出现在函数的声明中,而不能出现在定义体中。
  • 如果函数有多个参数,参数只能从后向前挨个儿缺省,否则将导致 函数调用语句怪模怪样。

第 9 章 类的构造函数、析构函数与赋值函数

  • 每个类只有一个析构函数和一个赋值函数,但可以有多个构造函数(包含一个拷贝 构造函数,其它的称为普通构造函数)
  • 如果不想编写上述函数, C++编译器将自动为 A 产生四个缺省的函数,
A(void); // 缺省的无参数构造函数 
A(const A &a); // 缺省的拷贝构造函数
~A(void); // 缺省的析构函数
A & operate =(const A &a); // 缺省的赋值函数

值得注意的是:“缺省的拷贝构造函数”和“缺省的赋值函数”均采用“位拷贝”而非“值拷贝” 的方式来实现,倘若类中含有指针变量,这两个函数注定将出错。

9.1 构造函数与析构函数

9.1.1 起源

Stroustrup 在设计 C++语言时充分考虑了这个问题 并很好地予以解决:把对象的初始化工作放在构造函数中,把清除工作放在析构函数中。 当对象被创建时,构造函数被自动执行。当对象消亡时,析构函数被自动执行。这下就 不用担心忘了对象的初始化和清除工作。

让构造函数、析构函数与类同名,由于析构函数的 目的与构造函数的相反,就加前缀‘~’以示区别。

9.1.2 构造函数的初始化表

构造函数有个特殊的初始化方式叫“初始化表达式表”(简称初始化表)。初始化表 位于函数参数表之后,却在函数体 {} 之前。

  • 如果类存在继承关系,派生类必须在其初始化表里调用基类的构造函数。
class A
{…
    A(int x); // A 的构造函数
};
class B : public A
{…
    B(int x, int y);// B 的构造函数
};
B::B(int x, int y)
: A(x) // 在初始化表里调用 A 的构造函数
{
 …
}
  • 类的 const 常量只能在初始化表里被初始化,因为它不能在函数体内用赋值的方式 来初始化
  • 类的数据成员的初始化可以采用初始化表或函数体内赋值两种方式,这两种方式的 效率不完全相同。 非内部数据类型的成员对象应当采用第一种方式初始化,以获取更高的效率。
9.1.3 构造和析构的次序

构造从类层次的最根处开始,在每一层中,首先调用基类的构造函数,然后调用成 员对象的构造函数。析构则严格按照与构造相反的次序执行,该次序是唯一的,否则编 译器将无法自动执行析构过程。

  • 成员对象初始化的次序完全不受它们在初始化表中次序的影响, 只由成员对象在类中声明的次序决定。这是因为类的声明是唯一的,而类的构造函数可 以有多个,因此会有多个不同次序的初始化表。如果成员对象按照初始化表的次序进行 构造,这将导致析构函数无法得到唯一的逆序。

第 10 章 类的继承与组合

对象(Object)是类(Class)的一个实例(Instance)。

10.1 类的继承

  • 如果类 A 和类 B 毫不相关,不可以为了使 B 的功能更多些而让 B 继承 A 的功能和属性。
  • 若在逻辑上 B 是 A 的“一种”,并且 A 的所有功 能和属性对 B 而言都有意义,则允许 B 继承 A 的功能和属性。

10.2 类的组合

  • 若在逻辑上 A 是 B 的“一部分”(a part of),则不允许 B 从 A 派生

第 11 章 其它编程经验

11.1 使用 const 提高函数的健壮性

11.1.1 用 const 修饰函数的参数
  • 如果输入参数采用“指针传递”,那么加 const 修饰可以防止意外地改动该指针,起 到保护作用。
  • 如果输入参数采用“值传递”,由于函数将自动产生临时变量用于复制该参数,该输 入参数本来就无需保护,所以不要加 const 修饰。
  • 对于非内部数据类型的参数而言,象 void Func(A a) 这样声明的函数注定效率比较低下。因为函数体内将产生 A 类型的临时对象用于复制参数 a,而临时对象的构造、 复制、析构过程都将消耗时间。
  • 为了提高效率,可以将函数声明改为 void Func(A &a),因为“引用传递”仅借用 一下参数的别名而已,不需要产生临时对象。但是函数 void Func(A &a) 存在一个缺点: “引用传递”有可能改变参数 a,这是我们不期望的。解决这个问题很容易,加 const 修饰即可,因此函数最终成为 void Func(const A &a)。

void Func(const A &a)


##### 11.1.2 const 成员函数 

- 任何不会修改数据成员的函数都应该声明为 const 类型

#### 11.2 提高程序的效率

- 不要一味地追求程序的效率,应当在满足正确性、可靠性、健壮性、 可读性等质量因素的前提下,设法提高程序的效率。

- 以提高程序的全局效率为主,提高局部效率为辅。

- 在优化程序的效率时,应当先找出限制效率的“瓶颈”,不要在无关 紧要之处优化。

- 先优化数据结构和算法,再优化执行代码。

已有 2 条评论

10届mstc老鸟 10届mstc老鸟
2 0

hello 听说纸飞机没有了,无聊搜微软学生俱乐部,找到你的博客 林锐这本书当年郑洪源给我们上c语言程学设计的时候就在传了,爷青回;)

Muyun Muyun 回复 @10届mstc老鸟
0 0

纸飞机的解散真是一个非常大的遗憾