奥门新浦京官方网站PHP中的防御性编程

本文由码农网 –
邱康原创翻译,转载请看清文末的转载要求,欢迎参与我们的付费投稿计划!

开发应用程序是一项压力很大的工作,人无完人,工作中遇到bug是很正常的事,有些程序员会生气,沮丧,郁闷,甚至泄气,也有一些程序员则会比较淡定。如何进行修复bug的过程,是值得我们好好推敲的。

简介

菲纳格动态逆定律:

我想分享一些有关程序员在努力修复bug时常说的话和冒出的想法。当氛围变得紧张的时候,这些话就会显得轻松幽默。最终,bug也会修复成功,你将会继续下一个任务。我相信许多web开发人员和软件工程师在编程中都会遇到困难,而事后回想起来,还会觉得很好笑。1、我不知道该删掉还是重写**回归曾经写的源代码,总有一种想要重新返工的冲动,逻辑性差,冗余代码多,让人难以理解。但是,如果功能没出现问题,千万不要去修改。这是我经常要面对的困扰,相信也困扰了其他不少的软件开发者。

本文是为了满足开发人员的需要而写的。我们总结了一套指南,无论作为开发人员还是顾问,这些指南多年来一直都很好地指导着我们,我们把它们作为建议提供给您,希望对您的工作有所帮助。您也许不赞同其中的某些指南,但我们希望您会喜欢其中的一些并在您的编程或移植项目中使用它们。

会出错的,终将会出错 —-  在最糟糕的时刻。

2、一开始架构时就该查Github相信绝大多数开发人员都知道Github,它上面每天都会发布的一些神奇的开源项目。涉足所有计算机语言的程序员,会利用网络对现有项目进行分叉,在维基论坛谈论或者回购他们自己的源代码,这些都为各种各样的项目的插件和模板提供了很多丰富的资源。

回页首

防御性编程是什么意思

防御性编程,简单的说,就是在编程的时候有目的地预测可能的故障点。目的是在那些可能发生的问题发生前解决它们。你看见了问题,对吧?预测意料之外的事情本来就有内在的难度,当你想要预测意料之外的事情并且解决它就更是难上了好几倍。

下面我们看几个实际的例子。

奥门新浦京官方网站 1

3、为什么这个脚本要依赖这么多库说到一些越来越被广泛使用的计算机语言,像Java和Objective-C,库文件的数量也不断增加。很明显可以看出,构建一个框架就需要许多的基础库,甚至一些JavaScript的插件也需要很多大量的附加文件。有时候这些乱七八糟的东西会很让人心烦,但是至少它能运行。

风格与指南

条件语句

这是最容易进行防御性编程的地方之一,也是最容易满足的地方。在用PHP编程的许多情况下你不会需要“else”。

假设,你在写一个函数并且需要一个条件语句。在这里,你只需要为你特定的变量使用三个条件语句如下:

if($var == a){ }
else if($var == b){ }
else if($var == c){ }

没有其他可能性了,你说,并且继续码代码。但是,让我们在这里停一下。我知道你知道这里没有其他可能性了。并且我相信你。但有时候(不可预测的)情况会发生。我们忘掉了一些情况。我们检查错误。我们最终重用了一些代码,超出了原本的预定范围。突然我们有了泄露错误或者有时候是静默的错误状态,因为我们没有使用catch。使用else代码块。在使用switch时要使用default。用它们来返回或者记录错误,这样你才知道发生了什么(如果发生了的话)。虽然会多用两行代码,但当一些你无法预测的事情发生时,这是值得的。

4、网上一定有解决办法遇到困难时,我的第一反应就是上网查资料,很多程序员会在论坛上发布他们的问题,最终这些问题都会被解决并存档。Google会很神奇地选择一些跟你的问题相关的关键字,你就能够轻而易举地得到一些对你有帮助的讨论信息。不幸的是,有时候对于一些特定的问题,相关的信息还不是很多。

  • 使用一种使代码具有可读性和一致性的源代码风格。如果没有团队代码风格或自己的风格,您可以使用与大多数
    C 程序员采用的 Kernighan 和 Ritchie
    风格相似的风格。然而,举一个极端的例子,有可能最终会写出与下面相似的代码:

      int i;main(){for(;i["]<i;++i){--i;}"];read('-'-'-',i+++"hell 
        o, world!n",'/'/'/'));}read(j,i,p){write(j/p+p,i---j,i/i);

绝不相信用户输入

你以前有没有听说过这个说法?大多数程序员听过。这有一点含糊,通俗点讲,理所当然。但它是真理。你绝不应该相信用户输入。这不是说你假设所有用户是疯狂的黑客,他们使用一些精心设计的命令来摧毁你的应用。没有必要妄想。但是,你应该假设用户不知道你的代码,他们不知道你需要填写什么参数,或者参数应该多长。他们不知道什么文件类型或者什么大小能上传(即使应用告诉了他们)。偶尔他们会是机器或者黑客并且他们希望在他们的输入中运行脚本,有时候甚至是在登陆后的输入中。你怎么知道你能相信认证或者验证码能在用户输入之前提供一个安全的堡垒?

答案:绝不。

你绝不相信用户输入。如果你信任的用户输入,那么你永远不会有一个突破。明白了吗?所以总是要评估你的输入,一定要保证你在处理数据尤其是要存入数据库或者要把它展示出来时使用了合适的技术。因此 – 绝不相信输入,即使来自不是用户的输入的地方 – 输入验证永远是你的朋友。看看Survive the Deep End: PHP Security 并且使用 validation library.吧。

5、有这个功能的插件吗何必要多此一举插件是扩展任何程序或者网站用户接口的很好的资源。另外它们还为开发者提供了一些定制以及独特的选项。如果没有可用的插件,那你为什么不自己创建一个呢?6、对于网站项目,我好担心坑爹的InternetExplorer使用IE渲染网页遇到的各种困难,我就不提了,从5。5版本到IE9-IE10,对于浏览器的支持问题的争议就一直不断。Web开发人员会很害怕网页调试,使用IE6进行渲染更是噩梦。,幸好那些日子已经慢慢成为历史了。

— 1984 年模糊 C
代码大赛“差劲奖”。应代码作者要求匿名。

对你的代码的假设

不要假设任何事情。如果前两个主题教会我们一些事情的话,那就是我们不应该做任何假设。作为程序员,尤其是致力于一个项目太久后,我们开始做很多假设。我们假设用户知道一些我们知道的事情。不一定是技术细节,也可以是程序的功能性细节。我们假设用户知道文件能有多大因为。。。我们已经知道。或者他们知道为了让邮件脚本。。。但事实不是,他们不知道以上任何东西。这好像更多的是前端的工作,但明显的是你在后端仍然要处理用户行为和用户输入,所以值得好好想想。

另一个许多程序员都会做的惊人的假设是我们在任何时候对于我们的函数,类或者其它代码段的明显的功能属性。一个具有防御性的程序员会仔细考虑的不仅仅是用一般的文档来描述函数是干什么的——他们也将写下他们对输入,参数,用例,或任何其他类似的东西做出的任何假设。因为我们都是人,我们过一段时间会忘掉一些事。我们最后也很可能会面临其他人维护,扩展或者替换我们的代码。如果没有别的,回想一下,编程是发生在一个充满技术变革的世界里。如果你的应用仍然能使用几年,可能会升级PHP版本并且失去一些功能,或者一些你自己代码里面具有交互的组件之间需要改变。预测这些是很困难的,所以好的注释和文档是非常重要的。

7、有些逻辑语句,并不符合逻辑有一些逻辑语句,像if/else循环,for循环,while循环,do循环…等等,还有很多。在回顾一些源代码时,我总是尽力想弄明白我的逻辑是怎么回事。我经常会回头更新代码,让逻辑更清晰。8、我花30分钟写个函数,运行它却要花2个小时这不是十年前的一个有关编程的故事吗?当一切都在按照你所所期待的顺利进行着,突然某个函数输出了一个致命的错误,所以你不得不回头删除代码块,试图定位出错的代码行。尽管这会让你筋疲力尽,但是一旦找到错误的原因,问题解决之后,你又会立马感到浑身轻松。

  • 通常将主例程定义为 main()。对应的 ANSI
    编写方式是 int
    main(void)
    (如果不考虑命令行参数的话)或 int main( int argc, char
    **argv )
    。ANSI 以前的编译器会省略 void声明,或列出变量名以及紧随其后的声明。
  • 空格

    充分利用水平和垂直空格。缩进和空格间距应反映出代码的块结构。

    应将条件运算符的长字符串分割成单独的几行。例如:

      if (foo->next==NULL && number < limit         && limit <=SIZE
                && node_active(this_input)) {...

视野狭窄

另一件可以使我们忘记好的评论实践以及标准的东西是视野狭窄。许多程序员都具有视野狭窄的毛病。你知道这种感觉 – 你解决问题,你处于最佳状态。你觉得与你的音乐(或没有)独立于自己的小世界中,并且你就在编码,突然两小时过了,你意识到你已经写了无数行没有注释的代码。我们所有人偶尔都会遇到这种事情,但重要的是在某处发现这个情况并且补上应有的注释。

9、读了几篇博客后,我才意识到我之前所做的全是错的我总是喜欢根据自己的编程思想直入主题,但是如果事情没有按照我原本的计划进行时,会导致很多麻烦。有很多次,我在做项目时,途中都遇到了麻烦,最后只得查找博客和相关文章去寻求帮助。然后又发现我的整个方法完全错了,还不如从头开始更容易点。所以从长远来看,在项目开始时多做点研究反而会节省时间。

最好改成:

<table>
<colgroup>
<col style="width: 100%" />
</colgroup>
<tbody>
<tr class="odd">
<td><pre class="displaycodeliquid"><code>        if (foo-&gt;next == NULL
              &amp;&amp; number &lt; limit &amp;&amp; limit &lt;= SIZE
              &amp;&amp; node_active(this_input))
        {
         ...</code></pre></td>
</tr>
</tbody>
</table>



同样,应将描述得很详细的 **for
循环**分割成不同的行:

<table>
<colgroup>
<col style="width: 100%" />
</colgroup>
<tbody>
<tr class="odd">
<td><pre class="displaycodeliquid"><code>  for (curr = *varp, trail = varp;
            curr != NULL;
        trail = &amp;(curr-&gt;next), curr = curr-&gt;next )
     {
           ...</code></pre></td>
</tr>
</tbody>
</table>



对于其它复杂表达式(如使用三元运算符 **?:**的表达式),最好也将其分割成数行。

<table>
<colgroup>
<col style="width: 100%" />
</colgroup>
<tbody>
<tr class="odd">
<td><pre class="displaycodeliquid"><code>  z = (x == y)
            ? n + f(x)
             : f(y) - n;
                        </code></pre></td>
</tr>
</tbody>
</table>

语法和命名的一致性

一致性是一个灰色地带 – 它更多的是关于编码标准之类的,但它和防御性编程也有联系。在PHP中,有标准规范你的代码格式以便别人查看,或者你以后使用。但常常没人让你的代码标准化。但是无论你是否按照标准编码,你至少要保持一致性 – 这能让你少犯错误。这对于需要大量时间返回并且修复的小的语法错误尤其适用。如果你总是使用相同的间隔,格式和语法,命名规格等等你就能更好的避免犯错以至于误读你自己的代码。你更可能快速浏览代码并且找到你需要的东西。

“码农”岂是你们叫的?

  • 注释

    注释应描述正在发生什么事、如何完成它、参数表示什么、使用了哪些全局变量以及任何限制或错误。但要避免不必要的注释。如果代码比较清晰,并且使用了良好的变量名,那么它应该能够较好地说明自身。因为编译器不检查注释,所以不保证它们是正确的。与代码不一致的注释会起到相反的作用。过多的注释会使代码混乱。

    下面是一种多余的注释风格:

      i=i+1;        /* Add one to i */
    

总结

总的来说,除去用户行为和动作,不要对你的程序做任何假设。假设是具有防御性编程习惯的程序员最大的敌人。不要假设你不需要 default 语句或者 else 代码块。尽量使用正确的用户错误信息,警告,日志或者任何其它你假设不会用到的代码。你的假设通常是正确的 – 但我们不在乎。我们在乎的是它们出错的时候。一定要计划得好,准备着你可能需要在几小时,几周,几个月甚至几年后回顾你的代码,或者其他人需要 – 相应的就要好好写文档。别假设它永远不需要升级,扩展或者维护。那是无知的,在更多的情况下是疏忽。有时候保持一颗防御性编程的心能帮你更有效更安全地估计,计划和编程。

10StackOverflow上有好心人或许能帮助我我已经数不清有多少次,遇到问题都是通过StackOverflow得到解决的。只要你提出问题,社区里就会有很多聪明,友好的热心人愿意帮助你。所有的在线论坛里,它绝对是支持软件编程和前后端web开发的最全面的网站。

很明显变量 *i*递增了
1。还有更糟的注释方法:

<table>
<colgroup>
<col style="width: 100%" />
</colgroup>
<tbody>
<tr class="odd">
<td><pre class="displaycodeliquid"><code>            /************************************
          *                                   *
          *          Add one to i             *
          *                                   *
           ************************************/
                    i=i+1;</code></pre></td>
</tr>
</tbody>
</table>

11、这个问题竟然就因为少了个右括号调试是我们经常要用的方法,向前两步,回退一步,再向前两步,如此反复。为了查找函数命名或者变量作用域等错误,盯着代码看了数个小时,结果发现只是缺少了一个括号,你会有种哭笑不得的感觉。所有的时间都浪费在了一个小小的语法错误上,那一刻,你会觉得自己既是天才,又是傻子。

  • 命名约定

    具有前导和尾随下划线的名称是为系统用途而保留的,不应当用于任何用户创建的名称。约定规定:

    变量名

    1. #define 常量应全部大写。
    2. enum 常量应以大写字母开头或全部大写。
    3. 函数、类型定义(typedef)和变量名以及结构(struct)、联合(union)和枚举(enum)标记名称应小写。

    为清晰起见,避免使用仅在大小写上有区别的名称,如 foo 和
    Foo。同样,避免使用 foobar 和 foo_bar
    这样的名称。避免使用看上去相似的名称。在许多终端和打印机上,“l”、“1”和“I”看上去非常相似。使用名为“l”的变量非常不明智,因为它看上去非常象常量“1”。

  • 选择变量名时,长度不重要,但清晰的表达很重要。长名称可用于全局变量,因为它不常用,而将在每行循环上要使用的数组下标命名为
    i
    就完全够了。如果使用“index”或“elementnumber”的话,不仅输入得更多,而且会使计算的细节不明确。如果使用长变量名,有时候会使代码更难理解。比较:

         for(i=0 to 100)
              array[i]=0

12、喝杯咖啡,休息一下有的时候你需要起身离开显示器,连续敲了几个小时的键盘,如果中间休息一下,会对你的身体有益。大多数健康指南都建议每30-60分钟休息一次。但是还是要取决于你的需要,如果你感觉中间暂停去休息会打断你的思维,让你很不爽,那就最好不要了。

和

<table>
<colgroup>
<col style="width: 100%" />
</colgroup>
<tbody>
<tr class="odd">
<td><pre class="displaycodeliquid"><code>     for(elementnumber=0 to 100)
          array[elementnumber]=0;

13、我应该先把这个项目放一放,稍后在处理它**休息的另一种方式就会暂停你手中的项目,而不是离开你的电脑桌。或许你还有其他的工作要做,那就继续下一项任务。比起试图在一个花了5个小时还没解决的问题上继续挣扎,这会是一种更合理地分配时间和资源的方式。

14、我在想或许古典音乐能够激发我的编程潜能呢有一种说法认为古典音乐能促进植物的早期生长,我个人更偏爱古典音乐错综复杂的注解和音乐理论。爵士,钢琴,大型乐队,优雅的音乐在全球各地的人类文化都占有一席之地。所以编程的时候听点美妙的音乐会让你调试起来更得心应手呢。当然也有可能,会让你更加心烦意乱。

  • 函数名

    函数名应反映函数执行什么操作以及返回什么内容。函数在表达式中使用,通常用于 if子句,因此它们的意图应一目了然。例如:

         if (checksize(x))

15、或许现在是验证鲍尔默峰值理论的好时机我相信很多读者都知道鲍尔默峰值,它是根据一个特殊的XKCD漫画得来的。简单来说,这个理论认为程序员的编码能力在喝了定量的酒后,会达到一个峰值。这个起源于SteveBallmer的些古怪滑稽的姿态被认为是像一个醉汉在说胡话。尽管这有点讽刺,因为鲍尔默在微软从来算不上一个真正的程序员,猜想我们只有等其他人来实践这个理论了。

没有帮助作用,因为它没有告诉我们
checksize 是在出错时返回 true 还是在不出错时返回 true;而

<table>
<colgroup>
<col style="width: 100%" />
</colgroup>
<tbody>
<tr class="odd">
<td><pre class="displaycodeliquid"><code>     if (validsize(x))

16、是谁动了我的代码?这个听起来有点像妄想症,但是有时候你很想知道是谁趁你补觉的时候写的这些东西。回顾过去几周或者几个月的项目,会给你一种晕乎乎的感觉。有时候你会不记得你写过这些东西—尽管上周你还在参与这个项目。好像是我很疯狂地写的代码,你却从来不知道…17****、完全不知道这是神马东东你遇到的最糟糕的情况应该是在研究源代码时,完全不知道它是在干什么,可能是来自你自己的项目,也可能是其他人的项目,但是问题都一样。这个时候,你必须确定是否值得花费更多的时间去寻找其它解决方案或者仔细剖析代码,研究它到底是干什么的。

18、直接google下错误提示鉴于多年的PHP经验,我不得不说Google真的是调试问题的最好的小伙伴。这对于Objective-C,C++,Java和其他的主流语言的境况一定是相同的。错误提示信息对我们很有用,但是你必须记住不同的错误代码代表什么意思。它读起来更像是被翻译过的计算机语言。幸好有这么多在线支持,让我们确定这些错误信息代表的真正意思。

则使函数的意图很明确。

19、今天应该到此为止了,可我真的想把这个问题解决了我们都知道想要退出时的那种极度沮丧的感觉,但是同时又觉得放弃不是正确的选择。你很想继续前进,找出新的解决方案来。但是如果到最后还是浪费了一个小时,那该怎么办?我对这种情况并不陌生,它会让人特别沮丧。

  • 声明

    所有的外部数据声明前都应加上 extern关键字。

    “指针”限定符“*”应紧邻变量名而不是类型。例如,应使用

      char        *s, *t, *u;
    

20、哦买糕的,为什么我都没写注释呢如果涉及到最基本的前端代码HTML/CSS/JS时,并不需要总是写注释。但是如果是比较复杂的脚本和程序时,就需要写一些标准的注释以便你几个月,甚至几年后来重温这些代码。有时候你会忘记给函数,参数,输出格式以及其他重要的数据写注释,这无疑会导致发生bug时你不得不调试整个脚本去寻求解决方案,感到非常困惑,到那个时候你会觉得要是有一些有用的注释该多好啊。

而不是

<table>
<colgroup>
<col style="width: 100%" />
</colgroup>
<tbody>
<tr class="odd">
<td><pre class="displaycodeliquid"><code>  char*   s, t, u;</code></pre></td>
</tr>
</tbody>
</table>



后一条语句没有错,但可能不是我们期望的,因为没有将“t”和“u”声明为指针。

21、这个20分钟之前还好好的呢或许构建程序时最让人沮丧的是,明明刚才还好好的东西,没有改过任何代码,这会儿却运行不起来了。我发誓这种情况绝对有发生,而且它没有任何意义—也许其它程序运行的是缓存版本呢然后也有一些时候我们只更新了一丁点代码,结果整个程序都崩溃并且完全停止运行。那就会回退到最新的备份版本,从那儿继续吧。

  • 头文件

    头文件应按功能组织在一起,即,对单独子系统的声明应在单独的头文件中。此外,当代码从一个平台移植到另一个平台时有可能发生更改的声明应位于单独的头文件中。

    避免使用与库头文件名相同的专用头文件名。语句
    #include “math.h”
    如果在当前目录中找不到所期望文件的话,会包括标准库 math
    头文件。如果这是您期望的结果,可以注释掉这行 include 语句。

    最后说明一点,对头文件使用绝对路径名不是一个好主意。C
    编译器的“include-path”选项(在许多系统上为 -I — 大写的
    i)是处理众多专用头文件库的首选方法;它允许在不改变源文件的情况下重新组织目录结构。

  • scanf

    在重要的应用程序中永远不要使用
    scanf。它的错误检测不够完善。请看下面的示例:

      #include <stdio.h>
        int main(void)
        {
            int i;
            float f;
            printf("Enter an integer and a float: ");
            scanf("%d %f", &i, &f);
            printf("I read %d and %fn", i, f);
            return 0;
        }

22、忘了一个该死的分号,整个程序都崩了几乎我用过的所有的编程语言都要求每行结束时都要有结束符,但并不是所有的语言都这样,不过C/C++系列语言绝对是这样。当你忘记添加分号结束符时,这是多明显的错误!但是解析器并不不理解,便抛出一个致命的错误。接下来就得再花费20分钟时间去研究代码,查找技术错误。最终发现只是少了一个分号。哈,这就是软件调试的乐趣。

**测试运行**

Enter an integer and a float: 182
52.38

I read 182 and 52.380001

**另一个测试运行**

Enter an integer and a float:
6713247896 4.4

I read -1876686696 and
4.400000

23、我想要招人来帮我修复bug,得花多少钱哪雇佣程序员的想法听起来很诱人,但显然在经济上是不可行的。另外,如果你连自己的的错误都没解决,你又怎么能从这些错误中学到东西呢?经历多次失败,最后当你真正理解了编程的概念后,你会很有成就感。但有时候脑子里难免还是会闪过这种想法。

  • ++ 和 —

    当对语句中的变量使用递增或递减运算符时,该变量不应在语句中出现一次以上,因为求值的顺序取决于编译器。编写代码时不要对顺序作假设,也不要编写在某一机器上能够如期运作但没有明确定义的行为的代码:

      int i = 0, a[5];
        a[i] = i++; /* assign to  a[0]?  or  a[1]? */
  • 不要被表面现象迷惑

    请看以下示例:

          while (c == 't' || c = ' ' || c == 'n')
                c = getc(f);
    

24、快速浏览下HackerNews,肯定能提高我的效率很多程序员对于浏览软件和创业等社会新闻的偏爱选择都是HackerNews首页。它有大量的关于自由职业,时间管理,软件开发,创业发布和筹资资金等方面很棒的信息。尽管HN能够模拟出通过自我教育更加高效的感觉,但其实是在浪费你的时间。每隔几小时去快速浏览下新闻也没那么糟糕。

乍一看, **while**子句中的语句似乎是有效的
C
代码。但是,使用赋值运算符而不是比较运算符却产生了语义上不正确的代码。=
的优先级在所有运算符中是最低的,因此将以下列方式解释该语句(为清晰起见添加了括号):

<table>
<colgroup>
<col style="width: 100%" />
</colgroup>
<tbody>
<tr class="odd">
<td><pre class="displaycodeliquid"><code>      while ((c == &#39;t&#39; || c) = (&#39; &#39; || c == &#39;n&#39;))
            c = getc(f);</code></pre></td>
</tr>
</tbody>
</table>



赋值运算符左边的子句是:

<table>
<colgroup>
<col style="width: 100%" />
</colgroup>
<tbody>
<tr class="odd">
<td><pre class="displaycodeliquid"><code>      (c == &#39;t&#39; || c)

25、这个API怎么没有说明文档啊?最让人沮丧的事情就是使用插件或者框架时,自带的文档很糟糕,你只好自己去深入阅读源代码。我更喜欢让开发人员花时间专门为项目设计一个文档页,对所有的参数和选项都给予解释,有可能的话,给出一些示例代码。但是很遗憾,这种情况几乎不可能。所以最简单的办法就是远离那些附带文档很糟的工作,以免给自己带来麻烦。

26、我真希望我已经对数据库进行备份了在编写和调试代码的时候,我有时候会想不到备份。然而,数据备份能够帮助我们回退到做出某个特定的改变之前的版本,这对一个即时的服务器环境是特别有用的,有些变化瞬间就会发生。切记在本地保留对网站文件和数据库的拷贝,以备急需。你可能会觉得这样太麻烦了,但是总比你重建一个SQL数据库强多了。

它不会产生左值。如果 c
包含制表符,则结果是“true”,并且不会执行进一步的求值,而“true”不能位于赋值表达式的左边。

27、怎样才能快速解决这个问题?如果花费了数小时后,仍然未找到一个解决办法,很明显你需要一个新的方案了。程序员总是想要先实现功能,然后再去设计和美化界面。先确定一个最快的,最准确的解决方案,并尽力去实现和完成,然后再去考虑美化界面的问题就会很轻松了.

  • 意图要明确。

    当您编写的代码可以解释成另一种意图时,使用括号或用其它方法以确保您的意图清楚。如果您以后必须处理该程序的话,这有助于您理解您当初的意图。如果其他人要维护该代码,这可以让维护任务变得更简单。

    用能预见可能出现错误的方式编码,有时是可行的。例如,可以将常量放在比较等式的左边。即,不编写:

      while (c == 't' || c == ' ' || c == 'n')
            c = getc(f);

28、我敢打赌,你更新下我的代码,这个问题就解决了那些为编程语言提供依赖包和插件的团队并不需要频繁地发布产品。有时候从本地传送文件到服务器的时候,更新PHP/Ruby/Python/SQL版本可能会解决一些调试问题。除非你的版本实在太旧了,否则本地更新很少能够帮助你修复源代码中的bug,不过还是值得一试!

而是编写:

<table>
<colgroup>
<col style="width: 100%" />
</colgroup>
<tbody>
<tr class="odd">
<td><pre class="displaycodeliquid"><code>  while (&#39;t&#39; == c || &#39; &#39; == c || &#39;n&#39; == c)
         c = getc(f);</code></pre></td>
</tr>
</tbody>
</table>



用以下方法却会得到编译器诊断:

<table>
<colgroup>
<col style="width: 100%" />
</colgroup>
<tbody>
<tr class="odd">
<td><pre class="displaycodeliquid"><code>  while (&#39;t&#39; = c || &#39; &#39; == c || &#39;n&#39; == c)
         c = getc(f);</code></pre></td>
</tr>
</tbody>
</table>



这种风格让编译器发现问题;上面的语句是无效的,因为它试图对“\t”赋值。

29、我真的该好好学习Git了,…还是下周吧开源的版本控制控制软件Git在程序员中广受欢迎。跟其他竞争对手相比,它提供了一条更简单的学习曲线,被应用在了许多在线仓库像Github和Bitbucket中。可能对初学者来说,会有点难度,但是一旦你掌握了基本命令,你会发现使用GIt就是小菜一碟。它还让版本控制更加清晰。

  • 意想不到的麻烦。

    各种 C
    实现通常在某些方面各有不同。坚持使用语言中可能对所有实现都是公共的部分会有帮助。通过这样做,您更容易将程序移植到新的机器或编译器,并且不大会遇到编译器特殊性所带来的问题。例如,考虑字符串:

      /*/*/2*/**/1

30、算了,我还是从头开始吧有时候尝试了数小时的解决方案后,你可能需要将你的工作文件归档,重新开始。这个决定的最大难点就是你会考虑到前面数小时的工作会毫无收获。但是如果你保留之前的想法,项目却毫无进展时。重新开始,才有可能让项目顺利完成。

这里利用了“最大适合(maximal
munch)”规则。如果可以嵌套注释,则可将该字符串解释为:

<table>
<colgroup>
<col style="width: 100%" />
</colgroup>
<tbody>
<tr class="odd">
<td><pre class="displaycodeliquid"><code>  /*  /*  /2  */  *  */  1</code></pre></td>
</tr>
</tbody>
</table>



两个 /* 符号与两个 */
符号匹配,因此该字符串的值为
1。如果注释不嵌套,那么在有些系统上,注释中的 /*
就被忽略。在另一些系统上会针对 /*
发出警告。无论哪种情况,该表达式可解释为:

<table>
<colgroup>
<col style="width: 100%" />
</colgroup>
<tbody>
<tr class="odd">
<td><pre class="displaycodeliquid"><code>  /*  /  */  2  *  /*  */  1</code></pre></td>
</tr>
</tbody>
</table>



2 * 1 求值得 2。
  • 清空输出缓冲区

    当应用程序异常终止时,其输出的尾部常常会丢失。应用程序可能没有机会完全清空它的输出缓冲区。输出的某一部分可能仍在内存中,并且永远不会被写出。在有些系统上,这一输出可能有几页长。

    以这种方式丢失输出会使人误解,因为它给人的印象是程序在它实际失败很久之前就失败了。解决这一问题的方法是强制将输出从缓冲区清除,特别是在调试期间。确切的方法随系统的不同而有所不同,不过也有常用的方法,如下所示:

      setbuf(stdout, (char *) 0);
必须在将任何内容写到标准输出之前执行该语句。理想情况下,这将是主程序中的第一条语句。
  • getchar() — 宏还是函数

    以下程序将其输入复制到其输出:

      #include  <stdio.h>
        int main(void)
        {
            register int a;
            while ((a = getchar()) != EOF)
                putchar(a);
        }
    
从该程序除去 #include
语句将使该程序无法编译,因为 EOF 将是未定义的。

我们可以用以下方法重新编写该程序:

<table>
<colgroup>
<col style="width: 100%" />
</colgroup>
<tbody>
<tr class="odd">
<td><pre class="displaycodeliquid"><code>  #define EOF -1
    int main(void)
    {
        register int a;
        while ((a = getchar()) != EOF)
            putchar(a);
    }

这在许多系统上都可行,但在有些系统上运行要慢很多。

因为函数调用通常要花较长时间,所以常常把 **getchar**实现为宏。这个宏定义在 **stdio.h**中,所以当除去 **#include
&lt;stdio.h&gt;**时,编译器就不知道 getchar
是什么。在有些系统上,假设 getchar 是返回一个 **int**的函数。

实际上,许多 C 实现在其库中都有 **getchar**函数,部分原因是为了防止这样的失误。于是,在 **#include
&lt;stdio.h&gt;**遗漏的情况下,编译器使用 **getchar**的函数版本。函数调用的开销使程序变慢。 **putchar**有同样的问题。
  • 空指针

    空指针不指向任何对象。因此,为了赋值和比较以外的目的而使用空指针都是非法的。

    不要重新定义 NULL 符号。NULL
    符号应始终是常量值零。任何给定类型的空指针总是等于常量零,而与值为零的变量或与某一非零常量的比较,其行为由实现定义。

    反引用 null
    指针可能会导致奇怪的事情发生。

  • a+++++b 表示什么?

    解析它的唯一有意义的方法是:

          a ++  +  ++  b
    
然而,“最大适合”规则要求将它分解为:

<table>
<colgroup>
<col style="width: 100%" />
</colgroup>
<tbody>
<tr class="odd">
<td><pre class="displaycodeliquid"><code>      a ++  ++  +  b

这在语法上是无效的:它等于:

<table>
<colgroup>
<col style="width: 100%" />
</colgroup>
<tbody>
<tr class="odd">
<td><pre class="displaycodeliquid"><code>      ((a++)++) +  b

但 a++ 的结果不是 **左值**,因此作为 ++
的操作数是不可接受的。于是,解析词法不明确性的规则使得以语法上有意义的方式解析该示例变得不可能。当然,谨慎的办法实际上是在不能完全确定它们的意义的情况下,避免这样的构造。当然,添加空格有助于编译器理解语句的意图,但(从代码维护的角度看)将这一构造分割成多行更可取:

<table>
<colgroup>
<col style="width: 100%" />
</colgroup>
<tbody>
<tr class="odd">
<td><pre class="displaycodeliquid"><code>  ++b;
    (a++) + b;

  • 小心处理函数

    函数是 C
    中最常用的结构概念。它们应用于实现“自顶向下的”问题解决方法 —
    即,将问题分解成越来越小的子问题,直到每个子问题都能够用代码表示。这对程序的模块化和文档记录有帮助。此外,由许多小函数组成的程序更易于调试。

    如果有一些函数参数还不是期望的类型,则将它们强制转换为期望的类型,即使您确信没有必要也应该这样做,因为(如果不转换的话)它们可能在您最意料不到的时候给您带来麻烦。换句话说,编译器通常将函数参数的类型提升和转换成期望的数据类型以符合函数参数的声明。但是,在代码中以手工方式这样做可以清楚地说明程序员的意图,并且在将代码移植到其它平台时能确保有正确的结果。

    如果头文件未能声明库函数的返回类型,那就自己声明它们。用
    #ifdef/#endif
    语句将您的声明括起来,以备代码被移植到另一个平台。

    函数原型应当用来使代码更健壮,使它运行得更快。

  • 悬空 else

    除非知道自己在做什么,否则应避免“悬空
    else”问题:

          if (a == 1)
                if (b == 2)
                    printf("***n");
                else
                    printf("###n");
    
规则是 else 附加至最近的
if。当有疑虑时,或有不明确的可能时,添加花括号以说明代码的块结构。
  • 数组界限

    检查所有数组的数组界限,包括字符串,因为在您现在输入“fubar”的地方,有人可能会输入“floccinaucinihilipilification”。健壮的软件产品不应使用
    gets()。

    C
    下标以零作为开始的这一事实使所有的计数问题变得更简单。然而,掌握如何处理它们需要花些努力。

  • 空语句

    for 或 while
    循环的空语句体应当单独位于一行并加上注释,这样就表明这个空语句体是有意放置的,而不是遗漏了代码。

      while (*dest++ = *src++)
         ;   /* VOID */
    
  • 测试真(true)还是假(false)

    不要以缺省方式测试非零值,即:

      if (f() != FAIL)
    
优于

<table>
<colgroup>
<col style="width: 100%" />
</colgroup>
<tbody>
<tr class="odd">
<td><pre class="displaycodeliquid"><code>  if (f())

尽管 FAIL 的值可能是 0(在 C
中视为假(false))。(当然,应当在这一风格与“函数名”一节中演示的构造之间作出权衡。)当以后有人认为失败的返回值应该是
-1 而不是 0 时,显式的测试对您会有帮助。

常见的问题是使用 strcmp
函数测试字符串是否相等,决不应该以缺省方式处理它的结果。更可取的方法是定义宏
STREQ:

<table>
<colgroup>
<col style="width: 100%" />
</colgroup>
<tbody>
<tr class="odd">
<td><pre class="displaycodeliquid"><code>#define STREQ(str1, str2) (strcmp((str1), (str2)) == 0)

用这种方法,语句

<table>
<colgroup>
<col style="width: 100%" />
</colgroup>
<tbody>
<tr class="odd">
<td><pre class="displaycodeliquid"><code>  If ( STREQ( inputstring, somestring ) ) ...

就具有隐含的行为,该行为不大会在您不知情的情况下改变(人们往往不会重新编写或重新定义象 **strcmp()**这样的标准库函数)。

不要用 1 检查相等性的布尔值(TRUE 和
YES 等);而要用 0 测试不等性(FALSE 和 NO
等)。绝大多数函数被确保在条件为假(false)时返回
0,但仅在条件为真(true)时才返回非零。因此,最好将

<table>
<colgroup>
<col style="width: 100%" />
</colgroup>
<tbody>
<tr class="odd">
<td><pre class="displaycodeliquid"><code>  if (func() == TRUE) {...</code></pre></td>
</tr>
</tbody>
</table>



写成

<table>
<colgroup>
<col style="width: 100%" />
</colgroup>
<tbody>
<tr class="odd">
<td><pre class="displaycodeliquid"><code>  if (func() != FALSE)</code></pre></td>
</tr>
</tbody>
</table>
  • 嵌入语句

    使用嵌入赋值语句要看时间和地点。在有些构造中,如果不使用更多且不易阅读的代码就没有更好的方法来实现结果:

      while ((c = getchar()) != EOF) {
          process the character
        }
使用嵌入赋值语句来提高运行时性能是可能的。但是,您应当在提高速度和降低可维护性之间加以权衡,在人为指定的位置使用嵌入赋值语句会导致可维护性降低。例如:

<table>
<colgroup>
<col style="width: 100%" />
</colgroup>
<tbody>
<tr class="odd">
<td><pre class="displaycodeliquid"><code>  x = y + z;
    d = x + r;</code></pre></td>
</tr>
</tbody>
</table>



不应被替换为:

<table>
<colgroup>
<col style="width: 100%" />
</colgroup>
<tbody>
<tr class="odd">
<td><pre class="displaycodeliquid"><code>  d = (x = y + z) + r;</code></pre></td>
</tr>
</tbody>
</table>



即使后者可能节省一个周期也不行。最终,这两者之间在运行时间上的差异将随着优化器的增强而减少,易维护性的差异却将增加。
  • goto 语句

    应保守地使用 goto。从数层 switch、 for和 while嵌套中跳出来时,使用该语句很有效,不过,如果有这样的需要,则表明应将内部构造分解成单独的函数。

          for (...) {
                while (...) {
                  ...
                      if (wrong)
                         goto error;
    
                        }
             }
                ...
                error:
               print a message
    
当必须使用 **goto**时,随附的标号应单独位于一行,并且同后续代码的左边相距一个制表符或位于一行的开头。对 **goto**语句和目标都应加上注释,说明其作用和目的。
  • switch
    中的“落空”(fall-through)

    当一块代码有数个标号时,将这些标号放在单独的行。这种风格与垂直空格的使用一致,并且使重新安排
    case 选项(如果那是必需的话)成了一项简单的任务。应对 C switch
    语句的“落空”特征加以注释,以便于以后的维护。如果这一特性曾给您带来“麻烦”,那么您就能够理解这样做的重要性!

      switch (expr) {
        case ABC:   
        case DEF:
                statement;
                break;
        case UVW:
                statement;  /*FALLTHROUGH*/ 
        case XYZ:
                statement;
            break;  
        }
    
尽管从技术上说,最后一个 break
不是必需的,但是,如果以后要在最后一个 case 之后添加了另一个
case,那么一致地使用 break 可以防止“落空”错误。如果使用 default case
语句的话,
它应当永远是最后一个,并且(如果它是最后的语句)不需要最后的 break
语句。
  • 常量

    符号常量使代码更易于阅读。应尽量避免使用数字常量;使用
    C 预处理器的 #define
    函数给常量赋予一个有意义的名称。在一个位置(最好在头文件中)定义值还会使得管理大型程序变得更容易,因为只需更改定义就可以统一地更改常量值。可以考虑使用枚举数据类型作为对声明只取一组离散值的变量的改进方法。使用枚举还可以让编译器对您枚举类型的任何误用发出警告。任何直接编码的数字常量必须至少有一个说明值的出处的注释。

    常量的定义与它的使用应该一致;例如,将
    540.0 用于浮点数,而不要通过隐式浮点类型强制转换使用
    540。也就是说,在有些情况下,常量 0 和 1
    可以以本身的形式直接出现,而不要以定义的形式出现。例如,如果某个 for循环遍历一个数组,那么:

      for (i = 0; i < arraysub; i++)
    
非常合理,而代码:

<table>
<colgroup>
<col style="width: 100%" />
</colgroup>
<tbody>
<tr class="odd">
<td><pre class="displaycodeliquid"><code>  gate_t *front_gate = opens(gate[i], 7);
    if (front_gate == 0)
       error(&quot;can&#39;t open %sn&quot;, gate[i]);</code></pre></td>
</tr>
</tbody>
</table>



就不合理。在第二个示例中,front_gate
是指针;当值是指针时,它应与 NULL 比较而不与 0 比较。即使象 1 或 0
这样的简单值,通常最好也使用象 TRUE 和 FALSE 这样的定义来表示(有时
YES 和 NO 读起来更清楚)。

不要在需要离散值的地方使用浮点变量。这是由于浮点数不精确的表示决定的(请参阅以上 **scanf**中的第二个测试)。使用
&lt;= 或 &gt;= 测试浮点数;精确比较(== 或
!=)也许不能检测出“可接受的”等同性。

应将简单的字符常量定义为字符文字而不是数字。不提倡使用非文本字符,因为它们是不可移植的。如果必须使用非文本字符,尤其是在字符串中使用它们,则应使用三位八进制数(不是一个字符)的转义字符(例如“\007”)来编写它们。即便如此,这样的用法应视为与机器相关,并且应按这一情况来处理。
  • 条件编译

    条件编译可用于机器相关性、调试以及在编译时设置某些选项。可以用无法预料的方式轻易地组合各种控制。如果将
    #ifdef
    用于机器相关性,应确保当没有指定机器时会出错,而不是使用缺省的机器。#error
    伪指令可以较方便地用于这一用途。如果使用 #ifdef
    进行优化,缺省值应是未优化的代码而不是不可编译或不正确的程序。要确保对未优化的代码进行了测试。

回页首

其它

  • 象 Make这样用于编译和链接的实用程序极大简化了将应用程序从一个环境移到另一个环境的任务。在开发期间, make仅对那些自上次使用
    make 以来发生了更改的模块进行重新编译。

    经常使用 lint。 lint是 C
    程序检查器,它检查 C
    源文件以检测并报告函数定义和调用之间类型的不匹配和不一致,以及可能存在的程序错误等。

    此外,研究一下编译器文档,了解那些使编译器变得“吹毛求疵”的开关。编译器的工作是力求精确,因此通过使用适当的命令行选项让它报告可能存在的错误。

  • 使应用程序中全局符号的数量最少。这样做的好处之一是与系统定义的函数冲突的可能性降低。

  • 许多程序在遗漏输入时会失败。对所有的程序都应进行空输入测试。这也可能帮助您理解程序的工作原理。
  • 不要对您的用户或您所用的语言实现有任何过多的假设。那些“不可能发生”的事情有时的确会发生。健壮的程序可以防范这样的情形。如果需要找到某个边界条件,您的用户将以某种方式找到它!

    永远不要对给定类型的大小作任何假设,尤其是指针。

    当在表达式中使用 char类型时,大多数实现将它们当作无符号类型,但有些实现把它们作为有符号的类型。当在算术表达式使用它们时,建议始终对它们进行类型强制转换。

    不要依靠对自动变量和 malloc返回的内存进行的初始化。

  • 使您程序的目的和结构清晰。

  • 要记住,可能会在以后要求您或别的人修改您的代码或在别的机器上运行它。细心编写您的代码,以便能够将它移植到其它机器。

回页首

结束语

应用程序的维护要花去程序员的大量时间,这是众所周知的事。部分原因是由于在开发应用程序时,使用了不可移植和非标准的特性,以及不令人满意的编程风格。在本文中,我们介绍了一些指南,多年来它们一直给予我们很大帮助。我们相信,只要遵守这些指南,将可以使应用程序维护在团队环境中变得更容易。

 

参考资料

  • Obfuscated C and Other Mysteries,由
    Don Libes 编写,John Wiley and Sons, Inc. ISBN
    0-471-57805-3

  • The C Programming Language,Second
    Edition,由 Brian W. Kernighan 和 Dennis M. Ritchie
    撰写,Prentice-Hall,ISBN 0-13-110370-9

  • Safer C,由 Les Hatton
    编写,McGraw-Hill,ISBN 0-07-707640-0

  • C Traps and Pitfalls 由 Andrew Koenig
    编写,AT&T Bell Laboratories,ISBN 0-201-17928-9

发表评论

电子邮件地址不会被公开。 必填项已用*标注