《Effective C++》重点条款心得

条款3 尽可能使用const

  总结:如果一个变量的值你以后不希望它变了,或者认为它不会再变了,就要把它加上const,用const修饰可以让编译器来帮你监督它,如果它敢变,编译器就敢报错。
  还有很多细节上要注意的地方:

  1.指针常量与常量指针。这是一个很容易弄混的东西,记住它的原则就是看const是修饰在*的左边还是右边。如果是在左边const int* p 或者 int const * p,那这就是指针常量,要注意”指针常量”这四个字前两个字是修饰的意思,也就是说这是个常量,这个指针指向的对象的值是常量是不可变的。如果在右边int* p const,那就是常量指针了。然而事实上死记硬背在左边与在右边也是一件容易弄混的事,细研究下这个星号,当它出现之前,只是声明了这个变量类型,所以在星号之前的const只是修饰这个变量的值,星号之后相当于是声明了指针,所以const修饰的是指针,这样就更好记了。

  2.迭代器,迭代器实现的功能其实和指针颇为相似,所以在用const时,可以把迭代器比作指针,所以用const修饰迭代器const vector<int>::iterator it意味着int* p const。那么如果想让迭代器所指向的值不变怎么办,用const_iteratorvector<int>::const_iterator

  3.两个成员函数之间只有常量性不同,依然可以被重载。 成员函数是在类中声明的,之所以创造类是因为我们要面向对象编程,用类来创造对象。而这个创造出来的对象当然可以选择是否用const来修饰了,如果修饰上了则代表这个对象里头的任何变量都是不可更改的,它调用的成员函数自然也需要被const修饰,这就是可以被重载的原因。

条款4 确定对象被使用前已先被初始化

  这个条款原本是被我直接跳过的,就字面意思来说这实在是常识。
  但仔细再读一下你就会发现有几个需要注意的好点。
  1.成员变量初始化次序与声明次序一致,这一点一定要注意,也是学C++的都必知的一点。初始化列表里的次序是假,以声明次序为准。
  2。文件的编译次序是不确定的,一个文件里调用另一个文件的变量可能会出错。因为这个文件可能还没编译构造好,变量还尚未初始化。应对方法是将这个变量放入类中,先构造出这个类的静态实例,调用静态实例里的变量。书里说这种方法与Singleton模式很类似,我突然想起来第一次读这本书的时候读到这都懵逼了,那时都还不知道Singleton是啥,所以在这里解释一下吧。
  3.Singleton是一种设计模式,一种设计理念,具体的做法是:你创建个类,在类中声明一个返回值为实例引用类型的getInstance方法。在这个方法中定义出来静态的类的实例,返回实例的引用。然后你把构造函数变成私有。这样做完,你以后想用这个类就声明这个类的指针,调用getInstance方法,然后使用指针调用方法。这样做你会发现你没有办法自己再定义类的实例了,因为构造函数被你放到私有里了,保证了全局只有一个实例,只能通过getInstance方法调用,单例模式由此而来。
  4.使用成员初始化列来初始化类中成员变量,别在构造函数中搞。因为在构造函数中初始化其实已经不是初始化了,是赋值,多了一步,低效。

条款5 了解C++默认编写并调用了哪些函数

  创建一个类时,如果自己没有定义构造函数,编译器会编写一个1.默认构造函数
  有构造当然就有2.析构函数(非虚的),所以当类内存在虚函数时一定要自己搞个虚析构函数。
  此外还有3.拷贝构造函数举个例子A a(b);就是定义一个类A的实例a,让这个小a完全和类A的另一个实例小b一样
  最后编译器还会默默的帮你4.重载=操作符,举个例子A& operator=(const A& a);这是个函数,operator=相当于函数名,但这是个有特殊意义的函数名,意为重载”等号“这个操作符,当然同理你也可以operator+,operator-等等。
  需要注意的一点是编译器帮忙重载的目的不是为了给=赋予新的含义,而是因为创建的是个类,里面可能有很多不同的东西,所以需要重载一下适应这个变化好让”=“能够正常的赋值成功。
  还需要注意一点,有时类内声明的对象是并不能直接赋值(比如说你声明了个引用,要知道引用可是绑定的,不能更改),那编译器也有心无力没办法帮你重载=操作符了。不用时不会报错,但想要使用=时当然就会报错了。

条款7 为多态基类声明virtual虚析构函数

  总结:如果一个类里没有虚函数,那么这个类并没有打算被继承(不是不可以,是一般不会),不需要声明虚析构函数。如果一个类里有虚函数,那铁定是会被继承了,一定要声明虚析构函数

细节问题:
1.为什么要声明虚析构函数?
  为了防止析构(销毁)带有基类指针的子类实例时子类实例得不到完全析构。
  用口语话来解释:你设了一个基类,它有一个子类。你设了一个基类的指针,让它指向子类的实例。指针用完后,你把它delete掉。这时如果基类不是虚析构函数,那你delete掉的只会是基类的成员变量与成员函数。

2.何时使用继承?
  这个问题可能在后面的条款里会有详细说明,但我现在就总结了出来是因为它与下一个细节问题息息相关。

  (1)从实现方法角度来看:
    你想实现个方法,这时存在三种情况:
      1)这个方法在一个类里有的话,那都不用实现直接调用这个类的方法就好了。
      2)这个方法哪个类都完全没有类似的的话创建一个类或者在某个类里面实现就好了。
      3)这个方法跟某个类里的某个方法很相似,但又不完全一样,这时你就该抉择了,如果想要实现的方法在这个类里实现的话逻辑上不太对路的话那就用继承的办法,否则就用重载。
  当然了,只因为这么一个方法就创建个子类未免不太划算,所以还得根据逻辑而定。

(2)从架构逻辑角度看:
  继承满足”is-a“关系,子类”是一个“基类。创建子类时一定要把这个子类与基类的关系搞对了。
  打个比方:我渴了,我可以喝水,我也可以喝可乐、喝橙汁等等。那么我就可以建一个名叫”饮品“的基类,这个基类里有”喝“方法,实现可乐,水,橙汁等等子类,这些子类都继承于饮品,它们也都是饮品。在子类里分别实现喝的方法。

3.为什么不用虚函数的类不适合做基类?
  看上一个问题,我们知道了何时使用继承,你会发现这两个角度有一个共同点:子类需要对基类的方法做一些修改,当子类需要对继承于基类的某个方法进行修改时,那基类的这个方法必然要用virtual了。这个原因在后面的条款也有详细说明,到时再说吧。
  可能有人会有疑问:我的子类只对基类做拓展,不对基类做修改,这不行么?这种情况较罕见但也确实可行,那这时还要不要声明虚析构函数呢?
  没必要了,你确实依然可以让基类的指针指向子类的实例,这样再销毁的话还是不会正确析构,但你这么做没有任何意义啊,没有必要让基类的指针指向子类的实例,因为指与不指输出的都是基类的方法。

条款8.别让异常逃离析构函数(prevent exceptions from leaving destructors)

  这条条款可能读起来没那么好懂,其实它想表达的意思是如果一个类在析构时存在出现异常的可能,一定要立即,最好提前对这个可能出现异常的操作做异常处理(try catch)
  我来好好解释一下上面这句话,首先要知道1.C++的异常处理机制是怎样的层层判断。举个例子,主函数调用A方法调用B方法,如果B方法里抛出了异常,且B方法没有对这个异常的异常处理操作,那就将异常转至A方法,A方法要是也没办法处理这个异常就转至主函数,主函数要是还不行,那程序就终止了。
  知道了异常的处理机制那这个条款就好懂多了,它是想说让异常在析构函数里就得到处理,而不是在声明实例的方法里,说实话我觉得虽然长了点,但这么说才好懂,更适合当条款。
  之所以觉得应该这么说,是因为我之前甚至把这个条款理解为了让异常在类内得到处理,而不是在类外。这差别就很大,在类外处理一些成员函数的异常并没有不妥。
  那2.为什么必须得在析构函数内处理异常?为了避免内存泄漏。当实例析构时,如果你没有在析构函数内对异常做处理,被外面的异常处理处理了,程序是OK能继续运行了,可析构没析构成功啊,内存就泄漏了。
  3.我看你说最好提前进行异常处理,为什么要提前,怎么个提前法? 为了尽量减少异常发生的可能性,增加客户手动调用方法的方式。出现异常总归不是好事,所以提前判断出析构函数内的哪些操作可能会出现异常,将这些操作写成成员函数供客户可提前调用,双重保险是为上策。

条款9 绝不在构造和析构过程中调用virtual函数

  这个条款就如同字面意思一般非常好懂了,但是要明白前因后果,1.为什么绝不能调用呢?因为虚函数起不了任何作用,不会是你想要的结果
  2.详细点解释一下,首先想想我们为什么用虚函数?为了运行时多态。基类很可能有很多的子类,我们的本意是想要在构造时根据不同的子类,实现不同的方法。虚函数确实能实现这个功能,但是在构造函数和析构函数中用的话就不能了。
  为什么呢?我们知道,一个子类在生成实例时它会先调基类的构造函数,而在调用基类的构造函数时C++会将目前的构造实例当作基类类型! 这是非常合理的,因为子类还完全没有生成,如果这时就当成子类实例类型,基类构造函数中所用的虚方法极有可能会使用子类里面的成员变量或者成员函数,,可子类都还完全没构造呢,这不麻烦就大了么。析构函数也是同理,析构时先析构子类,子类都析构完了,基类中析构中的虚方法还想有什么用呢。
  所以如果你在构造函数中设了纯虚函数,那在连接期编译器就会报错了,因为它找不到子类的实现方法。而如果你设了虚函数,编译器不会报错了,它会直接按照你在基类的实现方法实现,这绝对不是你想要的。
  3.可我就想要在构造函数中实现运行时多态该怎么办? 具体情况具体分析了,总会有办法的。比如说使用传递参数的方法来模拟。书中的例子是在基类构造函数中想实现日志打印功能,不同的子类输出不同的日志,这是很正常的需求。我的想法是这种需求可以写成一个虚方法,然后客户手工调用。但手工确实稍麻烦,书中的操作是在子类中将你想输出的日志变成字符串,使用初始化列表的方式传递给基类,这样在基类中构造函数中就直接使用这个字符串就好了。我突然想到,何不在子类构造函数中直接输出日志呢,何必搞得那么麻烦。
  需要注意的一点是,这个条款仅针对C++,C#是可以在构造函数内调用虚函数的。 话说为啥可以啊,我猜是因为构造顺序上更加智能了吧。

条款13 以对象管理资源

  这个条款又不是很好懂了,直白的解释的话,具体做法是别用delete,请使用智能指针,或者类似的东西,它想表达的意思是:能让编译器来做的事就别让程序员想着去做。下面我来具体解释一下。
  1.我们正常实例化一个类时,比如A a这个a所占用的内存是会分配到栈上的,这和声明个int、bool等等变量是一样的,用完了不用管,编辑器会帮你释放掉它所占用的内存。
  2.new 就不一样了,new代表着程序员自主申请内存,new出来的东西是存到堆上的(至于栈与堆等等内存空间我之前也写过相关博客)而自己申请的内存就得自己回收,也就是delete掉。
  任何让程序员想着去做的事情都会带来很大的不确定性,万一忘了delete,那就会内存泄漏。这也是原先C++被人诟病的一点,但其实现在根本不会出现这种情况,因为有智能指针的帮助。
  3.智能指针其实是个类,这个类将指针封装起来,这个类的析构函数会将指针delete掉。我们将new出来的指针,直接赋给智能指针的实例,这样一来因为智能指针实例是实例,会分配到栈内存上。当智能指针实例用完以后,编译器会自动把它清除掉,也就是调用析构函数。这时指针也就被自动delete掉了。
  综上所述,我觉得把条款13变为“以实例管理资源”更合适。
  再说几个细节问题:
  1)RAII是什么?
  Resource Aquisition Is Initialization。这其实也算是学C++人尽皆知的一个”条款“了。为什么突然要说RAII,因为这个条款所想表达的就是RAII的精髓。资源获取即初始化其实真就字面意思来看不就是获取到一个值就赶紧给它赋值上么,说不好听点我定义一个int a = 1;都是RAII的字面意思。而RAII真正想表达的其实是这个条款了。
  2)你最开始说的类似的东西是指的什么?
  要知道有很多事是程序员要想着去做的,我说的类似的东西是指根据具体情况,能让编译器去干就让它干,别希冀着程序员一定能想起来做。自己写个类,类似智能指针类一样在析构函数中让编译器搞定它。打个我遇到过的具体比方:C++多线程情况下,生成一个线程后,这个线程最后如果没有调用join()方法,程序就会卡在那里一直等着join。这是不是和指针有着异曲同工之妙,虽然不是泄漏内存,但是更严重,会让程序不动弹。解决方法就是自己写个类,在类的析构函数里判断当前join没有,没有的话就join一下。
  3)用实例管理资源的第二大好处
  一直忘了说,不要忘记程序在运行时有出现异常的可能。 一旦出现异常,如果你没有用实例管理资源,比如说指针,那这个指针还没delete掉呢,不就内存泄漏了么。而用了智能指针,之后就算出现了异常,OK,这个函数退出来了,实例正常析构,没有内存泄漏,美滋滋。

条款14 在资源管理类中小心copying行为

  还记得第一次读这本书的时候,这个条款完全让我懵逼了。主要是他举的例子我完全没看懂,那时还不熟悉智能指针,更别提mutex了,是啥都不清楚,看天书般过的这个条款。
  其实它想表达的意思很简单,在上一个条款中咱不是尽量用实例管理资源嘛,它想让咱在管理实例的时候要小心默认复制构造函数,因为很有可能那不是咱想要的。
  1.条款5里已经说了编译器会帮咱建类时自动建的函数里就有复制构造函数。这个条款里举了一个咱不想要它的例子:使用mutex的情况下,你不会想要这个mutex实例可以复制给其他实例,因为每个mutex都是独一无二的。
  2.mutex是啥?多线程情况下,会存在多个线程同时访问一个实例的情况,它们会对这个实例进行一些操作,如果是读操作那还好,没问题,但如果是写操作,实例必然会出现未定义的情况,可能这个实例的前半部分是这个线程写的,后半部分变成那个线程写的,这不是我们想要的。我们需要一个东西来控制各个线程的运行情况,使同一时刻只允许一个线程访问这个实例,其他线程需要等待这个线程完事了它再写。这个东西就是mutex(互斥元),mutex主要有两个方法lockunlocklock代表上锁,接下来的操作将同一时刻只有一个线程在做,如果其他线程也运行到了这里,发现互斥元已经上锁了,那它就只能阻塞在那里等着拥有互斥元的线程解锁。unlock解锁,代表上锁的线程完事了,阻塞在那的线程可以继续运行了,它该上锁了。
  3.如果你想说:“我为啥不能复制构造啊?我复制构造之后代表这个实例里面的互斥元跟被复制的实例里的互斥元是一样的互斥元,这不行吗?”
  首先,你想表达的意思其实是浅复制了。就是说你想声明的不是一个新实例了,而是原先实例的一个引用罢了。但复制构造函数生成的可是一个确确实实的实例这点要搞清楚。浅复制到确实可以实现,但这需要你自己定制复制构造了,而不是用默认的复制构造函数。
  4.你要是说:"我就是想构造一个新实例,这个新实例跟之前的那个实例一样,这不行吗?“
  你要构造的实例里面是什么?是互斥元啊!你可以把互斥元形象的比喻成锁,如果你想生成同样的锁,那你回归了上一个问题,如果你想生成不同的锁,那你为什么要复制构造?你应该直接构造一个新实例。
  正是因为每个互斥元都是独一无二的的特殊性质,所以C++直接把互斥元的=操作符给删掉了,避免咱用的时候出错。但是编译器并不知道哪个类用了互斥元哪个类没有用,所以复制构造函数它不会帮咱删了,所以咱得自己删,删除的方式就是在复制构造函数后面加上”= delete“就好了,不用费劲的放到私有域里。不删的话一不小心用了复制构造函数,程序就GG,不要想着让程序员想着不用,不要太轻信自己(笑)。
  最后再说个细节方面的注意事项:怎么自己定制复制构造函数?比如说刚才说的浅复制,怎么实现?
  如果是mutex这种=操作符都删掉的类型,怎么定制复制构造函数都没用,因为都赋不了值。可能大多数情况下自己定制都比较费劲,因为默认的已经做的可以了。那怎么办?可以使用浅复制的方法,只需使用智能指针,这样生成的mutex直接放到智能指针里靠引用技术的办法保管,这样生成的实例其实也就是个指针,当然可以使用默认复制构造了。
  还有一个更细节的点,智能指针析构时可是直接销毁里面的对象,而咱们的mutex可不会愿意被它销毁。智能指针的构造函数的第二个参数可以指定销毁的方式,默认是销毁,咱可以把它替换成unlock

条款17 以独立语句将newed对象置入智能指针

  条款13到17其实都是在说这个智能指针相关的东西。15写的是访问资源时用->,16写的是 new 和 delete时要成对匹配,这都没太多好说的,大家都懂。
  这个17其实也很简单,但需要解释一下原因。它的主要意思是创建智能指针时就好好的正常定义它,别用传参的形式。或者直接说一下深层次的理由,编译器对于函数参数的调用顺序并不确定
  (在这里更新一下,我看了看make_shared的源码,没太看懂,但我感觉现在好像是不会出现这种问题了,我用VS测试了下也是如此,但可能测的也不是特别准,算了。大家记住别这么用就行了,用了可能也没事,但我也不确定,算了不细纠了。)
  –(接下来的可以不用看了)–
  这是好好的正常定义shared_ptr<A> Asptr = make_shared<A>();,接着使用Asptr就OK了。
  有的情况下,你需要在一个方法里使用智能指针,你就设了这个方法,然后你就在方法的参数里放个智能指针,像是这样void BadFunction(shared_ptr<A> Asptr)
   (可以跳过这段注释部分,我想错了) /*这倒没什么,关键是你还想使用一下A类里的一个方法,然后你选择了将这个方法也当作参数放入了BadFunction中。
  其实这种情况还蛮少见的,我反正是不太理解。你要想调用一个方法,在BadFunction中调用不好嘛为啥要特意传参?后来想了想,有可能是因为存在很多个方法,比如说有AA方法和BB方法,AA方法想调用A类的a方法,BB方法想调用A类b方法,关键是除了想调用的方法不同以外其他想做的操作还是一样的,那OK了,就写了这个void BadFunction(shared_ptr<A> Asptr, Asptr
  我写不下去了,因为这是不对的,要知道你是不可能再没有声明一个实例的时候传入一个实例内的非静态方法啊。*/
  
  这倒没什么,你可能还有其他的参数,比方说:voidBadFunction(shared_ptr<A> Asptr,int p)。这也很正常,然后你这样调用了BadFunction,BadFunction(make_shared<A>(),DoSth()),这就大错特错了,你可能想这样图省事啊,我何必特意先定义一个智能指针,或者把p定义好,在参数中定义不就完了。可要知道函数参数的初始化顺序是不确定的,重要的事情说两边,这个条款也只是因为这个性质,这个性质是关键。
  不同于C#的确定次序,C++的参数初始化顺序不确定,也是情有可原的,因为一般情况下参数早初始化晚初始化都一样,还不都是全初始化完了再到方法里用。但是make_shared<A>()包含了两个步骤,先new出个指针,再将指针封装。
如果这两个步骤中间插入了doSth,且doSth出现了异常,那就自然会造成内存泄漏了

条款18 让接口容易被使用,不易被误用

  这个条款体现在诸多方面,条款13就是其中的一个。核心意思就是咱在写类的时候尽可能把其他人(或者自己也不是不会失误)在调用类时可能会出现的人为错误避免掉。
  打几个比方:
  1.重载operator = 返回类型前加个const来避免if(a +b= c)
  2.比如说使用智能指针避免忘记delete啊以及避免DLL问题啊。啥是DLL问题?在这个动态链接库中new的,在另一个DLL中delete,这在运行期间就会发生错误了。
  3.STL类的所有判断类型所含数量的方法都是size(),对比其他语言又是属性又是方法的,你就会觉得比较好记。

条款20 宁以pass by reference-to-const替换pass by value

  说实话这个翻译太书面了,直白说应该是当你想用传值的方式时别用传值,用成传引用
  为什么要传引用,因为消耗小啊,传值代表新构造一个,你这要是构造个庞大的类的实例得消耗多少时间空间。传引用相当于传个原实例的指针,就一个指针的消耗。
  需要注意的一点是:如果你不希望你的原实例被修改的话(你都想用传值了当然不会想要修改原实例),别忘了用const修饰你的引用类型。
  还要注意一点:1)一些内置类型,比如说int之类的,人家本来就够小了,你就别传引用了,反而不划算。2)还有STL里的容器,因为人家已经实现了迭代器了,你就别整引用了,用迭代器更好。3)还有函数也是,函数的引用是相当于耗费一个指针,函数构造本身也是一个指针。你说你还需要搞引用么。

条款21 必须返回对象时,别妄想返回其reference

  这个条款书里竟然用了4页纸来举例子解释,说实话我觉得解释的墨迹了。首先这个条款是承接上一个条款的,它想表达你有的时候是必须传值的,别传引用
  什么时候?比如说重载*操作符时,我们知道星号是个二元操作符,二元指的就是它需要两个值才能操作,比如说a+b,需要a和b,这不禁突然让我愣住一下子= 算是一元还是二元,我觉得它就理论上来说也是二元操作符,因为a=b你不能去掉前面的a,不像-a,a- -这种一元操作符。但是我之所以提几元操作符想表达的意思是重载操作符时需要几个参数。重载星号时明显是需要两个,而重载等号时只需要一个,所以按我的这个标准算的话等号算一元操作符也不过分。
  跑题了,重载需要两个参数,返回值是乘积值,这个乘积值再怎么的也得是以传值的方式返回啊,这都不用解释,a = b * c,首先你就不合逻辑,你要是想传址你的a是个引用?行,那你告诉我你的址在哪?你都没有址好不好。这种情况你应该要新构建一个东西啊。
  你说“我在重载函数里构建个值,计算完了,然后返回它的地址(传址),这样不是减少消耗嘛”,这句话是书里的一个问题,他还特意解释了解释,说实话这个想法本就漏洞百出,首先就是作用域问题,大家准定都能想到,你在函数里构建的值,出了函数不就被编译器销毁了么。你传的址立马就失去位置了啊。你别告诉我你还想用b或者c当址传,那你得看看b和c是值还是址,如果是值,那它首先没遵守第20条条款,其次它出了函数也会被销毁,如果是址那不就修改了原始参数了么,这不是咱想要的结果。这个问题就不多解释了,太简单了。
  总之,遇到该构造的情况,就要构造。传值就传值,有啥的。

条款22 将成员变量声明为private

  声明为private是为了保证封装性,封装的表现形式是隐藏内部实现,最终目的是实现重用,便于后期维护。
  实现重用的作用并没有体现在条款22中,因为咱是声明成员变量,成员变量就是个变量罢了。重用是指有一大段代码,在好几处都需要用到,那就新定义个方法,把这一段代码放进去,这样在这好几处直接调用这个方法就好了,没必要写好多遍相同的代码。这是方便重用。
  也就是说条款22的最终作用是为了方便后期维护,public变量意味着谁都可以访问,如果我们在函数外直接调用了这个变量,并且在很多地方都调用了这个变量,万一有一天突然不打算用这个变量了怎么办?那可能还好,比如说数字类型的,设置为0就OK了。但如果是想修改这个变量呢,那种新增判定条件的修改。比如说这个数字变量我突然需要判定一下它是不是0,是零的话让它等于100,不是0那就让它等于现在的值,这种类似新增判定条件的修改在后期维护时是非常非常有可能出现的。那可就麻烦了,得在每一处用到这个变量的地方增加修改。将这个变量声明为private的好处就体现出来了,将它声明为private意为着得在public里写个get到它的方法,在许多要使用它的地方直接使用这个方法,那后期如果还是要新增判定条件的修改,我可以直接在这个方法里判定一下,就OK啦。
   你说protected类型的变量用改成private吗?首先你既然要声明protected类型的,那说明你的这个变量只打算在子类中调用了。这OK,那你就该想想你会不会有很多的子类,你在子类中会不会调用很多次这个变量了。在哪里调用并不重要,重要的是你每多调用一次这个变量,后期维护时要修改的话,你就会多一个地方需要修改。所以一定要视情况做出正确的选择,尽量用private,免得维护时遭罪。

条款25 考虑写出一个不抛出异常的swap函数

  整个条款还蛮长的,花了整整5页来叙述该怎么写,再用1页里的几行解释为什么要不抛出异常。所以我觉的这个条款更应该把修饰词去掉。
  如何写swap,swap是已经有默认实现的函数了,大致就是temp = a; a= b; b = temp;如咱们所想。
  1.那为什么还要自己实现swap?
  有些类型用默认的swap太过浪费,需要自己定义。比如说书里举了个pimpl的例子
  2.什么是pimpl?
  pointer to implementation,A类的实例有些大,所以我在B类里不再创建A类的实例,我创建A类的指针,看似是占用同样的空间,但交换的时候就不一样了,只需swap指针就好了。
  默认的swap看到的可不会是指针,而指针指向的A类实例,这样swap消耗就会很大,所以需要自己实现swap
  3.实现的方式就是在类内建个swap成员函数,在类外建个swap非成员函数,在非成员函数中调用成员函数。为什么要定义一个成员函数还要定义一个非成员函数?
  成员函数的原因是因为你创建的指针是属于private的,不在成员函数中调用访问不到。非成员函数的原因是咱们一般swap都是swap(a,b);,因为这样更合逻辑,而不是a.swap(b),对吧。
  还需要注意具体实现细节,比如说如果你实现的类带有模板,需要注意全特化与偏特化的使用。偏特化只能用于类,不能用于函数
  最后解释一下条款里的修饰词“不抛出异常”,默认的swap是可能抛出异常的,因为复制构造与赋值的过程中都可能抛出异常,如果我们自定义一个不抛出异常的swap,那异常安全性会非常有保障,详见条款29.

条款29 为异常安全而努力是值得的

  条款26-31属于第五章“实现”,实现里面讲了讲具体写代码时要注意的细节:
  1.条款26告诉我们:在要使用变量前再定义它,尽量延后定义时间是为了易读,也怕异常出现时,早定义也白定义。
  2.条款27告诉我们:要尽量少用转型,因为只要使用转型,代表了你代码写的不够完美,而且增加了不少消耗与不确定性
。顺便提下C++的转型有1)static_cast,最普遍的转型,除了解除不了常量性以外都可以用它。2)const_cast,解除常量性专用,可将常量变量转为非常量的。3)dynamic_cast,向下转换专用,可将基类指针转换为子类的,消耗巨大,不建议使用。4)reinterpret_cast,底层方面转换使用,很少用。
  3.条款28有些绕口,告诉我们:在定义类时,尽量少使用其他类的指针、或者引用、或者迭代器。你要知道你可以通过指针修改那个类里的私有变量,而且如果那个类的实例提前销毁了的话,你的指针就是空指针了
  然后就到了第29条,我来好好解读一下这个条款,首先1.为什么值得?
  写一个完全不具备异常安全性的代码很轻松,但一旦运行时出现了异常,后果会很难受,或是运行不下去程序直接崩,或是资源泄漏越运行越卡。道理很简单,这都不是我们想要的。
  2.如何努力?
  首先我们要知道保证异常安全分三个等级:
  1)基本保证:保证程序能继续运行下去,资源不会泄漏。
  2)强烈保证:保证资源在出现异常时像数据库里的事物一样,要么做成,要么回滚至最初状态。
  3)至高保证:好吧这是我自己起的,书里是叫“no throw保证”,就是说程序就根本不会抛出异常。
  那我们的努力方向就很明确了,努力做到其中一个保证就好了。
  至高保证是最难实现的,大多数情况下因不可抗力根本实现不了,比如说有时就必须得new啊,new就有可能new失败,抛异常。很多方法都是会抛异常的,这是无法避免的。
  所以就尽量努力提供强烈保证,然而事实上强烈保证也超难实现。因为木桶原理:在一段代码里,如果有一小部分没有保证异常安全,就算其他地方全是至高保证都没有用,这段代码就是没有异常保证的弟弟代码。
  所以大多数情况下咱能提供基本保证就好了。
  3.如何提供基本保证?
  try catch能保证出异常了咱能继续运行下去,shared_ptr和一些RAII(详见条款13)类能保证出异常了资源不会泄漏。
  4.我想尝试尝试强烈保证,该怎么搞?
  上个条款里的swap就有用咯,创建个副本,在副本里面搞,搞成了,那OK,把副本与原版交换一下,搞不成也没关系,原版还是原版。这就实现了回滚嘛。所以上个条款也要实现个异常安全的swap,如果swap不安全,那哪能行。
  5.听你一说感觉强烈保证也不难嘛
  你对木桶原理的理解还不深刻。搞成了,交换了。接下来的代码若是报了异常,却只有基本保证,该怎么办?回滚不了了吧,你的原版已经改变了。一个方法要强烈保证就得全强烈保证,否则就没用,还浪费了副本的构造消耗。

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

  从条款32开始,我们进入了第六章,开始讲解OOP(面向对象)的注意事项。
  条款32是说的public继承是is-a关系,但也要注意咱们在编写时容易受常识误导,要具体问题具体分析。
  总结就是说基类的所有东西,子类都要能用,但子类的东西,基类不一定能用。这不能说子类完全包含基类, 因为子类是可以通过虚函数重载基类的方法的。只能说子类是一个特殊的基类,它完全拥有基类的一切,并且可以修改,扩充。说白了这就是继承嘛。
  这么一说好像我解释的只是继承,确实。
  1.大家可能还没搞懂public继承与private继承有啥区别。我先提前简单解释一下区别:
  1)public继承将基类的所有东西都继承在了public域里,也就是说是可以在外部通过子类的实例调用基类的方法或变量的。而private继承将基类的所有东西继承到了子类的private域里。外部没法调用,只能是在子类中的方法中可以调用。
  2)这就是根本区别了。A类说“我觉的B类有的方法很不错,我自己的方法可以用用”。就private继承成了B类的子类,把B类的东西通通收入囊中,想用哪个用哪个。其实是A类把B类当作了自己的工具类。所以private继承是”has-a“关系
  这么一解释private继承,和public继承的根本使用区别就凸显出来了,private继承没必要完全使用基类,public继承有必要完全使用基类
  2.完全使用是怎么个完全法?
  基类的任何方法,任何变量,子类都要考虑到该怎么处理。因为子类的实例是可以访问到的。如果是有用的方法,那最好。不合适的方法就修改。根本用不到的方法或变量,就该想想要不要在基类中把它去掉,或者最起码放到私有域里。总之,完全使用是指基类的所有东西都要进行考虑,考虑外部调用子类实例的基类东西的话OK不。
  3.有些人可能就犟了,我用public继承就不完全使用基类就不行了?
  行是行。你再好好看看上面那段话。如果外部瞎调用咋办,直接GG。

条款33 避免遮掩继承而来的名称

  本身是想直接说33的,没想到32墨迹了半天。
  33这条确实是一个非常容易忽视的点,一定要注意子类重写基类后基类本身的重写会失效
  用代码来解释更方便,看图
在这里插入图片描述
  原因很简单,调用的作用域在D里,D里只有一个Hello,这个Hello不带参数,就报错了。因为在最里层的作用域(子类)里已经有Hello了,所以不会去外面的作用域,也就是基类里再找,等于说是覆盖了基类里的重写。
  解决办法就是加上using,在上图104行前加句using D::Hello;就好了。
  在public继承中基类的两个重载准定是要都实现的(还要问为啥的看我上一个条款心得)但在private继承有时你不需要都重载。如果只想要无参数的那一版该咋办。很简单,如下图,就不解释了。
在这里插入图片描述

条款34 区分接口继承与实现继承

  这个条款就字面意思来说不好懂,但我一解释你就明白是咋回事了。
  首先,是在public继承的情况下。public继承里的方法,可以分三种情况进行修饰:纯虚函数(方法),虚函数(方法),以及普通函数(方法)
  这个条款的核心意思就是想讲解这三个函数该咋用
  1.纯虚函数:概念上的意思讲解太为枯燥,大家如果不了解定义建议百度,我直接说一说性质。一个类如果声明了纯虚函数,那它一定是个基类,需要被继承。 因为这个类是无法实例化的。你可以把它理解为因为纯虚函数没有定义(一般不会定义)(我以前是这么理解的),所以它所在的这个类没办法实例化。但是其实纯虚函数可以被定义,无论如何,这个类也没有办法实例化。根本原因在于声明纯虚函数的作用就是为了让子类继承这个纯虚的函数,子类重写自己的功能
  2.虚函数:声明虚函数代表它也是个基类,需要被继承。virtual的作用就是为了多态,基类指针指向子类实例,调用方法实现多态,必然需要继承。其实纯虚函数就是虚函数的一个特例,它让虚函数更加虚了,以至于是只能使用子类的方法。虚函数一般本身是有自己的定义的。
  3.普通函数:也就是非虚函数。类里全是普通函数的话很大可能是个基类。也不是完全不可能,反正切记一点:不要在子类中重写普通函数(详见条款36),因为不会有多态,很容易被误解。
  感觉自己好像解释的很不好,语言表达能力不是很强,算了,懒得再详尽的解释了。
  到底这个条款与这三个函数有什么关系?
  1)纯虚函数是接口继承:声明纯虚函数意味着声明一个接口,子类必须继承这个接口并实现它。
  2)虚函数是接口继承+实现继承:声明了一个接口,并且有一个默认实现(基类可定义虚函数)。子类可以继承接口,也可以不继承(继承了默认实现)。
  3)普通函数是实现继承: 子类完全继承了这个函数,并且不该对其进行修改,也就是继承了实现。
  书上还举了个需要用到定义纯虚函数实现的例子,很有意思,也很典型:飞机基类Class Plane里有一个飞行方式函数fly,本来A飞机与B飞机的飞行方式是一样的,所以声明一个虚方法virtual fly(),定义一个默认的飞行实现就OK了。
  但这样做并不好,因为有可能以后有了C飞机,C飞机的飞行方法不是默认飞行方式,但我忘了重写virtual fly()了,这就很糟糕了。
  最好的解决方式就是声明纯虚方法virtual fly() = 0; 这样会提醒我们必须实现这个方法,然后定义这个纯虚方法,在A、B类里的fly里直接调用Plane::fly(),此为上策

条款36 绝不重新定义继承而来的non-virtual函数

  其实条款35可以讲解下,但是由于我实践不多,对此还没有深刻的领悟,而且目前我也感觉不到实际编码时35的作用会有多大。所以建议大家自己好好看看,自行领悟。
  条款36非常好解释,如果你知道virtual的作用你就应该能够理解这个条款。
  没有virtual只是静态绑定,指针是A类型的调用的就是A类型的方法,与指针指向的实例的类型毫无关系。动态绑定相反。这就是多态。
  运行时多态是非常实用的一个特性,你一定不会想要放弃它的。退一万步你说:“我就是不打算用多态,我每次都会为了调用某个类型的方法特意生命对应类型的指针,这不行?”
  行是行,但是条款这东西是大家的准则,有一些东西定了标准后会非常方便大家的判断,而且确实实用。如果其他人要使用你的代码,而你做了那种事,他们是会会错意的,很有可能用你的代码做了错的事。就算是你自己,在你写了这种代码后你也得非常小心,必须特意记的有的地方的代码是不能用多态的。这样真的会很难受吧。
  顺便再把条款37说了吧,它们大同小异。条款37告诉我们:绝不重新定义继承来的缺省参数值
  这个看似很不合理的要求有其内在的原因:缺省参数值是静态绑定的,而咱们的virtual是动态绑定的实现。也就是说在进行多态调用时会出现调用子类的实现时使用的默认参数一定是基类的默认参数的情况,所以切忌重新定义缺省参数了,定义了也调用的是基类的,反而容易犯错。
  这里要多说一点,如果我们一定需要改变默认参数该咋办呢?
  可以使用NVI(non-virtual implementation)的实现方式,这个方式在条款35中有讲到,我当时没看出它有啥优势,在这种情况下就确实是个办法了。NVI是指将原有的virtual函数替换成non-virtual函数,在non-virtual中使用virtual,把这个virtual放到private里。说白了就是给virtual来层封装,我们在non-virtual里定义默认参数当然OK了,这相当于全是静态绑定了。就算使用多态也没关系,因为调用的non-virtual的里面有virtual。

条款39 明智而审慎的使用private继承

  此条款想表明使用private继承很有可能不如使用组合
  private继承是“has-a”关系,这在前面也有说过,就是说基类是子类的工具类。因为基类的一切都变成了子类的私有,外部没有办法通过子类访问基类的东西,这说明基类的东西都是子类里的实现需要用到的实现细节,就是工具。
  那么组合又是什么?
  组合在条款38中有解释。组合就是类中使用另一个类的实例,这样最大程度的解耦了,这个类是这个类,那个类只是用了这个类的东西。所以说同样是把这个类当工具类。跟private继承的效果一样。
  为什么组合要比private继承好呢?
  主要是因为关联性要少,耦合度低。试想private继承,A类想继承B类把B类当工具。若是B类本身有虚函数该怎么办?那就很有可能会犯错,因为我们只是想使用B类,而不是重新定义B类。当然你若是不重新定义它准定没事,但是是有犯错的可能的。也就是违反了条款18。但如果用组合的方式,因为已经构造了实例,用的是实例,所以不会出现这种情况,就算是本就想重新定义B类内的虚函数,你可能不想让以后继承A类的子类的也重新定义它。private继承无法做到这点因为是继承,继承会继承基类的虚函数并且可以重新定义。组合就没关系了,因为它在A类中已经是实例,继承的是实例,不能也不用管它背后的实现。

条款40 明智而审慎的使用多重继承

  不得不承认C#、Java在这点上处理的要比C++好,但是它们速度也慢,有利就有弊。
   C++是可以多重继承的,就是指一个类可以同时继承多个类。C#只能继承单个类,但是可以实现多个接口。这种方式是最佳的,我们也可以用C++来模拟成这样。
  1.为什么要审慎的用多重继承?因为有可能出现菱形继承的情况。就是说一个子类可能继承了类A、B,可A、B又都继承了C。你画一下这个继承关系会发现画出了个菱形。
  这种情况下这个子类实例化时就会多构造出一份同样的资源,没错那一份资源是来自C的,因为你构造时会调用两个类的构造函数,那两个类的构造函数又都会调用C的构造函数。这还只是最简单的情况。如果多重继承再复杂些,将会是很可怕的境地。
  2.如何避免菱形继承?
  很多时候你在使用多重继承时是根本避免不了菱形继承的。或是因为你自己都没注意到你继承的类有同样的父类,或是你注意到了但是你还是必须得用。幸好我们可以使用virtual继承,就是在public或者private继承前面加个virtual修饰。virtual继承可以保证同样的资源只构造一份,不好的是消耗也很大。 不论是空间上(虚表,指针数组,内部实现)还是时间上(调用指针,调用方法)都告诉我们尽量不要用virtual继承。
  3.最好的解决办法?
  想要多重继承时尽量保证继承的类里不要有数据,说白了这个类就是个接口。接口的话就算出现菱形继承,因为里面本就没有数据,也不会多构造一份出来了。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值