zoukankan      html  css  js  c++  java
  • 编程精粹--编写高质量C语言代码(4):为子系统设防(一)

    通常,子系统都要对事实上现细节进行隐藏,在进行细节隐藏的同一时候。子系统为用户提供了一些关键入口点。

    程序猿通过调用这些关键的入口点来实现与子系统的通信。因此假设在程序中使用这种子系统而且在其调用点加上了调试检查,那么不须要花费多少力气就能够进行很多错误检查。

    当子系统编写完毕后,要问自己:“程序猿什么情况下会错误地使用这个子系统。在这个子系统中如何才干自己主动检查出这些问题?”在这篇文章中。将讲述一些用来肃清子系统中错误的技术。使用这些技术。能够免除很多麻烦。本章将以C的内存管理程序为例,但所得到的结论相同适用于其他子系统。

    通常,我们能够直接在子系统中增加对应的測试代码,可是有时我们无法得到子系统的源码。所以这里我们将利用所谓的“外壳”函数把内存管理程序包装起来,并在这层包装的内部加上对应的測试代码。

    首先以malloc的外壳函数fNewMomory为例:

    flag fNewMemory(void** ppv,size_t size)
    {
    	byte** ppb=(byte**)ppv;
    	*ppb=(byte*)malloc(size);
    	return (*ppb!=NULL); 
    }

    从fNewMemory的定义我们能够看出,曾经我们须要这样调用malloc: pbBlock=(byte*)malloc(32);而如今假设使用fNewMemory,就须要这样调用,fNewMemory(&pbBlock,32)。同一时候,malloc通过推断pbBlock是否为NULL指针来推断分配内存是否成功,而fNewMemory直接通过函数的返回值来进行推断。这样设计是有原因的,笔者将会在后面的文章具体说明。

    <<编程精粹--编写高质量的C语言代码(2):自己设计并使用断言(一)>>中讲过,对于没有定义的特性,要么将其从程序设计里去掉,要么利用断言来验证其不会被用到。ANSI C中的malloc的没有定义特性有两点:1。当分配内存块的大小为0时。其结果没有定义;2。当内存块分配成功后,内存块的初始内容没有定义。

    对于第一点。我们能够使用断言来进行检查。可是对于第二点。我们无法用断言来进行验证。

    那假设我们人为地利用一个常规值(比如0)来填充这个内存块,这样就能够消去这个没有定义的特性。可是这样至少带来两点影响:1,对内存块填充一个常规值有可能会影响程序的结果。2。有可能会隐瞒错误(比如程序猿在分配内存后未初始化。可是因为事先对内存块填充了一个值,所以程序可能正常执行,从而隐瞒错误)。

    可是,不管怎样我们还是不希望内存块的初始内容没有定义,由于这样意味着错误难以再现。

    由于有可能程序仅仅有在某个特定的初始值时才出错。

    这样程序大部分时间都发现不了错误,但总是不明原因地失败。

    暴露错误的关键就是消除发生错误的随机性。所以对于malloc来说。仅仅有对其分配的内存块进行填充,才干消除其随机性。可是又要避免填充值对程序造成影响或者隐瞒程序中的错误,所以填充值应该离奇地看起来像无用信息。并且这样的填充应该在程序的调试版本号中。这样既能够解决这个问题,又不影响程序的发行版。

    在基于Intel 80x86的机器上,作者推荐这个值为OxCC。

    所以新版本号的fNewMemory的代码例如以下:

    #define bGarbage 0xCC
    flag fNewMemory(void** ppv,size_t size)
    {
    	byte** ppb=(byte**)ppv;
    	ASSERT(ppv!=NULL&&size!=0)
    	*ppb=(byte*)malloc(size);
    	#ifdef DEBUG
           {
        	 if(*ppb!=NULL)
        	    memset(*ppb,bGarbage,size); 
            }
            #endif
    	return (*ppb!=NULL); 
    } 

    fNewMemory不仅能够有助于错误的再现,并且经常使错误被非常easy的发现出来。比如当你调试跟踪时。发现某个值是0xCC,是不是让你瞬间想到这是个未初始化的数据。因此要查看子系统,确定子系统中引起随机错误的设计之处。一旦发现了这些地方,就能够通过改变对应的设计方法来把它们排除,或者在他们周围加上调试代码,最大限度地降低错误行为的随机性。

              要消除错误的随机性--使错误可再现

    接下来是内存释放函数free的外壳函数FreeMemory,在ANSI C中,假设传递给free函数的指针是个无效指针,那么free函数的结果是没有定义的。所以对于没有定义的特性,我们要么改变设计以消除没有定义的特性。要么使用断言检查没有定义的特性不会被使用。

    同一时候,另一点须要注意:即使我们把内存释放了,可是假设还有其它指针指向这块内存,并且继续对这块内存进行訪问。得到的似乎还是有效数据。所以已经释放了的无用内存仍然包括着好像有效的数据,这将让我们程序错误,并且难以发现。

    void FreeMemory(void* pv)
    {
    	ASSERT(pv!=NULL);
    	#ifdef DEBUG
        {
        	memset(pv,bGarbage,sizeofBlock(pv));
        }	 
        #endif
        free(pv);
    } 


    FreeMemory 中首先检查pv是否为空指针,作者不赞成为了实现方便,就把无意义的空指针传给FreeMemory函数,所以用断言检查pv不能为空指针,接着增加调试代码,把即将被释放的内存用垃圾填充。

    这样当我们对已经被释放的内存块进行訪问时。得到的就是垃圾信息。

    这样有助于我们发现错误。

    这里用到的sizeofBlock函数是须要我们自己编写的调试函数,用来获取指针所指向内存块的大小。

    再来看realloc的外壳函数fResizeMemory。fResizeMemory函数用来改变内存块的大小。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
        {
        	sizeOld=sizeof(*ppb);
        	/** 假设缩小,冲掉尾部没用的内存 */ 
        	if(sizeNew<sizeOld)
        	{
    	        memset((*ppb)+sizeNew,bGarbage,sizeOld-sizeNew);	
    	    }
        }
        #endif
        pbResize=(byte*)realloc(*ppb,sizeNew);
        if(pbResize!=NULL)
        {
        	#ifdef DEBUG
        	{
        		/** 假设扩大,对尾部添加的内容用无用信息填充 */ 
    	    	if(sizeNew>sizeOld)
    	    	{
    	           memset((*ppb)+sizeOld,bGarbage,sizeNew-sizeOld);		
    	    	}	    	 
    	    }
    	    #endif
    	    *ppb=pbResize;
        }
        return (pbResize!=NULL);   
        
    }

    代码中有一点须要说明,就是sizeOld这个用于调试的局部变量。

    用#ifdef来保证sizeOld仅仅有在程序调试时才干够使用,当程序交付版本号中不小心使用了这个变量,就会获得一个编译错误。上面的程序代码虽然看上去有些复杂,可是调试版本号本来就不必短小精悍。一般能够在程序中加上你觉得有必要的不论什么调试代码,以增强程序的查错能力。

              冲掉无用信息,以免被错误地使用。

    可是上述程序另一个隐藏的非常深的错误。ANSI C中说明了realloc扩大内存时有可能会让原有的内存块进行移动,也就是说扩大后的内存块有可能被分到新的地址处。该块原有的内容被复制到新的位置。这会导致什么后果呢?想象一下。假设有两个指针p,q,它们都指向同一块内存。然后realloc把指针p作为參数,对这块内存进行扩大。而此时内存块发生了移动,p指向了新的内存块位置,而q仍然指向的是原来的内存块位置。而原来的内存块位置事实上已经被释放了,可是数据可能看起来仍然有效。

    更要命的是。realloc的这个特性可能非常少发生。所以你的程序是震荡的。时而正确。时而出错。

    你可能给出一种解决方式:在fResizeMemory中增加调试代码,假设内存块发生移动时,就把原来的内存块用无用信息填充,当我们对原来的内存块进行訪问时。得到无用信息。就会发现这个错误。非常遗憾。这样的方案是不行的,由于原来的内存块是内存管理程序自己释放的,我们不知道内存管理程序会对其释放了的内存空间怎样处理。一旦我们动了这部分内存空间,就会有破坏整个系统的危急。

    虽然上面描写叙述的realloc的这个特性可能非常少发生。可是我们编写无错代码的一个准则就是:“不要让事情非常少发生”

    因此我们须要确定子系统可能发生哪些事情,而且使他们常常发生和一定发生。假设确实发现子系统中极罕见的行为,要千方百计地使其重现。

    对于realloc的这个特性,我们无法控制让realloc常常移动内存块。可是我们能够在调试代码中模仿realloc的这个特性,我们在realloc扩大内存块时。通过先新建一个新的内存块,然后把原来内存块的内容复制到这个新的内存块,最后释放掉原有的内存块,就能够准确的模仿出realloc的所有动作。

    flag fResizeMemory(void** ppv,size_t sizeNew)
    {
    	byte** ppb=(byte**) ppv;
    	byte* pbResize;
    	#ifdef DEBUG
    	size_t sizeOld;
    	#endif;
    	ASSERT(ppv!=NULL&&sizeNew!=0);
    	#ifdef DEBUG
    	{
    		sizeOld=sizeofBlock(*ppb);
    		if(sizeOld>sizeNew)
    		{
    			memset(ppb+sizeNew,bGarbage,sizeOld-sizeNew);
    		} 
    		else if(sizeOld<sizeNew)
    		{
                byte* pbNew;
                /** 模拟realloc的内存块移动 */ 
    			if(fMemoryNew(&pbNew,sizeNew))
    			{
    				memcpy(pbNew,*ppb,sizeOld);
    				FreeMemory(*ppb);
    				*ppb=pbNew;
    			}  
    		}
    	}
    	#endif
    	pbResize=(byte*)realloc(*ppb,sizeNew);
        /** 后面代码省略 */	
    }


    上面的程序代码不仅使对应的内存发生了移动,并且还充掉了原有内存块的内容。由于它调用了FreeMemory释放原有内存块的同一时候。该内存块的内容也会被垃圾信息填充。

    另一点须要说明。即使我们通过移动内存块的位置模仿了realloc的行为,可是我们还是调用了realloc函数,由于调试代码仅仅是多余的代码,而不是不同的代码,除非有很值得考虑的理由。否则永远运行原有的非调试代码。毕竟查出代码错误的最好方法是运行代码。所以我们尽可能运行原有的非调试代码。

    可能你还是对上述做法的原因不是非常清楚。笔者的理解是:realloc扩大内存块可能让内存块的位置发生移动,可是realloc的这个特性非常少发生。所以你的程序有可能长时间都是正确的,可是一旦realloc的这个特性发生了,有可能你的程序就会错误发生。

    那为了我们的程序可以在这样的情况下仍然成功,那我们在程序的调试版本号中。通过模拟realloc这个特性。检查我们程序中是否存在错误。假设程序可以正常执行,那我们就不用操心程序的交付版本号中realloc的这个特性了,由于我们已经在调试版本号中考虑过了。

    所以假设某件事情非常少发生,这并没有什么问题。仅仅要在程序的调试版本号中不少发生即可了。

              假设某件事甚少发生的话,设法使其常常发生。



    总结:

    1。考察所编写的子系统,问自己:“在什么样的情况下。程序猿在使用这些子系统时会犯错误。”在系统中加上对应的断言和确认检查代码。以捕捉难以发现的错误和常见的错误”。

    2。找出程序中可能引起随机行为的因素。将它们从程序的调试版本号中清除。

    这样至少每次程序出错时,都会得到相同的错误结果。

    3。假设编写的子系统释放了内存(或其它资源),并因此产生了“无用信息”。那么要把它搅乱,使它真的像无用信息。否则,这些被释放了的数据就有可能仍被引用,而又不会引起注意。

    4。假设编写的子系统中某些事情可能发生,那么要为子系统加上对应的调试代码,使这些事情一定发生。这样对于那些通常得不到运行的代码。能够提供检查出错误的可能性。


    最后依然以一句话结束这篇文章:


              错误处理程序之所以往往easy出错,正是由于它们非常少被运行到。



  • 相关阅读:
    HDU 6182 A Math Problem 水题
    HDU 6186 CS Course 位运算 思维
    HDU 6188 Duizi and Shunzi 贪心 思维
    HDU 2824 The Euler function 欧拉函数
    HDU 3037 Saving Beans 多重集合的结合 lucas定理
    HDU 3923 Invoker Polya定理
    FZU 2282 Wand 组合数学 错排公式
    HDU 1452 Happy 2004 数论
    HDU 5778 abs 数论
    欧拉回路【判断连通+度数为偶】
  • 原文地址:https://www.cnblogs.com/gavanwanggw/p/7000151.html
Copyright © 2011-2022 走看看