最新Effective C中文版第三版 高清PDF总结.docx

上传人:b****8 文档编号:10654234 上传时间:2023-02-22 格式:DOCX 页数:71 大小:929.89KB
下载 相关 举报
最新Effective C中文版第三版 高清PDF总结.docx_第1页
第1页 / 共71页
最新Effective C中文版第三版 高清PDF总结.docx_第2页
第2页 / 共71页
最新Effective C中文版第三版 高清PDF总结.docx_第3页
第3页 / 共71页
最新Effective C中文版第三版 高清PDF总结.docx_第4页
第4页 / 共71页
最新Effective C中文版第三版 高清PDF总结.docx_第5页
第5页 / 共71页
点击查看更多>>
下载资源
资源描述

最新Effective C中文版第三版 高清PDF总结.docx

《最新Effective C中文版第三版 高清PDF总结.docx》由会员分享,可在线阅读,更多相关《最新Effective C中文版第三版 高清PDF总结.docx(71页珍藏版)》请在冰豆网上搜索。

最新Effective C中文版第三版 高清PDF总结.docx

最新EffectiveC中文版第三版高清PDF总结

 

EffectiveC中文版第三版高清PDF总结

EffectiveC++阅读笔记

本来是写在XX空间的,但是不知道咋回事XX博客中图片看不到了,所以XX博客的不稳定性可见一斑。

于是我决定将我的领会和感受写在自己的云盘里面。

虽好也弄个目录啥的,最后再整车成PDF格式的。

这个标准我就参考我研究生期间论文的格式吧。

原则3:

尽可能使用const

《EffectiveC++》里面第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++默默编写并调用哪些函数

这一篇博客是《EffectiveC++》中第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:

若不想使用编译器自动生成的函数,就应该明确拒绝

这是《EffectiveC++》中第6个原则,在某些情况下你不想让某些类的对象被拷贝,那么在这种情况下即使你不写copy构造函数和copy赋值操作符编译器也会为你生成,那么你不得不自己写它们俩。

而你又不希望别人调用它们,所以这时你要将它们声明为private类型。

一旦你写了,编译器就不会自动调用父类的copy构造函数和copy赋值操作符。

即便这样本类内部成员函数和友元函数还是可以调用它们,该如何是好?

办法就是你只声明这些函数而不去实现,没有实现就自然没有功能了,而既然实际上没用,你甚至连形参都可以省略,只在形参列表中写个形参类型即可,就像下图类的定义所示的这样:

其中的几个函数实现如下所示:

从上图可见,copy构造函数和copy赋值操作符都没有实现。

在主程序中是如下调用的:

运行结果如下所示:

出现了错误提示,说copy构造函数无法解析。

现在我把copy构造函数和copy赋值操作符都注释掉。

在运行得如下结果:

而本思想只在阐述如果你不想让编译器为你自动生成函数,你就要自己手写。

原则7:

为多态基类声明virtual析构函数

这是《EffectiveC++》中第7条原则,其内容是在具有多态用途的父类中应该使用virtual析构函数。

首先要知道啥是多态。

我就好说直白的,显得没有深度的东西。

多态的一种体现就是通过父类的指针指向不同的子类来实现不同的功能,从而达到接口重用的目的。

在这种情况下用作多态的父类往往具有至少一个virtual成员函数留给子类来实现。

好了,现在铺垫完毕了,来说正题,为啥要有一个virtual析构函数呢?

那是因为如果没有这样一个virtual析构函数的话,子类的析构函数就不会被调用,那么对象的子类部分不会被析构,那么就会造成资源的泄露。

现在来看下面的例子:

derived继承了base,并且base中并没有virtual析构函数,那么调用过程如下所示:

运行结果如下所示:

从这个结果可以看到父类的析构函数执行了,说明对象的父类部分所占资源已经被释放,但是子类的析构函数并未调用这说明对象中子类部分所占资源并未得到释放。

但是如果在父类中加上一个virtual析构函数的话就不一样了。

同样的调用过程,运行结果如下所示:

这说明对象的子类部分所占资源也被释放掉了。

在这里再说点别的,一般来讲作为要被继承的父类的类中至少含有一个virtual的成员函数留给子类去实现。

而如果某类中一个virtual成员函数都没有的话,在很大程度上说明了该类不会被作为父类而存在,在这种情况下不应该把其析构函数设为virtual的。

为啥呢?

这与C++中virtual本身的实现机制有关,因为这样的类的对象必须要携带一个表,这个表叫vtbl,所以本来没必要多带这么个表,但是你非要多出一个来占个空间,这就是占个茅坑不拉屎的表现啊。

所以不准备被继承的类是没有必要设置virtual析构函数的。

在这里在介绍一种情况,当你希望在virtual类中把析构函数设为virtual的时候,应该吧析构函数设为纯virtual函数,并且给与空的实现,如下图所示:

为啥要这样做呢?

因为析构函数调用顺序是从没有子类的子类那里的析构函数逐层调用父类的析构函数,所以如果这个纯virtual函数没有实现的话,编译器就会报错。

这个原则简而言之就是,只有作为多态用途的父类才有必要使用virtual析构函数,其他的就是画蛇添足。

另外,处于继承机制的类对象包含了它所能涉及到的最低层次及其以上的所有层次的成分。

原则8:

别让异常逃离析构函数

这是《EffectiveC++》的第八条原则。

主要说的是程序出现的异常不要从析构函数这里漏掉,也就是说析构函数应该承担起拦截异常的责任才行。

如果异常越过了析构函数这一关,流窜到其他地方去,那么就会造成程序提早结束或者未知的风险,这个后果就很严重了。

对付这种情况通常有两种简单粗暴的手段:

1、在析构函数内发现异常,立刻捕捉到并且结束整个程序;2、在析构函数中发现异常,立刻捕捉到并将其扼杀,掩人耳目,继续执行程序。

其中第一种手段比第二种手段要好,这是为啥呢?

因为方法1直接结束程序,其结果是可预料的,不会造成太大破坏。

而方法2你这个异常是终止了,但是程序中其他部分与这个功能相关的势必会造成影响,也许还会因此带来其他异常的连锁反应,这个就不好办了。

不过以上这两种方法都没能去正面处理出现的异常,所以这两种方法都不提倡。

书中给出的解决方案是,再创建一个类用来处理异常,在这个类中有一个成员函数专门用来处理原来的类中的异常。

而这个成员函数是调用原类中的异常处理来完成的,这实际上就是变相的让原类自己处理异常,这是第一道关卡。

然后异常处理类的析构函数中也有一份处理异常的代码,这部分是异常处理类自己的,这是第二道关卡。

这个就是双保险,如果说在第二道关卡仍然不能有效处理异常,那没办法了,只能强行关闭程序了。

再总结一下本原则就是无论如何也不能让异常突破析构函数这一关。

原则9:

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

这是《EffectiveC++》中的第9条原则。

简单的来说,如果你在父类的构造函数中调用了虚拟函数,那么子类的成员就会始终处于未初始化的状态,这样对象的子类成分就会出现不可预知的行为,这是非常危险的。

那么这是为什么呢?

在继承体系当中,你声明了一个子类对象,那么这个子类对象其实是很复杂的,它包含了子类所有父类的成分。

而这些成分是要一层一层的进行初始化的,其顺序是按照类的继承层次从上而下进行的,即从最远的那个父类到最近的那个父类,然后是本类的初始化。

而C++的机制又是只有在父类成分初始化完毕以后才去处理子类成分的初始化工作,换句话说,如果父类的成分没有初始化完毕,它压根就不会去管子类的初始化工作。

因为你在父类的构造函数中调用了虚拟函数,而这个虚拟函数一般在父类中是不进行实现的。

鄙人以为,一个函数之所以被调用肯定是因为这个函数是有一定的功能实现的,要不你调用它干吗?

我想C++的构造函数也是这么想的。

但是,你现在非要在本来用于初始化的构造函数中去调用一个没法初始化virtual函数,C++就认为这个对象的父类成分还没有初始化完毕,现在不能去初始化子类成分。

所以,即使你现在声明了一个子类对象,子类中的成分还是没有得到初始化,所以就会出现不可预知的后果。

C++对未定位的成员变量是采取无视的态度的,因为还没轮到你,你给我一边凉快去。

在构造函数期间无视,在析构函数期间也是无视。

而析构的顺序又是先子类再父类,因为对象的子类成分被无视,只有父类成分被析构,所以那些未定义的成员变量自始至终都是未定义的,你也不知道它们最终会怎样。

为了证实这一点请看下面的例子:

运行结果是这样的:

看到这个结果,我不得不说现在的编译器已经很智能了,它直接把它拦下来了。

在介绍与本原则有关的内容时,作者举了一个应用场景。

那就是当类中有多个不同版本的构造函数,它们的共同的初始化代码都统一放到一个初始化成员函数里面了,并且这个初始化成员函数调用了一个virtual成员函数,并且这个virtual成员函数会有一定的实现代码。

当你建立子类对象时却调用了错误的virtual成员函数。

在此作者并没有详细地解释是为什么,但他给出了一种解决办法。

那就是在子类的构造函数的初始化成员列表中调用父类的构造函数去初始化对象中父类的部分,当然了,这时父类中的那个virtual函数你要改成非virtual函数了。

在这里作者使用了一个技巧,他不是在成员初始化列表直接把父类成分所需的东西直接给它,而是通过一个辅助函数返回一个值给父类进行初始化,这样写比较方便也比较可读。

而且,这个辅助函数的是一个static类型。

static类型的成员函数是静态成员函数,它的类的对象实例化之前就已经被实例化,换句话说它跟类中其他的成员不发生关系。

另外,子类对象的父类成分是在子类成分之前先被实例化,而在子类对象实例化的中间也就是父类成分正在实例化,还没轮到子类成分实例化之前,子类中的成员函数啥的是未定义的,也就工作不了。

而此时static成员函数却能工作,这有助于对象中父类成分的实例化,所以把此辅助函数设置为static类型。

原则10:

令operator=返回一个referenceto*this

原则11:

在operator=中处理“自我赋值”

在这篇博文里面我打算写两个《EffectiveC++》中的原则,因为第一个原则太短了。

现在介绍第一个原则:

条款10,此条款旨在说明在你自己编写的赋值操作符=一定要返回该左值的引用。

具体来说就是返回*this。

这很好解释,因为this是指向本对象的指针,那么*this就是该对象的本身实体了。

而你现在返回的只不过是该实体的一个代表符号而已。

现在一般的赋值操作符都采用这个原则,虽然不是强制的,但是大家都遵守。

下面来对条款11做一些介绍。

条款11的内容很简单,就是一定要妥善处理赋值操作符=的自我赋值问题,就是自己给自己赋值的情况。

那么这又是为什么呢?

因为在某些时候你要编写用来管理资源的类,那你知道一个资源不用了就要释放掉,以便留给下一个需要该资源的对象。

不过,这是很合理的。

但是,假设当前占用该资源的对恰好是用来赋值的右值,也就是它俩其实是一个东东。

如果还是按照上面处理的话,就会出现被用来赋值的右值实际上已经啥实质内容都没有了,this指针指向了NULL,那就会发生错误。

那么怎样处理这种情况呢?

第一种方法很简单,那就是在赋值操作符的实现中最先判断一下赋值的对象和被赋值的对象是不是一个,即if(this==&obj)。

不过这种方法不好,因为这需要额外写出一条语句,这无疑会增加运行时间。

所以实际上采用的办法就是所谓的COPYandSWAP方法。

什么意思呢?

首先赋值一份右值,生成一个COPY,然后用*this与这个COPY进行SWAP,那么就可以完美解决自我赋值的问题。

因为既然是COPY那么原来那个右值就没有改变,而this原本是空的,它和那个COPY交换以后,那个COPY就变成了NULL,而this的内容就成了COPY,也就是原来的那个右值的值了。

还有一种方法就是直接利用赋值操作符重载函数的传参机制是传值这一特性,直接传进来一个COPY。

该方法可行,但是作者不提倡,它说这样做的话清晰性变差了,我的这个清晰性大概就是可读性吧。

不过,我倒觉得没啥。

他又说这种方法有时候可能更高效。

原则12:

复制对象时勿忘其每一个成分

这个原则是《EffectiveC++》中第12个原则,这原则主要涉及到两个方面:

1、自定义的COPY构造函数和赋值操作符一定要却把本类中的所有成员,包括新增加的成员和父类的所有成员都要复制过来。

2、不要尝试COPY构造函数和赋值操作符进行互相调用。

第1点没啥好讨论的,现在来着重讨论第2点。

咱们只讨论一种情况,就是copy赋值操作符去掉用copy构造函数的情况。

因为copy构造函数本身就已经把复制了一个副本,换句话说它自身已经生成了一个崭新的对象。

而copy赋值操作符是把传进来的对象赋给这个崭新的对象,那不就是试图在构造一个已经存在的对象了吗,这很荒谬。

同理copy构造函数去掉用copy赋值操作符同样荒谬。

所以作者建议方法是把它们共同的代码放到第3个函数中,通常这个函数被命名为init。

原则13:

以对象管理资源

这是《EffectiveC++》中提到的第13个原则。

资源的管理这一主题从宏观上来讲就是你申请了资源,就一定要释放。

你不释放会造成内存泄露,资源泄露,你释放多了有可能导致程序行为异常。

所以简而言之就是你申请了多少个资源就释放多少个资源就行了。

可是你在编程的过程难免会忘掉或者没有处理好资源释放的过程,那么本原则就是告诉你如何去控制资源的释放的。

作者的经验之谈就是以对象作为资源的载体进行传递以代替用单个语句去实现资源的管理过程。

因为在这个过程中,程序流说不定就可能被return拐走,被作为异常抛出,被continue或者break跳过,当然还有那几乎被摒弃的goto语句等等,而没有到达delete。

因为对象本身有构造函数和析构函数,而且它会在对象的生存期末尾自动调用析构函数来释放资源,从而不会造成资源泄露的情况。

其实,我感觉此条款着重强调的是释放资源的重要性,但是它也强调在取得资源的时候马上就进行初始化。

而对于操纵对象作者又极力推荐了两种智能指针:

auto_ptr,shared_ptr。

这两个指针都会在资源使用结束后自动销毁它们,而不用你管。

auto_ptr的简单用法如下:

它有个特性,那就是一旦用这种指针指向某资源,那就不能有多个auto_ptr再去指向它了。

如果已经有一个指针指向了某资源,你再用新指针指向这个指针的话,就指针就被自动设为NULL。

shared_ptr叫“引用计数型指针”,它与auto_ptr的不同之处在于它能记录到底有多少个对象指向某资源,但是它无法解决环状引用,就是两个没用的指针互指。

不过,一般情况下,智能指针里面装的都是一个函数,这个函数返回一个对象的引用,并完成该对象的初始化工作。

tr1:

:

shared_ptrptb(factory());

其中factory()函数负责初始化并返回管理资源的对象的引用。

原则14:

在资源管理类中小心COPYING行为

这是《EffectiveC++》中第14个原则。

本原则阐述了资源管理类往往遵循RAⅡ原则,就是“资源在构造期间获得,在析构期间释放”。

因为是要用对象来承载资源的,而本原则考虑的是如果对这种对像进行复制要怎样处理。

因为这种管理资源的对象在复制的过程中很少COPY所谓的“同步化基础器物”,据我的理解就是构造和析构函数这里的问题,当然我的理解可能不对。

所以可能出现COPY过来的资源不能及时释放掉。

作者给出的4个解决上述问题的办法:

1、压根就不复制资源管理对象,这就不会有问题了嘛;2、采用“引用计数法”,即要达到COPY多少对象就释放多少对象。

这往往要用到shared_ptr;3、COPY要拷贝的全面,即在该类的所有继承体系中的类的成分都COPY过来;4、保持资源的独一性,即它不会有多分COPY,而这往往要用到auto_ptr。

原则15:

在资源管理类中提供对原始资源的访问

这是《EffectiveC++》中第15条原则,我感觉非常抽象,理解起来很费劲,那我就边理解边写博客吧。

首先你要明白啥叫原始资源,其实确切的概念我也说不准,但是你可以简单地理解为被资源管理类管理的资源。

管理资源要使用资源管理类,通常这个类被称为RAⅡ类,在理想的情况下,你总是试图使用RAⅡ类来进行资源管理,但是世事无常,总有一些API(ApplicationProgrammingInterface)会直接调用原始资源,它们会绕过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对象置入智能指针

这是《EffectiveC++》中第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的过程发生了异常,导致程序终止,而newC返回的指针会丢失。

又因为shared_ptr是用来防止资源泄露的,那么我们的目的没有达到,new出来的C还是泄露了。

所以作者在此原则中想着重强调的是,你最好不要在调用函数的过程中直接在参数列表里面进行new啊,类型转换之类的操作,一旦发生资源泄露难以察觉,所以你最好把这些都放在函数调用之前的单独语句里面。

原则18:

让接口容易被正确使用,不易被误用

这是《Effectiv

展开阅读全文
相关资源
猜你喜欢
相关搜索

当前位置:首页 > 求职职场 > 简历

copyright@ 2008-2022 冰豆网网站版权所有

经营许可证编号:鄂ICP备2022015515号-1