好文档 - 专业文书写作范文服务资料分享网站

优质C程序秘诀 - 章- 为子系统设防 

天下 分享 时间: 加入收藏 我要投稿 点赞

没有内存管理程序的源代码,也不必着急,在本章稍后的内容中,我们将介绍一种sizeofBlock的实现方法。资料个人收集整理,勿做商业用途 还是让我们假定已经有了sizeofBlock函数。利用这个函数,在释放之前可以破坏掉相应内存块的内容:资料个人收集整理,勿做商业用途 void FreeMemory(void* pv) { }

该函数中的调试代码不仅对所释放内存块的内容进行了废料填充,而且在调用sizeofBlock时,还顺便对pv进行了确认。如果该指针不合法,就会被sizeofBlock查出(该函数当然可以做到这一点,因为它肯定了解每个内存分配块的细节)。资料个人收集整理,勿做商业用途 ASSERT(pv != NULL); #ifdef DEBUG { } #endif free(pv);

memset(pv, bGarbage, sizeofBlock(pv) );

既然NULL是free的合法参数(根据ANSI标准,此时free什么也不做),为什么还要使用断言来检查pv是否为NULL,这不是很奇怪吗?这样做的原因应该是在意料之中:我不赞成只为了实现方便,就允许将无意义的NULL指针传递给函数。这一断言就是用来对这种用法进行确认。当然,你也许有不同的观点,所以可能想把该断言去掉。但我要说的是,用户不必盲目地遵守ANSI标准。其他人认为free应该接受NULL指针,并不意味你也得接受这一想法。资料个人收集整理,勿做商业用途 relloc是释放内存并产生无用信息的另一个函数。下面给出它的外壳函数,它与malloc的外壳函数fNewMemory很类似:资料个人收集整理,勿做商业用途

flag fResizeMemory(void** ppv, size_t size) { }

同fNewMemory一样,fResizeMemory也返回一个状态标志,该标志表明对相应内存块大小的改变是否成功。如果pbBlock指向的是一个已经分配了的内存块,那么可以这样改变

6 / 24

byte** ppb = (byte**)ppv; byte* pbResize;

pbResize = (byte*)realloc(*ppb, sizeNew); if( *pbResize != NULL )

*ppb = pbResize;

return(*pbResize != NULL);

其大小。资料个人收集整理,勿做商业用途 if( fResizeMemory(&pbBlock, sizeNew) )

成功 ─── pbBlock指向新的内存块

else

不成功 ─── pbBlock指向老的内存块

读者应该注意到了,同relloc不一样,fResizeMemory在操作失败的情况下并不返问空指针。此时,新返回的指针仍然指向原有的内存分配块,并且块内的内容不变。资料个人收集整理,勿做商业用途 有趣的是,realloc函数(fResizeMemory也是如此)既要调用free,又要调用malloc。执行时究竟调用哪个函数,取决于是要缩小还是扩大相应内存块的大小。在FreeMemory中,相应内存块的内容在被释放之前即被冲掉;而在fNewMemory中,在调用malloc之后新分配的内存块即被填上看起来很怪的“废料”。为了使fResizeMemory比较健壮,这两件事情都必须做。因此,该函数中要有两个不同的调试代码块:资料个人收集整理,勿做商业用途 flag fResizeMemory(void** ppv, size_t sizeNew) {

byte** ppb = (byte**)ppv; byte* pbResize; #ifdef DEBUG size_t sizeOld; #endif

ASSERT(ppb!=NULL && sizeNew!=0); #ifdef DEBUG { } #endif

pbResize = (byte*)realloc(*ppb, sizeNew); if(pbResize != NULL) {

#ifdef DEBUG {

/* 如果扩大,对尾部增加的内容进行初始化 */ if(sizeNew > sizeOld)

memset(pbResize+sizeOld, bGarbage, sizeNew-sizeOld);资料个人收7 / 24

sizeOld = sizeofBlock(*ppb); /* 如果缩小,冲掉块尾释放的内容 */ if(sizeNew

memset((*ppb)+sizeNew, bGarbage, sizeOld-sizeNew);

/* 在此引进调试局部变量 */

集整理,勿做商业用途 } #endif

*ppb = pbResize;

}

为了做这两件事在该函数中似乎增加了许多额外的代码。但仔细看过就会发现,其中的大部分内容都是虚的。如花括号、#ifdef伪指令和注解。就算它确实增加了许多的额外代码,也不必杞人忧天。因为调试版本本来就不必短小精悍,不必有特别快的响应速度,只要能够满足程序员和测试者的日常使用要求就够了。因此,除非调试代码会变得太大、太慢而没法使用,一般在应用程序中可以加上你认为有必要的任何调试代码。以增强程序的查错能力。资料个人收集整理,勿做商业用途 重要的是要对子系统进行考查,确定建立数据和释放数据的各种情况并使相应的数据变成无用信息。

}

return( pbResize != NULL );

冲掉无用的信息,以免被错误地使用 用#ifdef来说明局部变量很难看!

看看sizeOld,一个只用于调试的局部变量。虽然将sizeOld的说明括在#ifdef序列中,使程序变得很难看,但这却非常重要。因为在该程序的交付版本中,所有的调试代码都应该被去掉。我当然知道如果去掉这个#ifdef伪指令,相应的程序会变得更加可读,而且程序的调试版本和交付版本会同样地正确。但这样做的唯一问题是在其交付版本中,sizeOld虽被说明,但却没被使用。资料个人收集整理,勿做商业用途 在程序的交付版本中声明但不使用sizeOld变量,似乎没有问题。但事实并非如此,这样做会引起严重的问题。如果维护程序员没有注意到sizeOld只是一个调试专用的变量,而把它用在了交付版本中,那么由于它未经初始化,可能就会引起严重的问题。将sizeOld的声明用#ifdef伪指令括起来,就明确地表明了sizeOld只是一个调试专用的变量。因此,如果程序员在程序的非调试代码(即使是#ifdef)中使用了sizeOld,那么当构造该程序的

8 / 24

交付版本时就会遇到编译程序错误。这等于加了双保险。资料个人收集整理,勿做商业用途 使用#ifdef指令来除去调试用变量虽然使程序变得很难看,但这种用法可以帮助我们消除一个产生潜在错误的根源。资料个人收集整理,勿做商业用途

产生移动和震荡的程序

假定程序不是释放掉树结构的某个结点,而是调用fResizeMemory将该结点扩大,以适应变长数据结构的要求。那么当fResizeMemory对该结点进行扩展时,如果移动了该结点的存储位置,就会出现两个结点:一个是在新位置的真实结点,另一个是原位置留下的不可用的无用信息结点。资料个人收集整理,勿做商业用途 这样一来,如果编写expandnode的程序员没有考虑到当fResizeMemory在扩展结点时会引起相应结点的移动这种情况,会出现什么问题呢?相应树结构的状态会不会仍然不变,即该结点的邻接结点仍然指向虽然已被释放但看起来似乎仍然有效的原有内存块?扩展之后的新结点会不会漂浮在内存空间中,没有任何的指针指向它?事实确实会这样,它可能产生看起来好象有效但实际上是错误的树结构,并在内存中留下一块无法访问到的内存块。这样很不好。资料个人收集整理,勿做商业用途 我们可以想到通过修改fResizeMemory,使其在扩展内存块引起存储位置移动的情况下,冲掉原有的块内容。要达到这一目的,只需简单地调用memset即可:资料个人收集整理,勿做商业用途 flag fResizeMemory(void** ppv, size_t sizeNew) {

……

pbResize = (byte*)realloc(*ppb, sizeNew); if(pbResize != NULL) {

#ifdef DEBUG {

/* 如果发生移动,冲掉原有的块内容 */ if(pbResize != *ppb)

memset(*ppb, bGarbage, sizeOld);

/* 如果扩大,对尾部增加的内容进行初始化 */ if(sizeNew > sizeOld)

memset(pbResize+sizeOld, bGarbage, sizeNew-sizeOld);资料个人收集整理,勿做商业用途 }

9 / 24

#endif

*ppb = pbResize;

}

很遗憾,这样做不行。即使知道原有内存块的大小和位置,也不能破坏原有内存块的内容,因为我们不知道内存管理程序会对被其释放了的内存空间进行如何的处理。对于被释放了的内存空间,有些内存管理程序并不对其做些什么。但另外一些内存管理程序,却用它来存储自由空间链或者其它的内部实现数据。这一事实意味着一旦释放了内存空间,它就不再属于你了,所以你也不应该再去动它。如果你动了这部分内存空间,就有破坏整个系统的危险。资料个人收集整理,勿做商业用途 举一个非常极端的例子,有一次当我正在为Microsoft的内部68000交叉汇编程序增加新功能时,Macintosh Word和Excel的程序员请求我去帮助他们查明一个长期以来总是使系统偶然失败的错误。检查这个错误的难点在于虽然它并不经常发生,但却总是发生,因此引起了人们的重视。我不想谈过多的细节,但折腾了几周之后我才找到了使这个错误重现的条件,而找出该错误的实际原因却只用了三天的时间。资料个人收集整理,勿做商业用途 找出使这个错误重现的条件花了我很长时间,但我还是不清楚是什么原因引起了这个错误。每当我查看相应的数据结构时,它们看起来似乎都完全没有问题。我没想到这些所谓完全没有问题的数据结构,实际上竟是早先调用realloc遗留下的无用信息!资料个人收集整理,勿做商业用途 }

return( pbResize != NULL );

然而,真正的问题还不在于发现这个错误的准确原因花了我多长的时间,而在于为了找出使这个错误重现的条件花了那么多的时间。realloc在扩大内存块时不但确实会移动相应内存块的位置,而且原有的内存块必须被重新分配并被填写上新的数据。在汇编程序中,这两种情况都很少发生。资料个人收集整理,勿做商业用途 这使我们得出了编写无错代码的另一个准则:“不要让事情很少发生。”因此我们需要确定子系统中可能发生哪些事情,并且使它们一定发生和经常发生。如果发现子系统中有极罕见的行为,要干方百计地设法使其重现。资料个人收集整理,勿做商业用途 你有过跟踪错误跟到了错误处理程序中,并且感到“这段错误处理程序中的错误太多了,我敢肯定它从来都没有被执行过”这种经历吗?肯定有,每个程序员都有过这种经历。错误处理程序之所以往往容易出错,正是因为它很少被执行到。资料个人收集整理,勿做商业用途 同样,如果不是realloc扩大内存块时使原有存储位置发生移动这种现象很罕见,这一汇编程序中的错误在几个小时内就可以被发现,而用不着要耗上几年。可是,怎样才能使realloc经常地移动内存块呢?回答是做不到,至少在相应操作系统没有提供支持的情况下做不到。尽管如此,但我们却能够模拟realloc的所作所为。如果程序员调用fResizeMemory扩大了某个内存块,那么可以通过先建一个新的内存块,然后再把原有内存块的内容拷贝到这个新块中,最后释放掉原有内存块的方法,准确地模拟出realloc的全部动作。资料个人收集整理,勿做商业用途 10 / 24

优质C程序秘诀 - 章- 为子系统设防 

没有内存管理程序的源代码,也不必着急,在本章稍后的内容中,我们将介绍一种sizeofBlock的实现方法。资料个人收集整理,勿做商业用途还是让我们假定已经有了sizeofBlock函数。利用这个函数,在释放之前可以破坏掉相应内存块的内容:资料个人收集整理,勿做商业用途voidFreeMemory(void*pv){}该函数中的调试代码不仅对所释放内存
推荐度:
点击下载文档文档为doc格式
4e9bh1lkow7g2499ip734mu7526k9200fsi
领取福利

微信扫码领取福利

微信扫码分享