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

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

当子系统编写完毕后,要问自己:“程序猿什么情况下会错误地使用这个子系统。在这个子系统中如何才干自己主动检查出这些问题?”在这篇文章中。将讲述一些用来肃清子系统中错误的技术。使用这些技术。能够免除很多麻烦。本章将以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出错,正是由于它们非常少被运行到。

编程精粹--编写高质量C语言代码(4):为子系统设防(一)的更多相关文章

  1. 编程精粹--编写高质量C语言代码(3):自己设计并使用断言(二)

    接着上一遍文章<<编程精粹--编写高质量C语言代码(2):自己设计并使用断言(一)>>,继续学习怎样自己设计并使用断言,来更加easy,更加不费力地自己主动寻找出程序中的错误. ...

  2. <编程精粹:编写高质量C语言代码> 读书笔记

    0.规则<The Elements of Programming Style><The Elements of Style> 1.假想的编译程序(1)使用编译器提供的所有的可选 ...

  3. 编程精粹--编写高质量C语言代码(1):假想编译程序

    编译程序只能查找出程序的语法错误,而对于"数组越界訪问","对空指针解引用"等错误.编译程序是束手无策的.同一时候我们知道測试人员所使用的黑箱測试方法所能做的不 ...

  4. HTML Inspector – 帮助你编写高质量的 HTML 代码

    HTML Inspector 是一款代码质量检测工具,帮助你编写更优秀的 HTML 代码.HTML Inspector 使用 JavaScript 编写,运行在浏览器中,是最好的 HTML 代码检测工 ...

  5. iOS应用开发最佳实践系列一:编写高质量的Objective-C代码

          本文由海水的味道编译整理,转载请注明译者和出处,请勿用于商业用途! 点标记语法 属性和幂等方法(多次调用和一次调用返回的结果相同)使用点标记语法访问,其他的情况使用方括号标记语法. 良好的 ...

  6. 如何编写高质量的js代码--底层原理

    转自: 如何编写高质量的 JS 函数(1) -- 敲山震虎篇   本文首发于 vivo互联网技术 微信公众号 链接:https://mp.weixin.qq.com/s/7lCK9cHmunvYlbm ...

  7. 如何编写高质量的C#代码(一)

    从"整洁代码"谈起 一千个读者,就有一千个哈姆雷特,代码质量也同样如此. 想必每一个对于代码有追求的开发者,对于"高质量"这个词,或多或少都有自己的一丝理解.当 ...

  8. 怎样编写高质量的java代码

    代码质量概述     怎样辨别一个项目代码写得好还是坏?优秀的代码和腐化的代码区别在哪里?怎么让自己写的代码既漂亮又有生命力?接下来将对代码质量的问题进行一些粗略的介绍.也请有过代码质量相关经验的朋友 ...

  9. 怎样编写高质量的 Java 代码

    代码质量概述 怎样辨别一个项目代码写得好还是坏?优秀的代码和腐化的代码区别在哪里?怎么让自己写的代码既漂亮又有生命力?接下来将对代码质量的问题进行一些粗略的介绍.也请有过代码质量相关经验的朋友提出宝贵 ...

随机推荐

  1. .ashx 实现自动路由和参数填充

    在Mvc中访问控制器,参数填充和路由控制都非常方便,但之前项目用的是webFrom,和js交互的ashx页面,路由非常麻烦要根据传进来关键字来做switch,参数填充更坑,要一个一个去form中取出来 ...

  2. 洛谷——P1966 火柴排队

    https://www.luogu.org/problem/show?pid=1966 题目描述 涵涵有两盒火柴,每盒装有 n 根火柴,每根火柴都有一个高度. 现在将每盒中的火柴各自排成一列, 同一列 ...

  3. 读 Paxos 到 ZooKeeper ¥ 50大洋

    一  初识 ZooKeeper              高效且可靠的分布式协调服务.解决分布式一致性问题             统一命名服务.配置管理服务.分布式锁服务.      使用: 比如配 ...

  4. 绿色便携版Lazarus的制作教程

    本文来源: www.fpccn.com 原作者:逍遥派掌门人 http://msdn.microsoft.com/zh-cn/library/windows/apps/hh452791.aspx 本教 ...

  5. P2617 Dynamic Ranking

    题目描述 给定一个含有n个数的序列a[1],a[2],a[3]……a[n],程序必须回答这样的询问:对于给定的i,j,k,在a[i],a[i+1],a[i+2]……a[j]中第k小的数是多少(1≤k≤ ...

  6. ToF相机学习笔记之基本知识

    ToF相机属于一种非接触式光学传感器,通过计算发射激光的飞行时间获取对应像素的深度信息.就非接触式距离测量方法而言,其分类可用下表表示如下: 1.1 ToF传感器基础 一个逐点式的ToF传感器采用了雷 ...

  7. VBA 字符串操作(基础篇)

    转自:http://blog.csdn.net/jyh_jack/article/details/2315345 mid(字符串,从第几个开始,长度) 在[字符串]中[从第几个开始]取出[长度个字符串 ...

  8. unzip---解压缩“.zip”压缩包。

    unzip命令用于解压缩由zip命令压缩的“.zip”压缩包. 语法 unzip(选项)(参数) 选项 -c:将解压缩的结果显示到屏幕上,并对字符做适当的转换: -f:更新现有的文件: -l:显示压缩 ...

  9. C# Arcgis Engine 捕捉功能实现

    namespace 捕捉 { public partial class Form1 : Form { private bool bCreateElement=true; ; ; private IEl ...

  10. POJ 1014 Dividing 背包

    二进制优化,事实上是物体的分解问题. 就是比方一个物体有数量限制,比方是13,那么就须要把这个物体分解为1. 2, 4, 6 假设这个物体有数量为25,那么就分解为1, 2, 4. 8. 10 看出规 ...