- 头文件(.h)
头文件由三部分内容组成:
(1)头文件开头处的版权和版本声明
(2)预处理块。
(3)函数和类结构声明等。
【规则 1-2-1 】为了防止头文件被重复引用,应当用 ifndef/define/endif 结构产生预处理块。
【规则 1-2-2】 】用 #include <filename.h> 格式来引用标准库的头文件(编译器将从标准库目录开始搜索)。
【规则 1-2-3 】用 #include “filename.h” 格式来引用非标准库的头文件(编译器将从用户的工作目录开始搜索)。
【建议 1-2-1 】头文件中只存放“声明”而不存放“定义”
在 C++ 语法中,类的成员函数可以在声明的同时被定义,并且自动成为内联函数。
这虽然会带来书写上的方便,但却造成了风格不一致,弊大于利。建议将成员函数的定义与声明分开,不论该函数体有多么小。
【建议 1-2-2 】不提倡使用全局变量,尽量不要在头文件中出现象 extern int value 这类声明。
- 头文件的作用
(1)通过头文件来调用库功能。在很多场合,源代码不便(或不准)向用户公布,只要向用户提供头文件和二进制的库即可。用户只需要按照头文件中的接口声明来调用库功能,而不必关心接口怎么实现的。编译器会从库中提取相应的代码。
(2)头文件能加强类型安全检查。如果某个接口被实现或被使用时,其方式与头文件中的声明不一致,编译器就会指出错误,这一简单的规则能大大减轻程序员调试、改错的负担。
- 定义文件、源文件(.cpp)
定义文件有三部分内容:
(1) 定义文件开头处的版权和版本声明
(2) 对一些头文件的引用。
(3) 程序的实现体(包括数据和代码)。
- 空行
【规则 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 等关键字之后至少要留一个空格,否则无法辨析关键字。象 if、for、while 等关键字之后应留一个空格再跟左括号‘(’,以突出关键字。
【规则 2-3-2 】函数名之后不要留空格,紧跟左括号‘(’,以与关键字区别。
【规则 2-3-3】 】‘(’向后紧跟,‘)’、‘,’、‘;’向前紧跟,紧跟处不留空格。
【规则 2-3-4】 】‘,’之后要留空格,如 Function(x, y, z)。如果‘;’不是一行的结束符号,其后要留空格,如 for (initialization; condition; update)。
【规则 2-3-5】 】赋值操作符、比较操作符、算术操作符、逻辑操作符、位域操作符,如“=”、“+=” “>=”、“<=”、“+”、“*”、“%”、“&&”、“||”、“<<”,“^”等二元操作符的前后应当加空格。
【规则 2-3-6 】一元操作符如“!”、“~”、“++”、“--”、“&”(地址运算符)等前后不加空格。
【规则 2-3-7 】象“[]”、“.”、“->”这类操作符前后不加空格。
【建议 2-3-1 】对于表达式比较长的 for 语句和 if 语句,为了紧凑起见可以适当地去掉一些空格,如 for (i=0; i<10; i++)和 if ((a<=b) && (c<=d))
- 注释
【规则 2-7-1】 】注释是对代码的“提示”,而不是文档。程序中的注释不可喧宾夺主,注释太多了会让人眼花缭乱。注释的花样要少。
【规则 2-7-2】 】如果代码本来就是清楚的,则不必加注释。否则多此一举,令人厌烦。例如
i++; // i 加 1,多余的注释
【规则 2-7-3】 】边写代码边注释,修改代码同时修改相应的注释,以保证注释与代码的一致性。不再有用的注释要删除。
【规则 2-7-4】 】注释应当准确、易懂,防止注释有二义性。错误的注释不但无益反而有害。
【规则 2-7-5 】尽量避免在注释中使用缩写,特别是不常用缩写。
【规则 2-7-6】 】注释的位置应与被描述的代码相邻,可以放在代码的上方或右方,不可放在下方。
【规则 2-7-8】 】当代码比较长,特别是有多重嵌套时,应当在一些段落的结束处加注释,便于阅读。
- 类
类可以将数据和函数封装在一起,其中函数表示了类的行为(或称服务)。类提供关
键字 public、protected 和 private,分别用于声明哪些数据和函数是公有的、受保护的或
者是私有的。
类的版式主要有两种方式:
(1)将 private 类型的数据写在前面,而将 public 类型的函数写在后面,采用这种版式的程序员主张类的设计“以数据为中心”,重点关注类的内部结构。
(2)将 public 类型的函数写在前面,而将 private 类型的数据写在后面,采用这种版式的程序员主张类的设计“以行为为中心”,重点关注的是类应该提供什么样
的接口(或服务)。
- 命名规则
【规则 3-1-1 】标识符应当直观且可以拼读,可望文知意,不必进行“解码”。
【规则 3-1-2 】标识符的长度应当符合“min-length && max-information”原则。
【规则 3-1-3 】命名规则尽量与所采用的操作系统或开发工具的风格保持一致。
【规则 3-1-4 】程序中不要出现仅靠大小写区分的相似的标识符。
【规则 3-1-5】 】程序中不要出现标识符完全相同的局部变量和全局变量,尽管两者的作用域不同而不会发生语法错误,但会使人误解。
【规则 3-1-6 】变量的名字应当使用“名词”或者“形容词+名词”。
【建议 3-1-1 】尽量避免名字中出现数字编号,如 Value1,Value2 等,除非逻辑上的确需要编号。这是为了防止程序员偷懒,不肯为命名动脑筋而导致产生无意义的名字(因为用数字编号最省事)。
简单的应用程序命名规则
【规则 3-2-1 】类名和函数名用大写字母开头的单词组合而成。
【规则 3-2-2 】变量和参数用小写字母开头的单词组合而成。
【规则 3-2-3 】常量全用大写的字母,用下划线分割单词。
【规则 3-2-4 】静态变量加前缀 s_(表示 static)。
【规则 3-2-5 】如果不得已需要全局变量,则使全局变量加前缀 g_(表示 global)。
- 用if做零值比较
【规则 4-3-1 】不可将布尔变量直接与 TRUE、FALSE 或者 1、0 进行比较。
if (flag) // 表示 flag 为真
if (!flag) // 表示 flag 为假
【规则 4-3-2 】应当将整型变量用“==”或“!=”直接与 0 比较。
if (value == 0)
if (value != 0)
【规则 4-3-3 】不可将浮点变量用“==”或“!=”与任何数字比较。
千万要留意,无论是 float 还是 double 类型的变量,都有精度限制。所以一定要避免将浮点变量用“==”或“!=”与数字比较,应该设法转化成“>=”或“<=”形式。
假设浮点变量的名字为 x,应当将if (x == 0.0) // 隐含错误的比较
转化为if ((x>=-EPSINON) && (x<=EPSINON))其中 EPSINON 是允许的误差(即精度)。
【规则 4-3-4 】应当将指针变量用“==”或“!=”与 NULL 比较。
if (p == NULL) // p 与 NULL 显式比较,强调 p 是指针变量
if (p != NULL)
- 循环的使用
【建议 4-4-1】 】在多重循环中,如果有可能,应当将最长的循环放在最内层,最短的循环放在最外层,以减少 CPU跨切循环层的次数。
【建议 4-4-2】 】如果循环体内存在逻辑判断,并且循环次数很大,宜将逻辑判断移到循环体的外面。
【规则 4-5-1 】不可在 for 循环体内修改循环变量,防止 for 循环失去控制。
【建议 4-5-1 】建议 for 语句的循环控制变量的取值采用“半开半闭区间”写法。
【规则 4-6-1】 】每个 case 语句的结尾不要忘了加 break,否则将导致多个分支重叠(除非有意使多个分支重叠)。
【规则 4-6-2 】不要忘记最后那个 default 分支。
goto 语句它能从多重循环体中咻地一下子跳到外面,用不着写很多次的 break 语句
- 常量
【规则 5-1-1 】 尽量使用含义直观的常量来表示那些将在程序中多次出现的数字或字符串。
const 与 #define 的比较:
(1) const 常量有数据类型,而宏常量没有数据类型。编译器可以对前者进行类型安全检查。而对后者只进行字符替换,没有类型安全检查,并且在字符替换可能会产生意料不到的错误(边际效应)。
(2) 有些集成化的调试工具可以对 const 常量进行调试,但是不能对宏常量进行调试。
【规则 5-2-1 】在 C++ 程序中只使用 const 常量而不使用宏常量,即 const 常量完全取代宏常量。
【规则 5-3-1】需要对外公开的常量放在头文件中,不需要对外公开的常量放在定义文件的头部。为便于管理,可以把不同模块的常量集中存放在一个公共的头文件中。
【规则 5-3-2】如果某一常量与其它常量密切相关,应在定义中包含这种关系,而不应给出一些孤立的值。
不能在类声明中初始化 const 数据成员
class A
{…
const int SIZE = 100; // 错误,企图在类声明中初始化 const 数据成员
int array[SIZE]; // 错误,未知的 SIZE
};
const 数据成员的初始化只能在类构造函数的初始化表中进行,例如
class A
{…
A(int size); // 构造函数
const int SIZE ;
};
A::A(int size) : SIZE(size) // 构造函数的初始化表
{
…
}
用枚举常量可以建立在整个类中都恒定的常量。例如
class A
{…
enum { SIZE1 = 100, SIZE2 = 200}; // 枚举常量
int array1[SIZE1];
int array2[SIZE2];
};
枚举常量不会占用对象的存储空间,它们在编译时被全部求值。枚举常量的缺点是:它的隐含数据类型是整数,其最大值有限,且不能表示浮点数(如 PI=3.14159)。
- 函数
函数接口的两个要素是参数和返回值。C 语言中,函数的参数和返回值的传递方式有两种:值传递(pass by value)和指针传递(pass by pointer)。C++ 语言中多了引用传递(pass by reference)。
【规则 6-1-3 】如果参数是指针,且仅作输入用,则应在类型前加 const,以防止该指针在函数体内被意外修改。
【规则 6-1-4 】如果输入参数以值传递的方式传递对象,则宜改用“const &”方式来传递,这样可以省去临时对象的构造和析构过程,从而提高效率。
【建议 6-1-1 】避免函数有太多的参数,参数个数尽量控制在 5 个以内。如果参数太多,在使用时容易将参数类型或顺序搞错。
【规则 6-2-1 】不要省略返回值的类型。C 语言中,凡不加类型说明的函数,一律自动按整型处理。C++语言有很严格的类型安全检查,不允许这种情况发生。
【规则 6-2-3】 】不要将正常值和错误标志混在一起返回。正常值用输出参数获得,而错误标志用 return 语句返回。
【建议 6-2-1】 】有时候函数原本不需要返回值,但为了增加灵活性如支持链式表达,可以附加返回值。
【规则 6-3-1 】在函数体的“入口处”,对参数的有效性进行检查。
【规则 6-3-2 】在函数体的“出口处”,对 return 语句的正确性和效率进行检查。
(1)return 语句不可返回指向“栈内存”的“指针”或者“引用”,因为该内存在函数
体结束时被自动销毁。
(2)要搞清楚返回的究竟是“值”、“指针”还是“引用”。
(3)如果函数返回值是一个对象,要考虑 return 语句的效率。
断言 assert 是仅在 Debug 版本起作用的宏,它用于检查“不应该”发生的情况。
【规则 6-5-1】使用断言捕捉不应该发生的非法情况。不要混淆非法情况与错误情况
之间的区别,后者是必然存在的并且是一定要作出处理的。
【规则 6-5-2 】在函数的入口处,使用断言检查参数的有效性(合法性)。
【建议 6-5-1】 】在编写函数时,要进行反复的考查,并且自问:“我打算做哪些假定?”一旦确定了的假定,就要使用断言对假定进行检查。
【建议 6-5-2】 】一般教科书都鼓励程序员们进行防错设计,但要记住这种编程风格可能会隐瞒错误。当进行防错设计时,如果“不可能发生”的事情的确发生了,则要使用断言进行报警。
引用的一些规则如下:
(1)引用被创建的同时必须被初始化(指针则可以在任何时候被初始化)。
(2)不能有 NULL 引用,引用必须与合法的存储单元关联(指针则可以是 NULL)。
(3)一旦引用被初始化,就不能改变引用的关系(指针则可以随时改变所指的对象)。
- 内存管理
内存分配方式有三种:
(1) 从静态存储区域分配。内存在程序编译的时候就已经分配好,这块内存在程序的
整个运行期间都存在。例如全局变量,static 变量。
(2) 在栈上创建。在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函
数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集
中,效率很高,但是分配的内存容量有限。
(3) 从堆上分配,亦称动态内存分配。程序在运行的时候用 malloc 或 new 申请任意多
少的内存,程序员自己负责在何时用 free 或 delete 释放内存。动态内存的生存期
由我们决定,使用非常灵活,但问题也最多。
常见的内存错误及其对策如下:
1.内存分配未成功,却使用了它。
编程新手常犯这种错误,因为他们没有意识到内存分配会不成功。常用解决办法是,在使用内存之前检查指针是否为 NULL。如果指针 p 是函数的参数,那么在函数的入口处用 assert(p!=NULL)进行检查。如果是用 malloc 或 new 来申请内存,应该用 if(p==NULL)或 if(p!=NULL)进行防错处理。
2.内存分配虽然成功,但是尚未初始化就引用它。
犯这种错误主要有两个起因:一是没有初始化的观念;二是误以为内存的缺省初值全为零,导致引用初值错误(例如数组)。内存的缺省初值究竟是什么并没有统一的标准,尽管有些时候为零值,我们宁可信其无不可信其有。所以无论用何种方式创建数组,都别忘了赋初值,即便是赋零值也不可省略,不要嫌麻烦。
3.内存分配成功并且已经初始化,但操作越过了内存的边界。
例如在使用数组时经常发生下标“多 1”或者“少 1”的操作。特别是在 for 循环语句中,循环次数很容易搞错,导致数组操作越界。
4.忘记了释放内存,造成内存泄露。
含有这种错误的函数每被调用一次就丢失一块内存。刚开始时系统的内存充足,你看不到错误。终有一次程序突然死掉,系统出现提示:内存耗尽。动态内存的申请与释放必须配对,程序中 malloc 与 free 的使用次数一定要相同,否则肯定有错误(new/delete 同理)。
5.释放了内存却继续使用它。
有三种情况:
(1)程序中的对象调用关系过于复杂,实在难以搞清楚某个对象究竟是否已经释放了内存,此时应该重新设计数据结构,从根本上解决对象管理的混乱局面。
(2)函数的 return 语句写错了,注意不要返回指向“栈内存”的“指针”或者“引用”,因为该内存在函数体结束时被自动销毁。
(3)使用 free 或 delete 释放了内存后,没有将指针设置为 NULL。导致产生“野指针”。
【规则 7-2-1 】用 malloc 或 new 申请内存之后,应该立即检查指针值是否为 NULL。防止使用指针值为 NULL 的内存。
【规则 7-2-2】 】不要忘记为数组和动态内存赋初值。防止将未被初始化的内存作为右值使用。
【规则 7-2-3 】避免数组或指针的下标越界,特别要当心发生“多 1”或者“少 1”操作。
【规则 7-2-4 】动态内存的申请与释放必须配对,防止内存泄漏。
【规则 7-2-5 】用 free 或 delete 释放了内存之后,立即将指针设置为 NULL,防止产生“野指针”。
注意当数组作为函数的参数进行传递时,该数组自动退化为同类型的指针。
(1)指针消亡了,并不表示它所指的内存会被自动释放。
(2)内存被释放了,并不表示指针会消亡或者成了 NULL 指针。
“野指针”的成因主要有两种:
(1)指针变量没有被初始化。
(2)指针 p 被 free 或者 delete 之后,没有置为 NULL。
(3)指针操作超越了变量的作用范围。
malloc/free 不能执行构造函数与析构函数,new/delete会执行。
通常有三种方式处理“内存耗尽”问题
(1)判断指针是否为 NULL,如果是则马上用 return 语句终止本函数。
(2)判断指针是否为 NULL,如果是则马上用 exit(1)终止整个程序的运行。
(3)为 new 和 malloc 设置异常处理函数。
- C++ 函数的高级特性
C++增加了重载(overloaded)、内联(inline)、const 和 virtual四种新机制。
在 C++程序中,可以将语义、功能相似的几个函数用同一个名字表示,即函数重载。
如果类的某个成员函数要调用全局函数 Print,为了与成员函数 Print 区别,全局函数被调用时应加‘::’标志。如
::Print(…); // 表示 Print 是全局函数而非成员函数
当心隐式类型转换导致重载函数产生二义性
重载与覆盖
成员函数被重载的特征:
(1)相同的范围(在同一个类中);
(2)函数名字相同;
(3)参数不同;
(4)virtual 关键字可有可无。
覆盖是指派生类函数覆盖基类函数,特征是:
(1)不同的范围(分别位于派生类与基类);
(2)函数名字相同;
(3)参数相同;
(4)基类函数必须有 virtual 关键字。
隐藏规则:“隐藏”是指派生类的函数屏蔽了与其同名的基类函数
(1)如果派生类的函数与基类的函数同名,但是参数不同。此时,不论有无 virtual 关键字,基类的函数将被隐藏(注意别与重载混淆)。
(2)如果派生类的函数与基类的函数同名,并且参数也相同,但是基类函数没有 virtual关键字。此时,基类的函数被隐藏(注意别与覆盖混淆)。
#include <iostream.h>
class Base
{
public:
virtual void f(float x){ cout << "Base::f(float) " << x << endl; }
void g(float x){ cout << "Base::g(float) " << x << endl; }
void h(float x){ cout << "Base::h(float) " << x << endl; }
};
class Derived : public Base
{
public:
virtual void f(float x){ cout << "Derived::f(float) " << x << endl; }
void g(int x){ cout << "Derived::g(int) " << x << endl; }
void h(float x){ cout << "Derived::h(float) " << x << endl; }
};
void main(void)
{
Derived d;
Base *pb = &d;
Derived *pd = &d;
// Good : behavior depends solely on type of the object
pb->f(3.14f); // Derived::f(float) 3.14
pd->f(3.14f); // Derived::f(float) 3.14
// Bad : behavior depends on type of the pointer
pb->g(3.14f); // Base::g(float) 3.14
pd->g(3.14f); // Derived::g(int) 3 (surprise!)
// Bad : behavior depends on type of the pointer
pb->h(3.14f); // Base::h(float) 3.14 (surprise!)
pd->h(3.14f); // Derived::h(float) 3.14
}
参数的缺省值
【规则 8-3-1 】参数缺省值只能出现在函数的声明中,而不能出现在定义体中。
例如:
void Foo(int x=0, int y=0); // 正确,缺省值出现在函数的声明中
void Foo(int x=0, int y=0) // 错误,缺省值出现在函数的定义体中
{
…
}
【规则 8-3-2】如果函数有多个参数,参数只能从后向前挨个儿缺省,否则将导致函数调用语句怪模怪样。
运算符重载
在 C++语言中,可以用关键字 operator 加上运算符来表示函数,叫做运算符重载例如两个复数相加函数:
Complex Add(const Complex &a, const Complex &b);
可以用运算符重载来表示:
Complex operator +(const Complex &a, const Complex &b);
Complex a, b, c;
…
c = Add(a, b); // 用普通函数
c = a + b; // 用运算符
如果运算符被重载为全局函数,那么只有一个参数的运算符叫做一元运算符,有两个参数的运算符叫做二元运算符。
如果运算符被重载为类的成员函数,那么一元运算符没有参数,二元运算符只有一个右侧参数,因为对象自己成了左侧参数。
函数内联
C++ 语言支持函数内联,其目的是为了提高函数的执行效率(速度)。
在 C 程序中,可以用宏代码提高执行效率。宏代码本身不是函数,但使用起来象函数。预处理器用复制宏代码的方式代替函数调用,省去了参数压栈、生成汇编语言的CALL 调用、返回参数、执行 return 等过程,从而提高了速度。使用宏代码最大的缺点是容易出错,预处理器在复制宏代码时常常产生意想不到的边际效应。使用宏代码还有另一种缺点:无法操作类的私有数据成员。
对于任何内联函数,编译器在符号表里放入函数的声明(包括名字、参数类型、返回值类型)。如果编译器没有发现内联函数存在错误,那么该函数的代码也被放入符号表里。在调用一个内联函数时,编译器首先检查调用是否正确(进行类型安全检查,或者进行自动类型转换,当然对所有的函数都一样)。如果正确,内联函数的代码就会直接替换函数调用,于是省去了函数调用的开销。这个过程与预处理有显著的不同,因为预处理器不能进行类型安全检查,或者进行自动类型转换。
关键字 inline 必须与函数定义体放在一起才能使函数成为内联,仅将 inline 放在函数声明前面不起任何作用。
定义在类声明之中的成员函数将自动地成为内联函数,例如
class A
{
public:
void Foo(int x, int y) { … } // 自动地成为内联函数
}
以下情况不宜使用内联:
(1)如果函数体内的代码比较长,使用内联将导致内存消耗代价较高。
(2)如果函数体内出现循环,那么执行函数体内代码的时间要比函数调用的开销大。
- 类的构造函数、析构函数与赋值函数
每个类只有一个析构函数和一个赋值函数,但可以有多个构造函数(包含一个拷贝构造函数,其它的称为普通构造函数)。对于任意一个类 A,如果不想编写上述函数,C++编译器将自动为 A 产生四个缺省的函数,如
A(void); // 缺省的无参数构造函数
A(const A &a); // 缺省的拷贝构造函数
~A(void); // 缺省的析构函数
A & operate =(const A &a); // 缺省的赋值函数
(1)如果使用“缺省的无参数构造函数”和“缺省的析构函数”,等于放弃了自主“初始化”和“清除”的机会。
(2)“缺省的拷贝构造函数”和“缺省的赋值函数”均采用“位拷贝”而非“值拷贝”的方式来实现,倘若类中含有指针变量,这两个函数注定将出错。
把对象的初始化工作放在构造函数中,把清除工作放在析构函数中。当对象被创建时,构造函数被自动执行。当对象消亡时,析构函数被自动执行。构造函数与析构函数的另一个特别之处是没有返回值类型,这与返回值类型为 void 的函数不同。
构造函数初始化表的使用规则:
- 类存在继承关系,派生类必须在其初始化表里调用基类的构造函数。
- 类的 const 常量只能在初始化表里被初始化,因为它不能在函数体内用赋值的方式来初始化
- 类的数据成员的初始化可以采用初始化表或函数体内赋值两种方式,这两种方式的效率不完全相同。
构造从类层次的最根处开始,在每一层中,首先调用基类的构造函数,然后调用成员对象的构造函数。析构则严格按照与构造相反的次序执行,该次序是唯一的,否则编译器将无法自动执行析构过程。
如果不主动编写拷贝构造函数和赋值函数,编译器将以“位拷贝”的方式自动生成缺省的函数。倘若类中含有指针变量,那么这两个缺省的函数就隐含了错误。
拷贝构造函数和赋值函数非常容易混淆,常导致错写、错用。拷贝构造函数是在对象被创建时调用的,而赋值函数只能被已经存在了的对象调用。
基类的构造函数、析构函数、赋值函数都不能被派生类继承。如果类之间存在继承关系,在编写上述基本函数时应注意以下事项:
1.类的构造函数应在其初始化表里调用基类的构造函数。
2.与派生类的析构函数应该为虚(即加 virtual 关键字)。
3.在编写派生类的赋值函数时,注意不要忘记对基类的数据成员重新赋值。例如:
class Base
{
public:
…
Base & operate =(const Base &other); // 类 Base 的赋值函数
private:
int m_i, m_j, m_k;
};
class Derived : public Base
{
public:
…
Derived & operate =(const Derived &other); // 类 Derived 的赋值函数
private:
int m_x, m_y, m_z;
};
Derived & Derived::operate =(const Derived &other)
{
//(1)检查自赋值
if(this == &other)
return *this;
//(2)对基类的数据成员重新赋值
Base::operate =(other); // 因为不能直接操作私有数据成员
//(3)对派生类的数据成员赋值
m_x = other.m_x;
m_y = other.m_y;
m_z = other.m_z;
//(4)返回本对象的引用
return *this;
}
- 类的继承与组合
继承的一些使用规则:
【规则 10-1-1 】如果类 A 和类 B 毫不相关,不可以为了使 B 的功能更多些而让 B继承 A 的功能和属性。
【规则 10-1-2 】若在逻辑上 B 是 A 的“一种”(a kind of )允许 B 继承 A 的功能和属性。例如男人(Man)是人(Human)的一种,男孩(Boy)是男人的一种。那么类 Man 可以从类 Human 派生,类 Boy 可以从类 Man 派生。
若在逻辑上B是A 的“一种”,并且 A 的所有功能和属性对的所有功能和属性对 B 而言都有意义,则允许 B 继承 A 的功能和属性。
组合的一些使用规则:
【规则 10-2-1 】若在逻辑上 A 是 B 的“一部分”(a part of),则不允许 B 从 A 派生,而是要用 A 和其它东西组合出 B。例如眼(Eye)、鼻(Nose)、口(Mouth)、耳(Ear)是头(Head)的一部分,所以类 Head 应该由类 Eye、Nose、Mouth、Ear 组合而成,不是派生而成。
- Const的使用
用 const 修饰函数的参数
1.对于非内部数据类型的输入参数,应该将“值传递”的方式改为“const 引用传递”,目的是提高效率。例如将 void Func(A a) 改为 void Func(const A &a)。
2.对于内部数据类型的输入参数,不要将“值传递”的方式改为“const 引用传递”。否则既达不到提高效率的目的,又降低了函数的可理解性。例如 void Func(int x) 不应该改为 void Func(const int &x)。
用 const 修饰函数的返回值
- 如果给以“指针传递”方式的函数返回值加 const 修饰,那么函数返回值(即指针)的内容不能被修改,该返回值只能被赋给加 const 修饰的同类型指针。
const 成员函数:const 关键字只能放在函数声明的尾部
任何不会修改数据成员的函数都应该声明为 const 类型。如果在编写 const 成员函数时,不慎修改了数据成员,或者调用了其它非 const 成员函数,编译器将指出错误,这无疑会提高程序的健壮性。例如:
class Stack
{
public:
void Push(int elem);
int Pop(void);
int GetCount(void) const; // const 成员函数
private:
int m_num;
int m_data[100];
};
int Stack::GetCount(void) const
{
++ m_num; // 编译错误,企图修改数据成员 m_num
Pop(); // 编译错误,企图调用非 const 函数
return m_num;
}
- 提高程序的效率
【规则 11-2-1】不要一味地追求程序的效率,应当在满足正确性、可靠性、健壮性、可读性等质量因素的前提下,设法提高程序的效率。
【规则 11-2-2】以提高程序的全局效率为主,提高局部效率为辅。
【规则 11-2-3】在优化程序的效率时,应当先找出限制效率的“瓶颈”,不要在无关紧要之处优化。
【规则 11-2-4】先优化数据结构和算法,再优化执行代码。
【规则 11-2-5】有时候时间效率和空间效率可能对立,此时应当分析那个更重要,作出适当的折衷。例如多花费一些内存来提高性能。
【规则 11-2-6】不要追求紧凑的代码,因为紧凑的代码并不能产生高效的机器码。
【建议 11-3-7 】当心文件 I/O 有错误。
【建议 11-3-8 】避免编写技巧性很高代码。
【建议 11-3-9 】不要设计面面俱到、非常灵活的数据结构。
【建议 11-3-10】如果原有的代码质量比较好,尽量复用它。但是不要修补很差劲的代码,应当重新编写。
【建议 11-3-11】尽量使用标准库函数,不要“发明”已经存在的库函数。
【建议 11-3-12】尽量不要使用与具体硬件或软件环境关系密切的变量。
【建议 11-3-13】把编译器的选择项设置为最严格状态。
【建议 11-3-14】如果可能的话,使用 PC-Lint、LogiScope 等工具进行代码审查。