谁是黑客(1): 关于IIS LOG的MinFileBytesPerSec和Timer_ConnectionIdle错误

     前些天发现自己的网站无法访问,询问机房这边,说是机器最近常死机,我就把网站迁移到一个朋友的主机上, 结果没过几天机器又挂了,问朋友的机房那边说是硬件防火墙被攻击了而死掉了,详细情况不知。看来不是硬件问题,多半是被SYN FLOOD或者CC攻击了。恰好原来的机房说最近购买了新的防火墙,我又放了回去。

      既然不是硬件问题而可能是攻击,我就开始检查IIS log了,发现 IIS 里面很多Timer_ConnectionIdle和Timer_MinBytesPerSecond的错误,到网络上google了一下,常见说法是说错误是因为IIS的设置不当引起的,是因为连接超时时间设置太小,解决方法是设置连接超时为600秒,把MinFileBytesPerSec的设置从240修改到0(相当于关掉该设置)。觉得这些解决方法都有问题,假如车辆防盗警报经常响,正确的解决方法是看看有谁常来打你车子的主意,或者把车子放在更安全的地方,而绝对不是关掉警报。

      因为HTTP服务需要占用TCP连接,而TCP连接时是需要占用系统资源的,而且IIS为每个连接也需要分配相应的资源(至少一个FSM是要给的)。目前的主机能够处理上万的连接就可以说是软硬件设计都很不错了(可以参见C10K )。假如恶意人员通过一台或者多台机器发起大量的连接,而不请求内容(这样不需要消耗多少攻击机器的带宽),就可以大量消耗服务器资源而达到拒绝服务的目的。

     所以 IIS 需要关闭长时间非活动的连接,这个就是Timer_ConnectionIdle 的错误由来。

     既然盾牌改进了,当然矛也要发展一下,攻击者可以给服务器故意缓慢的发送和接收内容而消耗服务器的资源,这样可以避免服务器对于Timer_ConnectionIdle 的保护,相应的IIS的防范就是 MinFileBytesPerSec 设置,MinFileBytesPerSec 属性通过以最小的数据量保持连接,来禁止恶意的或软件工作不正常的客户端消耗资源。如果吞吐量低于 MinFileBytesPerSec 设置的值,则终止连接。LOG里面就会显示Timer_MinBytesPerSecond错误(一些Timer_MinBytesPerSecond错误是因为 windows 2003 的http.sys错误引起的,解决方式是打上最新 ServicePack : http://support.microsoft.com/kb/919797   http://support.microsoft.com/kb/919797/en-us )

      所以说这些设置都是用来保护IIS服务器的,可以一定程度上抵御一些恶意的行为消耗服务器的资源,所以我反而将IIS连接超时从原来的600秒改到了30秒,就让攻击来得更加猛烈些吧!!不过我还是很纳闷: 网站不大,所以我不可能去财消灾。无怨无仇的,谁这么无聊啊?!

 

最后发现结果挺搞笑的,欢迎看后继文章。(最近挺忙, 续集暂时不会出来。希望解决这种错误的同志:假如不是攻击的话,打上微软的补丁包就好了)

Posted in 未分类 | Tagged | 3 Comments

也说缓存

看了蝈蝈俊.net的《理解缓存》,觉得真的是一个对于web applcation 缓存应用的好文,难得的是覆盖了冰山海面下的部分。我现在做的应用可以说和缓存打的交道也不少(不过不是web应用),也写些东西来分享给大家。

1.缓存是什么?

在我看来,缓存是通过存储中间结果,缩短访问路径来减少开销,提高性能的方法。这个概括未必最科学全面,也不够具体。我们来看看一个http 动态页面访问的例子:

访问路径是 : 数据库->应用数据集->内存对象->动态页面->HTTP服务器->用户浏览器

一个简单的访问,中间经过了多个环节,我们称这些环节为访问路径,我们来看看哪些地方可以采用缓存:

  1. HTTP服务器->用户浏览器,大家都知道浏览器都有本地缓存,浏览过的页面图片脚本等都会根据http header还有html的相关指令临时保存在本地硬盘里面,假如再次访问,访问路径就变成了”本地硬盘缓存->用户浏览器”, 浏览的环节大幅度减少,性能也提高了。在这个环节,经常还使用带缓存的代理服务器来提高性能。
  2. 动态页面->HTTP服务器,这里有多种方式, 比如动态页面静态化,目前大量的大型网站使用这种方式。还有WEB服务器根据一定规则缓存整个动态页面,比如asp.net的Page Cache。这里的访问路径变成了”缓存页面->HTTP服务器->用户浏览器”
  3. 本地数据集->内存对象->动态页面。常见的就是缓存数据集还有对象,这个是ASP.NET cache里面相对浓墨重彩的部分,也是Memcached发力的侧重点, 也就不多说了。
  4. 数据库->应用数据集,不少数据库实现都有查询缓存

这里缓存都在访问路径中的环节存储了中间结果,用来减少相应的开销

2.缓存本身的开销

缓存本身也是有性能开销的,一种是将内存存储到缓存中开销,一种是将内容取出来的开销。另外,缓存往往还要付出空间上的开销。另外还要付出系统复杂度的开销,这增加了开发和维护成本。

大家也听说过IE缓存太大了或者是文件系统碎片太多以后,可能相反会拖累浏览速度,测试我倒是没有做过,但是的确是完全可能的。也就是说,缓存的开销可能会不缓存而是直接访问还要大,这就是大家不想看到的了。

3. 缓存的目的

其实前面已经说过缓存是为了减少开销,提高性能,这不就是缓存的目的吗?这倒是没错,但是也不尽然。

因为开销是一个很笼统的词,具体点有CPU开销,磁盘IO开销,网络开销,数据库访问开销等,缓存对于性能的优化,除了一些大众化的优化措施以外,还得有的放矢。

以前学习写程序,大家一定都听说过什么时间换空间,空间换时间,到底什么时候要拿空间换时间,什么时候要拿时间换空间,只能看具体应用了。前面说过缓存也有开销,其实缓存就是拿某些开销换取其他开销的下降而已。比如说动态页面静态化是一些大型网站常见的优化方法,他付出了磁盘的空间和读写开销,来换取更低的CPU消耗(不用解析动态页面)和数据库访问。有些网站每天访问量没有多少,却频繁生成和更新静态页面,同时还在服务器上做下载,本来磁盘就不堪重负,这下更加是雪上加霜,可以说是缓存优化的反面例子。或者是本来内存不大,磁盘swap很多影响性能,但是却使用大量内存做页面和对象缓存,也是反面的例子。

所以说不能盲目的进行缓存优化,一个系统,性能出现了问题,或者将来可能出现问题,性能总会有一个或者若干个瓶颈,我们要做的就是平衡或者削弱这个瓶颈,缓存是重要的手段。

所以缓存的目的是针对几个主要指标,兼顾若干个其他指标,来尽量实现低开销。

比如,数据库的CPU较高,那么一般是复杂的查询或者是存储过程导致的,在前面的各个环节进行缓存优化,比如缓存数据集和内存对象,都是好的解决方法,缓存整个页面也是个好方法,但是缓存页面要付出更多的空间开销,在某些情况下,缓存数据集或者内存对象已经够了。

假如WEB服务器的CPU较高,往往是因为动态页面处理造成的,找出开销大的处理,将处理的结果对象缓存,或者是页面静态化是不错的方法,而缓存数据库结果集往往收效不大。

4. 啥样的缓存才是好缓存?

蝈蝈俊认为是命中率最高的缓存最好。我做的领域是streaming server的磁盘IO缓存和CDN的网络边缘缓存,瓶颈就是磁盘或者网络IO,这种时候,命中率就是硬道理。

但是对于web服务器来讲,影响性能的因素很多,不同的内容访问开销相差很大,什么是好缓存,虽然命中率是极为重要的指标,但是还得要综合缓存的开销,原始的访问路径/开销和性能瓶颈来综合评价。也就是说不同的应用侧重不同,跑的硬件条件和瓶颈也不一样,很难有一个简单的指标。

比如缓存一个命中率稍低,但是原始访问开销很大的对象(比如要经过复杂查询和处理的对象) 比一个命中率较高,但是原始访问开销很小的对象要划得来。我觉得假如有一个”加权命中率”会更好,原始访问开销大的对象,要给与更高的权值,再进行命中率的计算。

假如一个缓存策略可以减少某个方面的性能开销,但是却带来了新的性能瓶颈,那么它也不是个好的缓存实现。比如磁盘IO紧张,页面更新非常频繁的情况下,静态页面缓存往往就不是一个好方法(好像没有用静态化页面做聊天室的吧:->)。

到底什么是好的缓存实现呢?前面也讲过了缓存的目的,我觉得充分利用了软硬件条件,消灭了性能瓶颈的就是好缓存。

5. 如何进行缓存优化

进行缓存优化,第一是要找到性能瓶颈,第二是找到瓶颈有关的应用部分。

性能瓶颈一般还是好找,系统有那么多性能计数器,数据库n多的调优工具,查询优化分析器。好好对照厂商的技术资料和google大法,很容易找到是CPU 内存还是IO限制了性能提高。

然后就是应用针对性能瓶颈的优化,有一个2/8定律,就是说80%的性能都消耗在20%的处理中,这20%也分为两种,一种是访问比例很高的,一种是开销贼大的,当然两个都占了,就更加没说的了。我们的任务就是集中火力提升这20%的性能,缓存往往是重要的方法。

寻找开销大的操作是个细活。我们在软件设计阶段可以预见瓶颈的部位,在出现问题的时候可以猜测出现瓶颈的部位,但是除非对行业模型,相关的架构性能还有一些性能细节非常了解(也就是说你实践过n次了),否则当初的预见和猜测都可能有较大偏差的,性能优化没有银弹,实践出真知。要多多使用工具进行分析,假如在系统里本身就有一些性能计数,可以在线或者离线提取就更好

Posted in 未分类 | Tagged | 9 Comments

从msn和qq文件传输速度说开去

     发现很多情况下,msn传输文件比qq要慢,倒不是说msn没有快的时候,但是大部分的时候是真的比QQ慢,连我这种神经比较大条的人都注意到了,google了一下,早就有人做了解答,基本上就是说msn传输文件是使用TCP,而QQ使用UDP,剩下的事情就是论证TCP传输文件没有UDP快。大概的就是下面的几个观点:

1. TCP是可靠的,需要验证数据是否到达和是否正确,而UDP是不可靠的,少做了很多事情,所以MSN的文件传输比QQ慢。

我看了当时就想笑,UDP传输不可靠,但是应用层肯定会写代码作些和TCP的可靠传输差不多的事情。也用了QQ不少时日了,从来也没有发现传输文件有问题的,用UDP作协议也很久了,不做应用层验证重传的代码,我还真不敢写。这个理由,失败。

2. TCP建立连接需要3次握手,而UDP不需要,所以TCP慢。

3次握手这个事实倒是千真万确,还好我没有那么容易被忽悠,两个人谈话之前要握握手的确要稍微费上几秒钟,但是这个关谈话的语速啥事情?假如网络的ping值达到300ms,各位看官喜欢网络游戏的,估计都不会玩了,否则垂死的boss会高兴的发现你忽然变成了木偶可以随便殴打不还手,最后你只能骂骂电信网通然后复活几分钟后又是一条好汉。但是对于TCP,这样的ping值,3次握手一般都不需要1秒钟,把这个定为文件慢慢传的罪魁祸首,似乎太不靠谱了,这个理由还是失败。

3. TCP一旦建立链接,路由就确定了,而UDP是不确定的路由方式,谁速度快走谁的线路。

这样说就说明没有作者好好理解TCP/IP协议了,TCP的链路只是一个逻辑的,又没有建立物理链路,下面跑的还是IP包,这个包走这条路,那个包完全可能走另外的路,这点TCP和UDP没有两样。理由继续失败。

4. msn服务器在国外?

有些道理,但是我听一个美国的朋友说他也喜欢用QQ传文件的。

 

     那到底是怎么回事呢?是因为微软没有做好?(题外话,个人的确觉得MSN相比QQ的飞速进步而显得动作迟缓)QQ的Fans开始摩拳擦掌,一些不那么喜欢M$的估计就要开始丢板砖了。不管立场如何,事实还是要探寻一下,本着不求甚解,薄积薄发,浅入浅出的精神,我认为有几个可能原因:

1. 两个传文件客户端都在NAT后面的时候 (你不知道NAT啥意思?比如多个人通过路由器共享一个猫上网,那么你们一般就是在NAT后面了),从技术实现上讲,TCP在这种情况下穿越NAT比UDP麻烦得多。UDP只要开始几个穿越NAT的协商包需要服务器转一下,后面的文件数据可以两个客户端之间直接传输搞定,但是一般TCP就只能全程由服务器中转了,你说哪一个会比较快? 为什么TCP需要服务器中转?先看看NAT吧,听说有高人可以用raw sock搞定,反正我没有中间服务器搞不定。

 

2. 但是即使上面的条件不成立,msn还是一般比QQ慢的。问题还是在出在前面提到的验证数据可靠性上面,TCP是可靠的,UDP是不可靠的,但是用UDP做传输文件这档子事情,肯定要在应用层写一个验证的协议,否则传过来的文件缺胳膊少腿,会被用户骂死的。说是协议,其实也不难,打个比方吧:

    long long ago,贾宝玉在北京,林黛玉在长沙,怎么互诉衷肠呢,派家丁送信!路途遥远,怎么知道信收到没有?打电话问?那时候发明了这个就不用送信了,只能看家丁是否带了回信来了。假如发现一个家丁一个月还没有回,那就多半迷路堵车遭遇山贼或者开小差到扬州花差快活去了,再派一个人送吧! TCP就是这么做的,UDP在应用层协议一般也需要这么做,但是实现上有时候往往有区别。

    北京到长沙之间的路,并不是只有一个人跑的,常常会发生拥堵,假如发现家丁好久没有回了,TCP版的贾宝玉再派人送信是要的,但是他会比较识大体,他会少写信,降低发送速度,原来一天一封,现在可能一周一封了。他想,大家假如都这样,路就不会那么拥挤了。这做法很有道理,假如塞车了大家都想见缝插针,只能越来越塞,最后大家都动不了,还不如彼此容让慢慢排队。而UDP版本的贾宝玉假如也这么集体主义,那么他就叫做TCP友好流,就假如它只管自己拼命挤,就是非TCP友好的。

    所有的TCP协议实现假如发现拥塞,都会马上降低自己的发送速度。假如基于UDP的协议不这么做的话,原来拥塞的IP包加上重发的包,网路只会越来越拥塞,最后所有的坚持集体主义的TCP流都会做出牺牲,把带宽让给一些非TCP友好的UDP流。所以除非网络状况非常好,否则TCP是拼不过非TCP友好的协议的。

    我怀疑(仅仅是怀疑而已,我也没有条件和时间验证),QQ的文件传输机制是不那么TCP友好的,它抢带宽更加”我能”,这样虽然对于整个网络负荷不是什么好事,但是对于单个用户,就见仁见智了,就好像大家看待多线程下载或者p2p一样。

Posted in 未分类 | Tagged | 30 Comments

More Effective 条款27(上):要求或禁止在堆中产生对象(上)(ZHC)

本文含有图片,无法贴上,请下载WORD文档阅读下载

有时你想这样管理某些对象,要让某种类型的对象能够自我销毁,也就是能够“delete this.” 很明显这种管理方式需要此类型对象要被分配在堆中。而其它一些时候你想获得一种保障:“不在堆中分配对象,从而保证某种类型的类不会发生内存泄漏。”如果你在嵌入式系统上工作,就有可能遇到这种情况,发生在嵌入式系统上的内存泄漏是极其严重的,其堆空间是非常珍贵的。有没有可能编写出代码来要求或禁止在堆中产生对象(heap-based object)呢?通常是可以的,不过这种代码也会把“on the heap”的概念搞得比你脑海中所想的要模糊。

要求在堆中建立对象

让我们先从必须在堆中建立对象开始说起。为了执行这种限制,你必须找到一种方法禁止以调用“new”以外的其它手段建立对象。这很容易做到。非堆对象(non-heap object)在定义它的地方被自动构造,在生存时间结束时自动被释放,所以只要禁止使用隐式的构造函数和析构函数,就可以实现这种限制。

把这些调用变得不合法的一种最直接的方法是把构造函数和析构函数声明为private。这样做副作用太大。没有理由让这两个函数都是private。最好让析构函数成为private,让构造函数成为public。处理过程与条款26相似,你可以引进一个专用的伪析构函数,用来访问真正的析构函数。客户端调用伪析构函数释放他们建立的对象。

例如,如果我们想仅仅在堆中建立代表unlimited precision numbers无限精确度数字的对象,可以这样做

class UPNumber {

public:

  UPNumber();

  UPNumber(int initValue);

  UPNumber(double initValue);

  UPNumber(const UPNumber& rhs);

 

  // 伪析构函数 (一个const 成员函数, 因为

  // 即使是const对象也能被释放。)

  void destroy() const { delete this; }

 

 

 

private:

  ~UPNumber();

};

然后客户端这样进行程序设计:

UPNumber n;                          // 错误! (在这里合法,但是

                                     // 当它的析构函数被隐式地

                                     // 调用时,就不合法了)

 

UPNumber *p = new UPNumber;          //正确

 

 

delete p;                            // 错误! 试图调用

                                     // private 析构函数

 

p->destroy();                        // 正确

另一种方法是把全部的构造函数都声明为private。这种方法的缺点是一个类经常有许多构造函数,类的作者必须记住把它们都声明为private。否则如果这些函数就会由编译器生成,构造函数包括拷贝构造函数,也包括缺省构造函数;编译器生成的函数总是public(参见Effecitve C++ 条款45)。因此仅仅声明析构函数为private是很简单的,因为每个类只有一个析构函数。

通过限制访问一个类的析构函数或它的构造函数来阻止建立非堆对象,但是在条款26已经说过,这种方法也禁止了继承和包容(containment):

class UPNumber { … };              // 声明析构函数或构造函数

                                     // private

 

class NonNegativeUPNumber:

  public UPNumber { … };           // 错误! 析构函数或

                                     //构造函数不能编译

 

class Asset {

private:

  UPNumber value;

                                  // 错误! 析构函数或

                                     //构造函数不能编译

};

这些困难不是不能克服的。通过把UPNumber的析构函数声明为protected(同时它的构造函数还保持public)就可以解决继承的问题,需要包含UPNumber对象的类可以修改为包含指向UPNumber的指针:

class UPNumber { … };              // 声明析构函数为protected

 

class NonNegativeUPNumber:

  public UPNumber { … };           // 现在正确了; 派生类

                                     // 能够访问

                                     // protected 成员

 

class Asset {

public:

  Asset(int initValue);

  ~Asset();

 

 

private:

  UPNumber *value;

};

 

Asset::Asset(int initValue)

: value(new UPNumber(initValue))      // 正确

{ … }

 

Asset::~Asset()

{ value->destroy(); }                 // 也正确

判断一个对象是否在堆中

如果我们采取这种方法,我们必须重新审视一下“在堆中”这句话的含义。上述粗略的类定义表明一个非堆的NonNegativeUPNumber对象是合法的:

NonNegativeUPNumber n;                // 正确

那么现在NonNegativeUPNumber对象n中的UPNumber部分也不在堆中,这样说对么?答案要依据类的设计和实现的细节而定,但是让我们假设这样说是不对的,所有UPNumber对象 —即使是做为其它派生类的基类—也必须在堆中。我们如何能强制执行这种约束呢?

没有简单的办法。UPNumber的构造函数不可能判断出它是否做为堆对象的基类而被调用。也就是说对于UPNumber的构造函数来说没有办法侦测到下面两种环境的区别:

NonNegativeUPNumber *n1 =

  new NonNegativeUPNumber;            // 在堆中

 

NonNegativeUPNumber n2;               //不再堆中

不过你可能不相信我。也许你想你能够在new操作符、operator newnew 操作符调用的构造函数的相互作用中玩些小把戏(参见条款8)。可能你认为你比他们都聪明,可以这样修改UPNumber,如下所示:

class UPNumber {

public:

  // 如果建立一个非堆对象,抛出一个异常

  class HeapConstraintViolation {};

 

  static void * operator new(size_t size);

 

  UPNumber();

 

 

private:

  static bool onTheHeap;                 //在构造函数内,指示

                                         // 对象是否被构造在

                                      // 堆上

 

};

 

// obligatory definition of class static

bool UPNumber::onTheHeap = false;

 

void *UPNumber::operator new(size_t size)

{

  onTheHeap = true;

  return ::operator new(size);

}

 

UPNumber::UPNumber()

{

  if (!onTheHeap) {

    throw HeapConstraintViolation();

  }

 

  proceed with normal construction here;

 

  onTheHeap = false;                    // 为下一个对象清除标记

}

如果不再深入研究下去,就不会发现什么错误。这种方法利用了这样一个事实:“当在堆上分配对象时,会调用operator new来分配raw memory”,operator new设置onTheHeaptrue,每个构造函数都会检测onTheHeap,看对象的raw memory是否被operator new所分配。如果没有,一个类型为HeapConstraintViolation的异常将被抛出。否则构造函数如通常那样继续运行,当构造函数结束时,onTheHeap被设置为false,然后为构造下一个对象而重置到缺省值。

这是一个非常好的方法,但是不能运行。请考虑一下这种可能的客户端代码:

UPNumber *numberArray = new UPNumber[100];

第一个问题是为数组分配内存的是operator new[],而不是operator new,不过(倘若你的编译器支持它)你能象编写operator new一样容易地编写operator new[]函数。更大的问题是numberArray100个元素,所以会调用100次构造函数。但是只有一次分配内存的调用,所以100个构造函数中只有第一次调用构造函数前把onTheHeap设置为true。当调用第二个构造函数时,会抛出一个异常,你真倒霉。

即使不用数组,bit-setting操作也会失败。考虑这条语句:

UPNumber *pn = new UPNumber(*new UPNumber);

这里我们在堆中建立两个UPNumber,让pn指向其中一个对象;这个对象用另一个对象的值进行初始化。这个代码有一个内存泄漏,我们先忽略这个泄漏,这有利于下面对这条表达式的测试,执行它时会发生什么事情:

new UPNumber(*new UPNumber)

它包含new 操作符的两次调用,因此要调用两次operator new和调用两次UPNumber构造函数(参见条款8)。程序员一般期望这些函数以如下顺序执行:

调用第一个对象的operator new

调用第一个对象的构造函数

调用第二个对象的operator new

调用第二个对象的构造函数

但是C++语言没有保证这就是它调用的顺序。一些编译器以如下这种顺序生成函数调用:

调用第一个对象的operator new

调用第二个对象的operator new

调用第一个对象的构造函数

调用第二个对象的构造函数

编译器生成这种代码丝毫没有错,但是在operator newset-a-bit的技巧无法与这种编译器一起使用。因为在第一步和第二步设置的bit,第三步中被清除,那么在第四步调用对象的构造函数时,就会认为对象不再堆中,即使它确实在。

这些困难没有否定让每个构造函数检测*this指针是否在堆中这个方法的核心思想,它们只是表明检测在operator new(operator new[])里的bit set不是一个可靠的判断方法。我们需要更好的方法进行判断。

如果你陷入了极度绝望当中,你可能会沦落进不可移植的领域里。例如你决定利用一个在很多系统上存在的事实,程序的地址空间被做为线性地址管理,程序的栈从地址空间的顶部向下扩展,堆则从底部向上扩展:

在以这种方法管理程序内存的系统里(很多系统都是,但是也有很多不是这样),你可能会想能够使用下面这个函数来判断某个特定的地址是否在堆中:

// 不正确的尝试,来判断一个地址是否在堆中

bool onHeap(const void *address)

{

  char onTheStack;                   // 局部栈变量

 

  return address < &onTheStack;

}

这个函数背后的思想很有趣。在onHeap函数中onTheSatck是一个局部变量。因此它在堆栈上。当调用onHeap时,它的栈框架(stack frame(也就是它的activation record)被放在程序栈的顶端,因为栈在结构上是向下扩展的(趋向低地址),onTheStack的地址肯定比任何栈中的变量或对象的地址小。如果参数address的地址小于onTheStack的地址,它就不会在栈上,而是肯定在堆上。

Posted in 未分类 | Leave a comment

More Effective C++ 条款27(下):要求或禁止在堆中产生对象(下)

到目前为止,这种逻辑很正确,但是不够深入。最根本的问题是对象可以被分配在三个地方,而不是两个。是的,栈和堆能够容纳对象,但是我们忘了静态对象。静态对象是那些在程序运行时仅能初始化一次的对象。静态对象不仅仅包括显示地声明为static的对象,也包括在全局和命名空间里的对象(参见条款47)。这些对象肯定位于某些地方,而这些地方既不是栈也不是堆。

它们的位置是依据系统而定的,但是在很多栈和堆相向扩展的系统里,它们位于堆的底端。先前内存管理的图片到讲述的是事实,不过是很多系统都具有的事实,但是没有告诉我们这些系统全部的事实,加上静态变量后,这幅图片如下所示:

onHeap不能工作的原因立刻变得很清楚了,不能辨别堆对象与静态对象的区别:

void allocateSomeObjects()

{

  char *pc = new char;               // 堆对象: onHeap(pc)

                                     // 将返回true

 

  char c;                            // 栈对象: onHeap(&c)

                                     // 将返回false

 

  static char sc;                    // 静态对象: onHeap(&sc)

                                     // 将返回true

 

 

}

现在你可能不顾一切地寻找区分堆对象与栈对象的方法,在走头无路时你想在可移植性上打主意,但是你会这么孤注一掷地进行一个不能获得正确结果的交易么?绝对不会。我知道你会拒绝使用这种虽然诱人但是不可靠的“地址比对”技巧。

令人伤心的是不仅没有一种可移植的方法来判断对象是否在堆上,而且连能在多数时间正常工作的“准可移植”的方法也没有。如果你实在非得必须判断一个地址是否在堆上,你必须使用完全不可移植的方法,其实现依赖于系统调用,只能这样做了。因此你最好重新设计你的软件,以便你可以不需要判断对象是否在堆中。

如果你发现自己实在为对象是否在堆中这个问题所困扰,一个可能的原因是你想知道对象是否能在其上安全调用delete。这种删除经常采用“delete this”这种声明狼籍的形式。不过知道“是否能安全删除一个指针”与“只简单地知道一个指针是否指向堆中的事物”不一样,因为不是所有在堆中的事物都能被安全地delete。再考虑包含UPNumber对象的Asset对象:

class Asset {

private:

  UPNumber value;

 

 

};

 

Asset *pa = new Asset;

很明显*pa(包括它的成员value)在堆上。同样很明显在指向pa->value上调用delete是不安全的,因为该指针不是被new返回的。

幸运的是“判断是否能够删除一个指针”比“判断一个指针指向的事物是否在堆上”要容易。因为对于前者我们只需要一个operator new返回的地址集合。因为我们能自己编写operator new函数(参见Effective C++条款8—条款10),所以构建这样一个集合很容易。如下所示,我们这样解决这个问题:

void *operator new(size_t size)

{

  void *p = getMemory(size);         //调用一些函数来分配内存,

                                     //处理内存不够的情况

 

  p加入到一个被分配地址的集合;

 

  return p;

 

}

 

void operator delete(void *ptr)

{

  releaseMemory(ptr);                // return memory to

                                     // free store

 

  从被分配地址的集合中移去ptr;

}

 

bool isSafeToDelete(const void *address)

{

  返回address是否在被分配地址的集合中;

}

这很简单,operator new在地址分配集合里加入一个元素,operator delete从集合中移去项目,isSafeToDelete在集合中查找并确定某个地址是否在集合中。如果operator new operator delete函数在全局作用域中,它就能适用于所有的类型,甚至是内建类型。

在实际当中,有三种因素制约着对这种设计方式的使用。第一是我们极不愿意在全局域定义任何东西,特别是那些已经具有某种含义的函数,象operator newoperator delete正如我们所知,只有一个全局域,只有一种具有正常特征形式(也就是参数类型)的operator newoperator delete 。这样做会使得我们的软件与其它也实现全局版本的operator new 和operator delete的软件(例如许多面向对象数据库系统)不兼容。

 

我们考虑的第二个因素是效率:如果我们不需要这些,为什么还要为跟踪返回的地址而负担额外的开销呢?

最后一点可能有些平常,但是很重要。实现isSafeToDelete让它总能够正常工作是不可能的。难点是多继承下来的类或继承自虚基类的类有多个地址,所以无法保证传给isSafeToDelete的地址与operator new 返回的地址相同,即使对象在堆中建立。有关细节参见条款24和条款31

我们希望这些函数提供这些功能时能够不污染全局命名空间,没有额外的开销,没有正确性问题。幸运的是C++使用一种抽象mixin基类满足了我们的需要。

抽象基类是不能被实例化的基类,也就是至少具有一个纯虚函数的基类。mixin(mix in)类提供某一特定的功能,并可以与其继承类提供的其它功能相兼容(参见Effective C++条款7)。这种类几乎都是抽象类。因此我们能够使用抽象混合(mixin)基类给派生类提供判断指针指向的内存是否由operator new分配的能力。该类如下所示:

class HeapTracked {                  // 混合类; 跟踪

public:                              // operator new返回的ptr

 

  class MissingAddress{};            // 异常类,见下面代码

 

  virtual ~HeapTracked() = 0;

 

  static void *operator new(size_t size);

  static void operator delete(void *ptr);

 

  bool isOnHeap() const;

 

private:

  typedef const void* RawAddress;

  static list<RawAddress> addresses;

};

这个类使用了list(链表)数据结构跟踪从operator new返回的所有指针,list标准C++库的一部分(参见Effective C++条款49和本书条款35)。operator new函数分配内存并把地址加入到list中;operator delete用来释放内存并从list中移去地址元素。isOnHeap判断一个对象的地址是否在list中。

HeapTracked类的实作(我觉得把implementation翻译成“实作”更好 译者注)很简单,调用全局的operator newoperator delete函数来完成内存的分配与释放,list类里的函数进行插入操作和删除操作,并进行单语句的查找操作。以下是HeapTracked的全部实作:

// mandatory definition of static class member

list<RawAddress> HeapTracked::addresses;

 

// HeapTracked的析构函数是纯虚函数,使得该类变为抽象类。

// (参见Effective C++条款14). 然而析构函数必须被定义,

//所以我们做了一个空定义。.

HeapTracked::~HeapTracked() {}

 

 

 

void * HeapTracked::operator new(size_t size)

{

  void *memPtr = ::operator new(size);  // 获得内存

 

  addresses.push_front(memPtr);         // 把地址放到list的前端

  return memPtr;

}

 

void HeapTracked::operator delete(void *ptr)

{

  //得到一个 “iterator”,用来识别list元素包含的ptr

  //有关细节参见条款35

  list<RawAddress>::iterator it =

    find(addresses.begin(), addresses.end(), ptr);

 

  if (it != addresses.end()) {       // 如果发现一个元素

    addresses.erase(it);             //则删除该元素

    ::operator delete(ptr);          // 释放内存

  } else {                           // 否则

    throw MissingAddress();          // ptr就不是用operator new

  }                                  // 分配的,所以抛出一个异常

}

 

bool HeapTracked::isOnHeap() const

{

  // 得到一个指针,指向*this占据的内存空间的起始处,

  // 有关细节参见下面的讨论

  const void *rawAddress = dynamic_cast<const void*>(this);

 

  // operator new返回的地址list中查到指针

  list<RawAddress>::iterator it =

    find(addresses.begin(), addresses.end(), rawAddress);

 

  return it != addresses.end();      // 返回it是否被找到

}

尽管你可能对list类和标准C++库的其它部分不很熟悉,代码还是很一目了然。条款35将解释这里的每件东西,不过代码里的注释已经能够解释这个例子是如何运行的。

只有一个地方可能让你感到困惑,就是这个语句(在isOnHeap函数中)

const void *rawAddress = dynamic_cast<const void*>(this);

我前面说过带有多继承或虚基类的对象会有几个地址,这导致编写全局函数isSafeToDelete会很复杂。这个问题在isOnHeap中仍然会遇到,但是因为isOnHeap仅仅用于HeapTracked对象中,我们能使用dynamic_cast操作符的一种特殊的特性来消除这个问题。只需简单地放入dynamic_cast,把一个指针dynamic_castvoid*类型(或const void*volatile void* 。。。。。),生成的指针指向“原指针指向对象内存”的开始处。但是dynamic_cast只能用于“指向至少具有一个虚拟函数的对象”的指针上。我们该死的isSafeToDelete函数可以用于指向任何类型的指针,所以dynamic_cast也不能帮助它。isOnHeap更具有选择性(它只能测试指向HeapTracked对象的指针),所以能把this指针dynamic_castconst void*,变成一个指向当前对象起始地址的指针。如果HeapTracked::operator new为当前对象分配内存,这个指针就是HeapTracked::operator new返回的指针。如果你的编译器支持dynamic_cast 操作符,这个技巧是完全可移植的。

使用这个类,即使是最初级的程序员也可以在类中加入跟踪堆中指针的功能。他们所需要做的就是让他们的类从HeapTracked继承下来。例如我们想判断Assert对象指针指向的是否是堆对象:

class Asset: public HeapTracked {

private:

  UPNumber value;

 

};

我们能够这样查询Assert*指针,如下所示:

void inventoryAsset(const Asset *ap)

{

  if (ap->isOnHeap()) {

    ap is a heap-based asset — inventory it as such;

  }

  else {

    ap is a non-heap-based asset — record it that way;

  }

}

HeapTracked这样的混合类有一个缺点,它不能用于内建类型,因为象intchar这样的类型不能继承自其它类型。不过使用象HeapTracked的原因一般都是要判断是否可以调用”delete this”,你不可能在内建类型上调用它,因为内建类型没有this指针。

禁止堆对象

判断对象是否在堆中的测试到现在就结束了。与此相反的领域是“禁止在堆中建立对象”。通常对象的建立这样三种情况:对象被直接实例化;对象做为派生类的基类被实例化;对象被嵌入到其它对象内。我们将按顺序地讨论它们。

禁止客户端直接实例化对象很简单,因为总是调用new来建立这种对象,你能够禁止客户端调用new。你不能影响new操作符的可用性(这是内嵌于语言的),但是你能够利用new操作符总是调用operator new函数这点(参见条款8),来达到目的。你可以自己声明这个函数,而且你可以把它声明为private.。例如,如果你想不想让客户端在堆中建立UPNumber对象,你可以这样编写:

class UPNumber {

private:

  static void *operator new(size_t size);

  static void operator delete(void *ptr);

 

};

现在客户端仅仅可以做允许它们做的事情:

UPNumber n1;                         // okay

 

static UPNumber n2;                  // also okay

 

UPNumber *p = new UPNumber;          // error! attempt to call

                                     // private operator new

operator new声明为private就足够了,但是把operator new声明为private,而把iperator delete声明为public,这样做有些怪异,所以除非有绝对需要的原因,否则不要把它们分开声明,最好在类的一个部分里声明它们。如果你也想禁止UPNumber堆对象数组,可以把operator new[]operator delete[](参见条款8)也声明为private。(operator newoperator delete之间的联系比大多数人所想象的要强得多。有关它们之间关系的鲜为人知的一面,可以参见我的文章counting objects里的sidebar部分。)

有趣的是,把operator new声明为private经常会阻碍UPNumber对象做为一个位于堆中的派生类对象的基类被实例化。因为如果operator newoperator delete没有在派生类中被声明为public,它们就会被继承下来,继承了基类private函数的类,如下所示:

class UPNumber { … };             // 同上

 

class NonNegativeUPNumber:          //假设这个类

  public UPNumber {                 //没有声明operator new

 

};

 

NonNegativeUPNumber n1;             // 正确

 

static NonNegativeUPNumber n2;      // 也正确

 

NonNegativeUPNumber *p =            // 错误! 试图调用

  new NonNegativeUPNumber;          // private operator new

如果派生类声明它自己的operator new,当在堆中分配派生对象时,就会调用这个函数,必须得找到一种不同的方法防止UPNumber基类部分缠绕在这里。同样,UPNumberoperator newprivate这一点,不会对分配包含做为成员的UPNumber对象的对象产生任何影响:

class Asset {

public:

  Asset(int initValue);

 

 

private:

  UPNumber value;

};

 

Asset *pa = new Asset(100);          // 正确, 调用

                                     // Asset::operator new

                                     // ::operator new, 不是

                                     // UPNumber::operator new

实际上,我们又回到了这个问题上来,即“如果UPNumber对象没有被构造在堆中,我们想抛出一个异常”。当然这次的问题是“如果对象在堆中,我们想抛出异常”。正像没有可移植的方法来判断地址是否在堆中一样,也没有可移植的方法判断地址是否不在堆中,所以我们很不走运,不过这也丝毫不奇怪,毕竟如果我们能辨别出某个地址在堆上,我们也就能辨别出某个地址不在堆上。但是我们什么都不能辨别出来。

Posted in 未分类 | Leave a comment

More Effective C++ 条款28(上):灵巧(smart)指针(上)

灵巧指针是一种外观和行为都被设计成与内建指针相类似的对象,不过它能提供更多的功能。它们有许多应用的领域,包括资源管理(参见条款9、10、25和31)和重复代码任务的自动化(参见条款17和29)

当你使用灵巧指针替代C++的内建指针(也就是dumb pointer),你就能控制下面这些方面的指针的行为:

构造和析构。你可以决定建立灵巧指针时应该怎么做。通常赋给灵巧指针缺省值0,避免出现令人头疼的未初始化的指针。当指向某一对象的最后一个灵巧指针被释放时,一些灵巧指针负责删除它们指向的对象。这样做对防止资源泄漏很有帮助。

拷贝和赋值。你能对拷贝灵巧指针或设计灵巧指针的赋值操作进行控制。对于一些类型的灵巧指针来说,期望的行为是自动拷贝它们所指向的对象或用对这些对象进行赋值操作,也就是进行deep copy(深层拷贝)。对于其它的一些灵巧指针来说,仅仅拷贝指针本身或对指针进行赋值操作。还有一部分类型的灵巧指针根本就不允许这些操作。无论你认为应该如何去做,灵巧指针始终受你的控制。

Dereferencing(取出指针所指东西的内容)。当客户端引用被灵巧指针所指的对象,会发生什么事情呢?你可以自行决定。例如你可以用灵巧指针实现条款17提到的lazy fetching 方法。

灵巧指针从模板中生成,因为要与内建指针类似,必须是strongly typed(强类型)的;模板参数确定指向对象的类型。大多数灵巧指针模板看起来都象这样:

template<class T>                    //灵巧指针对象模板

class SmartPtr {                    

public:

  SmartPtr(T* realPtr = 0);          // 建立一个灵巧指针

                                     // 指向dumb pointer所指的

                                     // 对象。未初始化的指针

                                     // 缺省值为0(null)

 

  SmartPtr(const SmartPtr& rhs);     // 拷贝一个灵巧指针

 

  ~SmartPtr();                       // 释放灵巧指针

 

  // make an assignment to a smart ptr

  SmartPtr& operator=(const SmartPtr& rhs);

 

  T* operator->() const;             // dereference一个灵巧指针

                                     // 以访问所指对象的成员

 

  T& operator*() const;              // dereference 灵巧指针

 

private:

  T *pointee;                        // 灵巧指针所指的对象

};                                 

拷贝构造函数和赋值操作符都被展现在这里。对于灵巧指针类来说,不能允许进行拷贝和赋值操作,它们应该被声明为private(参见Effective C++条款27)。两个dereference操作符被声明为const,是因为dereference一个指针时不能对指针进行修改(尽管可以修改指针所指的对象)。最后,每个指向T对象的灵巧指针包含一个指向T的dumb pointer。这个dumb pointer指向的对象才是灵巧指针指向的真正对象。

进入灵巧指针实作的细节之前,应该研究一下客户端如何使用灵巧指针。考虑一下,存在一个分布式系统(即其上的对象一些在本地,一些在远程)。相对于访问远程对象,访问本地对象通常总是又简单而且速度又快,因为远程访问需要远程过程调用(RPC),或其它一些联系远距离计算机的方法。

对于编写程序代码的客户端来说,采用不同的方法分别处理本地对象与远程对象是一件很烦人的事情。让所有的对象都位于一个地方会更方便。灵巧指针可以让程序库实现这样的梦想。

template<class T>                    // 指向位于分布式 DB(数据库)

class DBPtr {                        // 中对象的灵巧指针模板

public:                              //

 

  DBPtr(T *realPtr = 0);             // 建立灵巧指针,指向

                                     // 由一个本地dumb pointer

                                     // 给出的DB 对象

 

 

  DBPtr(DataBaseID id);              // 建立灵巧指针,

                                     // 指向一个DB对象,

                                     // 具有惟一的DB识别符

 

                                  // 其它灵巧指针函数

};                                   //同上

 

class Tuple {                        // 数据库元组类

public:                            

 

  void displayEditDialog();          // 显示一个图形对话框,

                                     // 允许用户编辑元组。

                                     // user to edit the tuple

 

  bool isValid() const;              // 返回*this是否通过了

};                                   // 合法性验证

 

// 这个类模板用于在修改T对象时进行日志登记。

// 有关细节参见下面的叙述:

template<class T>

class LogEntry {

public:

  LogEntry(const T& objectToBeModified);

  ~LogEntry();

};

 

void editTuple(DBPtr<Tuple>& pt)

{

  LogEntry<Tuple> entry(*pt);        // 为这个编辑操作登记日志

                                     // 有关细节参见下面的叙述

 

  // 重复显示编辑对话框,直到提供了合法的数值。

  do {

    pt->displayEditDialog();

  } while (pt->isValid() == false);

}

editTuple中被编辑的元组物理上可以位于本地也可以位于远程,但是编写editTuple的程序员不用关心这些事情。灵巧指针类隐藏了系统的这些方面。程序员只需关心通过对象进行访问的元组,而不用关心如何声明它们,其行为就像一个内建指针。

注意在editTuple中LogEntry对象的用法。一种更传统的设计是在调用displayEditDialog前开始日志记录,调用后结束日志记录。在这里使用的方法是让LogEntry的构造函数启动日志记录,析构函数结束日志记录。正如条款9所解释的,当面对异常时,让对象自己开始和结束日志记录比显示地调用函数可以使的程序更健壮。而且建立一个LogEntry对象比每次都调用开始记录和结束记录函数更容易。

正如你所看到的,使用灵巧指针与使用dump pointer没有很大的差别。这表明了封装是非常有效的。灵巧指针的客户端可以象使用dumb pointer一样使用灵巧指针。正如我们将看到的,有时这种替代会更透明化。

灵巧指针的构造、赋值和析构

灵巧指针的的析构通常很简单:找到指向的对象(一般由灵巧指针构造函数的参数给出),让灵巧指针的内部成员dumb pointer指向它。如果没有找到对象,把内部指针设为0或发出一个错误信号(可以是抛出一个异常)。

灵巧指针拷贝构造函数、赋值操作符函数和析构函数的实作由于所有权的问题所以有些复杂。如果一个灵巧指针拥有它指向的对象,当它被释放时必须负责删除这个对象。这里假设灵巧指针指向的的对象是动态分配的。这种假设在灵巧指针中是常见的(有关确定这种假设是真实的方法,参见条款27)。

看一下标准C++类库中auto_ptr模板。这如条款9所解释的,一个auto_ptr对象是一个指向堆对象的灵巧指针,直到auto_ptr被释放。auto_ptr的析构函数删除其指向的对象时,会发生什么事情呢?auto_ptr模板的实作如下:

template<class T>

class auto_ptr {

public:

  auto_ptr(T *ptr = 0): pointee(ptr) {}

  ~auto_ptr() { delete pointee; }

 

 

private:

  T *pointee;

};

假如auto_ptr拥有对象时,它可以正常运行。但是当auto_ptr被拷贝或被赋值时,会发生什么情况呢?

auto_ptr<TreeNode> ptn1(new TreeNode);

 

auto_ptr<TreeNode> ptn2 = ptn1;      // 调用拷贝构造函数

                                     //会发生什么情况?

 

auto_ptr<TreeNode> ptn3;

 

ptn3 = ptn2;                         // 调用 operator=;

                                     // 会发生什么情况?

如果我们只拷贝内部的dumb pointer,会导致两个auto_ptr指向一个相同的对象。这是一个灾难,因为当释放quto_ptr时每个auto_ptr都会删除它们所指的对象。这意味着一个对象会被我们删除两次。这种两次删除的结果将是不可预测的(通常是灾难性的)。

另一种方法是通过调用new,建立一个所指对象的新拷贝。这确保了不会有许多指向同一个对象的auto_ptr,但是建立(以后还得释放)新对象会造成不可接受的性能损耗。并且我们不知道要建立什么类型的对象,因为auto_ptr<T>对象不用必须指向类型为T的对象,它也可以指向T的派生类型对象。虚拟构造函数(参见条款25)可能帮助我们解决这个问题,但是好象不能把它们用在auto_ptr这样的通用类中。

如果quto_ptr禁止拷贝和赋值,就可以消除这个问题,但是采用“当auto_ptr被拷贝和赋值时,对象所有权随之被传递”的方法,是一个更具灵活性的解决方案:

template<class T>

class auto_ptr {

public:

 

 

  auto_ptr(auto_ptr<T>& rhs);        // 拷贝构造函数

 

  auto_ptr<T>&                       // 赋值

  operator=(auto_ptr<T>& rhs);       // 操作符

 

 

};

 

template<class T>

auto_ptr<T>::auto_ptr(auto_ptr<T>& rhs)

{

  pointee = rhs.pointee;             // *pointee的所有权

                                     // 传递到 *this

 

  rhs.pointee = 0;                   // rhs不再拥有

}                                    // 任何东西

 

template<class T>

auto_ptr<T>& auto_ptr<T>::operator=(auto_ptr<T>& rhs)

{

  if (this == &rhs)                  // 如果这个对象自我赋值

    return *this;                    // 什么也不要做

                                   

 

  delete pointee;                    // 删除现在拥有的对象

                                  

 

  pointee = rhs.pointee;             // *pointee的所有权

  rhs.pointee = 0;                   // rhs 传递到 *this

 

  return *this;

}

注意赋值操作符在接受新对象的所有权以前必须删除原来拥有的对象。如果不这样做,原来拥有的对象将永远不会被删除。记住,除了auto_ptr对象,没有人拥有auto_ptr指向的对象。

因为当调用auto_ptr的拷贝构造函数时,对象的所有权被传递出去,所以通过传值方式传递auto_ptr对象是一个很糟糕的方法。因为:

// 这个函数通常会导致灾难发生

void printTreeNode(ostream& s, auto_ptr<TreeNode> p)

{ s << *p; }

 

int main()

{

  auto_ptr<TreeNode> ptn(new TreeNode);

 

 

 

  printTreeNode(cout, ptn);          //通过传值方式传递auto_ptr

 

 

 

}

printTreeNode的参数p被初始化时(调用auto_ptr的拷贝构造函数),ptn指向对象的所有权被传递到给了p。当printTreeNode结束执行后,p离开了作用域,它的析构函数删除它指向的对象(就是原来ptr指向的对象)。然而ptr不再指向任何对象(它的dumb pointernull),所以调用printTreeNode以后任何试图使用它的操作都将产生不可定义的行为。只有在你确实想把对象的所有权传递给一个临时的函数参数时,才能通过传值方式传递auto_ptr。这种情况很少见。

这不是说你不能把auto_ptr做为参数传递,这只意味着不能使用传值的方法。通过const引用传递(Pass-by-reference-to-const)的方法是这样的:

// 这个函数的行为更直观一些

void printTreeNode(ostream& s,

                   const auto_ptr<TreeNode>& p)

{ s << *p; }

在函数里,p是一个引用,而不是一个对象,所以不会调用拷贝构造函数初始化p。当ptn被传递到上面这个printTreeNode时,它还保留着所指对象的所有权,调用printTreeNode以后还可以安全地使用ptn。从而通过const引用传递auto_ptr可以避免传值所产生的风险。(“引用传递”替代“传值”的其他原因参见Effective C++条款22)。

在拷贝和赋值中,把对象的所有权从一个灵巧指针传递到另一个中去,这种思想很有趣,而且你可能已经注意到拷贝构造函数和赋值操作符不同寻常的声明方法同样也很有趣。这些函数同上会带有const参数,但是上面这些函数则没有。实际上在拷贝和赋值中上述这些代码修改了这些参数。也就是说,如果auto_ptr对象被拷贝或做为赋值操作的数据源,就会修改auto_ptr对象!

是的,就是这样。C++是如此灵活能让你这样去做,真是太好了。如果语言要求拷贝构造函数和赋值操作符必须带有const参数,你必须去掉参数的const属性(参见Effective C++条款21)或用其他方法实现所有权的转移。准确地说:当拷贝一个对象或这个对象做为赋值的数据源,就会修改该对象。这可能有些不直观,但是它是简单的,直接的,在这种情况下也是准确的。

如果你发现研究这些auto_ptr成员函数很有趣,你可能希望看看完整的实作。在291页至294页上有(只原书页码),在那里你也能看到在标准C++库中auto_ptr模板有比这里所描述的更灵活的拷贝构造函数和赋值操作符。在标准C++库中,这些函数是成员函数模板,不只是成员函数。(在本条款的后面会讲述成员函数模板。也可以阅读Effective C++条款25)。

灵巧指针的析构函数通常是这样的:

template<class T>

SmartPtr<T>::~SmartPtr()

{

  if (*this owns *pointee) {

    delete pointee;

  }

}

有时删除前不需要进行测试,例如在一个auto_ptr总是拥有它指向的对象时。而在另一些时候,测试会更为复杂。一个使用了引用计数(参见条款29)灵巧指针必须在判断是否有权删除所指对象前调整引用计数值。当然还有一些灵巧指针象dumb pointer一样,当它们被删除时,对所指对象没有任何影响。

实作Dereference 操作符

让我们把注意力转向灵巧指针的核心部分,the operator*  operator-> 函数。前者返回所指的对象。理论上,这很简单:template<class T>
T& SmartPtr<T>::operator*() const
{
  perform "smart pointer" processing;
 
  return *pointee;
}

首先无论函数做什么,必须先初始化指针或使pointee合法。例如,如果使用lazy fetch(参见条款17),函数必须为pointee建立一个新对象。一旦pointee合法了,operator*函数就返回其所指对象的一个引用。

注意返回类型是一个引用。如果返回对象,尽管编译器允许这么做,这也将会导致灾难性后果。必须时刻牢记:pointee不用必须指向T类型对象;它也可以指向T的派生类对象。如果在这种情况下operator*函数返回的是T类型对象而不是派生类对象的引用,你的函数实际上返回的是一个错误类型的对象!(这是一个slicing问题,参见Effective C++条款22和本书条款13)。在返回的这种对象上调用虚拟函数,不会触发与所指对象的动态类型相符的函数。实际上就是说你的灵巧指针不能支持虚拟函数,象这样的指针再灵巧也没有用。而返回一个引用还能够具有更高的效率(不需要构造一个临时对象,参见条款19)。能够兼顾正确与效率当然是一件好事。

如果你是一个急性子的人,你可能会想如果一些人在null灵巧指针上调用operator*,也就是说灵巧指针的dumb pointernull。放松。随便做什么都行。dereference一个空指针的结果是未定义的,所以这不是一个“错误”的行为。想排除一个异常么?可以,抛出吧。想调用abort函数(可能被assert在失败时调用)?好的,调用吧。想遍历内存把每个字节都设成你生日与256模数么?当然也可以。虽说这样做没有什么好处,但是就语言本身而言,你完全是自由的。

operator->的情况与operator*是相同的,但是在分析operator->之前,让我们先回忆一下这个函数调用的与众不同的含义。再考虑editTuple函数,其使用一个指向Tuple对象的灵巧指针:

void editTuple(DBPtr<Tuple>& pt)
{
  LogEntry<Tuple> entry(*pt);
 
  do {
    pt->displayEditDialog();
  } while (pt->isValid() == false);
}

语句

pt->displayEditDialog();

被编译器解释为:
(pt.operator->())->displayEditDialog();

这意味着不论operator->返回什么,它必须使用member-selection operator(成员选择操作符)->)。因此operator->仅能返回两种东西:一个指向某对象的dumb pointer或另一个灵巧指针。多数情况下,你想返回一个普通dumb pointer。在此情况下,你这样实作operator->

template<class T>
T* SmartPtr<T>::operator->() const
{
  perform "smart pointer" processing;
 
  return pointee;
}

这样做运行良好。因为该函数返回一个指针,通过operator->调用虚拟函数,其行为也是正确的。

对于很多程序来说,这就是你需要了解灵巧指针的全部东西。条款29的引用计数代码并没有比这里更多的功能。但是如果你想更深入地了解灵巧指针,你必须知道更多的有关dumb pointer的知识和灵巧指针如何能或不能进行模拟。如果你的座右铭是“Most people stop at the Z-but not me(多数人浅尝而止,但我不能够这样) ”,下面讲述的内容正适合你。

Posted in 未分类 | Leave a comment

More Effective C++ 条款28(中) :灵巧(smart)指针(中)

 测试灵巧指针是否为NULL

目前为止我们讨论的函数能让我们建立、释放、拷贝、赋值、dereference灵巧指针。但是有一件我们做不到的事情是“发现灵巧指针为NULL”:

SmartPtr<TreeNode> ptn;

 

 

if (ptn == 0) …                    // error!

 

if (ptn) …                         // error!

 

if (!ptn) …                        // error!

这是一个严重的限制。

在灵巧指针类里加入一个isNull成员函数是一件很容易的事,但是仍然没有解决当测试NULL时灵巧指针的行为与dumb pointer不相似的问题。另一种方法是提供隐式类型转换操作符,允许编译上述的测试。一般应用于这种目的的类型转换是void*

template<class T>

class SmartPtr {

public:

 

  operator void*();                  // 如果灵巧指针为null

                                  // 返回0,否则返回

};                                   // 0

 

SmartPtr<TreeNode> ptn;

 

 

if (ptn == 0) …                    // 现在正确

 

if (ptn) …                         // 也正确

 

if (!ptn) …                        // 正确

这与iostream类中提供的类型转换相同,所以可以这样编写代码:

ifstream inputFile(“datafile.dat”);

 

if (inputFile) …                   // 测试inputFile是否已经被

                                     // 成功地打开。

象所有的类型转换函数一样,它有一个缺点,在一些情况下虽然大多数程序员希望它调用失败,但是函数还能够成功地被调用(参见条款5)。特别是它允许灵巧指针与完全不同的类型之间进行比较:

SmartPtr<Apple> pa;

SmartPtr<Orange> po;

 

 

if (pa == po) …                    // 这能够被成功编译!

即使在SmartPtr<Apple> SmartPtr<Orange>之间没有operator= 函数也能够编译因为灵巧指针被隐式地转换为void*指针对于内建指针类型有一个内建的比较函数。这种进行隐式类型转换的行为特性很危险。(再看一下条款5,必须反反复复地阅读,做到耳熟能详。)

void*类型转换方面,也有一些变通之策。有些设计者采用到const void*的类型转换,还有一些采取转换到bool的方法。这些变通之策都没有消除混合类型比较的问题。

有一种两全之策可以提供合理的测试空值的语法形式,同时把不同类型的灵巧指针之间进行比较的可能性降到最低。这就是在灵巧指针类中重载operator!,当且仅当灵巧指针是一个空指针时,operator!返回true:

template<class T>

class SmartPtr {

public:

 

  bool operator!() const;            // 当且仅当灵巧指针是

                                  // 空值,返回true

 

};

客户端程序如下所示

SmartPtr<TreeNode> ptn;

 

 

if (!ptn) {                          // 正确

                                  // ptn 是空值

}

else {

                                  // ptn不是空值

}

但是这样就不正确了:

if (ptn == 0) …                    // 仍然错误

 

if (ptn) …                         // 也是错误的

 

仅在这种情况下会存在不同类型之间进行比较:

SmartPtr<Apple> pa;

SmartPtr<Orange> po;

 

 

if (!pa == !po) …                 // 能够编译

幸好程序员不会经常这样编写代码。有趣的是,iostream库的实作除了提供void*隐式的类型转换,也有operator!函数,不过这两个函数被用于测试的流状态有些不同。(在C++类库标准中(参见Effective C++ 条款49和本书条款35),void*隐式的类型转换已经被bool类型的转换所替代,operator bool总是返回与operator!相反的值。)

把灵巧指针转变成dumb指针

有时你要在一个程序里或已经使用dumb指针的程序库中添加灵巧指针。例如,你的分布式数据库系统原来不是分布式的,所以可能有一些老式的库函数没有使用灵巧指针:

class Tuple { … };                 // 同上

 

void normalize(Tuple *pt);           // *pt 放入

                                     // 范式中; 注意使用的

                                     // dumb指针

考虑一下,如果你试图用指向Tuple的灵巧指针调用normalize,会出现什么情况:

DBPtr<Tuple> pt;

 

 

normalize(pt);                       // 错误!

 

这种调用不能够编译,因为不能把DBPtr<Tuple>转换成Tuple*。你可以这样做,从而使该该函数正常运行:

normalize(&*pt);                     // 繁琐, 但合法

不过我觉得你会讨厌这种调用方式。

在灵巧指针模板中增加指向Tdumb指针的隐式类型转换操作符,可以让以上函数调用成功运行:

template<class T>                   // 同上

class DBPtr {

public:

 

  operator T*() { return pointee; }

 

};

 

DBPtr<Tuple> pt;

 

 

normalize(pt);                       // 能够运行

 

并且这个函数也消除了测试空值的问题:

if (pt == 0) …                     // 正确, pt转变成

                                     // Tuple*

 

if (pt) …                          // 同上

 

if (!pt) …                         // 同上 (reprise)

然而,它也有类型转换函数所具有的缺点(几乎总是这样,看条款5)。它使得客户端能够很容易地直接访问dumb指针,绕过“类指针(pointer-like)”对象所提供的“灵巧”特性:

void processTuple(DBPtr<Tuple>& pt)

{

  Tuple *rawTuplePtr = pt;           // DBPtr<Tuple> 转变成

                                     // Tuple*

 

  使用raw TuplePtr 修改 tuple;

 

}

通常,灵巧指针提供的“灵巧”行为特性是设计中的主要组成部分,所以允许客户端使用dumb指针会导致灾难性的后果。例如,如果DBPtr实现了条款29中引用计数的功能,允许客户端直接对dumb指针进行操作很可能破坏“引用计数”数据结构,而导致引用计数错误。

甚至即使你提供一个从灵巧指针到dumb指针的隐式转换操作符,灵巧指针也不能真正地做到与dumb指针互换。因为从灵巧指针到dumb指针的转换是“用户定义类型转换”,在同一时间编译器进行这种转换的次数不能超过一次。例如假设有一个表示能够访问某一元组的所有客户的类:

class TupleAccessors {

public:

  TupleAccessors(const Tuple *pt);   // pt identifies the

                                  // tuple whose accessors

};                                   // we care about

通常,TupleAccessors的单参数构造函数也可以做为从Tuple*TupleAccessors的类型转换操作符(参见条款5)。现在考虑一下用于合并两个TupleAccessors对象内信息的函数:

TupleAccessors merge(const TupleAccessors& ta1,

                     const TupleAccessors& ta2);

因为一个Tuple*可以被隐式地转换为TupleAccessors,用两个dumb Tuple*调用merge函数,可以正常运行:

Tuple *pt1, *pt2;

 

 

merge(pt1, pt2);                     // 正确, 两个指针被转换为

                                     // TupleAccessors objects

如果用灵巧指针DBPtr<Tuple>进行调用,编译就会失败:

DBPtr<Tuple> pt1, pt2;

 

 

merge(pt1, pt2);                 // 错误!不能把 pt1

                                 // pt2转换称TupleAccessors对象

因为从DBPtr<Tuple>TupleAccessors的转换要调用两次用户定义类型转换(一次从DBPtr<Tuple>Tuple*,一次从Tuple*TupleAccessors),编译器不会进行这种转换序列。

提供到dumb指针的隐式类型转换的灵巧指针类也暴露了一个非常有害的bug。考虑这个代码:

DBPtr<Tuple> pt = new Tuple;

 

 

delete pt;

这段代码应该不能被编译,pt不是指针,它是一个对象,你不能删除一个对象。只有指针才能被删除,对么?

当然对了。但是回想一下条款5:编译器使用隐式类型转换来尽可能使函数调用成功,再回想一下条款8:使用delete会调用析构函数和operator delete,两者都是函数。编译器欲使在delete语句里的两个函数成功调用,就把pt隐式转换为Tuple*,然后删除它。这样做必然会破坏你的程序。

如果pt拥有它指向的对象,对象就会被删除两次,一次在调用delete时,第二次在pt的析构函数被调用时。如果pt不拥有对象,而是其他人拥有,拥有者可以删除pt,但是如果pt指向对象的拥有者不是删除pt的人,有删除权的拥有者以后还会再次删除该对象。不论是前者所述的情况还是后者的情况都会导致一个对象被删除两次,这样做会产生不能预料的后果。

这个bug极为有害,因为隐藏在灵巧指针后面的全部思想就是让它们不论是在外观上还是在使用感觉上都与dumb指针尽可能地相似。你越接近这种思想,你的客户端就越可能忘记正在使用灵巧指针。如果他们忘记了正在使用灵巧指针,肯定会在调用new之后调用delete,以防止资源泄漏,谁又能责备他们这样做不对呢?

底线很简单:除非有一个让人非常信服的原因去这样做,否则绝对不要提供转换到dumb指针的隐式类型转换操作符。

灵巧指针和继承类到基类的类型转换

假设我们有一个public继承层次结构,以模型化音乐商店的商品:

class MusicProduct {

public:

  MusicProduct(const string& title);

  virtual void play() const = 0;

  virtual void displayTitle() const = 0;

 

};

 

class Cassette: public MusicProduct {

public:

  Cassette(const string& title);

  virtual void play() const;

  virtual void displayTitle() const;

 

};

 

class CD: public MusicProduct {

public:

  CD(const string& title);

  virtual void play() const;

  virtual void displayTitle() const;

 

};

 

再接着假设,我们有一个函数,给它一个MusicProduct对象,它能显示产品名,并播放它:

void displayAndPlay(const MusicProduct* pmp, int numTimes)

{

  for (int i = 1; i <= numTimes; ++i) {

    pmp->displayTitle();

    pmp->play();

  }

}

这个函数能够这样使用:

Cassette *funMusic = new Cassette(“Alapalooza”);

CD *nightmareMusic = new CD(“Disco Hits of the 70s”);

 

displayAndPlay(funMusic, 10);

displayAndPlay(nightmareMusic, 0);

这并没有什么值得惊讶的东西,但是当我们用灵巧指针替代dumb指针,会发生什么呢:

void displayAndPlay(const SmartPtr<MusicProduct>& pmp,

                    int numTimes);

 

SmartPtr<Cassette> funMusic(new Cassette(“Alapalooza”));

SmartPtr<CD> nightmareMusic(new CD(“Disco Hits of the 70s”));

 

displayAndPlay(funMusic, 10);         // 错误!

displayAndPlay(nightmareMusic, 0);    // 错误!

如果灵巧指针这么聪明,为什么不能编译这些代码呢?

不能进行编译原因是不能把SmartPtr<CD>SmartPtr<Cassette>转换成SmartPtr<MusicProduct>。从编译器的观点来看,这些类之间没有任何关系。为什么编译器的会这样认为呢?毕竟SmartPtr<CD> SmartPtr<Cassette>不是从SmartPtr<MusicProduct>继承过来的,这些类之间没有继承关系,我们不可能要求编译器把一种对象转换成另一种类型的对象。

幸运的是,有办法避开这种限制,这种方法的核心思想(不是实际操作)很简单:对于可以进行隐式转换的每个灵巧指针类都提供一个隐式类型转换操作符(参见条款5)。例如在music类层次内,在CassetteCD的灵巧指针类内你可以加入SmartPtr<MusicProduct>函数:

class SmartPtr<Cassette> {

public:

  operator SmartPtr<MusicProduct>()

  { return SmartPtr<MusicProduct>(pointee); }

 

 

 

private:

  Cassette *pointee;

};

 

class SmartPtr<CD> {

public:

  operator SmartPtr<MusicProduct>()

  { return SmartPtr<MusicProduct>(pointee); }

 

 

 

private:

  CD *pointee;

};

这种方法有两个缺点。第一,你必须人为地特化(specializeSmartPtr类,所以你加入隐式类型转换操作符也就破坏了模板的通用性。第二,你可能必须添加许多类型转换符,因为你指向的对象可以位于继承层次中很深的位置,你必须为直接或间接继承的每一个基类提供一个类型转换符。(如果你想你能够克服这个缺点,方法是仅仅为转换到直接基类而提供一个隐式类型转换符,那么你再想想这样做行么?因为编译器在同一时间调用用户定义类型转换函数的次数不能超过一次,它们不能把指向T的灵巧指针转换为指向T的间接基类的灵巧指针,除非只要一步就能完成。)

如果你能让编译器为你编写所有的类型转换函数,这会节省很多时间。感谢最近的语言扩展,让你能够做到,这个扩展能声明(非虚)成员函数模板(通常就叫成员模板(member template)),你能使用它来生成灵巧指针类型转换函数,如下:

template<class T>                    // 模板类,指向T

class SmartPtr {                     // 灵巧指针

public:

  SmartPtr(T* realPtr = 0);

 

  T* operator->() const;

  T& operator*() const;

 

  template<class newType>             // 模板成员函数

  operator SmartPtr<newType>()        // 为了实现隐式类型转换.

  {

    return SmartPtr<newType>(pointee);

  }

 

 

};

现在请你注意,这可不是魔术——不过也很接近于魔术。它的原理如下所示。(如果下面的内容让你感到既冗长又令你费解,请不要失望,一会儿我会给出一个例子。我保证你看完例子后,就能够更深入地理解这段内容了)假设编译器有一个指向T对象的灵巧指针,它要把这个对象转换成指向“T的基类”的灵巧指针。编译器首先检查SmartPtr<T>的类定义,看其有没有声明必须的类型转换符,但是它没有声明。(这不是指:在模板上面没有声明类型转换符)编译器然后检查是否存在一个成员函数模板,可以被实例化,用来进行它所期望的类型转换。它发现了一个这样的模板(带有形式类型参数newType),所以它把newType绑定到T的基类类型上,来实例化模板。这时,惟一一个问题是实例化的成员函数代码能否被编译。传递(dumb)指针pointee到指向“T的基类”的灵巧指针的构造函数,这个语句是合法的,把它转变成指向其基类(public 或 protected)对象的指针也必然是合法的,因此类型转换操作符能够被编译,可以成功地把指向T的灵巧指针隐式地类型转换为指向“T的基类”的灵巧指针。

举一个例子会有所帮助。让我们回到CDs、cassettes、music产品的继承层次上来。我们先前已经知道下面这段代码不能被编译,因为编译器不能把指向CD的灵巧指针转换为指向music产品的灵巧指针:

void displayAndPlay(const SmartPtr<MusicProduct>& pmp,

                    int howMany);

 

SmartPtr<Cassette> funMusic(new Cassette(“Alapalooza”));

SmartPtr<CD> nightmareMusic(new CD(“Disco Hits of the 70s”));

 

displayAndPlay(funMusic, 10);         // 以前是一个错误

displayAndPlay(nightmareMusic, 0);    // 以前是一个错误

修改了灵巧指针类,包含了隐式类型转换操作符的成员函数模板以后,这个代码就可以成功运行了。拿如下调用举例,看看为什么能够成功运行:

displayAndPlay(funMusic, 10);

funMusic对象的类型是SmartPtr<Cassette>。函数displayAndPlay期望的参数是SmartPtr<MusicProduct>地对象。编译器侦测到类型不匹配并寻找把funMusic转换成SmartPtr<MusicProduct>对象的方法。它在SmartPtr<MusicProduct>类里寻找带有SmartPtr<Cassette>类型参数的单参数构造函数(参见条款5),但是没有找到。然后它们又寻找成员函数模板,能被实例化产生这样的函数。它们在SmartPtr<Cassette>发现了模板,把newType绑定到MusicProduct上,生成必须的函数。实例化函数,生成这样的代码:

SmartPtr<Cassette>::  operator SmartPtr<MusicProduct>()

{

  return SmartPtr<MusicProduct>(pointee);

}

能编译这行代码么?实际上这段代码就是用pointee做为参数调用SmartPtr<MusicProduct>的构造函数,所以真正的问题是能否用一个Cassette*指针构造一个SmartPtr<MusicProduct>对象,现在我们对dumb指针类型之间的转换已经很熟悉了,Cassette*能够被传递给需要MusicProduct*指针的地方。因此SmartPtr<MusicProduct>构造函数可以成功调用,同样SmartPtr<Cassette>SmartPtr<MusicProduct>之间的类型转换也能成功进行。太棒了,实现了灵巧指针之间的类型转换,还有什么比这更简单么?

 而且,还有什么比这功能更强大么?不要被这个例子误导,而认为这种方法只能用于把指针在继承层次中向上进行类型转换。这种方法可以成功地用于任何合法的指针类型转换。如果你有dumb指针T1*和另一种dumb指针T2*,当且仅当你能隐式地把T1*转换为T2*时,你就能够隐式地把指向T1的灵巧指针类型转换为指向T2的灵巧指针类型。

Posted in 未分类 | Leave a comment

More Effective C++ 条款28(下) 灵巧(smart)指针(下)

     译者注:由于我无法在文档区贴上图片(在论坛询问,结果无人回答),所以只能附上此译文的word文档。下载

这种技术能给我们几乎所有想要的行为特性。假设我们用一个新类CasSingle来扩充MusicProduct类层次,用来表示cassette singles。修改后的类层次看起来象这样:

现在考虑这段代码:

template<class T>                    // 同上, 包括作为类型
class SmartPtr { ... };              // 转换操作符的成员模板
 
void displayAndPlay(const SmartPtr<MusicProduct>& pmp,
                    int howMany);
 
void displayAndPlay(const SmartPtr<Cassette>& pc,
                    int howMany);
 
SmartPtr<CasSingle> dumbMusic(new CasSingle("Achy Breaky Heart"));
 
displayAndPlay(dumbMusic, 1);        // 错误!

在这个例子里,displayAndPlay被重载,一个函数带有SmartPtr<Cassette> 对象参数,其它函数的参数为SmartPtr<CasSingle>,我们期望调用SmartPtr<Cassette>,因为CasSingle是直接从Cassette上继承下来的,而它仅仅是间接继承自MusicProduct。当然这是dumb指针的工作方法,我们的灵巧指针不会这么灵巧。它们把成员函数做为转换操作符来使用,就C++编译器而言,所有类型转换操作符都一样,没有好坏的分别。因此displayAndPlay的调用具有二义性,因为从SmartPtr<CasSingle> SmartPtr<Cassette>的类型转换并不比到SmartPtr<MusicProduct>的类型转换好。

通过成员模板来实现灵巧指针的类型转换有还有两个缺点。第一,支持成员模板的编译器较少,所以这种技术不具有可移植性。以后情况会有所改观,但是没有人知道这会等到什么时候。第二,这种方法的工作原理不很明了,要理解它必须先要深入理解函数调用的参数匹配,隐式类型转换函数,模板函数隐式实例化和成员函数模板。有些程序员以前从来没有看到过这种技巧,而却被要求维护使用这种技巧的代码,我真是很可怜他们。这种技巧确实很巧妙,这自然是肯定,但是过于的巧妙可是一件危险的事情。

不要再拐弯抹角了,直接了当地说,我们想要知道的是在继承类向基类进行类型转换方面,我们如何能够让灵巧指针的行为与dumb指针一样呢?答案很简单:不可能。正如Daniel Edelson所说,灵巧指针固然灵巧,但不是指针。最好的方法是使用成员模板生成类型转换函数,在会产生二义性结果的地方使用casts(参见条款2)。这不是一个完美的方法,不过也很不错,在一些情况下去除二义性,所付出的代价与灵巧指针提供复杂的功能相比还是值得的。

灵巧指针和const

对于dumb指针来说,const既可以针对指针所指向的东西,也可以针对于指针本身,或者兼有两者的含义(参见Effective C++条款21):

CD goodCD("Flood");
 
const CD *p;                         // p 是一个non-const 指针
                                     //指向 const CD 对象
 
CD * const p = &goodCD;              // p 是一个const 指针 
                                     // 指向non-const CD 对象;
                                     // 因为 p const, 
                                     // 必须被初始化
 
const CD * const p = &goodCD;        // p 是一个const 指针
                                     // 指向一个 const CD 对象

我们自然想要让灵巧指针具有同样的灵活性。不幸的是只能在一个地方放置const,并只能对指针本身起作用,而不能针对于所指对象:

const SmartPtr<CD> p =                // p 是一个const 灵巧指针

  &goodCD;                             // 指向 non-const CD 对象

好像有一个简单的补救方法,就是建立一个指向cosnt CD的灵巧指针:

SmartPtr<const CD> p =            // p 是一个 non-const 灵巧指针
  &goodCD;                        // 指向const CD 对象
现在我们可以建立constnon-const对象和指针的四种不同组合:
SmartPtr<CD> p;                          // non-const 对象
                                         // non-const 指针
 
SmartPtr<const CD> p;                    // const 对象,
                                         // non-const 指针
 
const SmartPtr<CD> p = &goodCD;          // non-const 对象
                                         // const指针
 
const SmartPtr<const CD> p = &goodCD;    // const 对象
                                         // const 指针
 

但是美中不足的是,使用dumb指针我们能够用non-const指针初始化const指针,我们也能用指向non-cosnt对象的指针初始化指向const对象的指针;就像进行赋值一样。例如:

CD *pCD = new CD("Famous Movie Themes");
 
const CD * pConstCD = pCD;               // 正确

但是如果我们试图把这种方法用在灵巧指针上,情况会怎么样呢?

SmartPtr<CD> pCD = new CD("Famous Movie Themes");
 
SmartPtr<const CD> pConstCD = pCD;       // 正确么?

SmartPtr<CD> SmartPtr<const CD>是完全不同的类型。在编译器看来,它们是毫不相关的,所以没有理由相信它们是赋值兼容的。到目前为止这是一个老问题了,把它们变成赋值兼容的惟一方法是你必须提供函数,用来把SmartPtr<CD>类型的对象转换成SmartPtr<const CD>类型。如果你使用的编译器支持成员模板,就可以利用前面所说的技巧自动生成你需要的隐式类型转换操作。(我前面说过,只要对应的dumb指针能进行类型转换,灵巧指针也就能进行类型转换,我没有欺骗你们。包含const类型转换也没有问题。)如果你没有这样的编译器,你必须克服更大的困难。

包括const的类型转换是单向的:从non-constconst的转换是安全的,但是从constnon-const则不是安全的。而且用const指针能的事情,用non-const指针也能做,但是用non-const指针还能做其它一些事情(例如,赋值操作)。同样,用指向const的指针能做的任何事情,用指向non-const的指针也能做到,但是用指向non-const的指针能够完成一些使用指向const的指针所不能完成的事情(例如,赋值操作)。

这些规则看起来与public继承的规则相类似(Effective C++ 条款35)。你能够把一个派生类对象转换成基类对象,但是反之则不是这样,你对基类所做的任何事情对派生类也能做,但是还能对派生类做另外一些事情。我们能够利用这一点来实作灵巧指针,就是说可以让每个指向T的灵巧指针类public派生自一个对应的指向const-T的灵巧指针类:

template<class T>                    // 指向const对象的
class SmartPtrToConst {              // 灵巧指针
 
  ...                                // 灵巧指针通常的
                                     // 成员函数
 
protected:
  union {
    const T* constPointee;           //  SmartPtrToConst 访问
    T* pointee;                      //  SmartPtr 访问
  };
};
 
template<class T>                    // 指向non-const对象
class SmartPtr:                      // 的灵巧指针
  public SmartPtrToConst<T> {
  ...                                // 没有数据成员
};

使用这种设计方法,指向non-const-T对象的灵巧指针包含一个指向const-Tdumb指针,指向const-T的灵巧指针需要包含一个指向cosnt-Tdumb指针。最方便的方法是把指向const-Tdumb指针放在基类里,把指向non-const-Tdumb指针放在派生类里,然而这样做有些浪费,因为SmartPtr对象包含两个dumb指针:一个是从SmartPtrToConst继承来的,一个是SmartPtr自己的。

一种在C世界里的老式武器可以解决这个问题,这就是union,它在C++中同样有用。Unionprotected中,所以两个类都可以访问它,它包含两个必须的dumb指针类型,SmartPtrToConst<T>对象使用constPointee指针,SmartPtr<T>对象使用pointee指针。因此我们可以在不分配额外空间的情况下,使用两个不同的指针(参见Effective C++条款10中另外一个例子)这就是union美丽的地方。当然两个类的成员函数必须约束它们自己仅仅使用适合的指针。这是使用union所冒的风险。

利用这种新设计,我们能够获得所要的行为特性:

SmartPtr<CD> pCD = new CD("Famous Movie Themes");
 
SmartPtrToConst<CD> pConstCD = pCD;     // 正确
 

评价

有关灵巧指针的讨论该结束了,在我们离开这个话题之前,应该问这样一个问题:灵巧指针如此繁琐麻烦,是否值得使用,特别是如果你的编译器缺乏支持成员函数模板时。

经常是值得的。例如通过使用灵巧指针极大地简化了条款29中的引用计数代码。而且正如该例子所显示的,灵巧指针的使用在一些领域受到极大的限制,例如测试空值、转换到dumb指针、继承类向基类转换和对指向const的指针的支持。同时灵巧指针的实作、理解和维护需要大量的技巧。Debug使用灵巧指针的代码也比Debug使用dumb指针的代码困难。无论如何你也不可能设计出一种通用目的的灵巧指针,能够替代dumb指针。

达到同样的代码效果,使用灵巧指针更方便。灵巧指针应该谨慎使用, 不过每个C++程序员最终都会发现它们是有用的。

Posted in 未分类 | Leave a comment

在线词典大全

韦氏在线词典 在线金山词霸
 
 
剑桥在线词典 牛津在线词典
 
 
朗文在线词典 牛津高阶英汉双解在线词典
 
 
同义词在线词典 反义词在线词典
 
 

总列表

翻译类字典辞典

英汉类字典辞典

金山词霸在线版 —— 国人自主开发的最权威的电子词典,词霸搜索-免费在线词典查词翻译_英汉_日汉_英语_成语
百度词典搜索—— 百度词典搜索支持强大的英汉互译功能,中文成语的智能翻译
Yahoo学生英汉字典 —— 英语单词查询、举例
Dict_CN 在线词典 —— 在线搜索不重复汉英词条100万,英汉词条103万。
牛津英汉双解词典
顏氏美語 —— 颜元叔教授主编-网路英英/英汉辞典
 
中国专家翻译网—— 英文翻译公司–日文翻译公司–多语种翻译公司- 英语、日语、德语、法语翻译
中国译典@中国在线翻译网 —— 线上最庞大的英汉-汉英翻译语料库
『中国百帆』智能在线翻译系统
金桥翻译中心 _WWW_NETAT_NET
Free Website Translator – Online Website Translation 网页翻译

汉英类字典辞典

在线汉英双解新华字典
林语堂当代汉英词典(繁) —— 较权威的在线汉英词典,繁体,备有汉字部首索引和汉语拼音检索功能
华翼翻译-多语种在线电脑字典
太阳雨英汉\汉英词典 汉语输入方式:拼音、简体繁体
洪恩双语词典 中英文查询,提供词义、例句、词组、同义词、反义词

英文类字典辞典

朗文英语辞典
韦氏英语辞典
English Dictionary from freesearch_co_uk —— 例句不错
LEO English-German Dictionary —— 英文、德文互译在线词典,英文界面
Wordsmyth —— 美国英语,有难易之分
Latin-English Dictionary —— 在线拉丁语和英语词典,历史较久,英文界面
Webster’s Collegiate Dictionary —— 著名的韦氏大词典在线版,使用方便,英文词汇多
American Sign Language Dictionary —— 美国形体语言词典,独特的在线词典
Dictionary.com—— 查询、互译、流行词汇、站点导航,英文
Thesaurus.com
—— 英语同义词字典,英文
LEO English-German Dictionary 英文、德文互译在线词典,英文界面
Onelook Dictionary 英语、法语、德语、意大利语五种语言629本词典的在线检索,权威,英文
Latin-English Dictionary 在线拉丁语和英语词典,历史较久,英文界面 
Travlang’s Translating Dictionaries 欧洲主要语言互译,很多链接资源,英文
American Sign Language Dictionary 美国形体语言词典,独特的在线词典 
Oxford English Dictionary 牛津英语大词典在线版,须注册才能使用,英文
剑桥在线辞典 —— 包括剑桥国际英语辞典、美国英语辞典、国际短语辞典及国际习语辞典,英文
Thesaurus.com 英语同义词字典,英文
ANTONYM Finder and SYNONYM Thesaurus —— 同义词反义词词典

其他语言类字典辞典

日英 日韓 日中 – infoseek マルチ翻訳
德汉字典网
华翼电脑字典 —— 荷兰语、中文、英语、法语、 …
线索中国 – 网上字典
承隆科技 
face=Arial>Amasoft
線上、離線英漢翻譯軟體,英漢/漢英字典 
颜元叔教授主编-网路英英/英汉辞典
DictionaryHK.com 提供英文对英文、中文和日文的翻译。
纳西东巴象形文字纳汉在线字典 – 提供汉字与东巴象形文字在线翻译工具。 
YourDictionary.com —— 几十个语种和数百本词典的在线检索,最权威的在线词典门户之一,英文界面
法汉汉法词典 —— 法语 汉语 在线 词典
西班牙语汉语
 
汉语类字典辞典

解字类字典辞典

中华在线词典 —— 12部在线汉语词典免费使用
在线汉语字典
引经据典
中文字谱
—— 为学汉语的外国人所设,有英汉对照分析汉字的由来、汉字发音、象形文字、英汉索引
字典网
—— 国语辞典、台语辞典、中文字典、中文字谱、JDIC等

成语类字典辞典

成语词典 —— 金石网
在线成语大字典
中华成语网上字典
瓷都成语知识系统

诗词类字典辞典

中华诗词 —— 共收录了4577位诗词作家61981首作品
稻香居电脑作诗机网络版 —— 包含电脑智能作诗(填词),电脑辅助作诗(填词),电脑诗词游戏三个部分
A Chinese-Cantonese Syllabary – 黄锡凌《粤音韵汇》 … [ 繁体]
 
 
专业类字典辞典

缩写类字典辞典

常用缩写
电脑术语与缩写
世界各国和地区名称代码表(标准代码,国际域名缩写)
行业代码表
Acronym Finder —— (英)收錄408,000縮寫頭字語款目,主題包括電腦、科技、電訊和軍事方面

百科类字典辞典

大英百科全书 —— (英)著名的百科全书
维基百科 —— 维基百科,自由的百科全书

法律类字典辞典

尚华法律 —— 深圳律师咨询、聘请律师、深圳律师培训、律师邮局、律师建站
法律与经济学资料库 —— 综合性的经贸与法律大辞典,可按类别和题目搜索
Duhaime’s Law Dictionary ——(英)

金融类字典辞典

巴克利金融术语汇编 —— 简单易学的金融术语辞典
金融专业术语大辞典 —— 有些词很难找,是不是,来我们这儿试试吧!
中國經貿新詞詞典 —— 香港贸发局维护的经贸词典
交易商必备辞典 —— 完整地收集了交易中可能用到的各种专业术语

医药类字典辞典

英汉医学词典 —— 大型英汉医学词典
药品字典

计算机类字典辞典

英汉计算机及网络通讯技术词汇
CNPedia 华文电脑百科
趨勢科技-病毒百科
Encyberpedia Dictionary and Glossary —— (英文)因特网上主要的字典和词汇一览表,
On-line Dictionary of Computing —— 计算机专业词典,英文
ComputerUser_com High-Tech Dictionary
Whatis_com, the leading IT encyclopedia and learning center

科技类字典辞典

郑州大学在线英汉-汉英科技大词典 —— 本词典固定词库中现收录英汉科技词语490269条,汉英科技词语744542条
高技术辞典网

其他类字典辞典

中国水灾年表 —— 中国历史各年水灾灾情、损失等介绍
植物病理学在线术语汇编 [English] —— 详细的带有解说的大辞典,其中收集了植物病理学的专业术语及其发音
土木建筑电子词典 —— 台湾國科會科學技術資料中心國內資源組维护
包装词汇英汉对照 —— 有关包装词汇的英汉对照词库,约29000条
英汉电子工程辞典
宗教小词典
涉藏汉英词汇字典
Movie Terminology Glossary —— 电影专业术语辞典
化工字典 —— 现有词汇量 300,000 条
化工字典 _ 中国试剂信息网 —— 现有词汇量351681条。其中包含CAS编号的有246435条
纺织字典 —— 收录有关纺织方面的词汇量高达 2,5000 多条
纺织字典 —— 共收录了30,000条与纺织有关的词汇
第五代简体仓颉字典
繁体仓颉字典
泵阀字典 —— 收录有关泵阀方面的词汇量高达 20,000 多条
Posted in 未分类 | Leave a comment

一百万(1000000)以内的素数表检索

Posted in 未分类 | Leave a comment