关闭

关闭

关闭

封号提示

内容

首页 Effective+C++中文版.pdf

Effective+C++中文版.pdf

Effective+C++中文版.pdf

上传者: 隔岸风景好 2011-09-19 评分 0 0 0 0 0 0 暂无简介 简介 举报

简介:本文档为《Effective+C++中文版pdf》,可适用于IT/计算机领域,主题内容包含从C转向C对每个人来说习惯C需要一些时间对于已经熟悉C的程序员来说这个过程尤其令人苦恼。因为C是C的子集所有的C的技术都可以继续使用但很多用起来又不符等。

从C转向C对每个人来说习惯C需要一些时间对于已经熟悉C的程序员来说这个过程尤其令人苦恼。因为C是C的子集所有的C的技术都可以继续使用但很多用起来又不太合适。例如C程序员会认为指针的指针看起来很古怪他们会问:为什么不用指针的引用来代替呢?C是一种简单的语言。它真正提供的只有有宏、指针、结构、数组和函数。不管什么问题C都靠宏、指针、结构、数组和函数来解决。而C不是这样。宏、指针、结构、数组和函数当然还存在此外还有私有和保护型成员、函数重载、缺省参数、构造和析构函数、自定义操作符、内联函数、引用、友元、模板、异常、名字空间等等。用C比用C具有更宽广的空间因为设计时有更多的选择可以考虑。在面对这么多的选择时许多C程序员墨守成规坚持他们的老习惯。一般来说这也不是什么很大的罪过。但某些C的习惯有悖于C的精神本质他们都在下面的条款进行了阐述。条款:尽量用const和inline而不用#define这个条款最好称为:“尽量用编译器而不用预处理”因为#define经常被认为好象不是语言本身的一部分。这是问题之一。再看下面的语句:#defineASPECTRATIO编译器会永远也看不到ASPECTRATIO这个符号名因为在源码进入编译器之前它会被预处理程序去掉于是ASPECTRATIO不会加入到符号列表中。如果涉及到这个常量的代码在编译时报错就会很令人费解因为报错信息指的是而不是ASPECTRATIO。如果ASPECTRATIO不是在你自己写的头文件中定义的你就会奇怪是从哪里来的甚至会花时间跟踪下去。这个问题也会出现在符号调试器中因为同样地你所写的符号名不会出现在符号列表中。解决这个问题的方案很简单:不用预处理宏定义一个常量:constdoubleASPECTRATIO=这种方法很有效。但有两个特殊情况要注意。首先定义指针常量时会有点不同。因为常量定义一般是放在头文件中(许多源文件会包含它)除了指针所指的类型要定义成const外重要的是指针也经常要定义成const。例如要在头文件中定义一个基于char*的字符串常量你要写两次const:constchar*constauthorName="ScottMeyers"关于const的含义和用法特别是和指针相关联的问题参见条款。另外定义某个类(class)的常量一般也很方便只有一点点不同。要把常量限制在类中首先要使它成为类的成员为了保证常量最多只有一份拷贝还要把它定义为静态成员:classGamePlayer{private:staticconstintNUMTURNS=constantdeclarationintscoresNUMTURNSuseofconstant}还有一点正如你看到的上面的语句是NUMTURNS的声明而不是定义所以你还必须在类的实现代码文件中定义类的静态成员:constintGamePlayer::NUMTURNSmandatorydefinitiongoesinclassimplfile你不必过于担心这种小事。如果你忘了定义链接器会提醒你。旧一点的编译器会不接受这种语法因为它认为类的静态成员在声明时定义初始值是非法的而且类内只允许初始化整数类型(如:int,bool,char等)还只能是常量。在上面的语法不能使用的情况下可以在定义时赋初值:classEngineeringConstants{thisgoesintheclassprivate:headerfilestaticconstdoubleFUDGEFACTOR}thisgoesintheclassimplementationfileconstdoubleEngineeringConstants::FUDGEFACTOR=大多数情况下你只要做这么多。唯一例外的是当你的类在编译时需要用到这个类的常量的情况例如上面GamePlayer::scores数组的声明(编译过程中编译器一定要知道数组的大小)。所以为了弥补那些(不正确地)禁止类内进行整型类常量初始化的编译器的不足可以采用称之为“借用enum”的方法来解决。这种技术很好地利用了当需要int类型时可以使用枚举类型的原则所以GamePlayer也可以象这样来定义:classGamePlayer{private:enum{NUMTURNS=}"theenumhack"makesNUMTURNSasymbolicnameforintscoresNUMTURNSfine}除非你正在用老的编译器(即写于年之前)你不必借用enum。当然知道有这种方法还是值得的因为这种可以追溯到很久以前的时代的代码可是不常见的哟。回到预处理的话题上来。另一个普遍的#define指令的用法是用它来实现那些看起来象函数而又不会导致函数调用的宏。典型的例子是计算两个对象的最大值:#definemax(a,b)((a)>(b)(a):(b))这个语句有很多缺陷光想想都让人头疼甚至比在高峰时间到高速公路去开车还让人痛苦。无论什么时候你写了象这样的宏你必须记住在写宏体时对每个参数都要加上括号否则别人调用你的宏时如果用了表达式就会造成很大的麻烦。但是即使你象这样做了还会有象下面这样奇怪的事发生:inta=,b=max(a,b)a的值增加了次max(a,b)a的值只增加了次这种情况下max内部发生些什么取决于它比较的是什么值!幸运的是你不必再忍受这样愚笨的语句了。你可以用普通函数实现宏的效率再加上可预计的行为和类型安全这就是内联函数(见条款):inlineintmax(inta,intb){returna>ba:b}不过这和上面的宏不大一样因为这个版本的max只能处理int类型。但模板可以很轻巧地解决这个问题:template<classT>inlineconstTmax(constTa,constTb){returna>ba:b}这个模板产生了一整套函数每个函数拿两个可以转换成同种类型的对象进行比较然后返回较大的(常量)对象的引用。因为不知道T的类型返回时传递引用可以提高效率(见条款)。顺便说一句在你打算用模板写象max这样有用的通用函数时先检查一下标准库(见条款)看看他们是不是已经存在。比如说上面说的max你会惊喜地发现你可以后人乘凉:max是C标准库的一部分。有了const和inline你对预处理的需要减少了但也不能完全没有它。抛弃#include的日子还很远#ifdef#ifndef在控制编译的过程中还扮演重要角色。预处理还不能退休但你一定要计划给它经常放长假。条款:尽量用<iostream>而不用<stdioh>是的scanf和printf很轻巧很高效你也早就知道怎么用它们这我承认。但尽管他们很有用事实上scanf和printf及其系列还可以做些改进。尤其是他们不是类型安全的而且没有扩展性。因为类型安全和扩展性是C的基石所以你也要服从这一点。另外scanfprintf系列函数把要读写的变量和控制读写格式的信息分开来就象古老的FORTRAN那样。是该向五十年代说诀别的时候了!不必惊奇scanfprintf的这些弱点正是操作符>>和<<的强项:intiRationalrr是个有理数cin>>i>>rcout<<i<<r上面的代码要通过编译>>和<<必须是可以处理Rational类型对象的重载函数(可能要通过隐式类型转换)。如果没有实现这样的函数就会出错(处理int不用这样做因为它是标准用法)。另外编译器自己可以根据不同的变量类型选择操作符的不同形式所以不必劳你去指定第一个要读写的对象是int而第二个是Rational。另外在传递读和写的对象时采用的语法形式相同所以不必象scanf那样死记一些规定比如如果没有得到指针必须加上地址符而如果已经得到了指针又要确定不要加上地址符。这些完全可以交给C编译器去做。编译器没别的什么事好做的而你却不一样。最后要注意的是象int这样的固定类型和象Rational这样的自定义类型在读写时方式是一样的。而你用sacnf和printf试试看!你所写的表示有理数的类的代码可能象下面这样:classRational{public:Rational(intnumerator=,intdenominator=)private:intn,d分子分母friendostreamoperator<<(ostreams,constRationalr)}ostreamoperator<<(ostreams,constRationalr){s<<rn<<''<<rdreturns}上面的代码涉及到operator<<的一些微妙(但很重要)的用法这在本书其他地方详细讨论。例如:上面的operator<<不是成员函数(条款解释了为什么)而且传递给operator<<的不是Rational对象而是定义为const的对象的引用(参见条款)。operator>>的声明和实现也类似。尽管我不大愿意承认可有些情况下回到那些经过证明而且正确的老路上去还是很有意义的。第一有些iostream的操作实现起来比相应的Cstream效率要低所以不同的选择会给你的程序有可能(虽然不一定参见条款M)带来很大的不同。但请牢记这不是对所有的iostream而言只是一些特殊的实现参见条款M。第二在标准化的过程中iostream库在底层做了很多修改(参见条款)所以对那些要求最大可移植性的应用程序来说会发现不同的厂商遵循标准的程度也不同。第三iostream库的类有构造函数而<stdioh>里的函数没有在某些涉及到静态对象初始化顺序的时候如果可以确认不会带来隐患用标准C库会更简单实用。iostream库的类和函数所提供的类型安全和可扩展性的价值远远超过你当初的想象所以不要仅仅因为你用惯了<stdioh>而舍弃它。毕竟转换到iostream后你也不会忘掉<stdioh>。顺便说一句本条款的标题没有打印错我确实说的是<iostream>而非<iostreamh>。从技术上说其实没有<iostreamh>这样的东西标准化委员会在简化非C标准头文件时用<iostream>取代了它。他们这样做的原因在条款进行了解释。还必须知道的是如果编译器同时支持<iostream>和<iostreamh>那头文件名的使用会很微妙。例如如果使用了#include<iostream>,得到的是置于名字空间std(见条款)下的iostream库的元素如果使用#include<iostreamh>得到的是置于全局空间的同样的元素。在全局空间获取元素会导致名字冲突而设计名字空间的初衷正是用来避免这种名字冲突的发生。还有打字时<iostream>比<iostreamh>少两个字这也是很多人用它的原因。:)条款:尽量用new和delete而不用malloc和freemalloc和free(及其变体)会产生问题的原因在于它们太简单:他们不知道构造函数和析构函数。假设用两种方法给一个包含个string对象的数组分配空间一个用malloc另一个用new:    string*stringArray=    staticcast<string*>(malloc(*sizeof(string)))    string*stringArray=newstring其结果是stringArray确实指向的是可以容纳个string对象的足够空间但内存里并没有创建这些对象。而且如果你不从这种晦涩的语法怪圈(详见条款M和M的描述)里跳出来的话你没有办法来初始化数组里的对象。换句话说stringArray其实一点用也没有。相反stringArray指向的是一个包含个完全构造好的string对象的数组,每个对象可以在任何读取string的操作里安全使用。假设你想了个怪招对stringArray数组里的对象进行了初始化那么在你后面的程序里你一定会这么做:free(stringArray)deletestringArray参见条款:这里为什么要加上个""调用free将会释放stringArray指向的内存但内存里的string对象不会调用析构函数。如果string对象象一般情况那样自己已经分配了内存那这些内存将会全部丢失。相反当对stringArray调用delete时数组里的每个对象都会在内存释放前调用析构函数。既然new和delete可以这么有效地与构造函数和析构函数交互选用它们是显然的。把new和delete与malloc和free混在一起用也是个坏想法。对一个用new获取来的指针调用free或者对一个用malloc获取来的指针调用delete其后果是不可预测的。大家都知道“不可预测”的意思:它可能在开发阶段工作良好在测试阶段工作良好但也可能会最后在你最重要的客户的脸上爆炸。newdelete和mallocfree的不兼容性常常会导致一些严重的复杂性问题。举个例子<stringh>里通常有个strdup函数它得到一个char*字符串然后返回其拷贝:char*strdup(constchar*ps)返回ps所指的拷贝在有些地方C和C用的是同一个strdup版本所以函数内部是用malloc分配内存。这样的话一些不知情的C程序员会在调用strdup后忽视了必须对strdup返回的指针进行free操作。为了防止这一情况有些地方会专门为C重写strdup并在函数内部调用了new这就要求其调用者记得最后用delete。你可以想象这会导致多么严重的移植性问题因为代码中strdup以不同的形式在不同的地方之间颠来倒去。C程序员和C程序员一样对代码重用十分感兴趣。大家都知道有大量基于malloc和free写成的代码构成的C库都非常值得重用。在利用这些库时最好是你不用负责去free掉由库自己malloc的内存并且或者你不用去malloc库自己会free掉的内存这样就太好了。其实在C程序里使用malloc和free没有错只要保证用malloc得到的指针用free或者用new得到的指针最后用delete来操作就可以了。千万别马虎地把new和free或malloc和delete混起来用那只会自找麻烦。既然malloc和free对构造函数和析构函数一无所知把mallocfree和newdelete混起来用又象嘈杂拥挤的晚会那样难以控制那么你最好就什么时候都一心一意地使用new和delete吧条款:尽量使用C风格的注释旧的C注释语法在C里还可以用C新发明的行尾注释语法也有其过人之处。例如下面这种情形:if(a>b){inttemp=aswapaandba=bb=temp}假设你出于某种原因要注释掉这个代码块。从软件工程的角度看写这段代码的程序员也做得很好他最初的代码里也写了一个注释以解释代码在做什么。用C形式的句法来注释掉这个程序块时嵌在里面的最初的注释不受影响但如果选择C风格的注释就会发生严重的错误:if(a>b){*inttemp=a*swapaandb*a=bb=temp*}请注意嵌在代码块里的注释是怎么无意间使本来想注释掉整个代码块的注释提前结束的。C风格的注释当然还有它存在的价值。例如它们在C和C编译器都要处理的头文件中是无法替代的。尽管如此只要有可能你最好尽量用C风格的注释。值得指出的是有些老的专门为C写的预处理程序不知道处理C风格的注释所以象下面这种情形时事情就不会象预想的那样:#defineLIGHTSPEEDemsec(inavacuum)对于不熟悉C的预处理程序来说行尾的注释竟然成为了宏的一部分!当然正象条款所说的那样你无论如何也不会用预处理来定义常量的。内存管理C中涉及到的内存的管理问题可以归结为两方面:正确地得到它和有效地使用它。好的程序员会理解这两个问题为什么要以这样的顺序列出。因为执行得再快、体积再小的程序如果它不按你所想象地那样去执行那也一点用处都没有。“正确地得到”的意思是正确地调用内存分配和释放程序而“有效地使用”是指写特定版本的内存分配和释放程序。这里“正确地得到”显得更重要一些。然而说到正确性C其实从C继承了一个很严重的头疼病那就是内存泄露隐患。虚拟内存是个很好的发明但虚拟内存也是有限的并不是每个人都可以最先抢到它。在C中只要用malloc分配的内存没有用free返回就会产生内存泄露。在C中肇事者的名字换成了new和delete但情况基本上是一样的。当然因为有了析构函数的出现情况稍有改善因为析构函数为所有将被摧毁的对象提供了一个方便的调用delete的场所。但这同时又带来了更多的烦恼因为new和delete是隐式地调用构造函数和析构函数的。而且因为可以在类内和类外自定义new和delete操作符这又带来了复杂性增加了出错的机会。下面的条款(还有条款M)将告诉你如何避免产生那些普遍发生的问题。条款:对应的new和delete要采用相同的形式下面的语句有什么错?string*stringArray=newstringdeletestringArray一切好象都井然有序一个new对应着一个delete然而却隐藏着很大的错误:程序的运行情况将是不可预测的。至少stringArray指向的个string对象中的个不会被正确地摧毁因为他们的析构函数永远不会被调用。用new的时候会发生两件事。首先内存被分配(通过operatornew函数详见条款和条款M)然后为被分配的内存调用一个或多个构造函数。用delete的时候也有两件事发生:首先为将被释放的内存调用一个或多个析构函数然后释放内存(通过operatordelete函数详见条款和M)。对于delete来说会有这样一个重要的问题:内存中有多少个对象要被删除?答案决定了将有多少个析构函数会被调用。这个问题简单来说就是:要被删除的指针指向的是单个对象呢还是对象数组?这只有你来告诉delete。如果你在用delete时没用括号delete就会认为指向的是单个对象否则它就会认为指向的是一个数组:string*stringPtr=newstringstring*stringPtr=newstringdeletestringPtr删除一个对象deletestringPtr删除对象数组如果你在stringPtr前加了""会怎样呢?答案是:那将是不可预测的如果你没在stringPtr前没加上""又会怎样呢?答案也是:不可预测。而且对于象int这样的固定类型来说结果也是不可预测的即使这样的类型没有析构函数。所以解决这类问题的规则很简单:如果你调用new时用了调用delete时也要用。如果调用new时没有用那调用delete时也不要用。在写一个包含指针数据成员并且提供多个构造函数的类时牢记这一规则尤其重要。因为这样的话你就必须在所有初始化指针成员的构造函数里采用相同的new的形式。否则析构函数里将采用什么形式的delete呢?关于这一话题的进一步阐述参见条款。这个规则对喜欢用typedef的人来说也很重要因为写typedef的程序员必须告诉别人用new创建了一个typedef定义的类型的对象后该用什么形式的delete来删除。举例如下:typedefstringAddressLines一个人的地址共行每行一个string因为AddressLines是个数组使用new:string*pal=newAddressLines注意"newAddressLines"返回string*,和"newstring"返回的一样delete时必须以数组形式与之对应:deletepal错误!deletepal正确为了避免混乱最好杜绝对数组类型用typedefs。这其实很容易因为标准C库(见条款)包含有stirng和vector模板使用他们将会使对数组的需求减少到几乎零。举例来说AddressLines可以定义为一个字符串(string)的向量(vector)即AddressLines可定义为vector<string>类型。条款:析构函数里对指针成员调用delete大多数情况下执行动态内存分配的的类都在构造函数里用new分配内存然后在析构函数里用delete释放内存。最初写这个类的时候当然不难做你会记得最后对在所有构造函数里分配了内存的所有成员使用delete。然而这个类经过维护、升级后情况就会变得困难了因为对类的代码进行修改的程序员不一定就是最早写这个类的人。而增加一个指针成员意味着几乎都要进行下面的工作:在每个构造函数里对指针进行初始化。对于一些构造函数如果没有内存要分配给指针的话指针要被初始化为(即空指针)。删除现有的内存通过赋值操作符分配给指针新的内存。在析构函数里删除指针。如果在构造函数里忘了初始化某个指针或者在赋值操作的过程中忘了处理它问题会出现得很快很明显所以在实践中这两个问题不会那么折磨你。但是如果在析构函数里没有删除指针它不会表现出很明显的外部症状。相反它可能只是表现为一点微小的内存泄露并且不断增长最后吞噬了你的地址空间导致程序夭折。因为这种情况经常不那么引人注意所以每增加一个指针成员到类里时一定要记清楚。另外删除空指针是安全的(因为它什么也没做)。所以在写构造函数赋值操作符或其他成员函数时类的每个指针成员要么指向有效的内存要么就指向空那在你的析构函数里你就可以只用简单地delete掉他们而不用担心他们是不是被new过。当然对本条款的使用也不要绝对。例如你当然不会用delete去删除一个没有用new来初始化的指针而且就象用智能指针对象时不用劳你去删除一样你也永远不会去删除一个传递给你的指针。换句话说除非类成员最初用了new,否则是不用在析构函数里用delete的。说到智能指针这里介绍一种避免必须删除指针成员的方法即把这些成员用智能指针对象来代替比如C标准库里的autoptr。想知道它是如何工作的看看条款M和M。条款:预先准备好内存不够的情况operatornew在无法完成内存分配请求时会抛出异常(以前的做法一般是返回一些旧一点的编译器还这么做。你愿意的话也可以把你的编译器设置成这样。关于这个话题我将推迟到本条款的结尾处讨论)。大家都知道处理内存不够所产生的异常真可以算得上是个道德上的行为但实际做起来又会象刀架在脖子上那样痛苦。所以你有时会不去管它也许一直没去管它。但你心里一定还是深深地隐藏着一种罪恶感:万一new真的产生了异常怎么办?你会很自然地想到处理这种情况的一种方法即回到以前的老路上去使用预处理。例如C的一种常用的做法是定义一个类型无关的宏来分配内存并检查分配是否成功。对于C来说这个宏看起来可能象这样:#defineNEW(PTR,TYPE)try{(PTR)=newTYPE}catch(std::badalloc){assert()}“慢!std::badalloc是做什么的?”你会问。badalloc是operatornew不能满足内存分配请求时抛出的异常类型std是badalloc所在的名字空间(见条款)的名称。“好!”你会继续问“assert又有什么用?”如果你看看标准C头文件<asserth>(或与它相等价的用到了名字空间的版本<cassert>见条款)就会发现assert是个宏。这个宏检查传给它的表达式是否非零如果不是非零值就会发出一条出错信息并调用abort。assert只是在没定义标准宏NDEBUG的时候即在调试状态下才这么做。在产品发布状态下即定义了NDEBUG的时候assert什么也不做相当于一条空语句。所以你只能在调试时才能检查断言(assertion))。NEW宏不但有着上面所说的通病即用assert去检查可能发生在已发布程序里的状态(然而任何时候都可能发生内存不够的情况)同时它还在C里有另外一个缺陷:它没有考虑到new有各种各样的使用方式。例如想创建类型T对象一般有三种常见的语法形式你必须对每种形式可能产生的异常都要进行处理:newTnewT(constructorarguments)newTsize这里对问题大大进行了简化因为有人还会自定义(重载)operatornew所以程序里会包含任意个使用new的语法形式。那么怎么办?如果想用一个很简单的出错处理方法可以这么做:当内存分配请求不能满足时调用你预先指定的一个出错处理函数。这个方法基于一个常规即当operatornew不能满足请求时会在抛出异常之前调用客户指定的一个出错处理函数一般称为newhandler函数。(operatornew实际工作起来要复杂一些详见条款)指定出错处理函数时要用到setnewhandler函数它在头文件<new>里大致是象下面这样定义的:typedefvoid(*newhandler)()newhandlersetnewhandler(newhandlerp)throw()可以看到newhandler是一个自定义的函数指针类型它指向一个没有输入参数也没有返回值的函数。setnewhandler则是一个输入并返回newhandler类型的函数。setnewhandler的输入参数是operatornew分配内存失败时要调用的出错处理函数的指针返回值是setnewhandler没调用之前就已经在起作用的旧的出错处理函数的指针。可以象下面这样使用setnewhandler:functiontocallifoperatornewcan'tallocateenoughmemoryvoidnoMoreMemory(){cerr<<"Unabletosatisfyrequestformemoryn"abort()}intmain(){setnewhandler(noMoreMemory)int*pBigDataArray=newint}假如operatornew不能为,,个整数分配空间noMoreMemory将会被调用程序发出一条出错信息后终止。这就比简单地让系统内核产生错误信息来结束程序要好。(顺便考虑一下假如cerr在写错误信息的过程中要动态分配内存那将会发生什么)operatornew不能满足内存分配请求时newhandler函数不只调用一次而是不断重复直至找到足够的内存。实现重复调用的代码在条款里可以看到这里我用描述性的的语言来说明:一个设计得好的newhandler函数必须实现下面功能中的一种。产生更多的可用内存。这将使operatornew下一次分配内存的尝试有可能获得成功。实施这一策略的一个方法是:在程序启动时分配一个大的内存块然后在第一次调用newhandler时释放。释放时伴随着一些对用户的警告信息如内存数量太少下次请求可能会失败除非又有更多的可用空间。安装另一个不同的newhandler函数。如果当前的newhandler函数不能产生更多的可用内存可能它会知道另一个newhandler函数可以提供更多的资源。这样的话当前的newhandler可以安装另一个newhandler来取代它(通过调用setnewhandler)。下一次operatornew调用newhandler时会使用最近安装的那个。(这一策略的另一个变通办法是让newhandler可以改变它自己的运行行为那么下次调用时它将做不同的事。方法是使newhandler可以修改那些影响它自身行为的静态或全局数据。)卸除newhandler。也就是传递空指针给setnewhandler。没有安装newhandleroperatornew分配内存不成功时就会抛出一个标准的std::badalloc类型的异常。抛出std::badalloc或从std::badalloc继承的其他类型的异常。这样的异常不会被operatornew捕捉所以它们会被送到最初进行内存请求的地方。(抛出别的不同类型的异常会违反operatornew异常规范。规范中的缺省行为是调用abort所以newhandler要抛出一个异常时一定要确信它是从std::badalloc继承来的。想更多地了解异常规范参见条款M。)没有返回。典型做法是调用abort或exit。abortexit可以在标准C库中找到(还有标准C库参见条款)。上面的选择给了你实现newhandler函数极大的灵活性。处理内存分配失败的情况时采取什么方法取决于要分配的对象的类:classX{public:staticvoidoutOfMemory()}classY{public:staticvoidoutOfMemory()}X*p=newX若分配成功调用X::outOfMemoryY*p=newY若分配不成功调用Y::outOfMemoryC不支持专门针对于类的newhandler函数而且也不需要。你可以自己来实现它只要在每个类中提供自己版本的setnewhandler和operatornew。类的setnewhandler可以为类指定newhandler(就象标准的setnewhandler指定全局newhandler一样)。类的operatornew则保证为类的对象分配内存时用类的newhandler取代全局newhandler。假设处理类X内存分配失败的情况。因为operatornew对类型X的对象分配内存失败时每次都必须调用出错处理函数所以要在类里声明一个newhandler类型的静态成员。那么类X看起来会象这样:classX{public:staticnewhandlersetnewhandler(newhandlerp)staticvoid*operatornew(sizetsize)private:staticnewhandlercurrentHandler}类的静态成员必须在类外定义。因为想借用静态对象的缺省初始化值所以定义X::currentHandler时没有去初始化。newhandlerX::currentHandler缺省设置currentHandler为(即)类X中的setnewhandler函数会保存传给它的任何指针并返回在调用它之前所保存的任何指针。这正是标准版本的setnewhandler所做的:newhandlerX::setnewhandler(newhandlerp){newhandleroldHandler=currentHandlercurrentHandler=preturnoldHandler}最后看看X的operatornew所做的:调用标准setnewhandler函数输入参数为X的出错处理函数。这使得X的newhandler函数成为全局newhandler函数。注意下面的代码中用了"::"符号显式地引用std空间(标准setnewhandler函数就存在于std空间)。调用全局operatornew分配内存。如果第一次分配失败全局operatornew会调用X的newhandler因为它刚刚(见)被安装成为全局newhandler。如果全局operatornew最终未能分配到内存它抛出std::badalloc异常X的operatornew会捕捉到它。X的operatornew然后恢复最初被取代的全局newhandler函数最后以抛出异常返回。假设全局operatornew为类型X的对象分配内存成功,X的operatornew会再次调用标准setnewhandler来恢复最初的全局出错处理函数。最后返回分配成功的内存的指针。C是这么做的:void*X::operatornew(sizetsize){newhandlerglobalHandler=安装X的newhandlerstd::setnewhandler(currentHandler)void*memorytry{尝试分配内存memory=::operatornew(size)}catch(std::badalloc){恢复旧的newhandlerstd::setnewhandler(globalHandler)throw抛出异常}std::setnewhandler(globalHandler)恢复旧的newhandlerreturnmemory}如果你对上面重复调用std::setnewhandler看不顺眼可以参见条款M来除去它们。使用类X的内存分配处理功能时大致如下:voidnoMoreMemory()X的对象分配内存失败时调用的newhandler函数的声明X::setnewhandler(noMoreMemory)把noMoreMemory设置为X的newhandling函数X*px=newX如内存分配失败调用noMoreMemorystring*ps=newstring如内存分配失败调用全局newhandling函数X::setnewhandler()设X的newhandling函数为空X*px=newX如内存分配失败立即抛出异常(类X没有newhandling函数)你会注意到处理以上类似情况如果不考虑类的话实现代码是一样的这就很自然地想到在别的地方也能重用它们。正如条款所说明的继承和模板可以用来设计可重用代码。在这里我们把两种方法结合起来使用从而满足了你的要求。你只要创建一个“混合风格”(mixinstyle)的基类这种基类允许子类继承它某一特定的功能这里指的是建立一个类的newhandler的功能。之所以设计一个基类是为了让所有的子类可以继承setnewhandler和operatornew功能而设计模板是为了使每个子类有不同的currentHandler数据成员。这听起来很复杂不过你会看到代码其实很熟悉。区别只不过是它现在可以被任何类重用了。template<classT>提供类setnewhandler支持的classNewHandlerSupport{“混合风格”的基类public:staticnewhandlersetnewhandler(newhandlerp)staticvoid*operatornew(sizetsize)private:staticnewhandlercurrentHandler}template<classT>newhandlerNewHandlerSupport<T>::setnewhandler(newhandlerp){newhandleroldHandler=currentHandlercurrentHandler=preturnoldHandler}template<classT>void*NewHandlerSupport<T>::operatornew(sizetsize){newhandlerglobalHandler=std::setnewhandler(currentHandler)void*memorytry{memory=::operatornew(size)}catch(std::badalloc){std::setnewhandler(globalHandler)throw}std::setnewhandler(globalHandler)returnmemory}thissetseachcurrentHandlertotemplate<classT>newhandlerNewHandlerSupport<T>::currentHandler有了这个模板类对类X加上setnewhandler功能就很简单了:只要让X从newHandlerSupport<X>继承:noteinheritancefrommixinbaseclasstemplate(Seemyarticleoncountingobjectsforinformationonwhyprivateinheritancemightbeprefera

用户评论(0)

0/200

精彩专题

上传我的资料

每篇奖励 +2积分

资料评价:

/49
仅支持在线阅读

意见
反馈

立即扫码关注

爱问共享资料微信公众号

返回
顶部