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 {
sizeOld = sizeofBlock(*ppb);
/* 如果缩小,先把将被释放的内存空间填写上废料
* 如果扩大,通过模拟realloc的操作来迫使新的内存块产生移动 *(不让它在原有的位置扩展)如果新块和老块的长度相同,不 * 做任何事情 */
if(sizeNew < sizeOld)
memset((*ppb)+sizeNew, bGarbage, sizeOld-sizeNew);
else if(sizeNew > sizeOld) { }
}
在上面的程序中,所增加的新代码只有在相应的内存块被扩大时,才会被执行。通过在释放原有内存块之前分配一个新的内存块,可以保证只要分配成功,相应内存块的存储位置就会被移动。如果分配失败,那么所增加的新代码相当于一个很大的空操作指令。资料个人收集整理,勿做商业用途 byte* pbNew;
if( fNewMemory(&pbNew, sizeNew) ) { }
memcpy(pbNew, *ppb, sizeOld); FreeMemory(*ppb); *ppb = pbNew;
} #endif
pbResize = (byte*)realloc(*ppb, sizeNew); ……
11 / 24
但是,请注意上面程序中所增加的新代码不仅使相应内存块不断地移动而,还顺便冲掉了原有内存块的内容。当它调用FreeMemory释放原有的内存块时,该内存块的内容即被冲掉。资料个人收集整理,勿做商业用途 现在你可能会想:既然上面的程序是用来模拟realloc的,那它为什么还要调用realloc呢?而且在所增加的代码中加入一条return语句,例如:资料个人收集整理,勿做商业用途 if( fNewMemory(&pbNew, sizeNew) ) { }
不是可以提高其运行速度吗?
我们为什么不这样做呢?我们可做到这点,但切记不要这么做,因为它是个不良的习惯。要记住调试代码是多余的代码,而不是不同的代码。除非有非常值得考虑的理由,永远应该执行原有的非调试代码,即使它在加入了调试代码之后已经变得多余。毕竟查出代码错误的最好方法是执行代码,所以要尽可能地执行原有的非调试代码。资料个人收集整理,勿做商业用途 有时在我向程序员解释这些概念时,他们会反驳说:“总是移动内存块正如水远不移动内存块一样有害,你已经走到了另一个极端。”他们确实非常机敏,因此有必要解释一下。
资料个人收集整理,勿做商业用途 memcpy(pbNew, *ppb, sizeOld); FreeMemory(*ppb); *ppb = pbNew; return(TRUE);
假如在程序的调试版本和交付版本中都总是做某件事情,那么它确实如同永远不做一样有害。但在这个例子中,fResizeMemory实际上并不紧张,尽管其调试版本是那样不屈不挠地对内存块进行移动,就好象吃了安非他明一样。资料个人收集整理,勿做商业用途 如果某事件很少发生并没有什么问题,只要在程序的交付版本和调试版本中不少发生就行。
如果某件事甚少发生的话,设法使其经常发生 保存一个日志,以唤起你的注意
从调试的端点看,内存管理程序的问题是当第一次创建内存块时知道其大小,但随后几乎马上就会失去这一信息,除非在某个地方保存了一个有关的记录。我们已经看到函数sizeofBlock的价值很大,但如果能够知道已分配内存块的数目及其在内存中的具体存储位置,用处会更大。假如能够知道这些信息,那么不管指针的值是什么,我们都能够确定它是否有效。如果能这样,该有多大的用处,尤其是对于函数参数的确认。资料个人收集整理,勿做商业用途 假定我们有函数fValidPointer,该函数有指针pv和大小size两个参数;当pv实际
12 / 24
指向的内存分配块正好有size个字节时,该函数返回TRUE。利用这一函数我们可以为常用的子程序编写出更加严格的专用版本。例如,如果发现内存分配块的部分内容常常被填掉,那么我们可以绕过对指针检查得不太严格的memset函数,而调用自己编写的FillMemory程序。该程序能够对其指针参数进行更加严格的确认:资料个人收集整理,勿做商业用途 void FillMemory(void* pv, byte b, size_t size) { }
通过应用fValidPointer,该函数可以保证pv指向的是一个有效的内存块。而且,从pv到该内存块的尾部至少会有size个字节。资料个人收集整理,勿做商业用途 如果愿意的话我们可以在程序的调试版本中调用FillMemory,而在其交付版本中直接调用memset。要做到这一点,只需在其交付版本中包括如下的全局宏定义:资料个人收集整理,勿做商业用途 ASSERT(fValidPointer(pv, size)); Memset(pv, b, size);
#define FillMemory(pb, b, size) memset((pb), (b), (size))资料个人收集整理,勿做商业用途 这些内容已经有点离题了。
这里一直强调的是如果在程序的调试版本中保存额外的信息,就经常可以提供更强的错误检查。
到目前为止,我们介绍了在FillMemory和fResizeMemory中使用sizeofBlock填充内存块的方法。但这种方法同通过保存一个含有所有分配内存块信息的记录所能做到的相比,只是个相对“弱”的错误发现方法。资料个人收集整理,勿做商业用途 同前面一样,我们仍然假定会遇到最坏的情况:从相应的子系统本身,我们得不到关于分配内存块的任何信息。这意味着通过内存管理程序,我们得不到内存块的大小,不知道指针是否有效,甚至不知道某个内存块是否存在或者已经分配了多少个内存块。因此如果程序中需要这些信息,就必须自己提供出来。这就是说,在程序中得保存一个某种类型的分配日志。至于如何保存这个日志并不重要,重要的是在需要这些信息时就能够得到。资料个人收集整理,勿做商业用途 维护日志的一种可能方法是:当在fNewMemory中分配一个内存块时,为日志信息也分配一个内存块;当在fFreeMemory中释放一个内存块时,还要释放相应的日志信息;当在fResizeMemory中改变了内存块的大小时,要修改相应的日志信息,使它反映出相应内存块的新大小和新位置。显然,我们可以把这三个动作封装在三个不同的调试界面中:资料个人收集整理,勿做商业用途 /* 为新分配的内存块建立一个内存记录 */
flag fCreateBlockInfo(byte* pbNew, size_t sizeNew);资料个人收集整理,勿做商业用途 /* 释放一个内存块对应的日志信息 */ void FreeBlockInfo(byte* pb);
13 / 24
/* 修改现有内存块对应的日志信息 */
void UpdateBlockInfo(byte* pbOld, byte* pbNew, size_t sizeNew);资料个人收集整理,勿做商业用途 当然,只要它们不使相应系统的运行速度降低到无法使用的程度,这三个程序维护日志信息的方法就不很重要。读者在附录B中可以找到上述函数的实现代码。资料个人收集整理,勿做商业用途 对FreeMemory和fResizeMemory进行修改,使其调用适当的子程序非常简单。修改后的FreeMemory变成了如下形式:资料个人收集整理,勿做商业用途 void FreeMemory(void* pv) { }
在fResizeMemory中,如果realloc成功地改变了相应内存块的大小,那么就调用UpdateBlockInfo(如果realloc失败,自然就没有什么要修改的内容)。fResizeMemory的后一部分如下:资料个人收集整理,勿做商业用途 flag fResizeMemory(void** ppv, size_t sizeNew) {
……
pbResize = (byte*)realloc(*ppb, sizeNew); if(pbResize != NULL) {
#ifdef DEBUG {
UpdateBlockInfo(*ppb, pbResize, sizeNew); /* 如果扩大,对尾部增加的内容进行初始化 */ if(sizeNew > sizeOld)
memset(pbResize+sizeOld, bGarbage, sizeNew-sizeOld);资料个人收集整#ifdef DEBUG { } #endif free(pv);
memset(pv, bGarbage, sizeofBlock(pv)); FreeBlockInfo(pv);
理,勿做商业用途
} #endif
*ppb = pbResize;
14 / 24
}
}
return(pbResize != NULL);
fNewMemory的修改相对要复杂一些,所以把它放到了最后来讨论。当调用fNewMemory分配一个内存块时系统必须分配两个内存块:一个用来满足调用者的请求,另一个用来存放相应的日志信息。只有在两个内存块的分配都成功时,fNewMemory的调用才会成功。如果不这样规定就会使某些内存块没有日志信息。要求内存块必须有对应的日志信息非常重要,因为如果没有日志信息,那么在调用对指针参数进行确认的函数时,就会产生断言失败。资料个人收集整理,勿做商业用途 在下面的代码中我们将会看到,如果fNewMemory成功地进行了用户请求空间的分配,但相应日志内容所需内存的分配失败,该函数会把第一个内存块释放掉,并返回一个内存块分配失败标志。这样做可以使所分配的内存内容与相应的日志信息同步。资料个人收集整理,勿做商业用途 fNewMemory的代码如下:
flag fNewMemory(void** ppv, size_t size) { }
15 / 24
byte** ppb = (byte**)ppv; ASSERT(ppv!=NULL && size!=0); *ppb = (byte*)malloc(size); #ifdef DEBUG { } #endif
return(*ppb != NULL);
if(*ppb != NULL) { }
memset(*ppb, bGarbage, size); /* 如果无法创建日志块信息, * 那么模拟一个总的内存分配错误。 */
if( !fCreateBlockInfo(*ppb, size) ) { }
free(*ppb); *ppb = NULL;
优质C程序秘诀 - 章- 为子系统设防
![](/skin/haowen/images/icon_star.png)
![](/skin/haowen/images/icon_star.png)
![](/skin/haowen/images/icon_star.png)
![](/skin/haowen/images/icon_star.png)