<<Effective C++>>读书笔记(3)

五.实现

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)如果我们返回handles的话,当对象被销毁,而handle还在使用的话,那么程序就会崩溃.

29.为"异常安全"而努力是值得的
异常安全在写东西的时候用得比较少.不过这里所说的并非都是指try和catch,而是我们为了保证程序在出现问题的时候仍然能够正常运行的代码,这些都是为了异常安全而做的.
a)异常安全的代码具有下面的特性:当异常抛出的时候,第一是不泄露任何资源.比如发生异常,锁住的互斥锁没有被释放,就会永久被锁住.第二是不允许数据被破坏,比如先释放了原本对象的资源,而新资源申请失败,就会造成麻烦.
b)第一种特性可以用资源管理类达成,资源出了作用域会自动释放.
c)关于异常安全有三个保证:第一个是基本保证,保证如果失败,还是可以运行,使用默认的状态.第二个是强烈保证,如果失败了,保证恢复到函数调用之前的状态.第三个是不抛出异常保证,这个要求最高,需要把能够处理的东西都在函数内部处理掉.
d)第二种特性通常使用copy and swap来保证.比如替换一个资源,那么先申请一个临时的指针,在这个临时的指针上new一个对象,成功后才将原理的对象销毁,然后将这个临时指针所指的对象赋值给原来的指针.
f)函数提供的异常安全保证,通常只等于其所调用的各个函数中异常安全保证中最低的.

30.透彻了解inline的里里外外
a)inline是个好东西,看起来像函数,动作也和函数一样,比宏好得多,但是调用inline函数又不需要承受函数调用的开销.合理使用inline合一大大的进行性能优化,而这些优化都是编译器帮我们做得.
b)但是inline也有坏处.第一,inline的实现是将函数本体替换掉函数调用,直接将代码拷贝过去,没一个调用都会有一份函数的拷贝,这样肯定会导致程序体积增大.第二,inline函数无法随着程序库的升级而升级,因为inline函数会被编译到程序本体中,一旦inline函数内部改变了,那么所有使用该函数的程序都需要重新编译;而如果不是inline的话,客户只需要重新连接就好,如果是动态连接,那么函数甚至可以不知不觉地升级.第三,inline函数不容易调试,有的编译器会为了方便调试,在debug版时不进行inline.
c)inline只是一个命令,函数会不会被inline最终还是编译器决定的.如果函数太复杂(比如巡更或者递归),编译器拒绝inline,而且有virtual的函数也是拒绝inline的(因为virtual调用什么是运行时决定的,而inline是编译时决定的,不确定会调用什么,那么编译的时候就拒绝inline).而且调用函数的方式也会决定函数是否会被inline,比如函数是inline的,但是使用一个函数指针来调用的话,就不会inline了.
d)怎样写一个inline函数呢?两种方式,第一种是隐喻版的,即在类的定义式中写函数体的话,那么这个函数会被inline,一般为成员函数,但是友元函数也可以.第二种就是显式声明inline,在函数声明前面加上inline.
e)到底什么时候该用inline?在程序代码很短小,被频繁调用时才设为inline.

31.将文件间的编译依存关系降到最低
这点最大的影响就是编译的速度,当然,影响编译速度的原因是耦合度,如果耦合越大,那么不但编译变慢了,修改一下也是灾难性的
a)一种原因是我们只要用到一个变量,发现它是未声明的,就会把它定义的头文件#include尽量了,其实有时候是没有必要非得引入头文件的.比如简单的使用一个对象的引用或指针时,不调用对象的特有方法,那么,完全可以直接在使用对象的文件前面加上一个该类的声明即可.所以,我们能有指针或引用的时候,就尽量使用指针和引用.
b)但是只是用指针和引用也不行,要调用对象方法的话就必须要有对象方法的定义,这时候可以用一个经典的方法:"针对接口编程,而不是针对实现编程".当要实现一系列方法的时候,可以先定义一个接口,接口中可以都是纯虚函数,然后实现类继承接口.调用的时候,通过多态进行调用.修改的时候,只需要修改实现的文件即可,只要给客户端的接口不修改的话,客户端完全不需要修改和编译.
c)另一种方法是为声明式和定义式提供不同的头文件。定义式包含类的具体实现类.比如Person的定义类包含PersonImpl.h.PersonImpl类包含和Person完全相同成员函数,两者的接口完全相同.这样做的话只要接口没有发生改变,就不需要重新编译.

六.继承与面向对象设计

32.确定你的public继承塑模出is-a关系
随着学习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)a)对virtual和non-virtual都适用.如果直接用子类调用函数的话,那么调用的都是子类的函数,父类的是会被屏蔽掉的.如果使用父类指针或者引用来调用子类对象的话,会触发多态,这时,virtual版本的会调用子类特有的内容,而non-virtual版本的依然会调用父类的内容.
c)如果必须要用父类的函数的话,不想被子类盖掉,可以在子类实现中加上using 父类::函数名 的声明,显式引入父类的命名空间.也可以使用转交函数,就是一个inline函数,内部为 父类::父类函数() 调用.

34.区分接口继承和实现继承
a)如果继承的类是pure virtual的,那么我们就只继承了接口.我们在子类中必须重新实现这个接口,否则会在编译的时候报错.但是,我们也可以在基类中编写纯虚函数的实现,当然,调用这个实现的唯一途径是通过类名::函数来调用.
b)如果继承的类是virtual的,但不是pure的.就是说继承的是自带一份实现的virtual函数的类,那么即使我们不写子类的实现也不会报错,调用的时候就默认调用基类的函数,当子类重写该函数的实现时,才会调用子类的函数.
c)如果继承的类是一个非virtual,这种继承就是强制我们在继承接口的同时,继承实现.这时我们就应该再去复写这个函数了.
d)简单总结下就是pure-virtual继承接口,impure-virtual继承接口和默认实现,non-virtual继承接口以及实现.

35.考虑virtual函数以外的其他选择
这条大致说的是设计模式中的策略模式(运用基于对象的方法 std::bind+std::function
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 = &recetangle;此时s动态类型就变成了rectangle.
b)virtual函数通过动态类型绑定来确定调用哪个函数.但是缺省参数机制是根据静态绑定来的,不会根据动态绑定来确定缺省参数.所以,如果要在virtual函数中使用缺省参数,那就出了大麻烦了,我们虽然在派生类中定义了缺省参数,但是这个缺省参数却是取自静态类型,即基类定义的缺省参数.如果两个不一样,那么麻烦就大了.即使保证一样,如果修改了基类的缺省参数,忘了修改所有派生类中的某个缺省参数,就很难发现这个错误.
c)那么干脆就不要在virtual函数中定义缺省参数了,换一种方式,即上面说过的用一个non-virtual函数来封装virtual函数,然后在non-virtual函数中给出缺省参数,调用virtual函数就可以避免上面那种情况.

class Shape{
     public:
              enum Color{RED,GREEN,BLUE};
              void draw(Color color = RED) const{
                     ...
                     doDraw(color);
                     ...
              }
              ...
     private:
             virtual void doDraw(Color color) const = 0;  
     };
     class Circle:public Shape{
                   ...
     private:
              virtual void doDraw(Color color){ ... }
     };

38.通过符合塑模出has-a或者"根据某物实现出"的关系:
a)注意复合关系是has-a关系,而普通的public继承的话是is-a关系,两者有着本质的区别.这一点还算比较好区分.
b)在应用领域,复合一位着has-a关系.而在实现领域,复合意味着is-implemented-in-terms-of关系.

39.明智而又审慎地使用private继承
a)private继承的特性:第一,private并没有is-a的特性,换句话说,使用基类指针指向子类对象时,并不会发送动态绑定.第二,private继承而来的东西,到子类中都会变成private,不管其在基类中是不是private或者public.
b)private继承的特性决定了其只是继承实现部分,不继承接口.
c)private继承可以有is-implemented-in-terms-of的效果.但是,除非特殊情况 

40.明智而又审慎地使用多继承
a)多重继承可能导致子类同时继承不同基类的同名函数,当调用这个函数时会发生歧义.比如Base1和Base2都有Func这样一个函数,Derived通过public继承了Base.那么调用Func的时候,就不知道是哪个Func了.可以通过调用像Derived.Base1.Func()来明确指出调用哪个函数.
b)万一两个基类都继承了同一个基类的话,就会出现菱形继承.这种情况一般最好的解决方法是使用虚拟继承.但是虚拟继承是有代价的.
c)可以考虑使用public继承和private继承同时使用.public继承接口相关内容,private继承内部实现相关内容.
d)多重继承可以被使用单一继承的方案替代,而且使用单一继承可以避免很多不必要的麻烦.不过有时候,多重继承确实是最好的方案,所以不要害怕使用,只是在使用时要更多考虑.

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值