头文件由三部分构成:
为了防止头文件被重复引用,应当用 ifndef/define/endif
结构产生预处理块。
头文件中只存放「声明」而不存放「定义」。
定义文件由三部分组成:
*
和 &
紧靠变量名。简单 Windows 程序命名规则:
s_
(表示 static)。如果不得已需要全局变量,则使全局变量加前缀 g_
(表示 global)。m_
(表示 member),这样可以避免数据成员与成员函数的参数同名。
+ - *
的优先级高于对应的二元运算符。if
语句中各种类型变量与零值比较:
bool
变量:不可将布尔变量直接与 TRUE
、FALSE
或者 1
、0
进行比较。(应为if(flag)
,if(!flag)
)。==
或 !=
直接与 0
比较。==
或 !=
与任何数字比较。(浮点数有精度限制,应为 if((x>=-EPSINON) && (x<=EPSINON))
,其中 EPSINON
是允许的误差(即精度))。==
或 !=
与 NULL
比较。if(p==NULL)
写成 if(p=NULL)
,可以将其写成 if(NULL==p)
。在多重循环中,如果有可能,应当将最长的循环放在最内层,最短的循环放在最外层,以减少 CPU 跨切循环层的次数。
如果循环体内存在逻辑判断,并且循环次数很大,宜将逻辑判断移到循环体的外面。(循环「流水线」作业)。
不可在 for
循环体内修改循环变量,防止 for
循环失去控制。
建议 for
语句的循环控制变量的取值采用「半开半闭区间」写法。
每个 case
语句的结尾不要忘了加 break
,否则将导致多个分支重叠(除非有意使多个分支重叠)。
不要忘记最后那个 default
分支。即使程序真的不需要 default
处理,也应该保留语句 default: break;
这样做并非多此一举,而是为了防止别人误以为你忘了 default
处理。
常量是一种标识符,它的值在运行期间恒定不变。C 语言用 #define
来定义常量(称为宏常量)。C++ 语言除了 #define
外还可以用 const
来定义常量(称为 const 常量)。
const
与 #define
的比较:
const
常量有数据类型,而宏常量没有数据类型。编译器可以对前者进行类型安全检查。而对后者只进行字符替换,没有类型安全检查,并且在字符替换可能会产生意料不到的错误(边际效应)。有些集成化的调试工具可以对 const
常量进行调试,但是不能对宏常量进行调试。
在 C++ 程序中只使用 const
常量而不使用宏常量,即 const 常量完全取代宏常量。
不能在类声明中初始化 const
数据成员。const
数据成员的初始化只能在类构造函数的初始化表中进行。
怎样才能建立在整个类中都恒定的常量呢?别指望 const
数据成员了,应该用类中的枚举常量来实现(是不是只能定义为整型常量?!见下一条)
class A{
enum { SIZE1 = 100, SIZE2 = 200}; // 枚举常量
int array1[SIZE1];
int array2[SIZE2];
};
PI=3.14159
)。const
,以防止该指针在函数体内被意外修改。const &
方式来传递,这样可以省去临时对象的构造和析构过程,从而提高效率。printf
是采用不确定参数的典型代表,其原型为:int printf(const chat *format[, argument]…);
void
类型。getchar
。return
语句返回。strcpy
的原型:char *strcpy(char *strDest, const char *strSrc);
在函数体的出口处,对 return
语句的正确性和效率进行检查。注意事项如下:
return
语句不可返回指向「栈内存」的「指针」或者「引用」,因为该内存在函数体结束时被自动销毁。return String(s1 + s2);
这是临时对象的语法,表示「创建一个临时对象并返回它」。不要以为它与「先创建一个局部对象 temp 并返回它的结果」是等价的,如 String temp(s1 + s2); return temp;
实质不然,上述代码将发生三件事。首先, temp
对象被创建,同时完成初始化;然后拷贝构造函数把 temp
拷贝到保存返回值的外部存储单元中;最后, temp
在函数结束时被销毁(调用析构函数)。然而「创建一个临时对象并返回它」的过程是不同的,编译器直接把临时对象创建并初始化在外部存储单元中,省去了拷贝和析构的化费,提高了效率。【建议 6-4-4】不仅要检查输入参数的有效性,还要检查通过其它途径进入函数体内的变量的有效性,例如全局变量、文件句柄等。
断言 assert
是仅在 Debug 版本起作用的宏,它用于检查「不应该」发生的情况。为了不在程序的 Debug 版本和 Release 版本引起差别, assert 不应该产生任何副作用。所以 assert 不是函数,而是宏。
【规则 6-5-1】使用断言捕捉不应该发生的非法情况。不要混淆非法情况与错误情况之间的区别,后者是必然存在的并且是一定要作出处理的。
指针与引用:
一旦引用被初始化,就不能改变引用的关系(指针则可以随时改变所指的对象)。
引用的主要功能是传递函数的参数和返回值。 C++ 语言中,函数的参数和返回值的传递方式有三种:值传递、指针传递和引用传递。
「引用传递」的性质象「指针传递」,而书写方式象「值传递」。
内存分配方式有三种:
malloc
或 new
申请任意多少的内存,程序员自己负责在何时用 free
或 delete
释放内存。动态内存的生存期由我们决定,使用非常灵活,但问题也最多。常见的内存错误及其对策如下:
释放了内存却继续使用它。有三种情况:
return
语句写错了,注意不要返回指向「栈内存」的「指针」或者「引用」,因为该内存在函数体结束时被自动销毁。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
,防止产生「野指针」。
指针与数组:
指针可以随时指向任意类型的内存块,它的特征是「可变」,所以我们常用指针来操作动态内存。指针远比数组灵活,但也更危险。
不能对数组名进行直接复制与比较。应该用标准库函数 strcpy
进行复制,用标准库函数 strcmp
进行比较。
用运算符 sizeof
可以计算出数组的容量(字节数)。但是 sizeof(指针)
的值却是 4。C++/C 语言没有办法知道指针所指的内存容量,除非在申请内存时记住它。
当数组作为函数的参数进行传递时,该数组自动退化为同类型的指针。
如果函数的参数是一个指针,不要指望用该指针去申请动态内存。如果非得要用指针参数去申请内存, 那么应该改用 「指向指针的指针」,或者用函数返回值来传递动态内存。
free
和 delete
只是把指针所指的内存给释放掉,但并没有把指针本身干掉。指针的地址仍然不变(非 NULL
),只是该地址对应的内存是垃圾。此时如果不将指针设为 NULL
,那么 if(NULL==p)
语句将不起作用。
我们发现指针有一些「似是而非」的特征:
内存被释放了,并不表示指针会消亡或者成了 NULL
指针。
「野指针」不是 NULL
指针,是指向「垃圾」内存的指针。成因主要有几种:
p
被 free
或者 delete
之后,没有置为 NULL
。malloc/free
和 new/delete
:前者是库函数,对于自定义的对象,不能自动调用其构造和析构函数;后者是关键字,可以调用构造函数和析构函数。对于内部数据类型两者是等价的。
如果用 free
释放「 new
创建的动态对象」,那么该对象因无法执行析构函数而可能导致程序出错。 如果用 delete
释放「 malloc
申请的动态内存」,理论上讲程序不会出错,但是该程序的可读性很差。
malloc、free、new、delete:
malloc
返回值的类型是 void *
,所以在调用 malloc
时要显式地进行类型转换,将 void *
转换成所需要的指针类型。malloc
函数本身并不识别要申请的内存是什么类型,它只关心内存的总字节数。free
函数不像 malloc
函数那样复杂呢?这是因为指针 p
的类型以及它所指的内存的容量事先都是知道的,语句 free(p)
能正确地释放内存。如果 p
是 NULL
指针,那么 free
对 p
无论操作多少次都不会出问题。如果 p
不是 NULL
指针,那么 free
对 p
连续操作两次就会导致程序运行错误。new
内置了 sizeof
、类型转换和类型安全检查功能。对于非内部数据类型的对象而言, new
在创建动态对象的同时完成了初始化工作。如果对象有多个构造函数,那么 new
的语句也可以有多种形式。new
创建对象数组,那么只能使用对象的无参数构造函数。eg:Obj *objects = new Obj[100]; // 创建 100 个动态对象
不能写成 Obj *objects = new Obj[100](1);// 创建 100 个动态对象的同时赋初值 1
delete
释放对象数组时,留意不要丢了符号 ' []'。
extern C
来解决这个问题。::
标志。成员函数的重载、覆盖与隐藏:
成员函数被重载的特征:
virtual
关键字可有可无。覆盖是指派生类函数覆盖基类函数,特征是:
virtual
关键字。隐藏是指派生类的函数屏蔽了与其同名的基类函数,规则如下:
virtual
关键字,基类的函数将被隐藏(注意别与重载混淆)。virtual
关键字。此时,基类的函数被隐藏(注意别与覆盖混淆)。【规则 8-3-1】参数缺省值只能出现在函数的声明中,而不能出现在定义体中。
【规则 8-3-2】如果函数有多个参数,参数只能从后向前挨个儿缺省,否则将导致函数调用语句怪模怪样。
运算符与普通函数在调用时的不同之处是:对于普通函数,参数出现在圆括号内;而对于运算符,参数出现在其左、右侧。
如果运算符被重载为全局函数,那么只有一个参数的运算符叫做一元运算符,有两个参数的运算符叫做二元运算符。 如果运算符被重载为类的成员函数,那么一元运算符没有参数,二元运算符只有一个右侧参数,因为对象自己成了左侧参数。
9. 在 C++ 运算符集合中,有一些运算符是不允许被重载的。这种限制是出于安全方面的考虑,可防止错误和混乱。(sizeof
、::
、.
、.*
、 ?:
、typeid
等等)
int
,float
等)的运算符。.
,因为 .
在类中对任何成员都有意义,已经成为标准用法。#
、@
、$
等。原因有两点,一是难以理解,二是难以确定优先级。对已经存在的运算符进行重载时,不能改变优先级规则,否则将引起混乱。
在 C 程序中,可以用宏代码提高执行效率。宏代码本身不是函数,但使用起来象函数。预处理器用复制宏代码的方式代替函数调用, 省去了参数压栈、生成汇编语言的 CALL
调用、返回参数、执行 return
等过程,从而提高了速度。使用宏代码最大的缺点是容易出错,预处理器在复制宏代码时常常产生意想不到的边际效应。使用宏代码还有另一种缺点:无法操作类的私有数据成员。
对于任何内联函数,编译器在符号表里放入函数的声明(包括名字、参数类型、返回值类型)。如果编译器没有发现内联函数存在错误,那么该函数的代码也被放入符号表里。在调用一个内联函数时,编译器首先检查调用是否正确(进行类型安全检查,或者进行自动类型转换,当然对所有的函数都一样)。如果正确,内联函数的代码就会直接替换函数调用,于是省去了函数调用的开销。
所以在 C++ 程序中,应该用内联函数取代所有宏代码,assert
恐怕是唯一的例外。 assert
是仅在 Debug 版本起作用的宏,它用于检查「不应该」发生的情况。为了不在程序的 Debug 版本和 Release 版本引起差别, assert
不应该产生任何副作用。 如果 assert
是函数, 由于函数调用会引起内存、 代码的变动, 那么将导致 Debug 版本与 Release 版本存在差异。 所以 assert
不是函数, 而是宏。
关键字 inline
必须与函数定义体放在一起才能使函数成为内联,仅将 inline
放在函数声明前面不起任何作用。所以说, inline
是一种「用于实现的关键字」,而不是一种「用于声明的关键字」。
定义在类声明之中的成员函数将自动地成为内联函数。
以下情况不宜使用内联:
既然能自动生成函数,为什么还要程序员编写?原因如下:
构造函数有个特殊的初始化方式叫「初始化表达式表」(简称初始化表)。初始化表位于函数参数表之后,却在函数体 {} 之前。这说明该表里的初始化工作发生在函数体内的任何代码被执行之前。
构造函数初始化表的使用规则:
const
常量只能在初始化表里被初始化,因为它不能在函数体内用赋值的方式来初始化。类的数据成员的初始化可以采用初始化表或函数体内赋值两种方式,这两种方式的效率不完全相同。
非内部数据类型的成员对象应当采用第一种方式初始化,以获取更高的效率。(第二种方式会先创建其对象,调用无参构造函数,再调用其赋值函数)。
对于内部数据类型的数据成员而言,两种初始化方式的效率几乎没有区别,但后者的程序版式似乎更清晰些。
构造从类层次的最根处开始,在每一层中,首先调用基类的构造函数,然后调用成员对象的构造函数。析构则严格按照与构造相反的次序执行,该次序是唯一的,否则编译器将无法自动执行析构过程。成员对象初始化的次序完全不受它们在初始化表中次序的影响,只由成员对象在类中声明的次序决定。
缺省的拷贝构造函数以「位拷贝」方式执行,以 string 类为例,将造成三个错误:
b.m_data
原有的内存没被释放,造成内存泄露;b.m_data
和 a.m_data
指向同一块内存, a
或 b
任何一方变动都会影响另一方;m_data
被释放了两次。拷贝构造函数和赋值函数非常容易混淆,常导致错写、错用。拷贝构造函数是在对象被创建时调用的,而赋值函数只能被已经存在了的对象调用。
类 string 拷贝构造函数与普通构造函数的区别是:在函数入口处无需与 NULL
进行比较,这是因为「引用」不可能是 NULL
,而「指针」可以为 NULL
。
string 类拷贝构造函数四部曲:
strlen
返回的是有效字符串长度,不包含结束符 \0
。函数 strcpy
则连 \0
一起复制。a = b = c
这样的链式表达。注意不要将 return *this;
错写成 return this;
。偷懒的办法处理拷贝构造函数与赋值函数:只需将拷贝构造函数和赋值函数声明为私有函数,不用编写代码。
基类的构造函数、析构函数、赋值函数都不能被派生类继承。如果类之间存在继承关系,在编写上述基本函数时应注意以下事项:
virtual
关键字)。如果析构函数不为虚,将不会调用派生类的析构函数。
使用 const
提高函数的健壮性:const
不止能定义 const
常量,更大的魅力是它可以修饰函数的参数、返回值,甚至函数的定义体。
const
修饰函数参数:
const
修饰,否则该参数将失去输出功能。const
只能修饰输入参数:
const
修饰可以防止意外地改动该指针,起到保护作用。const
修饰。对于非内部数据类型的参数而言,像 void Func(A a)
这样声明的函数注定效率比较底。因为函数体内将产生 A
类型的临时对象用于复制参数 a
,而临时对象的构造、复制、析构过程都将消耗时间。
const 修饰函数的返回值:
const
修饰,那么函数返回值(即指针)的内容不能被修改,该返回值只能被赋给加 const
修饰的同类型指针。eg:const char * GetString(void);
const
修饰没有任何价值。A GetA(void)
改写为 const A& GetA(void)
的确能提高效率。但此时千万千万要小心,一定要搞清楚函数究竟是想返回一个对象的「拷贝」还是仅返回「别名」就可以了,否则程序会出错。函数返回值采用「引用传递」的场合并不多,这种方式一般只出现在类的赋值函数中,目的是为了实现链式表达。
const
成员函数:任何不会修改数据成员的函数都应该声明为 const
类型。const
成员函数的声明看起来怪怪的: const
关键字只能放在函数声明的尾部,大概是因为其它地方都已经被占用了。