《Effective C++》读书笔记
第一章 从C转向C++
**条款1:尽量用const和inline而不用#define
**条款2:尽量用
**条款3:尽量用new和delete而不用malloc和free
malloc和free不知道构造函数和析构函数。
**条款4:尽量使用c++风格的注释
/* */ 这个C风格的注释可能会提前结束
第二章 内存管理
**条款5:对应的new和delete要采用相同的形式
如果new的时候有[]那么delete的时候也要有[],让delete知道是要析构一个对象还是一组对象。
**条款6:析构函数里对指针成员调用delete
这个,防止内存泄露。
**条款7:预先准备好内存不够的情况
在VC下new之后在检查是否为NULL。标准C++规定new一个对象时如果分配内存失败就应抛出一个std::bad_alloc异常,如果不希望抛出异常而仅仅传回一个NULL指针,可以用new的无异常版本:new(nothrow)。在VC6.0中默认是不会抛出异常的,而是会返回一个NULL。但是在linux下编译可能会抛出异常。这个new失败分两种情况来处理,一、如果是内存不够那么这种情况抛出还是不抛出异常其实已经无所谓了。二、如果分配空间ok但是构造函数中出现问题,这个时候就比较麻烦了,所以对于复杂的对象,比如要启动一个线程,那么最好线程启动函数单独写一个init函数,而不是放在构造函数中,构造函数中只放那些简单对象的初始化。注意这里的init是负责“可能出现错误或异常”的处理,并不是说这个对象成员变量很多,如果成员变量很多可以写成单独的一个函数init,但目的是抽离复杂的初始化,让程序可扩展比较容易读。
**条款8: 写operator new和operator delete时要遵循常规
自己重写operator new时,很重要的一点是函数提供的行为要和系统缺省的operator new一致。实际做起来也就是:要有正确的返回值;可用内存不够时要调用出错处理函数;处理好0字节内存请求的情况。此外,还要避免不小心隐藏了标准形式的new,有关返回值的部分很简单。如果内存分配请求成功,就返回指向内存的指针;如果失败,则遵循**条款7的规定抛出一个std::bad_alloc类型的异常。通常来说,如果要实现内存池可以尝试重写new操作符。
**条款9: 避免隐藏标准形式的new
两种解决办法:一个办法是在类里写一个支持标准new调用方式的operator new,它和标准new做同样的事,相当于重载。另一种方法是为每一个增加到operator new的参数提供缺省值。
**条款10: 如果写了operator new就要同时写operator delete
自定义的内存管理程序可以很好地改善程序的性能,而且它们可以封装在象pool这样的类里,operator new和operator delete需要同时工作,那么你写了operator new,就也一定要写operator delete。
第三章 构造函数,析构函数和赋值操作符
**条款11: 为需要动态分配内存的类声明一个拷贝构造函数和一个赋值操作符
这个是为了防止内存泄露,但是有的时候实现拷贝构造函数和赋值函数很困难而且不会用到的时候,可以声明这些函数,注意是声明为private,但不实现它,这样在有人误用,编译器会警告的。
**条款12:尽量使用类的初始化列表而不是构造函数对类的成员变量初始化。
这个主要优点两个:1.如果有继承关系,使用初始化列表可能少调用一次类成员变量的构造函数,直接调用拷贝构造函数。2.可以满足const和引用成员初始化的要求。(同时可以有一定扩展性,可以适应原来的非const变成const)。如果参数很多,初始化列表太长不方便阅读,可以单独抽象出一个init函数。
**条款13:初始化列表中成员列出的顺序和它们在类中声明的顺序相同
首先,要明确一点“类成员是按照它们在类里被声明的顺序进行初始化的,和它们在成员初始化列表中列出的顺序没一点关系”,因为如果不这样设计的话,类析构的时候与初始化顺序相反,如果与初始化列表中顺序一致,那么类就要有一种机制来负责记录初始化的顺序,显然是没有必要的。初始化顺序与在构造函数中的顺序就更没有关系了,在进入构造函数中的时候就已经为所有的成员变量分配好了,在构造函数中做的其实是赋值或者说拷贝。没有关系为什么要相同呢?这是一种编程习惯,如果你确信相同,那么就可以知道参数构造的逻辑(有些参数是与顺序相关的,所以在设计类中参数顺序的时候也要注意),对与程序员来说就会多知道一层逻辑,少一点错误。
**条款14: 确定基类有虚析构函数
子类的删除的时候会自动调用父类的析构函数——这句话有歧义的,这是一种机制和虚函数没有关系,但是如果是用基类的指针来析构子类对象就不会调用子类析构函数,这个时候和虚函数就有关系了。
*条款15: 让operator=返回this的引用
c++的设计者bjarne stroustrup下了很大的功夫想使用户自定义类型尽可能地和固定类型的工作方式相似。这就是为什么你可以重载运算符,写类型转换函数(见条款m5),控制赋值和拷贝构造函数,等等。如果不返回*this的引用,它妨碍了连续(链式)赋值操作.
**条款16: 在operator=中对所有数据成员赋值
包括基类的成员变量。
**条款17: 在operator=中检查给自己赋值的情况 这个不用在多说了。
第四章 类和函数:设计与声明
**条款18: 争取使类的
接口完整并且最小 这个,单一职责。
**条款19: 分清成员函数,非成员函数和友元函数
这个情况遇见很少,只适用于«和»操作符,如果他们是成员函数,那么调用的时候有c«cout(类需要在«左边),虽然和法但是平常不是这么用的。operator»和operator«决不能是成员函数。如果f是operator»或operator«,让f成为非成员函数。如果f还需要访问c的非公有成员,让f成为c的友元函数。只有非成员函数对最左边的参数进行类型转换。如果f需要对最左边的参数进行类型转换,让f成为非成员函数。如果f还需要访问c的非公有成员,让f成为c的友元函数。
**条款20: 避免public接口出现数据成员
很少有人把数据直接暴露给用户,也不安全。
**条款21: 尽可能使用const
mutable类型的成员变量,可以在const成员函数被修改。
**条款22: 尽量用“传引用”而不用“传值”
在传类类型的对象时要注意。引用几乎都是通过指针来实现的,所以通过引用传递对象实际上是传递指针。因此,如果是一个很小的对象——例如int——传值实际上会比传引用更高效。
**条款23: 必须返回一个对象时不要试图返回一个引用
**条款24: 在函数重载和设定参数缺省值间慎重选择
两个函数都必须对新对象赋一个初值。这会导致在两个构造函数里出现重复代码,所以要写一个“包含有两个构造函数公共代码”的私有成员函数init来解决这个问题。这个方法——在重载函数中调用一个“为重载函数完成某些功能”的公共的底层函数——很值得牢记,因为它经常有用。
**条款25: 避免对指针和数字类型重载
void f(int x); void f(char*ps); 在调用f(0)的时候可能会引起歧义,所以,不要试图去重载指针和数字类型。
**条款26: 当心潜在的二义性
在类的继承中常会发生。
**条款27: 如果不想使用隐式生成的函数就要显式地禁止它
一般都是赋值函数和拷贝构造函数,为防止用户误用,可以把它们定义为私有的。赋值和拷贝构造函数具有行为上的相似性,这意味着几乎任何时候当你想禁止它们其中的一个时,就也要禁止另外一个。
**条款28: 划分全局名字空间
大型程序很重要。
第五章 类和函数: 实现
**条款29: 避免返回内部数据的句柄
可以返回一个const char * getP() const;需要该值得指针也必须是一个const char *。
**条款30: 避免这样的成员函数:其返回值是指向成员的非const指针或引用,但成员的访问级比这个函数要低
意思是返回了一个私有成员的指针或引用,这样就破坏了访问限制。返回值是某个访问级较低的成员的指针或引用。但同时,你又不想牺牲private和protected为你提供的访问限制。这种情况下,你可以通过返回指向const对象的指针或引用来达到两全其美的效果。
**条款31: 千万不要返回局部对象的引用,也不要返回函数内部用new初始化的指针的引用
**条款32: 尽可能地推迟变量的定义
代理模式也可以这么用~
**条款33: 明智地使用内联
一般来说类的构造函数与析构函数不要定义为内联函数,他们很可能被父类扩展。一个程序往往花80%的时间来执行程序中20%的代码。这是一条很重要的定律,因为它提醒你,作为程序员的一个很重要的目标,就是找出这20%能够真正提高整个程序性能的代码。你可以选择内联你的函数,或者没必要就不内联,但这些选择只有作用在”正确”的函数上才有意义。一旦找出了程序中那些重要的函数,以及那些内联后可以确实提高程序性能的函数(这些函数本身依赖于所在系统的体系结构),就要毫不犹豫地声明为inline。同时,要注意代码膨胀带来的问题。
**条款34: 将文件间的编译依赖性降至最低
在类内的成员变量为类类型的时候,可以使用类类型的指针,然后再头文件中声明这个类即可,不用包含这个类的头文件,而在实现中包含,这样依赖性降低,只要类类型的成员变量接口不变就不用编译。为什么必须是指针呢?因为编译器编译的时候需要知道类型的大小,像int,编译器知道为4。编译器也可以不知道,但是只是为了安全,或者说达到类型安全的一个级别吧才需要知道。
**条款35: 使公有继承体现 “是一个” 的含义
共有继承是强侵入的,继承的子类有父类的所有的方法。这可能导致子类(或者孙子类)有不希望的行为,所以多用组合,少用继承。
第六章
**条款36: 区分接口继承和实现继承
一般,接口继承是为了实现多太,实现继承是为了代码服用
**条款37: 决不要重新定义继承而来的非虚函数
这个语法上虽然合法,但是很容易造成接口的混乱,要重定义使用虚函数,实现多态。
**条款38: 决不要重新定义继承而来的缺省参数值
缺省参数是静态绑定,如果基类中默认参数为Va,子类中重定义为Vb,涉及到多态的时候可能会调用子类函数,却使用基类默认的参数Va。C++这样做是为了提高效率,否则要一种机制来实现参数的动态绑定。
**条款39: 避免 “向下转换” 继承层次
也就是使用dynamic_cast,首先这个转换是效率很低的,指针转换失败为NULL,引用好像是抛出异常。最好可以使用虚函数,非到万不得已,不要使用这个。
**条款40: 通过分层来体现 “有一个” 或 “用…来实现”
也就产生依赖,多用组合少用继承。
**条款41: 区分继承和模板
通常情况下使用模板都会涉及到算法,当类型改变的时候却不影响使用到的这个类的行为,那么就可以使用模板。例如STL中大量使用模板,同常是对某一个类型的存储,增加删除等操作,在操作的过程中一般不会用到对这个类型特定的接口,使用到也就简单的new,delete等。而使用继承比较常见于面向对象的设计,这一组类通常有着相同的接口。简单来说使用模板一般是会用到算法,而使用继承通常是为了有不同的行为。
**条款42: 明智地使用私有继承
私有继承意味着”用…来实现”这一事实会给带来一点混淆,”分层”也就是组合也具有相同的含义。怎么在二者之间进行选择呢?答案很简单:尽可能地使用分层,必须时才使用私有继承。什么时候必须呢?这往往是指有保护成员和/或虚函数介入的时候。“用…实现”的类不想被随便的实例化(构造函数被定义为保护或私有)(一般这个类如果实例化安全性很差,例如有void*这种类型),这个时候可以使用私有继承。还有就是有虚函数的时候。
**条款43: 明智地使用多继承
多继承可能导致二义性,使用过程中不要出现钻石型继承和虚函数歧义的情况,使用多继承可以使得代码服用,但是不能只是为了“使用一个类”而无限制的私有继承类,有的时候为了逻辑清晰,可以增加一个类来代替共有继承,使得代码容易维护。
**条款44: 说你想说的;理解你所说的
公有继承和 “是一个” 的等价性,以及非虚成员函数和 “特殊性上的不变性” 的等价性,是C++构件如何和设计思想相对应的例子。下面的列表总结了这些对应关系中最重要的几个。
- 共同的基类意味着共同的特性。如果类D1和类D2都把类B声明为基类,D1和D2将从B继承共同的数据成员和/或共同的成员函数。见条款43。
- 公有继承意味着 “是一个”。如果类D公有继承于类B,类型D的每一个对象也是一个类型B的对象,但反过来不成立。见条款35。
- 私有继承意味着 “用…来实现”。如果类D私有继承于类B,类型D的对象只不过是用类型B的对象来实现而已;类型B和类型D的对象之间不存在概念上的关系。见条款42。
- 分层意味着 “有一个” 或 “用…来实现”。如果类A包含一个类型B的数据成员,类型A的对象要么具有一个类型为B的部件,要么在实现中使用了类型B的对象。见条款40。
下面的对应关系只适用于公有继承的情况:
- 纯虚函数意味着仅仅继承函数的接口。如果类C声明了一个纯虚函数mf,C的子类必须继承mf的接口,C的具体子类必须为之提供它们自己的实现。见条款36。
- 简单虚函数意味着继承函数的接口加上一个缺省实现。如果类C声明了一个简单(非纯)虚函数mf,C的子类必爰坛衜f的接口;如果需要的话,还可以继承一个缺省实现。见条款36。
- 非虚函数意味着继承函数的接口加上一个强制实现。如果类C声明了一个非虚函数mf,C的子类必须同时继承mf的接口和实现。实际上,mf定义了C的 “特殊性上的不变性”。见条款36
第七章 杂项
**条款45: 弄清C++在幕后为你所写、所调用的函数
当C++编译器通过它的时候。如果你没有声明下列函数,体贴的编译器会声明它自己的版本。这些函数是:一个拷贝构造函数,一个赋值运算符,一个析构函数,一对取址运算符。 **条款46: 宁可编译和链接时出错,也不要运行时出错
没有运行时检查,程序会更小更快。 将检查从运行时转移到编译或链接时一直是值得努力的目标,只要实际可行,这样程序会更小,更快,更可靠。例如对一个参数检查,为了减少在运行时检查,可以定义一种专门的类型,只要编译通过,那么运行时需要做的检查就很少了。
**条款47: 确保非局部静态对象在使用前被初始化 **条款48: 重视编译器警告 **条款49: 熟悉标准库 **条款50: 提高对C++的认识