Effective C++读书笔记(50条款)
2008-07-13 10:43
1.为方便调试,最好使用常量。
注意:常量定义一般放在头文件中,可将指针和指针所指的类型都定义成const,如const char *const authorName = “Scott Meyers”;
类中常量通常定义为静态成员, 而且需要先声明后定义。可以在声明时或定义时赋值,也可使用借用enum的方法。如enum{Num = 5};
2.#define语句造成的问题。
如#definemax(a, b) ((a) > (b) ? (a) : (b))
在下面情况下:
Int a= 5, b = 0;
max(++ a, b);
max(++ a, b + 10);
max内部发生些什么取决于它比较的是什么值。解决方法是使用inline函数,可以使用template来产生一个函数集。
条款2:尽量用而不用
用>> 和<<使得编译器自己可以根据不同的变量类型选择操作符的不同形式,而采取的语法形式相同。
条款3:尽量用new和delete而不用malloc和free
使用malloc和free的时候不会自己调用构造函数和析构函数,因此如果对象自己分配了内存的话,那么这些内存会全部丢失。另外,将new和malloc混用会导致不可预测的后果。
条款4:尽量使用C++风格的注释
C++的注释可以在注释里还有注释,所以注释掉一个代码块不用删除这段代码的注释。C则不行。
条款5:对应的new和delete要采用相同的形式
调 用new时用了[],调用delete时也要用 []。如果调用new时没有用[],那调用delete时也不要用[]。对于typedef来说,用new创建了一个typedef定义的类型的对象后, delete时必须根据typedef定义的类型来删除。因此,为了避免混乱,最好杜绝数组类型用typedef。
条款6:析构函数里对指针成员调用delete
删除空指针是安全的,因此在析构函数里可以简单的delete类的指针成员,而不用担心他们是否被new过。
条款7:预先准备好内存不足的情况
1.用try-cache来捕获抛出的异常。
2. 当内存分配请求不能满足时,调用预先指定的一个出错处理函数。这个方法基于一个常规,即当operator new不能满足请求时,会在抛出异常之前调用客户指定的一个出错处理函数—一般称之为new-handler函数。还可以创建一个混合风格的基类—这种基类允许子类继承它某一特定的功能(即函数)。
条款8:写operator new和operator delete时要遵循常规
内存分配程序支持new-handler函数并正确地处理了零内存请求,并且内存释放程序处理了空指针。此外还必须要有正确的返回值。
条款9:避免隐藏标准形式的new
在 类里定义了一个称为“operator new”的函数后,会不经意地阻止了对标准new的访问(到底如何隐藏的???)。一个办法是在类里写一个支持标准new调用方式的operatornew,它和标准new做同样的事,这可以用一个高效的内联函数来封装实现。另一种方法是为每一个增加到operator new的参数提供缺省值。
条款10:如果写了operator new就要同时写operator delete
operator new和operator delete需要同时工作,如果写了operator new,就一定要写operator delete。对于为大量的小对象分配内存的情况,可以考虑使用内存池,以牺牲灵活性来换取高效率。
条款11:为需要动态分配内存的类声明一个拷贝构造函数和一个赋值操作符
如果没有自定已拷贝构造函数和赋值操作符,C++会生成并调用缺省的拷贝构造函数和赋值操作符,它们对对象里的指针进行逐位拷贝,这会导致内存泄漏和指针重复删除。因此,只要类里有指针时,就要写自己版本的拷贝构造函数和赋值运算符函数。
条款12:尽量使用初始化而不要在构造函数里赋值
尽量使用成员初始化列表,一方面对于成员来说只需承担一次拷贝构造函数的代价,而非构造函数里赋值时的一次(缺省)构造函数和一次赋值函数的代价;另一方面const和引用成员只能被初始化而不能被赋值。
条款13:初始化列表中的成员列出的顺序和它们在类中声明的顺序相同
类的成员是按照它们在类里被声明的顺序进行初始化的,和它们在成员初始化列表中列出的顺序没有关系。
条款14:确定基类有虚析构函数
通过基类的指针去删除派生类的对象,而基类有没有虚析构函数时,结果将是不可确定的。因此必须将基类的析构函数声明为virtual。但是,无故的声明虚析构函数和永远不去声明一样是错误的,声明虚函数将影响效率。
条款15:让operator=返回*this的引用
当 定义自己的赋值运算符时,必须返回赋值运算符左边参数的引用,*this。如果不这样做,就会导致不能连续赋值,或导致调用时的隐式类型转换不能进行(隐式类型转换时要用到临时对象,而临时对象是const的),或两种情况同时发生。对于没有声明相应参数为const的函数来说,传递一个const对象是 非法的。
条款16:在operator=中对所有数据成员赋值
当类里增加新的数据成员时,要记住更新赋值运算符函数。对基类的私有成员赋值时,可以显示调用基类的operator=函数。派生类的拷贝构造函数中必须调用基类的拷贝构造函数而不是缺省构造函数,否则基类的数据成员将不能初始化。
条款17:在operator=中检查给自己赋值的情况
显 示的自己给自己赋值不常见,但是程序中可能存在隐式的自我赋值:一个对象的两个不同名字(引用)互相赋值。首先,如果检查到自己给自己赋值就立即返回,可以节省大量的工作;其次,一个赋值运算符必须首先释放掉一个对象的资源,然后根据新值分配新的资源,在自己给自己的情况下,释放旧的资源将是灾难性的。
条款18:争取使类的接口完整并且最小
必要的函数是拷贝构造函数,赋值运算符函数,然后在此基础上选择必要的、方便的函数功能进行添加。
条款19:分清成员函数,非成员函数和友元函数
■虚函数必须是成员函数。如果f必须是虚函数,就让它称为类c的成员函数。
■ioerator>>和operator<<决不能是成员函数。如果f是operator>>或operator<<,让f称为非成员函数。如果f还需要 访问c的非公有成员,让f称为c的友元。
■其它情况下都声明为成员函数。如果以上情况都不是,让f称为c的成员函数。
Result = onehalf * 2;能通过编译的原因:调用重载*操作符的成员函数,对参数2进行隐式类型转换。
Result = 2 * onehalf;不能通过编译的原因:不能对成员函数所在对象(即成员函数中this指针指向的对象)进行转换。
条款20:避免public接口出现数据成员
访问一致性,public接口里都是函数。
精确的访问控制,可以精确设置数据成员的读写权限。
功能分离,可以用一段计算来取代一个数据成员。举例:计算汽车行驶的平均速度。
条款21:尽量使用const
如果const出现在*号左边,指针指向的数据为常量;如果const出现在*号右边,则指针本身为常量;如果const在两边都出现,二者都是常量。
将operator的返回结果声明为const,以防止对返回结果赋值,这样不合常规。
c+ +中的const:成员函数不修改对象中的任何数据成员时,即不修改对象中的任何一个比特时,这个成员函数才是const的。造成的问题是可以修改指针指 向的值,而且不能修改对象中的一些必要修改的值。解决方案是将必要修改的成员运用mutable关键字。另一种方法是使用const_cast初始化一个 局部变量指针,使之指向this所指的同一个对象来间接实现。还有一种有用又安全的方法:在知道参数不会在函数内部被修改的情况下,将一个const对象 传递到一个取非const参数的函数中。
条款22:尽量用“传引用”而不用“传值”
传值将导致昂贵的对象开销,而传引用则非常高效。
传引用避免了“切割问题”,即当一个派生类的对象作为基类对象被传递是,派生类的对象的作为派生类所具有的所有行为特性会被“切割”掉,从而变成了一个简单的基类对象。
条款23:必须返回一个对象时不要试图返回一个引用缩写
典型情况:操作符重载。
常见的错误:
返回引用,返回的是局部对象的引用。
堆中构造,使用new分配内存,但是无人负责delete的调用,从而造成内存泄漏。
返回静态对象,导致调用同一函数比较时总是相等。
正确的方法是直接在堆栈中创建对象并返回。
条款24:在函数重载和设定参数缺省值间慎重选择
如果可以选择一个合适的缺省参数,否则就使用函数重载。
有一些情况必须使用重载:函数的结果取决于传入参数的个数;需要完成一项特殊的任务。
条款25:避免对指针和数字类型重载
对于f(0):0代表int还是null。编译器认为是int,这和人们的想象不一样。解决办法是使用成员模板,构造一个可以产生null指针对象的类。最重要的是,只要有可能,就要避免对一个数字和一个指针类型重载。
条款26:当心潜在二义性
情形1:可以通过构造函数和转换运算符产生另一个类的对象,这时编译器将拒绝对其中的一种方法进行选择。
情形2:f(int);f(char);对于f(double)时产生二义。
情形3:多继承时,两个基类有同名的成员。此时必须指定基类方可调用,而不考虑访问控制权限和返回值。
条款27:如果不想使用隐式生成的函数就要显式地禁止它
方法是声明该函数,并使之为private。显式地声明一个成员函数,就防止了编译器去自动生成它的版本;使函数为private,就防止了别人去调用它。为了防止成员函数和友元函数的调用,只声明而不定义这个函数。
条款28:划分全局名字空间
使用名字空间,以防止不同库的名字冲突。对于不支持名字空间的编译器,可以使用struct来模拟名字空间,但是此时运算符只能通过函数调用来使用。
条款29:避免返回内部数据的句柄
对于const成员函数来说,返回句柄可能会破坏数据抽象。如果返回的不是指向const数据的句柄,数据可能被修改。对非const成员函数来说,返回句柄会带来麻烦,特别是涉及到临时对象时。句柄就象指针一样,可以是悬浮的。
条款30:避免这样的成员函数:其返回值是指向成员的非const指针或引用,但成员的访问级比这个函数要低
如果获得了私有或保护成员(包括成员函数)的地址(指针或引用),那么就可以象对待公有成员一样进行访问。如果不得不返回其引用或指针,可以通过返回指向const对象的指针或引用来达到两全其美的效果。
条款31:千万不要返回局部对象的引用,也不要返回函数内部用new初始化的指针的引用
如 果返回局部对象的引用,那个局部对象其实已经在函数调用者使用它之前就被销毁了。而返回废弃指针的问题是必须要有人负责调用delete,而且对于product=one*two*three*four;的情况,将产生内存泄漏。因此,写一个返回废弃指针的函数无异于坐等内存泄漏的来临。
条款32:尽可能地推迟变量的定义
不 仅要强变量的定义推迟到必须使用它的时候,还要尽量推迟到可以为它提供一个初始化参数位置。这样做,不仅可以避免对不必要的对象进行构造和析构,还可以避免无意义的对缺省构造函数的调用。而且,在对变量初始化的场合下,变量本身的用途不言自明,在这里定义变量有益于表明变量的含义。
条款33:明智使用内联
内联函数的本质是将每个函数调用以它的代码体来替换。
大多数编译器拒绝“复杂”的内联函数(例如,包含循环和递归的函数);还有,即使是最简单的虚函数调用,编译器的内联处理程序对它也爱莫能助。
若编译器不进行内联,则将内联函数当作一般的“外联”函数来处理。这称为“被外联的内联”。
找出重要的函数,将它内联。同时要注意代码膨胀带来的问题,并监视编译器的警告信息,看看是否有内联函数没有被编译器内联。
条款34:将文件间的编译依赖性降至最低
■如果可以使用对象的引用和指针,就要避免使用对象本身。定义某个类型的引用和指针只会涉及到这个类型的声明,定义此类型的对象则需要类型定义的参与。
■尽可能使用类的声明,而不使用类的定义。因为在声明一个函数时,如果用到某个类,是绝对不需要这个类的定义的,即使函数是通过传值来传递和返回这个类。
■不要在头文件中再包含其它头文件,除非缺少了它们就不能编译。相反,要一个一个地声明所需要的类,让使用这个头文件的用户自己去包含其它的头文件。
■最后一点,句柄类和协议类都不大会使用类联函数。使用任何内联函数时都要访问实现细节,而设计句柄类和协议类的初衷正是为了避免这种情况。
条款35:使公有继承体现“是一个”的含义
如果类D从类B公有继承时,类型D的每一个对象也是类型B的一个对象,但反之不成立。任何可以使用类型B的对象的地方,类型D的对象也可以使用。
特别注意一般理解中的“是一个”,比如企鹅是鸟,并不严密。如果涉及到飞这个动作,二者之间不适合使用公有继承。
条款36:区分接口继承课实现继承
定义纯虚函数的目的在于,使派生类仅仅只是继承函数的接口。也可以为纯虚函数提供一种缺省实现。
声明简单虚函数的目的在于,使派生类继承函数的接口和缺省实现。
声明非虚函数的目的在于,使派生类继承函数的接口和强制性实现。
条款37:绝不要重新定义继承而来的非虚函数
如果重新定义继承而来的非虚函数,将导致对象对函数的调用结果由指向其的指针决定,而不是由对象本身的类型来决定。另外,也是类的设计产生矛盾,因为公有继承的含义是“是一个”,改变继承而来的方法显然是不合理的。
条款38:绝不要重新定义继承而来的缺省参数值
虚函数动态绑定,而缺省参数是静态绑定。因此重新定义继承而来的缺省参数值可能造成调用的是定义在派生类,但使用了基类中缺省参数值的虚函数。
条款39:避免“向下转换”继承层次
采用向下转换时,将不利于对代码进行维护,可以采用虚函数的方法来解决。
不得不进行向下转换时,采用安全的向下转换:dynamic_cast运算符。dynamic_cast运算符先尝试转换,若转换成功就返回新类型的合法指针,若失败则返回空指针。
条款40:通过分层来体现“有一个”或“用...来实现”
公有继承的含义是“是一个”。对应地,分层的含义是“有一个”或“用...来实现”。例如,要实现set类,因为list中可以包含重复元素,因此set不是一个list。set可以用list来实现,即在set中包含一个list。
条款41:区分继承和模板
当对象的类型不影响类中函数的行为时,就要使用模板来生成这样一组类。
当对象的类型影响类中函数的行为时,就要使用继承来得到这样一组类。
条款42:明智地使用私有继承
关于私有继承的两个规则:和公有继承相反,如果两个类之间的继承关系为私有,编译器一般不会将派生类对象转换为基类对象;从私有基类继承而来的成员都称为了派生类的私有成员,即使它们在基类中是保护或公有成员。
私有继承意味这“用...”来实现,但是应尽可能使用分层,必须时才使用私有继承。
条款43:明智地使用多继承
多 继承后果:二义性,如果一个派生类从多个基类继承了一个成员名,所有对这个名字的访问都是二义的,你必须明确说出你所指的是哪个成员。这可能导致虚函数的失效,并且不能对多继承而来的几个相同名称虚函数同时进行重定义。钻石型继承,此时向虚基类传递构造函数参数时要在继承结构中最底层派生类的成员初始化列表中指定。同时还要仔细想想虚函数的优先度。
然而在适当时候还是可以使用多继承,例如将接口的公有继承和实现的私有继承结合起来的情况。
以增加中间类的代价来消除多继承有时侯是值得的。一般应该避免使用多继承以减少继承结构的复杂性。
条款44:说你想说的,理解你所说的
理解不同的面向对象构件在C++中的含义:
· 共同的基类意味着共同的特性。
· 公有继承意味着 "是一个"。
· 私有继承意味着 "用...来实现"。
· 分层意味着 "有一个" 或 "用...来实现"。
下面的对应关系只适用于公有继承的情况:
· 纯虚函数意味着仅仅继承函数的接口。
· 简单虚函数意味着继承函数的接口加上一个缺省实现。
· 非虚函数意味着继承函数的接口加上一个强制实现。
条款45:弄清C++在幕后为你所写、所调用的函数
如 果没有声明下列函数,编译器会声明它自己的版本:一个拷贝构造函数,一个赋值运算符,一个析构函数,一对取址运算符和一个缺省构造函数。对于拷贝构造函数和赋值运算符,官方的规则是:缺省拷贝构造函数(赋值运算符)对类的非静态数据成员进行“以成员为单位的”逐一拷贝构造(赋值)。
特别要注意由于编译器自动生成的函数造成的编译错误。
条款46:宁可编译和链接是出错,也不要运行时出错
通常,对设计做一点小小的改动,就可以在编译期间消除可能产生的运行时错误。这常常涉及到在程序中增加新的数据类型。例如对于需要类型检查的Month,可以将其设为一个Month类:构造函数私有,产生对象使用静态成员函数,每个Month对象为const。
条款47:确保非局部静态对象在使用前被初始化
如果在某个被编译单元中,一个对象的初始化要依赖于另一个被编译单元中的另一个对象的值,并且这第二个对象本身也需要初始化,就有可能造成混乱。
虽 然关于 "非局部" 静态对象什么时候被初始化,C++几乎没有做过说明;但对于函数中的静态对象(即,"局部" 静态对象)什么时候被初始化,C++却明确指出:它们在函数调用过程中初次碰到对象的定义时被初始化。如果不对非局部静态对象直接访问,而用返回局部静态对象引用的函数调用来代替,就能保证从函数得到的引用指向的是被初始化了的对象。
条款48:重视编译器警告
重视编译器产生的每一条警告信息。在忽略一个警告之前,一定要准确理解它想告诉你的含义。
条款49:熟悉标准库
对 于C++头文件,最大的挑战是把字符串头文件理清楚:是旧的C头文件,对应的是基于char*的字符串处理函数; 是包装了std的C++头文件,对应的是新的string类(看下文);是对应于旧C头文件 的std版本。如果能掌握这些,其余的也就容易了。
库中的一切都是模板。
条款50:提高对C++的认识
C++的设计目标:和C兼容,效率,和传统开发工具及环境的兼容性,解决真实问题的可应用性。
参考C++标准,理解C++的设计过程。