6

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

 3 years ago
source link: https://muyun.work/Note4C.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

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

第 0 章 前言

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

第 1 章 文件结构

每个 C++/​C 程序通常分为两个文件。一个文件用于保存程序的声明(de­c­la­ra­tion), 称为头文件 ('.h' 后缀)。另一个文件用于保存程序的实现(im­ple­men­ta­tion),称为定义(de­f­i­n­i­tion) 文件 ('.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 every­body — Bill Gates 1981

7.1 内存分配方式

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

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

(3) 从堆上分配,亦称动态内存分配。程序在运行的时候用 mal­loc 或 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++ 增加了重载(over­loaded)、内联(in­line)、const 和 vir­tual 四种新机制。其中重载和内联机制既可用于全局函数也可用于类的成员函数,const 与 vir­tual 机制仅用于类的成员函数。

8.1 函数重载

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

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

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

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

(2)函数名字相同;

(3)参数不同;

(4)vir­tual 关键字可有可无。

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

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

(2)函数名字相同;

(3)参数相同;

(4)基类函数必须有 vir­tual 关键字。

8.3 参数的缺省值

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

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

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

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

9.1 构造函数与析构函数

9.1.1 起源

Strous­trup 在设计 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 章 类的继承与组合

对象(Ob­ject)是类(Class)的一个实例(In­stance)。

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 提高程序的效率

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

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

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

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

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK