16防护式编程2

一 辅助调试的代码

1. 不要自动地把产品版本的限制强加于开发版之上

​ 程序员们经常有这样一个误区,即认为产品级软件的种种限制也适用于开发中的软件。产品级的软件要求可以快速的运行,而开发中的软件则容许运行缓慢。产品级的软件要节约适用资源,而开发中的软件在使用资源时能够比较奢侈。产品级的软件不该向用户暴露可能引发危险的操做,而开发中的软件则能够提供一些额外的、没有安全网的操做。程序员

​ 我曾参与编写的一个程序中大量地使用了四重链表。链表的代码是很容易出错的,链表自己的结构很容易损坏。所以我给程序加了一个菜单项来检测链表的完整性。编程

​ 在调试模式下,Microsoft Word 在空闲循环中加入了一些代码,它们每隔几秒中就检查一次 Document 对象的完整性。这样既有助于快速检测到数据的损坏,也方便了对错误的诊断。安全

​ 应该在开发期间牺牲一些速度和对资源的使用,来换取一些可让开发更顺畅的内置工具。工具

2. 尽早引入辅助调试的代码

​ 你越早引入辅助调试的代码,它可以提供的帮助也越大。一般,除非被某个错误反复地纠缠,不然你是不肯意花精力去编写一些调试辅助的代码的。然而,若是你一遇到问题立刻就编写或使用前一个项目中用过的某个调试助手的话,它就会自始至终在整个项目中帮助你。性能

3. 采用进攻式编程

​ 应该以这么一种方式来处理异常状况:在开发阶段让它显示出来,而在产品代码运行时让它能自我恢复。debug

​ 下面列出一些可让你进行进攻式编程的方法。版本控制

  • 确保断言语句使程序终止运行。不要让程序员养成坏习惯,一碰到已知问题就按回车键把它跳过。让问题引发的麻烦越大越好,这样才能被修复。指针

  • 彻底填充分配到的全部内存,这样可让你检测到内存分配错误。调试

  • 彻底填充已分配到的全部文件或流,这样可让你排查出文件格式错误。日志

  • 确保每个case语句中的default分支或者else分支都能产生严重错误(好比让程序终止运行),或者至少让这些错误不会被忽视。

  • 在删除一个对象以前把它填满垃圾数据。

  • 让程序把它的错误日志文件用电子邮件发给你,这样你就能了解到在已发布的软件中还发生了那些错误——若是这对于你所开发的软件使用的话。

    有时候,最好的防守正式大胆进攻。在开发时惨痛地失败,能让你在发布产品后不会败的太惨。

4. 计划移除调试辅助的代码
若是你是写程序给本身用,那么把调试用的代码都留在程序里可能并没有大碍。但若是是商用软件,则此举会使软件的体积变大且速度变慢,从而给程序形成巨大的性能影响。要事先作好计划,避免调试用的代码和程序代码纠缠不清。下面是一些能够采用的方法。
  • 使用相似ant和make这样的版本控制工具好make工具

    ​ 版本控制工具能够从同一套源码编译出buto9ng版本的程序。在开发模式下,你可让make工具把全部的调试代码都包含进来一块儿编译。而在产品模式下,又可让make工具把那些你不但愿包含在商用版本中的调试代码排除在外。

  • 使用内置的预处理器

    若是你所用的编程环境里有一个预处理器——好比C++开发环境——你能够用编译器开关来包含或排除调试用的代码。你既能够直接使用预处理器,还能够写一个能与预处理器指令同时使用的宏。下面是一个直接使用预处理器的例子:

#define DEBUG
#if defined(DEBUG)
// debugging code 
#endif

​ 这一用法能够有几种变化。好比说,除了能够直接定义DEBUG之外,还能够给他赋一个值,而后就能够判断其数值,而不只是去判断它是否已经定义了。这么作可让你区分不一样级别的调试代码。你可能但愿让某些调试代码永远留在程序里,这是你就能够用相似#if DEBUG > 0 这样的语句把这些代码括起来。另外一些调试代码可能只是针对一些特定的用途,你能够用相似#if DEBUG == POINTER_ERROR这样的语句把这些代码括起来。在另一些地方,你可能想设置调试级别,这时就能够写相似#if DEBUG > LEVEL_A 这样的语句。

​ 若是你不喜欢让#if defined() 一类语句散布在代码的各处,那么能够写一个预处理器宏来完成一样的任务。

#define DEBUG
#if defined(DEBUG)
#define DebugCode(code_fragment){code_fragment}
#else
#define DebugCode(code_fragment)
#endif
//根据是否认义DEBUG符号,可选择是否编译此处代码
DebugCode(statemenmt 1;
         statement 2;
          ...
         statement n);
  • 使用调试存根
    不少状况下,你能够调用一段子程序进行调试检查。在开发阶段,该子程序可能要执行若干操做以后才能把控制权交还给其调用方代码。而在产品代码中,你能够用一个存根子程序(stub routine)来替换这个复杂的子程序,而这段stub子程序要么当即把控制权交换调用方,要么是执行几项快速的操做就返回。这种方法仅会带来很小的性能损耗,而且比本身编写预处理器要快一些。把开发版本和产品版本的stub子程序都保留起来,以便未来能够随时在二者之间来回切换。

    你能够先写一个检查传入指针是否有效的子程序。

    void DoSomething(SOME_TYPE* pointer)
    {
        //check parameters passed in
        CheckPointer(pointer);
    }

    在开发阶段,CheckPointer() 子程序会对传入的指针进行全面检查。这一检查可能至关耗时,但必定要很是有效,好比说这样:

    void CheckPointer(void* pointer)
    {
        //执行第1项检查——多是检查它不为NULL
        //执行第2项检查——多是检查它的地址是否合法
        //执行第3项检查——多是检查它所指向的数据无缺无损
        //...
        //执行第n项检查——...
    }

    当代码准备稳当,即将要编译为产品时,你可能不但愿这项指针检查影响性能。这时你就能够用下面这个子程代替前面那段代码:

    void CheckPointer(void* pointer)
    {
        //no code; just return to caller.
    }
5. 肯定在产品代码中该保留多少防护式代码

​ 防护式编程中存在这么一种矛盾的观念,即在开发阶段你但愿错误能引人注意——你宁愿看它的脸色,也不想冒险去忽视他。但在产品发布阶段,你却想让错误尽量地偃旗息鼓,让程序能十分稳妥地恢复或中止。下面给出一些指导性建议。

  • 保留那些检查重要错误的代码

    你须要肯定程序的哪些部分能够承担未检测出错误而形成的后果,而哪些部分不能承担。

  • 去掉检测细微错误的代码

    若是一个错误带来的影响确实微乎其微的话,能够把检查它的代码去掉。

  • 去掉能够致使程序硬性崩溃的代码

    当你的程序在开发阶段检测到了错误,你确定想让它尽量地引人注意,以便能修复它。实现这一目的最好的方法一般是让程序在检测到错误后打印出一份调试信息,而后崩溃退出。这种方法甚至对于细微的错误也颇有用。

    然而在生成产品的时候,软件的用户须要在程序崩溃以前有机会保存他们的工做成果,为了让程序给他们留出足够长的保存时间,用户甚至会忍受程序表现出的一些怪异行为。相反,若是 程序中的一些代码致使了用户工做成果的丢失,那么不管这些代码对帮助调试程序并最终改善程序质量有多大的贡献,用户也不会心从存感激的。所以,若是你的程序里存在着可能致使数据丢失的调试代码,必定要把它们从最终软件产品中去掉。

  • 保留可让程序稳妥地崩溃的代码

    若是你的程序里有可以检测出潜在严重错误的调试代码,那么应该保留那些能让程序稳妥地崩溃的代码。

  • 为你的技术支持人员记录错误信息

    能够考虑在产品代码中保留辅助调试用的代码,但要改变他们的工做方式,以便与最终产品软件相适应。

  • 确认留在代码中的错误消息是友好的

    若是你在程序中留下了内部错误消息,请确认这些消息的用于对用户而言是友好的。

相关文章
相关标签/搜索