Effective C中文版第三版 高清PDF总结[共97页]

上传人:1528****253 文档编号:58036058 上传时间:2022-02-25 格式:DOC 页数:97 大小:1,015.66KB
返回 下载 相关 举报
Effective C中文版第三版 高清PDF总结[共97页]_第1页
第1页 / 共97页
Effective C中文版第三版 高清PDF总结[共97页]_第2页
第2页 / 共97页
Effective C中文版第三版 高清PDF总结[共97页]_第3页
第3页 / 共97页
点击查看更多>>
资源描述
Effective C+阅读笔记Effective C+阅读笔记1原则3:尽可能使用const4原则5:了解C+默默编写并调用哪些函数6原则6:若不想使用编译器自动生成的函数,就应该明确拒绝8原则7:为多态基类声明virtual析构函数11原则8:别让异常逃离析构函数15原则9:绝不在构造和析构过程中调用virtual函数16原则10:令operator=返回一个reference to *this19原则11:在operator=中处理“自我赋值”19原则12:复制对象时勿忘其每一个成分21原则13:以对象管理资源22原则14:在资源管理类中小心COPYING行为24原则15:在资源管理类中提供对原始资源的访问25原则16:成对使用new和delete时要采用相同形式27原则17:以独立语句将newed对象置入智能指针28原则18:让接口容易被正确使用,不易被误用29原则19:设计class犹如设计type30原则20:宁以引用传递代替值传递31原则21:必须返回对象时,别妄想返回其引用32原则22:将成员变量声明为private33原则23:宁以非member、非friend替换member函数34原则24:若所有参数皆需要类型转换,请为此采用非member函数35原则25:考虑写出一个不抛出异常的swap函数37原则26:尽可能延后变量定义式的出现时间39原则27:尽量少做类型转换动作40原则28:避免返回handles指向对象的内部成分43原则29:为“异常安全”而努力是值得的45原则30:透彻了解inline(内联)的里里外外48原则31:将文件间的变异依存关系降至最低50原则32:确定你的public继承塑造出了IS-A关系53条款33:避免屏蔽继承而来的名字54原则34:区分接口继承和实现继承55原则35:考虑virtual函数以外的其他选择57原则36:决不能重新定义继承而来的非virtual函数60原则37:绝不重新定义继承而来的缺省参数值62原则38:通过复合塑造出HAS-A关系或者根据某物实现出来63原则39:明知而审慎地使用PRIVATE继承64原则40:明智而审慎地使用多重继承69原则41:了解隐式接口和编译期多态71原则42:了解typename的双重意义72原则43:学习处理模版化基类内的名称74原则44:将与参数无关的代码抽离templates76原则45:运用成员函数模版接受所有兼容类型78原则46:需要类型转换时请为模版定义非成员函数80原则47:请使用traits classes表现类型信息82原则48:认识template元编程85原则49:了解new-handler的行为87原则50:了解new和delete的合理替换时机90条款51:编写new和delete时需固守常规92原则52:写了placement new也要写placement delete94原则54:不要忽视编译器的警告95原则54:让自己熟悉包括TR1在内的标准程序库96原则55:让自己熟悉Boost97本来是写在百度空间的,但是不知道咋回事百度博客中图片看不到了,所以百度博客的不稳定性可见一斑。于是我决定将我的领会和感受写在自己的云盘里面。虽好也弄个目录啥的,最后再整车成PDF格式的。这个标准我就参考我研究生期间论文的格式吧。原则3:尽可能使用constEffective C+里面第3条原则是尽量使用const。其原因是防止无意中更改而本来不应该更改的变量。本条款也提到const成员函数的重要性,原因之一就是只有const函数才能用来操纵const对象。而所谓const对象就像下图所示的这样:有的时候会遇到在const函数中更改非const成员变量的情况,这个时候就要用到mutable关键字了。如果一个成员变量被mutable修饰,那么它在const函数中仍然可以被修改,但是前提是该成员变量是非const成员。还有一种情况就是为了防止代码重复,比如两个函数实现了同样的功能只是类型不同而已,这样就会导致两段几乎相同的代码段,这无疑会增加编译时间、维护和代码膨胀等风险。在本原则的有关叙述中,作者采用了强制类型转换来解决之,虽然作者本身在大多数情况下并不提倡做法。为了给用户一个一目了然的接口,一看就知道那些成员函数可以操纵const对象而哪些不能,作者建议在类中明确将那些不改变对象的成员函数声明为const函数,虽然const成员函数可以使用非const成员变量,但是遵守这一原则会给客户带来极大的便利。因为const成员函数不更改对象,这就防止了由于误操作而带来的问题,因为最好用非const成员函数去调用const的实现,说白了就是直接return这个const成员函数,只不过需要对作为这个return的表达式的const成员函数进行一下强制类型转换使其成为非const型的。所以,在这里不得不提一下纯粹的C+的强制类型转换。关键在static_cast(value)是纯粹的C+强制类型转换的关键词和用法,它的使用频率是最高的。const_cast(value)是用来消除const属性时用的。不过它不能用于基本类型。reinterpret_cast(value)它用于无关类型之间的转换。dynamic_cast(value)用于父子类指针之间的转换。在C+语言中只有这4中强制类型转换。原则5:了解C+默默编写并调用哪些函数这一篇博客是Effective C+中第5个条款。但现在感觉我还没太理解它到底说了什么,所以想写写博客,万一写着写着就明白了呢。首先在这里叙述一个机制,那就是空类,在默认的情况下,编译器会给它自动生成默认的构造函数、拷贝构造函数、拷贝赋值操作符=和析构函数。并且他们都是public的和inline的。它与下面这个类是一样的。至于这些成员函数和操作符是干啥用的,我在前边的博文中阐述过了。其中,默认的构造函数负责调用父类和非static的构造函数和析构函数。如上图可见编译器自动生成的析构函数是非virtual的,如果父类中本身存在virtual的析构函数,编译器就不会自动产生非virtual的析构函数了。而默认的copy构造函数和copy赋值操作符只是copy非static成员到目标对象。不过,如果你手动写了它们中的一些,编译器就只会自动生成你没写的。比如你只写了构造函数,那么其他的东西编译器负责给你自动生成。至于说copy构造函数和copy赋值操作符的用法我以前的博文有提到过。而copy构造函数总是层层调用底层的copy构造函数来进行赋值,比如说copy构造函数要copy一个string类型的变量,那么它就会调用string的copy构造函数,实在没办法了,它再自己进行赋值操作。其实本原则着重讨论的是在什么情况下编译器不会自动生成这些东东。对于默认的构造函数而言,当你手动写了一个构造函数的话,编译器就不会再费那个劲了。而对于copy赋值操作符呢也是有自动生成条件的,那就是这个copy赋值操作符确实有存在的意义,并且它能在使用场合能正确工作,否则除非你自己手动写一个,要不然编译器是不会给你生成这些东东的。而在书中作者举了2个例子1个是引用,另一个是const常量,这两者所指的对象都是不能更改的,那你非要给它们赋值,那肯定会导致copy赋值操作符的失败。书中还举个1个例子,一般情况下父类中如果有copy赋值操作符,在子类中编译器是不会再给自动生成copy赋值操作符,直接使用父类的就好了,因为编译器认为子类的copy赋值操作符是要能够处理父类的赋值操作的。所以如果你此时把父类的copy赋值操作符设置为private的,那么你就没有copy赋值操作符可用了,除非你自己在子类中写一个。原则6:若不想使用编译器自动生成的函数,就应该明确拒绝这是Effective C+中第6个原则,在某些情况下你不想让某些类的对象被拷贝,那么在这种情况下即使你不写copy构造函数和copy赋值操作符编译器也会为你生成,那么你不得不自己写它们俩。而你又不希望别人调用它们,所以这时你要将它们声明为private类型。一旦你写了,编译器就不会自动调用父类的copy构造函数和copy赋值操作符。即便这样本类内部成员函数和友元函数还是可以调用它们,该如何是好?办法就是你只声明这些函数而不去实现,没有实现就自然没有功能了,而既然实际上没用,你甚至连形参都可以省略,只在形参列表中写个形参类型即可,就像下图类的定义所示的这样:其中的几个函数实现如下所示:从上图可见,copy构造函数和copy赋值操作符都没有实现。在主程序中是如下调用的:运行结果如下所示:出现了错误提示,说copy构造函数无法解析。现在我把copy构造函数和copy赋值操作符都注释掉。在运行得如下结果:而本思想只在阐述如果你不想让编译器为你自动生成函数,你就要自己手写。原则7:为多态基类声明virtual析构函数这是Effective C+中第7条原则,其内容是在具有多态用途的父类中应该使用virtual析构函数。首先要知道啥是多态。我就好说直白的,显得没有深度的东西。多态的一种体现就是通过父类的指针指向不同的子类来实现不同的功能,从而达到接口重用的目的。在这种情况下用作多态的父类往往具有至少一个virtual成员函数留给子类来实现。好了,现在铺垫完毕了,来说正题,为啥要有一个virtual析构函数呢?那是因为如果没有这样一个virtual析构函数的话,子类的析构函数就不会被调用,那么对象的子类部分不会被析构,那么就会造成资源的泄露。现在来看下面的例子:derived继承了base,并且base中并没有virtual析构函数,那么调用过程如下所示:运行结果如下所示:从这个结果可以看到父类的析构函数执行了,说明对象的父类部分所占资源已经被释放,但是子类的析构函数并未调用这说明对象中子类部分所占资源并未得到释放。但是如果在父类中加上一个virtual析构函数的话就不一样了。同样的调用过程,运行结果如下所示:这说明对象的子类部分所占资源也被释放掉了。在这里再说点别的,一般来讲作为要被继承的父类的类中至少含有一个virtual的成员函数留给子类去实现。而如果某类中一个virtual成员函数都没有的话,在很大程度上说明了该类不会被作为父类而存在,在这种情况下不应该把其析构函数设为virtual的。为啥呢?这与C+中virtual本身的实现机制有关,因为这样的类的对象必须要携带一个表,这个表叫vtbl,所以本来没必要多带这么个表,但是你非要多出一个来占个空间,这就是占个茅坑不拉屎的表现啊。所以不准备被继承的类是没有必要设置virtual析构函数的。在这里在介绍一种情况,当你希望在virtual类中把析构函数设为virtual的时候,应该吧析构函数设为纯virtual函数,并且给与空的实现,如下图所示:为啥要这样做呢?因为析构函数调用顺序是从没有子类的子类那里的析构函数逐层调用父类的析构函数,所以如果这个纯virtual函数没有实现的话,编译器就会报错。这个原则简而言之就是,只有作为多态用途的父类才有必要使用virtual析构函数,其他的就是画蛇添足。另外,处于继承机制的类对象包含了它所能涉及到的最低层次及其以上的所有层次的成分。原则8:别让异常逃离析构函数这是Effective C+的第八条原则。主要说的是程序出现的异常不要从析构函数这里漏掉,也就是说析构函数应该承担起拦截异常的责任才行。如果异常越过了析构函数这一关,流窜到其他地方去,那么就会造成程序提早结束或者未知的风险,这个后果就很严重了。对付这种情况通常有两种简单粗暴的手段:1、在析构函数内发现异常,立刻捕捉到并且结束整个程序;2、在析构函数中发现异常,立刻捕捉到并将其扼杀,掩人耳目,继续执行程序。其中第一种手段比第二种手段要好,这是为啥呢?因为方法1直接结束程序,其结果是可预料的,不会造成太大破坏。而方法2你这个异常是终止了,但是程序中其他部分与这个功能相关的势必会造成影响,也许还会因此带来其他异常的连锁反应,这个就不好办了。不过以上这两种方法都没能去正面处理出现的异常,所以这两种方法都不提倡。书中给出的解决方案是,再创建一个类用来处理异常,在这个类中有一个成员函数专门用来处理原来的类中的异常。而这个成员函数是调用原类中的异常处理来完成的,这实际上就是变相的让原类自己处理异常,这是第一道关卡。然后异常处理类的析构函数中也有一份处理异常的代码,这部分是异常处理类自己的,这是第二道关卡。这个就是双保险,如果说在第二道关卡仍然不能有效处理异常,那没办法了,只能强行关闭程序了。再总结一下本原则就是无论如何也不能让异常突破析构函数这一关。原则9:绝不在构造和析构过程中调用virtual函数这是Effective C+中的第9条原则。简单的来说,如果你在父类的构造函数中调用了虚拟函数,那么子类的成员就会始终处于未初始化的状态,这样对象的子类成分就会出现不可预知的行为,这是非常危险的。那么这是为什么呢?在继承体系当中,你声明了一个子类对象,那么这个子类对象其实是很复杂的,它包含了子类所有父类的成分。而这些成分是要一层一层的进行初始化的,其顺序是按照类的继承层次从上而下进行的,即从最远的那个父类到最近的那个父类,然后是本类的初始化。而C+的机制又是只有在父类成分初始化完毕以后才去处理子类成分的初始化工作,换句话说,如果父类的成分没有初始化完毕,它压根就不会去管子类的初始化工作。因为你在父类的构造函数中调用了虚拟函数,而这个虚拟函数一般在父类中是不进行实现的。鄙人以为,一个函数之所以被调用肯定是因为这个函数是有一定的功能实现的,要不你调用它干吗?我想C+的构造函数也是这么想的。但是,你现在非要在本来用于初始化的构造函数中去调用一个没法初始化virtual函数,C+就认为这个对象的父类成分还没有初始化完毕,现在不能去初始化子类成分。所以,即使你现在声明了一个子类对象,子类中的成分还是没有得到初始化,所以就会出现不可预知的后果。C+对未定位的成员变量是采取无视的态度的,因为还没轮到你,你给我一边凉快去。在构造函数期间无视,在析构函数期间也是无视。而析构的顺序又是先子类再父类,因为对象的子类成分被无视,只有父类成分被析构,所以那些未定义的成员变量自始至终都是未定义的,你也不知道它们最终会怎样。为了证实这一点请看下面的例子:运行结果是这样的:看到这个结果,我不得不说现在的编译器已经很智能了,它直接把它拦下来了。在介绍与本原则有关的内容时,作者举了一个应用场景。那就是当类中有多个不同版本的构造函数,它们的共同的初始化代码都统一放到一个初始化成员函数里面了,并且这个初始化成员函数调用了一个virtual成员函数,并且这个virtual成员函数会有一定的实现代码。当你建立子类对象时却调用了错误的virtual成员函数。在此作者并没有详细地解释是为什么,但他给出了一种解决办法。那就是在子类的构造函数的初始化成员列表中调用父类的构造函数去初始化对象中父类的部分,当然了,这时父类中的那个virtual函数你要改成非virtual函数了。在这里作者使用了一个技巧,他不是在成员初始化列表直接把父类成分所需的东西直接给它,而是通过一个辅助函数返回一个值给父类进行初始化,这样写比较方便也比较可读。而且,这个辅助函数的是一个static类型。static类型的成员函数是静态成员函数,它的类的对象实例化之前就已经被实例化,换句话说它跟类中其他的成员不发生关系。另外,子类对象的父类成分是在子类成分之前先被实例化,而在子类对象实例化的中间也就是父类成分正在实例化,还没轮到子类成分实例化之前,子类中的成员函数啥的是未定义的,也就工作不了。而此时static成员函数却能工作,这有助于对象中父类成分的实例化,所以把此辅助函数设置为static类型。原则10:令operator=返回一个reference to *this原则11:在operator=中处理“自我赋值”在这篇博文里面我打算写两个Effective C+中的原则,因为第一个原则太短了。现在介绍第一个原则:条款10,此条款旨在说明在你自己编写的赋值操作符=一定要返回该左值的引用。具体来说就是返回*this。这很好解释,因为this是指向本对象的指针,那么*this就是该对象的本身实体了。而你现在返回的只不过是该实体的一个代表符号而已。现在一般的赋值操作符都采用这个原则,虽然不是强制的,但是大家都遵守。下面来对条款11做一些介绍。条款11的内容很简单,就是一定要妥善处理赋值操作符=的自我赋值问题,就是自己给自己赋值的情况。那么这又是为什么呢?因为在某些时候你要编写用来管理资源的类,那你知道一个资源不用了就要释放掉,以便留给下一个需要该资源的对象。不过,这是很合理的。但是,假设当前占用该资源的对恰好是用来赋值的右值,也就是它俩其实是一个东东。如果还是按照上面处理的话,就会出现被用来赋值的右值实际上已经啥实质内容都没有了,this指针指向了NULL,那就会发生错误。那么怎样处理这种情况呢?第一种方法很简单,那就是在赋值操作符的实现中最先判断一下赋值的对象和被赋值的对象是不是一个,即if(this=&obj)。不过这种方法不好,因为这需要额外写出一条语句,这无疑会增加运行时间。所以实际上采用的办法就是所谓的COPY and SWAP方法。什么意思呢?首先赋值一份右值,生成一个COPY,然后用*this与这个COPY进行SWAP,那么就可以完美解决自我赋值的问题。因为既然是COPY那么原来那个右值就没有改变,而this原本是空的,它和那个COPY交换以后,那个COPY就变成了NULL,而this的内容就成了COPY,也就是原来的那个右值的值了。还有一种方法就是直接利用赋值操作符重载函数的传参机制是传值这一特性,直接传进来一个COPY。该方法可行,但是作者不提倡,它说这样做的话清晰性变差了,我的这个清晰性大概就是可读性吧。不过,我倒觉得没啥。他又说这种方法有时候可能更高效。原则12:复制对象时勿忘其每一个成分这个原则是Effective C+中第12个原则,这原则主要涉及到两个方面:1、自定义的COPY构造函数和赋值操作符一定要却把本类中的所有成员,包括新增加的成员和父类的所有成员都要复制过来。2、不要尝试COPY构造函数和赋值操作符进行互相调用。第1点没啥好讨论的,现在来着重讨论第2点。咱们只讨论一种情况,就是copy赋值操作符去掉用copy构造函数的情况。因为copy构造函数本身就已经把复制了一个副本,换句话说它自身已经生成了一个崭新的对象。而copy赋值操作符是把传进来的对象赋给这个崭新的对象,那不就是试图在构造一个已经存在的对象了吗,这很荒谬。同理copy构造函数去掉用copy赋值操作符同样荒谬。所以作者建议方法是把它们共同的代码放到第3个函数中,通常这个函数被命名为init。原则13:以对象管理资源这是Effective C+中提到的第13个原则。资源的管理这一主题从宏观上来讲就是你申请了资源,就一定要释放。你不释放会造成内存泄露,资源泄露,你释放多了有可能导致程序行为异常。所以简而言之就是你申请了多少个资源就释放多少个资源就行了。可是你在编程的过程难免会忘掉或者没有处理好资源释放的过程,那么本原则就是告诉你如何去控制资源的释放的。作者的经验之谈就是以对象作为资源的载体进行传递以代替用单个语句去实现资源的管理过程。因为在这个过程中,程序流说不定就可能被return拐走,被作为异常抛出,被continue或者break跳过,当然还有那几乎被摒弃的goto语句等等,而没有到达delete。因为对象本身有构造函数和析构函数,而且它会在对象的生存期末尾自动调用析构函数来释放资源,从而不会造成资源泄露的情况。其实,我感觉此条款着重强调的是释放资源的重要性,但是它也强调在取得资源的时候马上就进行初始化。而对于操纵对象作者又极力推荐了两种智能指针:auto_ptr,shared_ptr。这两个指针都会在资源使用结束后自动销毁它们,而不用你管。auto_ptr的简单用法如下:它有个特性,那就是一旦用这种指针指向某资源,那就不能有多个auto_ptr再去指向它了。如果已经有一个指针指向了某资源,你再用新指针指向这个指针的话,就指针就被自动设为NULL。shared_ptr叫“引用计数型指针”,它与auto_ptr的不同之处在于它能记录到底有多少个对象指向某资源,但是它无法解决环状引用,就是两个没用的指针互指。不过,一般情况下,智能指针里面装的都是一个函数,这个函数返回一个对象的引用,并完成该对象的初始化工作。tr1:shared_ptr ptb(factory();其中factory()函数负责初始化并返回管理资源的对象的引用。原则14:在资源管理类中小心COPYING行为这是Effective C+中第14个原则。本原则阐述了资源管理类往往遵循RA原则,就是“资源在构造期间获得,在析构期间释放”。因为是要用对象来承载资源的,而本原则考虑的是如果对这种对像进行复制要怎样处理。因为这种管理资源的对象在复制的过程中很少COPY所谓的“同步化基础器物”,据我的理解就是构造和析构函数这里的问题,当然我的理解可能不对。所以可能出现COPY过来的资源不能及时释放掉。作者给出的4个解决上述问题的办法:1、压根就不复制资源管理对象,这就不会有问题了嘛;2、采用“引用计数法”,即要达到COPY多少对象就释放多少对象。这往往要用到shared_ptr;3、COPY要拷贝的全面,即在该类的所有继承体系中的类的成分都COPY过来;4、保持资源的独一性,即它不会有多分COPY,而这往往要用到auto_ptr。原则15:在资源管理类中提供对原始资源的访问这是Effective C+中第15条原则,我感觉非常抽象,理解起来很费劲,那我就边理解边写博客吧。首先你要明白啥叫原始资源,其实确切的概念我也说不准,但是你可以简单地理解为被资源管理类管理的资源。管理资源要使用资源管理类,通常这个类被称为RA类,在理想的情况下,你总是试图使用RA类来进行资源管理,但是世事无常,总有一些API(Application Programming Interface)会直接调用原始资源,它们会绕过RA类,而这不符合你的原则,而你又不得不去用。那么本原则会叫你处理这种情况的一些方法。作者举了两个例子:1、通过传递资源管理类的对象的某些方法间接传递一份原始资源的COPY,这样真正的原始资源不会得到改变。那就需要RA类提供一个接口使对象能够暴露出其内所含的原始资源。而这通常是通过显式转换和隐式转换来实现的。书中仍然是以智能指针auto_ptr和shared_ptr为例加以说明,它们提供接口get来显式获取原始资源指针,另外还通过重载-和.来隐式获取原始资源。2、作者又举了一个字体调用的例子。字体本身是一种原始资源,我们创建了一个类用来管理字体。因为这种原始资源比较特殊应用场合也很多,所以存在让资源管理类提供一个向外界开放的接口的必要性,外界通过调用这个接口从而使用字体这种原始资源。又因为最终是要使用这种原始资源的,所以必然会调用字体的类型,从而这就存在一个类型转换的过程。同理,作者有提供了显式和隐式转换两种转换手段。显式转换自不必提,其实也是get,它极大地减少了资源泄露的可能性。而隐式转换可以自动转换为原始资源类型,但是这存在一个问题。那便是如果用户现在就是想使用一个资源管理类RA的对象,那没办法他现在必须转换为原始资源类型才能使用。而这隐含的凶兆就是如果你不经意间删除了RA对象,那也就意外地删除了原始资源的对象,那么你转换过来的也就没了。总结一下,作者强调无论是显示还是隐式转换都是要视情况而定的,没有完全的绝对。另外,RA是的职责是资源管理重在资源释放,虽然访问原始资源突破了类的封装特性,但是这不是RA的首要存在意义。原则16:成对使用new和delete时要采用相同形式这个原则太简单了。当你new一个数组的时候你要使用delete 释放,当你new一个指针的时候,你要使用delete释放。如果搭配错了,后果都是未定义的。这其中的原理,只要你懂得new和delete是操作符,并且把内存分配看成对象来处理,并会调用构造和析构函数就会明白了。原则17:以独立语句将newed对象置入智能指针这是Effective C+中第17个原则,作者以一个示例形象地说明了这一点。有一个资源处理函数A,这个函数中接收两个参数,它们分别是shared_ptr类型的指针和一个整形参数。但是,因为用对象来管理资源的原则,所以在这里首先有了一个资源管理类的对象,并且想把它作为A的第一个参数传进去,而A的第二个参数用一个能返回整形参数的成员函数B作为实参传进来。程序员为了图省事,他直接在A的第一个参数的位置上new了一个对象C,这个对象当然就是资源管理类的类型了,但是A接受的是智能指针类型,所以他还在此基础上进行强制类型转换到智能指针类型。这里还要介绍一个机制,那就是编译器在产生函数调用码之前,首先要对实参进行核算。那么在核算期间,上述内容就可以分成3步:1、new 对象C;2、调用B;3、强制把C转换成shared_ptr类型。在上述三个步骤中,1和3的顺序是确定的,那就是1在先3在后。但是2却不一定了,这是根据语言和编译器的不同而异的,所以它们的顺序可能是213、123、132。但是在123的情况下,如果调用B的过程发生了异常,导致程序终止,而new C返回的指针会丢失。又因为shared_ptr是用来防止资源泄露的,那么我们的目的没有达到,new出来的C还是泄露了。所以作者在此原则中想着重强调的是,你最好不要在调用函数的过程中直接在参数列表里面进行new啊,类型转换之类的操作,一旦发生资源泄露难以察觉,所以你最好把这些都放在函数调用之前的单独语句里面。原则18:让接口容易被正确使用,不易被误用这是Effective C+中的第18条原则。1、作者在本原则中举了一个函数接口的例子,在一般的情况下,用户可能错误地输入了参数,而导致程序运行不正确。针对这种情况作者推荐采用导入新类型来解决此问题。而这些类型,你可以使用结构体、枚举类型和带有特定返回值的成员函数等来实现。而带有特定返回值的成员函数一般来讲是以函数体带对象。2、再有就是,作为接口设计者,你要限制用户能做什么不能做什么。比如说不让用户去染指资源管理的任务。3、让你接口提供的行为与内置类型的一般性行为一致。因为用户总喜欢对他们熟悉的东西反复用,并且喜欢套用在新的东西上。所以这样做不仅可以让你的接口更快被用户接受,还不容易犯错。在这里作者举了泛型算法的例子。4、让接口对客户提出最少的要求。许多接口总是要求用户注意这注意那,要求一多,用户就容易头晕,这样使用接口就更容易出错。所以一定要让接口被傻瓜式地使用。在这里作者又举了智能指针的例子。原则19:设计class犹如设计type英文是:Treat class design as type design。这是Effective C+中第19个原则。对类的设计归根结底可以归结为类型的设计,因为类也是一种类型的存在。该原则是若干个原则的集合,而这些原则是设计一个类需要遵循的。这些原则具体如下:1、类对象的创建和销毁如何进行;2、类对象赋值和初始化的区别是什么;3、类对象在何时进行值传递;4、类对象所能接受的值,不能接受哪些值,如何让用户容易使用,不容易犯错;5、类对象需要考虑的继承体系;6、类对象的类型转换该如何实现。比如资源处理类的对象;7、对类对象来说那些操作符是必要的;8、什么样的编译器自动生成的或者已经存在的成员函数你应该自己重写并取而代之;9、该类中哪些成员能提供给外部;10、哪些是新类的未声明接口;(这个目前我不太懂)11、你是否要定义很多类型?如果是,你还不去写个泛型类。否则,你只写一个type就好了;12、你真的有必要定义一个新类型吗?原则20:宁以引用传递代替值传递这是Effective C+中第20个原则。对于类对象而言,采用值传递是非常不明智的,因为它会涉及到COPY构造函数和析构函数的调用,如果你COPY的那个对象还包含了其他类的对象,那就会涉及到更多的函数调用,而且这是呈指数级增长的。而采用const&不仅极大地提高了效率而且还没有任何构造函数和析构函数的调用。而之所以采用const则是由于先前的原则3.另外采用const&可以避免对象切割问题,虽然这个问题我还没认识到那么深刻。当一个子类对象采用值传递方式被调用,但是被调用时的类型要求是父类类型时,那么这时父类的COPY构造函数会被调用,不要以为这种情况不能发生,记住这正是多态的体现,而父类类型不过是个接口。这样做的话,该对象本身的子类特性就会被无视。引用的底层是用指针来实现的,所以你会发现引用和指针的行为有些相仿。所以对于内置类型,如果你以值传递的话效率会高些,比如说VS中指针占4个字节,而一个char占1个字节。另外对于STL迭代器和函数,都被设计为采用以值传递,这是合理的,为什么呢,答案在原则1。但是不能因为对象小就采用值传递,这是因为一方面对象虽小但牵连广泛,它所涉及的COPY构造函数和析构函数也不可小觑。另一方面,小对象也不排除将来因为需求的改变而变大的倾向。原则21:必须返回对象时,别妄想返回其引用Effective C+中第21个原则,因为引用是要指向某已存在的对象的,但如果该对象某一瞬间突然消失了,这个引用被架空了,那就出错了。为了证实这一点作者举了一个有理数相乘的例子。有这个一个有理数类,其中有一个有理数相乘的成员函数,该成员函数返回该有理数类的对象。在此例中该对象是一个本地对象,什么叫本地对象呢?就是一个普通的,局部的对象,它随着作用域的结束而被自动销毁。因为具备这一性质,一旦你把这个函数的返回值赋给某一变量,然后该函数使命完成被自动销毁,那么它所返回的对象也就被自动销毁了,那么被赋值的变量的行为就未定义了。然后作者又举了动态分配对象的例子。因为是new一个对象出来,所以它肯定是要调用构造函数进行初始化工作的,但是你往往找不到一个合理的时机进行析构工作,从而导致资源泄露。因为上述两个例子都是因为为本地对象调用构造函数而导致的,那么如果把本地变量写成static的不就避免了构造函数和析构函数的问题了么。作者的回答是no。因为static对象是静态分配,它在内存中的位置是固定的,这样多个操作对static对象进行修改,static对象最后的内容是最后修改的那个内容,基于此种性质。如果你的业务逻辑是多个对象之间才存在的,那么这样做的后果肯定不是你想要的。最后作者给出了自己的建议你既然要返回一个对象,那就直接返回一个对象。原则22:将成员变量声明为private这是Effective C+中第22个原则,原话是Declare data members private,即把数据成员称名为私有的访问权限。为什么要这样做?先说public,public是提供用户的接口,它根本不具备封装特性。从实际来看,被声明为public的都是成员函数,用户通过操作成员函数来操作类内的成员,而至于说这个接口的内部细节对于用户来说是不可见的,其中一个典型的例子就是getter和setter的运用。再有就是通过把public成员函数作为接口提供给用户,那么这个接口的实现会有若干种可能以应对不同的需求。还是因为它是接口,是给用户使用的,所以它不能变来变去的,否则你叫用户怎么用。所以一旦某个成员函数被声明为public的,那么一般情况下它就不能再有任何变化了,当然这个变化是从用户的角度来看。从而推出越是广泛使用的类,public成员声明就越不能改变。在这里有个普遍的准则,那就是封装的越好,改变成员时所破坏的代码量就越少。protected并不比public封装性更好,因为你改变public只是改变用户代码,而你改变protected则可能导致所有子类的代码的破坏,它俩的代码破坏量都不可估量,后者更甚。所以成员一旦声明为protected和public那就意味着它们应该是永远不变的。所以从封装的角度讲只有两中访问权限可言,那就是private封装和其他不封装。原则23:宁以非member、非friend替换member函数这是Effective C+中第23条原则的内容,我感到很奇怪,成员函数调用本类的成员怎么了?难道这还不够封装吗?看下面的解释吧。类中可能存在一系列函数用来处理一系列操作,而这一系列函数可以放在类中某一成员函数中一起执行,但是也可以放在一个非成员函数中一起执行。那么从封装性上来讲哪一个好呢?答案是非成员函数,这是因为成员函数还可以访问类中的私有成员,但是非成员函数连私有成员都访问不了,所以非成员函数的封装性更好。要知道越多东西被封装,就会有更大的余地在不为人知的情况下更改被封装的数据,在这里友元函数和成员函数一样,所以本原则推崇的是非友元函数和非成员函数。这对于那些纯面向对象语言,像JAVA,而言很不幸,因为它们没有独立的函数存在。所以在这种情况下可以考虑写一个工具类,在此类中添加函数,这样这些函数就不是成员函数了。而C+的做法是把这些独立的函数和被操作的类放在同一命名空间下。本原则这样做的另一个原因是出于可扩展原则。因为完成某一特定功能可能需要一系列的函数,但是你可能需要完成很多功能,而这些功能都属于同一个工程。这个时候你可以为这个工程明明一个名字空间,然后把每个功能写在一个独立的头文件里面,因为它们共同隶属于同一个命名空间,所以在某个头文件中你就可以把操作那一系列成员函数的非成员非友元函数也写在同一头文件同一命名空间下。这样做既降低了编译相依性也提高程序扩展性。原则24:若所有参数皆需要类型转换,请为此采用非member函数这是Effective C+中第24个原则,即非成员函数能够完美解决题目中所叙述的情景。作者以一个有理数类的例子来诠释本原则所述内容。这个例子大致是这样的,这个有理数类有一个带有默认值参数的构造函数,并且也有一个重载的乘法操作符,并且这个重载操作符函数只接受一个有理数类的对象。现在在它参数的位置上放上一个整数,因为构造函数并非显示,所以它允许将这个整数隐式转换成该类的类型而参与运算。但是奇怪的现象来了,因为这个重载操作符是单目操作符,这时出现了一个赋值语句,这个*就是这个单目操作符,oneHalf是一个有理数类对象,2是整数,2可以被隐式转换成有理数类型。但是下面这个表达式从逻辑上将实现的功能是一样的,但是它确报错,这是为啥呢?那是因为这个*的函数原型如下所示:因为2并不在参数列表中,根本不存在类型转换,而又因为它要求的是两个有理数类型参与运算,因此2压根不符合类型要求,所以会报错。而这就是所谓的只有被列于参数列表内,这个参数才是隐式转换的合格参与者。那这里你一定会冒出一个疑问,既然参数列表里面只有一个参数,那你就改写一下这个重载操作符的函数呗,这样两个参数不就都能进行隐式转换了吗?嗯,这个想法是好的,不过类中的*重载操作符不支持两个参数的形式,请看下面的图示:但是非成员函数的*重载操作符函数却能支持两个参数的形式,这样两个参数都能进行隐式转换了。那么为什么不把非成员函数设成有原函数呢?那是因为有原函数的存在极大地破坏了封装特性,不符合面向对象的思想,乱用的话会带来很大麻烦,所以能不用就不用。原则25:考虑写出一个不抛出异常的swap函数Effective C+中第25个原则,颇为复杂,作者用了很长的篇幅来讲这个原则,我在理解这个原则上也花了不少功夫。我还是先叙述一下这个作者所述的例子吧。话说在STL有这么一个泛型算法名叫SWAP,其功能如其名,就是用来交换,它能交换两个类型的对象,那更不用说内置类型,因为内置类型也可以说是某种类型的对象吧,只要被交换类型支持复制COPY操作就行。但是这个泛型算法的思路还是让一个临时变量tmp先接收一个对象a,然后把另一个对象b赋给这个对象a,之后再把这个临时变量tmp的值再赋给a。思路很简单,也很正常,然后在某些情况下这种做法带来的却是效率的低下。作者举了一个常见的场景,那就是用指针指向一个对象的时候(pimpl,pointer to implementaion)这种做法具体就是一个类A用来操作指针,这个指针指向类B,另一个类B中的内容才是实质的内容。指针很小,但是对象很庞大,这个时候你再用复制的方法来交换那花费的时间就不是一般两般的长了,占用的临时空间也非常大。如果用泛型算法SWAP来交换两个A对象的话,因为A对象中有指针已经指向了B的对象,再加上SWAP中间的临时变量tmp,那么SWAP不仅仅赋值了3个A对象,而且还复制了3个B对象,而后者并不是我们想要的,其效率之低下由此可见一斑。而我们想要的只是进行到A的指针就可以了。那这个时候该怎么办呢?那我们就私人定制一个A类专属的SWAP好了,这就是所谓的SWAP针对A类的特化。具体的做法就是弄一个全特化的SWAP版本,用它来交换给定对象中所包含的指针,然而这一版本并不能成功因为这个指针是私有的。解决这个问题的办法你可以考虑使用友元函数,但是基于友元函数对于封装性的破坏,我们能不用就不用。所以最好的做法就是声明一个A类的公有成员函数SWAP,然后在这个公有成员的SWAP函数里面调用泛型算法只用来交换指针,这样它就能访问A的私有成员指针了。同时,在同一命名空间下再声明一个非成员函数的SWAP,它的参数被特化为A类型,这样它直接调用实参对象,也就是A类型对象的公有接口SWAP就可以完成交换了。而这种做法也恰恰符合了STL的风格。不过,上面这种做法并不适合泛型函数级别上进行偏特化,因为C+只允许对泛型类级别上进行。那一般而言应对这种情况的做法就是写一个重载函数,这个重载函数只有参数是特化的。不过,不要往std里面添加重载的泛型函数,也可以说成是你不要往std里面添加新东西,因为这些都是标准。所以,你还是自己弄个命名空间然后把这些放到里面去。有的时候纯粹是为了方便,你希望你的特化SWAP被最大化使用,你还是应该在你自定义的命名空间内std的特化版本SWAP和本类的专版,这样编译器就会根据实参的具体类型自动选择最合适的版本了。另外,SWAP作为成员函数决不能抛出异常,因为它是帮助类提供一个异常安全性的保障。不过这一点不适用于非成员函数的SWAP,因为它是基于拷贝构造函数和拷贝赋值操作符函数,而这两者是可能抛出异常的。一个规律就是你的SWAP效率越高,它的异常安全性越好,一个特例就是当SWAP操作内置类型时SWAP根本不可能抛出异常。什么是全特化?就是凡事涉及到泛型类型的地方都用特定类型代替之。什么是偏特化?就是非全特化。通过上述内容作者想表达3个观点:1、当那个泛型SWAP效率实在低下时,你自己写一个,但是要确保不抛出异常;2、如果你的SWAP是成员函数,那你一定要用一个非成员函数来调用这个成员SWAP,并且特化一个std:SWAP;3、不要往std里添加全新的东西。原则26:尽可能延后变量定义式的出现时间Effective C+中第26条原则,总的来说就是这个变量你定义早了,你很难一眼看到这个变量在哪用了;如果你定义早了,这个变量你可能压根没用到过,那你就是白白浪费了空间。如果你定义的变量是个对象,那你还要花费构造函数和析构函数的代价。如果在你定义变量之后程序由于异常而中断了,这个变量你根本没用着,那你赔的更多了。再有就是在你定义对象时最好直接给它赋值,因为如果你不这样做,它是首先调用默认构造函数,然后再调用拷贝赋值函数,但是如果你直接拷贝那就不会调用默认构造函数了,所以这在效率上是一个提升。本原则还讲到一个用于循环的变量是定义在循环内还是外?如果在内,那么你每次都要要调用拷贝构造函数和析构函数,你循环多少次就调用多少次。如果在外,你只调用一次构造和析构函数,循环多少次就调用多少次拷贝赋值操作函数多少次。作者说这是有条件的,这取决于拷贝构造函数的代价,如果一次拷贝的代价小于一次构造函数+析构函数的代价,那么在外比较好,尤其是在循环次数比较多的情况下,反之在内比较好。不过,从代码的可读性和可维护性方面来讲,作者比较倾向于在内的做法。原则27:尽量少做类型转换动作这是Effective C+中第27个原则,作者花了很长的篇幅来介绍这一原则。总而言之一句话,因为类型转换会导致破坏类型系统,从而带来明显的和不明显麻烦,而且这是C+独有的特性。C+提供了四种新型的类型转换,就是下面这四种:const_cast是把const类型转换成非const类型;dynamic_cast是类体系中进行转换;reinterpret_cast是一个很强大的类型转换,最常用的是指针转换为指针,而且是两个毫不相关的指针之间的转换,它传递的是比特位。换句话说这些比特位是不变的,但是它能按照另一种方式来解释它。static_cast就是平常的类型转换,用的最广泛。须知,在C+中,类型转换往往能够令编译器编译出运行时代码,可想而知,这个代码并不是你写的。在这里作者举了一个多态的例子,当你用父类的指针指向子类的对象时,父类的指针指向的地址和子类对象所在的地址并不是一个,它俩之间有时候是有个偏移量的。作者又举了一个例子。现有一父类和其子类,两类有两个同名的虚函数,现要求子类虚函数中首先调用父类虚函数,这是子类虚函数的代码是这样子的。通过子类的this指针强制类型转换成父类类型,然后调用同名函数。但是次转换的过程并不是你所想象的那样简单,这个强制类型转换会创造出一个被转型对象的副本,它是在这个副本身上执行父类的同名函数,而在该对象身上执行本类专属的同名函数,两者本应作用于同一个对象而实际上却没有。而解决这个问题的办法就是不用强制类型转换,你该用父类的成员函数就用就行了。而dynamic_cast这个转换你最好不要用,因为它是动态转换,它会在整个类层次上进行寻找,而且每转换一次就寻找一次,并且它是按照类名进行字符串匹配的。通常你使用dynamic_cast进行强制类型转换的情景是用一个父类的指针指向子类对象,这种情况你应该使用的是子类类型的只能指针,作者使用的是一个装有智能指针的vector。如果子类很多,并且你非要用父类类型的话,那你可以在父类中提供一个同名空虚函数,并在各个子类中去实现它,然后你再用vector去容纳装有父类类型的智能指针,然后去操作。为啥要使用vector呢?因为它是类型安全容器。在这里一定要记住不要在程序中多次使用dynamic_cast做没有必要的类型转换,因为这样做不仅代码多,而且运行慢,因为dynamic_cast需要查找嘛。最后作者衷心地告诉大家尽量不要使用强制类型转换,尤其是除static_cast之外的另外三种;强制类型转换如果必要,那也要把它包裹在一个函数里面,不要让用户接触到;推荐使用C+强制类型转换,它不仅容易分辨,而且各司其职。原则28:避免返回handles指向对象的内部成分在这里首先要明确啥是handles,这里所说的handles就是通常所说的引用、指针或者迭代器等具有指代性质的标签。那么题目所说的意思就是你返回的那个handles不要涉及对象内部的东西。在叙述此原则的过程中作者举了一个矩形Rectangular的例子,说举行的四个顶点存储在一个结构体中,Rectangular提供了两个公有接口upperleft和lowerright它们返回的是该矩形左上角和右下角的顶点的坐标结构体,并且是以引用的形式返回的。因为用户只需知道这些顶点的坐标而无需对这些顶点进行操作,因此这俩接口都是常函数。但是矛盾出现了,常函数的作用是不允许用户去修改,但是这两个常函数却返回了Rectangular的私有成员。另外常函数只是说在常函数体内不进行更改数据成员的操作,它返回的东西并不一定是常量。既然如此用户就很有可能通过这俩接口去改变Rectangular类原本私有成员,这是极其不符合该程序的初衷和封装性原则的。所以作者在这里说“成员变量的封装性最多等于其返回引用的访问级别”是很有道理的。作者还说非公有的成员函数也是内部数据,这不是废话嘛,我早就知道啊。作者想表达是啥呢,就是从访问级别上来讲,不要让那些访问级别高的成员函数返回指向访问级别低的成员函数的handles,不过在我看来,具体来讲就是不要让公有接口返回任何非公有的成员的handles。那么这原则中所提及的问题是怎么解决的呢?正如我所说的,常函数只是在函数体内不能对数据成员更改,但是它返回的东西并不一定不能被更改,所以呢,那么就把返回值也设成const就OK了。尽管如此呢还是有个问题存在,那就是函数虽然返回了const成员不能被更改,但是作为右值的语句结束以后,它的生命期就结束了,那右值的东西被析构掉左值不就成了空吊子。所以返回只想对象内部成分的handles总是危险的,作者最后总结道:能不用handles只想对象内部就不用,这样可以增加封装性,最好在const函数的返回类型上也加个const,也尽量避免空吊的出现。原则29:为“异常安全”而努力是值得的我怎么发现最近记录的这几条原则的叙述内容都很冗长无法一眼两眼就能看懂。作者首先从一个类似于从MFC取材的例子,就是更换背景色的一个功能,并且它是在多线程环境下工作的。这个功能的具体实现首先加上互斥锁,删除旧背景,记录图像更改次数,生成最新的背景图片,去掉互斥锁。然而,这种步骤并不符合异常安全性。因为作者说这种实现不符合异常安全性,而所谓的异常安全性,具体来讲包括2个方面,它们是:1、不泄露任何资源:上例很难做到,因为咋生成新的背景图片这一步,如果出现异常,程序就不会继续往下执行,那么互斥锁就永远不会解锁,那么占用的资源就会被释放,那么就会造成资源泄露。怎样做到不泄露呢?条款14说过就是用资源管理类。这一点有点区别,在不使用资源管理类时使用互斥锁下图这样的:而使用资源管理类机制之后是如下的情形:Ml是Lock的一个对象,这样它有自己的构造函数和析构函数,这就避免了资源泄露,而且这不仅使程序较短而且还免去了写unlock,降低了程序的复杂性也就有效地降低了出错的可能性。
展开阅读全文
相关资源
正为您匹配相似的精品文档
相关搜索

最新文档


当前位置:首页 > 图纸专区 > 课件教案


copyright@ 2023-2025  zhuangpeitu.com 装配图网版权所有   联系电话:18123376007

备案号:ICP2024067431-1 川公网安备51140202000466号


本站为文档C2C交易模式,即用户上传的文档直接被用户下载,本站只是中间服务平台,本站所有文档下载所得的收益归上传人(含作者)所有。装配图网仅提供信息存储空间,仅对用户上传内容的表现方式做保护处理,对上载内容本身不做任何修改或编辑。若文档所含内容侵犯了您的版权或隐私,请立即通知装配图网,我们立即给予删除!