《Effective C++》读书笔记(三)

五.实现


26.尽可能延后变量定义式出现的时间:

a)      只有真的要用这个变量了,才进行定义。防止不必要的定义造成浪费。    

b)      如果可以通过copy构造函数构造就能省去对象默认构造并赋值的浪费。

c)      关于循环内使用一个对象,定义在循环外还是循环内的问题,这个本人纠结了好久。现在终于有相对明确的答案了:

定义在循环外需要:1个构造函数,1个析构函数,n个赋值操作。

定义在循环内需要:n个构造函数,n个析构函数。

定义在循环外的话,会造成作用域污染。所以除非明确知道一组赋值操作比构造和析构操作效率高,或者效率高度敏感时,尽量还是讲变量定义在循环内。

 

27.尽量少做转型动作:

a)      C++提供了四种类型转换运算符,是时候放弃C语言那种单一的类型转换运算符了。一是容易辨识,而是四种类型各有各的用处,更加好把控。

b)      虽然转型很方便,但是很容易出错,而且会有很大的性能损失。所以,能不转型就不转型。比如管理各种对象时,保存对象的基类指针。但是调用时,想使用子类的方法,那么第一种是转型,但是不推荐。第二种是将基类包含各种对象的需要的虚函数,然后通过多态调用。第三种最直接,建立多个对象管理的数据结构,根据不同对象进行不同调用,没有转型,效率最高。

c)      如果必须要转型,可以将转型隐藏在某个函数背后,客户调用该函数,而不需要将转型放到自己的代码内。

 

28.避免返回handles指向对象内部成分:

a)      这一条也比较好理解,所谓handles其实就是指针或者引用或者迭代器。我们把对象的属性封装起来,但是有时候为了某些功能,有时候会需要对象内部的属性,这时候就有可能有危险了。

b)      有时候我们希望返回的东西不被更改,那么就在函数后面加个const,但是,返回的如果是handle的话,这个const并不能保证handle指向的对象被改变。所以返回handle严重破坏了封装性。

c)      第三个危险的地方就是,如果我们返回了handle的话,当对象被销毁,而handle还在使用的话,那么程序就等着崩吧。

 

29.为“异常安全”而努力是值得的:

异常处理说实话除了学习的时候写过小的例子,真正在写东西的时候貌似木有用过。不过这里所说的并非全都是所谓的try和catch,而是我们为了保证程序在出问题的时候仍然能够正常运行的代码,这些都是为了异常安全而做的。

a)      有异常安全的代码具有下面的特性:在异常抛出的时候,第一是不泄露任何资源,比如有了异常,锁住的互斥锁没有被释放,那就永久锁住了。第二是不允许数据被破坏,比如先释放了原本对象的资源,而新资源申请失败,那就完蛋鸟。        

b)      关于第一种不泄露资源的,那就是之前写的资源管理类,出作用域自动释放,可以达到作用,即使崩了,出了作用域,也会进行释放。

c)      关于异常安全有三种保证:第一个是基本保证,保证如果失败,还是可以运行,使用默认的状态,但是还是失败了。第二个是强烈保证,如果失败了,保证恢复到调用函数之前的状态。第三个是不抛异常保证,这个要求最高,需要把能够处理的东西都在函数内部处理掉。

d)      关于第二种保证,通常使用copyand swap来保证。比如换掉一个资源,那么先申请一个临时的指针,在这个临时的指针上new一个对象,成功之后,才将原来的对象销毁,然后将这个临时指针所指的对象赋值给原来的指针。

f)       函数提供的异常安全保证,通常只等于其所调用的各个函数中的异常安全保证中最低的。

 

30.透彻了解inline的里里外外:

a)      inline是个好东西,看起来像函数,动作也和函数一样,比宏好得多,但是调用它又不需要承受函数调用的开销。合理使用inline可以大大的进行性能优化,而这些优化都是编译器帮我们做的。

b)      但是inline也有坏处,第一,inline的实现是将函数本体替换掉函数调用,直接将代码拷贝过去,每一个调用都会有一份函数的拷贝,这样肯定会导致程序体积增大。第二,inline函数无法随着程序库的升级而升级,因为inline函数会被编译到程序本体中,一旦inline函数内部改变了,那么所有使用该函数的程序都需要重新编译;而如果不是inline的话,客户只需要重新连接就好,如果是动态链接,那么函数甚至可以不知不觉的升级。第三,inline函数不容易调试,有的编译器会为了方便调试,在debug版时不进行inline。

c)      inline只是一个命令,函数会不会被inline最终还是编译器决定的。如果函数太复杂(比如有循环或者递归),编译器拒绝Inline,而且有virtual的函数也是拒绝的(因为virtual调用什么是运行时决定的,而inline是编译时决定的,不确定会调用什么,那么编译的时候就拒绝inline)。而且调用函数的方式也会决定函数是否会被inline,比如函数是Inline的,但是使用一个函数指针来调用的话,就不会inline了。

d)      怎样写一个inline函数呢?两种方式,第一种是隐喻版的,即在类的定义式中写函数体的话,那么这个函数会被inline,一般为成员函数,但是友元函数也可以。第二种就是显式声明inline,在函数声明前面加上inline。

e)      到底什么时候该用inline?在程序代码很短小,被频繁调用时才设定为inline。

 

31.将文件间的编译依存关系降到最低:

这点最大的影响就是编译的速度,当然,影响编译速度的原因是耦合度,如果耦合越大,那么不但编译变慢了,修改一下也是灾难性的。

a)      一种原因是我们只要用到一个变量,发现他是未声明的,就把它的定义的头文件#include进来了,其实有时候是没有必要非得要引入头文件的,比如简单的使用一个对象的指针或者引用时,不调用对象的特有方法,那么,完全可以直接在使用对象的文件前面加上一个该类的声明即可。所以,我们能用指针或者引用的时候,就尽量要使用指针和引用。

b)      但是只是用指针和引用也不行,要调用对象方法的话就必须要有定义对象的方法,所以这时候就是一条经典登场的时候了:“针对接口编程,而不是针对实现编程”。当要实现一系列方法的时候,可以先定义一个接口,接口中可以都是纯虚函数,然后实现类继承接口,调用的时候,通过多态进行调用。修改的时候,只需要修改实现的文件即可,只要给客户端的接口不修改的话,客户端完全不需要修改和编译的。

 

 

六.继承与面向对象设计


32.确定你的public继承塑模出is-a关系:

继承是个好东东,刚开始学C++的时候感觉想出继承这个点子的人简直是个天才,然后就各种想把能从基类继承的东西都继承过来,最后整个程序框架就是一大颗继承树。不过,随着学习C++渐渐深入了一点点,渐渐发现,继承并不是那么好,is-a关系有时候远远没有has-a来得灵活。

a)      使用public继承,必须明确一点!!!非常重要的一点B继承了A,那么,B就是A的一种特例,A能干的,B必须能干。但是反过来,B能干的,A却不一定能干。

b)      这种关系听起来感觉很正常,但是有时候is-a却并不能很好的帮我们解决问题。比如,基类是鸟,有一个fly的方法,但是子类企鹅的话,却不会飞。所以,什么时候,针对那种情况使用public继承显得尤为重要。

c)      面向对象设计中的常见的几种关系:is-a,has-a,is-implemented-in-terms-of。即是一个,有一个,实现一个接口。灵活运用这几种关系,才能写出便于维护和运行良好的代码。

 

33.避免遮掩继承得来的名称:

这一条主要其实主要是命名空间的问题。

a)      继承的话,子类的命名空间是嵌套在父类的命名空间中的。根据local命名空间会屏蔽上一层命名空间的规律,如果子类有和父类相同的函数名,子类的是会覆盖掉父类的函数的。注意!!!只要是同名的就会被屏蔽掉,不要妄想什么父类的有函数重载,子类只是盖掉了一个,还可以调用参数不一样的。是全被屏蔽掉了,如果还掉重载的,编译时会报错。

b)      上面的那条,对于virtual或者non-virtual都适用。如果直接用子类调用函数的话,那么调用的都是子类的函数,父类的是会被屏蔽掉的。如果使用父类指针或者引用来调用子类对象的话,会触发多态,这时,virtual版本的会调用子类特有的内容,而non-virtual版本的则会仍然调用父类的内容。

c)      如果必须要用父类的函数的话,不想被子类盖掉,可以在子类实现中加上using 父类::函数名的声明,显示引入父类的命名空间。也使用转交函数,就是一个inline函数,内部为父类::父类函数()调用。

 

34.区分接口继承和实现继承:

当一个类称为Base类的时候,我们public继承它,就获得了其中的各种接口和方法。但是,这种实现是有不同的,关键就在于一个virtual!

a)      如果继承的东东是个purevirtual的,那么,我么就只继承了接口。我们在子类必须重新实现这个接口,否则会在编译的时候报错。但是,我们竟然也可以在基类中实现一个纯虚函数的实现,当然,调用这个实现的唯一途径是通过类名::函数来调用。

b)      第二种继承的话,继承的东东是个virtual的,但是不是pure的。就是说自带一份实现的virtual函数,如果继承的话,我们不写子类的实现,是不会报错的,调用的时候默认调用基类的函数,当有自己的实现时,才调用子类的函数。这样方便了一些,但是容易出现忘记实现自己的函数时,造成我们不想看到的情况。

c)      第三种继承的话,就是继承的东东是一个非virtual的,这种继承就是强制我们在继承接口的同时,继承实现。这时,我们就不应该再去覆写这个函数了。

d)      简单总结下就是pure-virtual继承接口。Impure-virtual继承接口和默认实现,non-virtual继承接口以及实现。

 

35.考虑virtual函数以外的其他选择:

 这条赶脚有点儿高大上,不是太理解,不过感觉大致说的就是设计模式中的策略模式。

a)      虚函数可能有各种问题,所以一种流派比较极端,就是不直接使用虚函数,而是将虚函数设置为private的,然后用非虚函数调用这个函数。这种做法保证了在调用之前和之后可以进行相关的处理。比如验证,log,或者互斥锁等等。

b)      第二种方式就是使用函数指针类型的策略模式代替virtual函数。对象进行相关操作是一个函数指针,这个函数我们可以自己定义。但是由于是非成员函数的话,访问不到成员对象内容,可以添加friend修饰,不过可能会破坏封装性。

c)      更好的方法是使用函数对象,参数设定为this指针,就可以根据不同的对象来进行操作了。还可以使用std::bind这个高大上的东东进行绑定。

d)      更加易扩展的就是,函数对象中的函数是virtual的,然后就可以继承这个函数对象,增加新的功能。

 

36.绝不重定义继承而来的non-virtual函数:

这条很简单,记住就好。来看看为什么。

a)      如果是virtual函数的话,会进行动态绑定,那么不管你用的是基类指针还是派生类指针,都会调用派生类的函数(如果有的话)。

b)      而如果不是virtual函数的话,进行的是静态绑定。调用什么函数,最终就看你用的是什么的指针,如果是基类的指针,就调基类函数;如果是子类指针,就调子类函数。所以如果重定义了一个non-virtual函数,就会覆盖基类的函数,造成调用行为不确定的后果。

 

37.绝不重定义继承而来的缺省参数值:

a)      先看一下静态绑定和动态绑定。静态类型是对象被声明时所处的类型,比如,shape* s = new circle(),这时候,静态类型就是shape。不论s指向什么,它们静态类型都是声明的类型,即shape*。而动态类型是指目前所指的对象类型(或者引用),这个类型是可以切换的,通过赋值操作就可以切换动态类型。比如shape* s = new circle(),此时动态类型就是circle。而如果s =&rectangle; 此时s动态类型就变成了rectangle*。

b)      virtual函数通过动态类型绑定来确定调用哪个函数。But!缺省参数的机制是根据静态绑定来的,不会根据动态绑定来确定缺省参数。所以,如果要在virtual函数中使用缺省参数,那就出了大麻烦了,我们虽然在派生类中定义了缺省参数,但是这个缺省参数却是取自静态类型,即基类定义的缺省参数。如果两个不一样,那么就麻烦大了。即使保证一样,那麻烦更大,如果修改了基类的缺省参数,忘了修改所有派生类中的某个缺省参数,那很难发现这个奇葩的错误。

c)      那么干脆就不要在virtual函数中定义缺省参数了,换一种方式,即上一条中的,用一个non-virtual函数来封装virtual函数,然后在这个non-virtual函数中给出缺省参数,调用virtual函数就可以避免上面的那种情况。

 

38.通过复合塑模出has-a或者“根据某物实现出”的关系:

a)      注意复合关系是has-a关系,而普通的public继承的话是is-a关系。两者有着本质的区别。这一点还算比较好区分。

b)      在应用领域,复合意味着has-a关系。而在实现领域,复合意味着is-implemented-in-terms-of关系。


39.明智而又审慎地使用private继承:

之前从来没考虑过private继承…这次看一下。

a)      private继承的特性:第一,private并没有is-a的特性。换句话说,使用基类指针指向子类对象时,并不会发生动态绑定,第二,private继承而来的东西,到子类中都会变成private的,不管其在基类中是不是private或者public。

b)      private继承的特性决定了其只是继承实现部分,而不继承接口部分。

c)     private继承可以有is-implemented-in-terms-of的效果,但是,除非特殊情况(empty base class)最好还是使用聚合来实现这种效果。

 
40.明智而又审慎地使用多重继承:

多重继承,有人感觉这个是个天才的发明,而有人确认为是个糟糕的发明,不过不管怎么样,多重继承都为我们提供了一种解决问题的手段。

a)      多重继承可能导致子类同时继承不同基类的同名函数,当调用这个函数时,会发生歧义。比如Base1和Base2都有Func这样一个函数,Derived通过public继承了Base,那么,调用Func的时候,就不知道是哪个里面的Func了。偶们可以像这样derived.Base1::Func();来明确指出调用哪个函数。

b)      但是,万一两个基类都继承了同一个基类的话,就会出现传说中的菱形继承。这个更加的麻烦!这种情况一般最好的解决方案就是让最上面的基类成为virtual函数。但是virtual是有代价的,会增加大小,速度,初始化复杂度。所以pure-virtual可能更好。

c)      一种方式可以考虑使用public继承和private继承同时使用。Public继承接口相关内容,private继承内部实现相关内容。

d)      多重继承可以被使用单一继承的方案替代,而且使用单一继承避免很多不必要的麻烦。不过有时候,多重继承确实是最好的方案,所以不要害怕使用,只是在使用的时候要多考虑考虑就好啦!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值