effective C++记录了改善程序与设计的55个具体做法,阅读中记录那些个人认为的关键部分。
这里附上《Effective C++ 中文版第三版 高清PDF 》的免费下载地址,Effective C++ 中文版第三版 高清PDF 免费下载
条款04:确定对象被使用前已先被初始化。“为免除跨编译单元之初始化次序问题,请以local static对象替换non-local static对象”。“这个手法的基础在于:c++保证,函数内的local static对象会在“该函数被调用期间”“首次遇上该对象之定义式”时被初始化”,“如果你从未调用non-local static 对象的“仿真函数”,就绝不会引发构造函数和析构成本:真正的non-local static对象可没这等便宜”。 实际上,static变量保存在静态存贮区,程序开始运行时就存在,不管是局部还是全局。
条款08:别让异常逃离析构函数。
这里引用c++primer关于异常那一章的原话:
栈展开期间,释放局部对象所用的内存并运行类类型局部对象的析构函数。如果一个块直接分配资源,而且在释放资源之前发生异常,在栈展开期间将不会释放该资源。例如,一个块可以通过调用 new 动态分配内存,如果该块因异常而退出,编译器不会删除该指针,已分配的内在将不会释放。如果一个块直接分配资源,而且在释放资源之前发生异常,在栈展开期间将不会释放该资源。例如,一个块可以通过调用 new 动态分配内存,如果该块因异常而退出,编译器不会删除该指针,已分配的内在将不会释放。--这就是为什么要用智能指针来保证即使发生异常,对象也能被正确析构的理由。
栈展开期间会经常执行析构函数。在执行析构函数的时候,已经引发了异常但还没有处理它。如果在这个过程中析构函数本身抛出新的异常,又会发生什么呢?新的异常应该取代仍未处理的早先的异常吗?应该忽略析构函数中的异常吗?答案是:在为某个异常进行栈展开的时候,析构函数如果又抛出自己的未经处理的另一个异常,将会导致调用标准库 terminate 函数。一般而言,terminate函数将调用 abort 函数,强制从整个程序非正常退出。因为 terminate 函数结束程序,所以析构函数做任何可能导致异常的事情通常都是非常糟糕的主意。在实践中,因为析构函数释放资源,所以它不太可能抛出异常。标准库类型都保证它们的析构函数不会引发异常。
那么如果确实要在析构函数中抛出异常,作者的提议有3个:析构函数中用try catch捕获异常然后abort退出程序;析构函数中用try catch捕获异常,然后只记录但是不处理这个异常;让用户自己调用close,给用户一个机会去自己处理异常,析构函数做的事情是,如果用户没有调用close,则调用,如果出现异常则用try catch捕获但是不处理。
条款20:宁以pass-by-reference-to-const替换pass-by-value。
条款21:必须返回对象时,别妄想返回其reference。
注意:条款20针对的是参数,也就是函数的argument,而条款21针对的是函数的返回值。
条款22:将成员变量声明为private。 (请与条款23一起阅读)
作者提出:“protected并不比public更具封装性”,作者提出的原因是如果删除了一个protected成员,那么所有使用它的derived classes都会被破坏。这一点上面我的理解是,按照作者的分析,protected确实并不比public更具封装型,但是protected存在确实有它的必要性,为了让derived classes对它可见。在这里,不要因为protected的封装性不好而不使用它。就好比条款21,当返回指针或者引用的情况下我们都很难处理时,我们就返回对象,效率的话就让编译器去处理吧。
其实,作者在条款23中提出了对封装的见解(p99第三段):如果某些东西被封装,它就不再可见。愈多东西被封装,越少人可以看到它。而越少人看到它,我们就有越大的弹性去改变它。因为我们的改变仅仅直接影响看到改变的那些人事物。因此,越多东西被封装,我们改变那些东西的能力也就越大。这就是我们首先推崇封装的原因:它使我们能够改变事物而只影响有限客户。这里,我对作者的“改变那些东西”的理解为:日后修改一个成员变量,包括增删改。从这一点来理解条款22就更明确了,作者对条款22的总结是:其实只有两种访问权限:private(提供封装)和其他(不提供封装)。这是因为对于private,被封装的东西是最多的(可见性范围最小),private在整个继承体系中,只在基类及其友元可见,我们对private成员的增删改也只影响到少量客户(这里是基类的使用了private成员的成员函数以及友元);而protected虽然对于基类和继承类是私有的,但是其可见性也延伸到了继承类的使用了它的成员函数,也就是protected的可见性范围包括基类成员函数以及继承类成员函数以及基类的友元;而对于public就更不用说了,它的可见性范围是所有用户。
条款23:宁已non-member、non-friend替换member函数。(请与条款22一起阅读)
我对这个条款的理解是:当遇到选择的时候,考虑面向对象重要守则,那就是封装。封装的一个重要作用就是以后维护一个项目代码的时候,不需要对太多代码重写、重新测试、重新编写文档、重新编译,因为当我们对一个成员进行增删改时我们只需要更改较少的代码,因为我们的封装保证了这个成员的可见性范围的小的。
作者提出:越少代码可以看到数据(也就是访问它),越多的数据可被封装,而我们也就越能自由地改变对象数据,例如改变成员变量的数量、类型等。如何量测“有多少代码可以看到某一块数据”呢?我们计算能够访问该数据的函数数量,作为一种粗糙的量测。越多函数可以访问它,数据的封装性就越低。
c++primer第14章操作符的重载中指出:一般将算术和关系操作符定义非成员函数,而将赋值操作符定义为成员。读完effective c++再回头理解这句话。选择成员或非成员实现为类设计重载操作符的时候,必须选择是将操作符设置为类成员还是普通非成员函数。在某些情况下,程序员没有选择,操作符必须是成员;在另一些情况下,有些经验原则可指导我们做出决定。下面是一些指导原则,有助于决定将操作符设置为类成员还是普通非成员函数:
*1 赋值(=)、下标([])、调用(())和成员访问箭头(->)等操作符必须定义为成员,将这些操作符定义为非成员函数将在编译时标记为错误。
“类赋值操作符必须是类的成员,以便编译器可以知道是否需要合成一个。”“赋值操作符可以重载。无论形参为何种类型,赋值操作符必须定义为成员函数,这一点与复合赋值操作符有所不同。”
*2 像赋值一样,复合赋值操作符通常应定义为类的成员,与赋值不同的是,不一定非得这样做,如果定义非成员复合赋值操作符,不会出现编译错误。
*3 改变对象状态或与给定类型紧密联系的其他一些操作符,如自增、自减和解引用,通常就定义为类成员。
*4 对称的操作符,如算术操作符、相等操作符、关系操作符和位操作符,最好定义为普通非成员函数。
*5 IO操作符必须定义为非成员函数,我们不能将该操作符定义为类的成员函数,否则,左操作数将只能是该类类型的对象.考虑如下例子,
这个用法与正常的应用正好相反,如果需要支持正常的用法,则左操作数必须是ostream类型。这意味着,如果该操作符是类的成员,则他必须是ostream类的成员,然而,ostream类是标准库的组成部分,是不能为标准库中的类增加成员的。将该函数在类内部设置为friend,访问类私有的数据成员。
可以参考条款24:若所有参数皆需类型转换,请为此采用non-member函数。
条款24:若所有参数皆需类型转换,请为此采用non-member函数。