《高质量C/C++编程指南》阅读总结
C/C++程序编写规范
原著链接:https://download.****.net/download/zhangyuanxuevaq/10354477
头文件的结构:
【建议】 头文件中只存放“声明”而不存放“定义”
在 C++ 语法中,类的成员函数可以在声明的同时被定义,并且自动成为内联函数。这虽然会带来书写上的方便,但却造成了风格不一致,弊大于利。建议将成员函数的定义与声明分开,不论该函数体有多么小。
【建议】 不提倡使用全局变量,尽量不要在头文件中出现象 extern int value 这类声明。
空行:
【规则】 在每个类声明之后、每个函数定义结束之后都要加空行
【规则】 在一个函数体内,逻揖上密切相关的语句之间不加空行,其它地方应加空行分隔。
代码行:
【规则】 一行代码只做一件事情,如只定义一个变量,或只写一条语句。这样
的代码容易阅读,并且方便于写注释。
【规则】 if、 for、 while、 do 等语句自占一行,执行语句不得紧跟其后。不论执行语句有多少都要加{}。这样可以防止书写失误。
【规则】 应当将修饰符 * 和 & 紧靠变量名 如:int *x, y; // 此处 y 不会被误解为指针。
注释:
【规则】 边写代码边注释,修改代码同时修改相应的注释,以保证注释与代码的一致性。不再有用的注释要删除。
【规则】 注释应当准确、易懂,防止注释有二义性。错误的注释不但无益反而有害。
命名规则:
【规则】 静态变量加前缀 s_(表示 static) 、
布尔变量与零值比较
【规则】 不可将布尔变量直接与 TRUE、 FALSE 或者 1、 0 进行比较。
不可将浮点变量用“==”或“!=”与任何数字比较
循环语句的效率
【建议】 在多重循环中,如果有可能,应当将最长的循环放在最内层,最短的循环放在最外层,以减少 CPU 跨切循环层的次数。
【建议】 如果循环体内存在逻辑判断,并且循环次数很大,宜将逻辑判断移到 循环体的外面。示例 4-4(c)的程序比示例 4-4(d)多执行了 N-1 次逻辑判断。并且由于前者老要进行逻辑判断,打断了循环“流水线”作业,使得编译器不能对循环进行优化处理,降低了效率。如果 N 非常大,最好采用示例 4-4(d)的写法,可以提高效率。如果 N 非常小,两者效率差别并不明显,采用示例 4-4(c)的写法比较好,因为程序更加简洁。
【规则】 每个 case 语句的结尾不要忘了加 break,否则将导致多个分支重叠(除非有意使多个分支重叠)。
【规则】不要忘记最后那个 default 分支。 即使程序真的不需要 default 处理,也应该保留语句 default : break; 这样做并非多此一举,而是为了防止别人误以为你忘了 default 处理。
有关goto 语句
很多人建议废除 C++/C 的 goto 语句,以绝后患。但实事求是地说,错误是程序员自
己造成的,不是 goto 的过错。goto 语句至少有一处可显神通,它能从多重循环体中咻地一下子跳到外面,用不着写很多次的 break 语句;
就象楼房着火了,来不及从楼梯一级一级往下走,可从窗口跳出火坑。所以我们主
张少用、慎用 goto 语句,而不是禁用。
const 与 #define 的比较
【规则】 尽量使用含义直观的常量来表示那些将在程序中多次出现的数字或字符串。
#define |
MAX 100 |
/* C 语言的宏常量 */ |
const int |
MAX = 100; |
// C++ 语言的 const 常量 |
const float |
PI = 3.14159; // C++ 语言的 const 常量 |
|
【规则】 在 C++ 程序中只使用 const 常量而不使用宏常量,即 const 常量完全取代宏常量(const 常量有数据类型)。
【规则】 需要对外公开的常量放在头文件中,不需要对外公开的常量放在定义文件的头部。为便于管理,可以把不同模块的常量集中存放在一个公共的头文件中。
【规则】 如果某一常量与其它常量密切相关,应在定义中包含这种关系,而不应给出一些孤立的值。例如:
const float RADIUS = 100;
const float DIAMETER = RADIUS * 2;
使用 const 提高函数的健壮性
注意:所以很多 C++程序设计书籍建议:“Use const whenever you need” const 只能修饰输入参数,如果参数作输出用,不论它是什么数据类型,也不论它采用“指针传递”还是“引用传递”,都不能加 const 修饰,否则该参数将失去输出功能。
1、如果输入参数采用“指针传递”,那么加 const 修饰可以防止意外地改动该指针,起到保护作用 。
2、如果输入参数采用“值传递”,由于函数将自动产生临时变量用于复制该参数,该输入参数本来就无需保护,所以不要加 const 修饰
3、对于非内部数据类型的参数而言,象 void Func(A a) 这样声明的函数注定效率比较低。因为函数体内将产生 A 类型的临时对象用于复制参数 a,而临时对象的构造、复制、析构过程都将消耗时间。为了提高效率,可以将函数声明改为 void Func(A &a), 因为“引用传递”仅借用一下参数的别名而已,不需要产生临时对象。但是函数 void Func(A &a) 存在一个缺点:“引用传递”有可能改变参数 a,这是我们不期望的。解决这个问题很容易,加 const修饰即可,因此函数最终成为 void Func(const A &a)。
4、以此类推,是否应将 void Func(int x) 改写为 void Func(const int &x),以便提高效率?完全没有必要,因为内部数据类型的参数不存在构造、析构的过程,而复制也非常快, “值传递”和“引用传递”的效率几乎相当。
5、任何不会修改数据成员的函数都应该声明为 const 类型 。
注意:不能在类声明中初始化 const 数据成员。 const 数据成员的初始化只能在类构造函数的初始化表中进行
函数参数的规则
【规则】 参数的书写要完整,不要贪图省事只写参数的类型而省略参数名字。
【规则】 如果参数是指针,且仅作输入用,则应在类型前加 const,以防止该指针在函数体内被意外修改。
例如:void StringCopy(char *strDestination,const char *strSource);
【规则】 如果输入参数以值传递的方式传递对象,则宜改用“const &”方式来传递,这样可以省去临时对象的构造和析构过程,从而提高效率。
注意:函数 getchar的原型为int getchar(void);
【建议】 如果函数的返回值是一个对象,有些场合用“引用传递”替换“值传递”可以提高效率。
【规则】 在函数体的“入口处”,对参数的有效性进行检查。很多程序错误是由非法参数引起的,我们应该充分理解并正确使用“断言”(assert)来防止此类错误,assert 可以帮助我们找到发生错误的原因。
【规则】 在函数体的“出口处”,对 return 语句的正确性和效率进行检查。 return 语句不可返回指向“栈内存”的“指针”或者“引用”,因为该内存在函数体结束时被自动销毁。例如
char * Func(void)
{
char str[] = “hello world”; // str 的内存位于栈上
…
return str; // 将导致错误
}
【建议 6-4-3】 尽量避免函数带有“记忆”功能。 建议尽量少用 static 局部变量,除非必需。
引用:
一旦引用被初始化,就不能改变引用的关系(指针则可以随时改变所指的对象)。
以下示例程序中,k 被初始化为 i 的引用。语句 k = j 并不能将 k 修改成为 j 的引
用,只是把 k 的值改变成为 6。由于 k 是 i 的引用,所以 i 的值也变成了 6。
int i = 5;
int j = 6;
int &k = i;
k = j; // k 和 i 的值都变成了 6;
内存管理:
【规则】 用 malloc 或 new 申请内存之后,应该立即检查指针值是否为 NULL。防止使用指针值为 NULL 的内存。
【规则 7-2-2】 不要忘记为数组和动态内存赋初值。防止将未被初始化的内存作为右值使用。
【规则 7-2-3】 避免数组或指针的下标越界,特别要当心发生“多 1”或者“少 1”操作。
【规则 7-2-4】 动态内存的申请与释放必须配对,防止内存泄漏。
【规则 7-2-5】 用 free 或 delete 释放了内存之后,立即将指针设置为 NULL,防止产生“野指针” 。
计算内存容量
用运算符 sizeof 可以计算出数组的容量(字节数)。示例下图 7-3-3(a)中,sizeof(a)的值是 12(注意别忘了’\0’)。指针 p 指向 a,但是 sizeof(p)的值却是 4。这是因为sizeof(p)得到的是一个指针变量的字节数,相当于 sizeof(char*),而不是 p 所指的内存容量。C++/C 语言没有办法知道指针所指的内存容量,除非在申请内存时记住它。
注意:当数组作为函数的参数进行传递时,该数组自动退化为同类型的指针。 示例7-3-3(b)中,不论数组 a 的容量是多少,sizeof(a)始终等于 sizeof(char *)
【规则】如果函数的参数是一个指针,不要指望用该指针去申请动态内存。如果非得要用指针参数去申请内存, 那么应该改用“指向指针的指针” 由于“指向指针的指针”这个概念不容易理解,我们可以用函数返回值来传递动态内存。这种方法更加简单,见示例 7-4-3
用函数返回值来传递动态内存这种方法虽然好用,但是常常有人把 return 语句用错
了。这里强调不要用 return 语句返回指向“栈内存”的指针,因为该内存在函数结束时
自动消亡,见示例 7-4-4。
注意:指针变量在创建的同时应当被初始化,要么将指针设置为 NULL,要么让它指向合法的内存。例如
char *p = NULL;
char *str = (char *) malloc(100)
malloc/free 的使用要点
函数 malloc 的原型如下:
void * malloc(size_t size);
例如:用 malloc 申请一块长度为 length 的整数类型的内存,程序如下:
int *p = (int *) malloc(sizeof(int) * length);
我们应当把注意力集中在两个要素上: “类型转换”和“sizeof”。malloc 返回值的类型是 void *,所以在调用 malloc 时要显式地进行类型转换,将void * 转换成所需要的指针类型。
注意: C++时用new/delete来管理内存的,C 程序只能用 malloc/free 管理动态内存(C++语言中之所以还保留malloc/free是因为有时会调用C写的库)。
new/delete 的使用要点
运算符 new 使用起来要比函数 malloc 简单得多,例如:
int *p1 = (int *)malloc(sizeof(int) * length);
int *p2 = new int[length];
这是因为 new 内置了 sizeof、类型转换和类型安全检查功能。
参数的缺省值
【规则】 参数缺省值只能出现在函数的声明中,而不能出现在定义体中。
例如:
void Foo(int x=0, int y=0); // 正确,缺省值出现在函数的声明中
void Foo(int x=0, int y=0) // 错误,缺省值出现在函数的定义体中
{
…
}
如何在派生类中实现类的基本函数
注意:如果类存在继承关系,派生类必须在其初始化表里调用基类的构造函数
注意:继承规则应当是: 若在逻辑上 B 是 A 的“一种”,并且 A 的所有功能和属性对 B 而言都有意义,则允许 B 继承 A 的功能和属性
其他一些建议
【规则】 先优化数据结构和算法,再优化执行代码 。
【建议】 当心数据类型转换发生错误。尽量使用显式的数据类型转换(让人们知道发生了什么事),避免让编译器轻悄悄地进行隐式的数据类型转换。