C语言中史上最愚蠢的Bug

C语言中史上最愚蠢的Bug

本文来自“The most stupid C bug ever”,很有意思,分享给大家。我相信这样的bug,就算你是高手你也会犯的。你来看看作者犯的这个Bug吧。。

首先,作者想用一段程序来创建一个文件,如果有文件名的话,就创建真正的文件,如果没有的话,就调用?tmpfile()?创建临时文件。他这段程序就是HTTP下载的C程序。code==200就是HTTP的返回码。

else if (code == 200) {     // Downloading whole file
    /* Write new file (plus allow reading once we finish) */
    g = fname ? fopen(fname, "w+") : tmpfile();
}

但是这个程序,只能在Unix/Linux下工作,因为 Microsoft 的?tmpfile()的实现?居然选择了 C:\ 作为临时文件的存放目录,这对于那些没有管理员权限的人来说就出大问题了,在Windows 7下,就算你有管理员权限也会有问题。所以,上面的程序在Windows平台下需要用不同的方式来处理,不能直接使用Windows的tmpfile()函数。

于是作者就先把这个问题记下来,在注释中写下了FIXME:

else if (code == 200) {     // Downloading whole file
    /* Write new file (plus allow reading once we finish) */

    // FIXME Win32 native version fails here because
    //   Microsoft's version of tmpfile() creates the file in C:\
    g = fname ? fopen(fname, "w+") : tmpfile();
}

然后,作者觉得需要写一个跨平台的编译:

FILE * tmpfile ( void ) {
#ifndef _WIN32
    return tmpfile();
#else
    //code for Windows;
#endif
}

然后,作者觉得这样实现很不好,会发现名字冲突,因为这样一来这个函数太难看了。于是他重构了一下他的代码——写一个自己实现的tmpfile() – w32_tmpfile,然后,在Windows 下用宏定义来重命名这个函数为tmpfile()。(陈皓注:这种用法是比较标准的跨平台代码的写法)

#ifdef _WIN32
  #define tmpfile w32_tmpfile
#endif

FILE * w32_tmpfile ( void ) {
    //code for Windows;
}

搞定!编译程序,运行。靠!居然没有调用到我的w32_tmpfile(),什么问题?调试,单步跟踪,果然没有调用到!难道是问号表达式有问题?改成if – else 语句,好了!

if(NULL != fname) {
    g = fopen(fname, "w+");
} else {
    g = tmpfile();
}

问号表达式不应该有问题吧,难道我们的宏对问号表达式不起作用,这难道是编译器的预编译的一个bug?作者怀疑到。

现在我们把所有的代码连在一起看,并比较一下:

能正常工作的代码

#ifdef _WIN32
#  define tmpfile w32_tmpfile
#endif

FILE * w32_tmpfile ( void ) {
    code for Windows;
}

else if (code == 200) {     // Downloading whole file
    /* Write new file (plus allow reading once we finish) */
    // FIXME Win32 native version fails here because
    //     Microsoft's version of tmpfile() creates the file in C:\
    //g = fname ? fopen(fname, "w+") : tmpfile();
    if(NULL != fname) {
        g = fopen(fname, "w+");
    } else {
        g = tmpfile();
    }
}

不能正常工作的代码

#ifdef _WIN32
#  define tmpfile w32_tmpfile
#endif

FILE * w32_tmpfile ( void ) {
    code for Windows;
}

else if (code == 200) {     // Downloading whole file
    /* Write new file (plus allow reading once we finish) */
    // FIXME Win32 native version fails here because
    //    Microsoft's version of tmpfile() creates the file in C:\
    g = fname ? fopen(fname, "w+") : tmpfile();
}

也许你在一开始就看到了这个bug,但是作者没有。所有的问题都出在注释上:

/* Write new file (plus allow reading once we finish) */
// FIXME Win32 native version fails here because
//     Microsoft's version of tmpfile() creates the file in C:\

你看到了最后那个C:\吗?在C中,“\” 代表此行没有结束,于是,后面的代码也成了注释。这就是这个bug的真正原因

而之所以改成if-else能工作的原因是因为作者注释了老的问号表达式的代码,所以,那段能工作的代码成了:

/* Write new file (plus allow reading once we finish) */
// FIXME Win32 native version fails here because Microsoft's version of tmpfile() creates the file in C:    //g = fname ? fopen(fname, "w+") : tmpfile();
if(NULL != fname) {
    g = fopen(fname, "w+");
} else {
    g = tmpfile();
}

我相信,当作者找到这个问题的原因后,一定会骂一句“妈的”!我也相信,这个bug花费了作者很多时间!

最后,我也share一个我以前犯的一个错。

我有一个小函数,需要传入一个int* pInt的类型,然后我需要在我的代码里 把这个int* pInt作除数。于是我的代码成了下面的这个样子:

float result = num/*pInt;
….

/*  some comments */

-x<10 ? f(result):f(-result);

因为我在我当时用vi编写代码,所以没有语法高亮,而我的程序都编译通过了,但是却出现了很奇怪的事。我也不知道,用gdb调式的时候,发现有些语句直接就过了。这个问题让我花了很多时间,最后发现问题原来是没有空格导致的,TNND,下面我用代码高亮的插件来显示上面的代码,

float result = num/*pInt;
....

/*  some comments */

-x<10 ? f(result):f(-result); 

Holly Shit!  我的代码成了:

float result = num-x<10 ? f(result):f(-result);

妈的!我的这个错误在愚蠢程度上和上面那个作者出的错误有一拼。

(全文完)

(转载本站文章请注明作者和出处 酷 壳 – CoolShell ,请勿用于任何商业用途)

好烂啊有点差凑合看看还不错很精彩 (43 人打了分,平均分: 4.40 )
Loading...

C语言中史上最愚蠢的Bug》的相关评论

  1. 所以说,人都会犯错,重中之重永远是测试而不是严谨的风格(虽然也很重要)

  2. 我很喜欢这篇文章,让我想起了我写的一个小工具遇到的问题,以及那篇”21天教你学会C++”。抛开良好的编码规约不谈。除了钻研算法、专业知识、业务知识等等,程序员更应该认真的去好好学习你用到的编程语言,“工欲善其事,必先利其器”,语言作为工具,全面的了解你用到的语言是非常重要的,尤其是C和C++这种有很多历史遗留问题的语言。这些基本的东西了解之后,今后这些代码摆在你面前,你可以轻松看出来。
    自己喜欢关注细节,C++的二联符和三联符早在初学时就在C++之父的TC++PL一书的附录上看到了。C/C++支持的续行符更是研究的比较多,许多这样的问题依赖于编辑器的语法高亮显然是不靠谱的,就我所知,VS2008的编辑器(VS2005没用过)开始才开始支持“单行注释接续行符”的语法高亮,而许多其他的编辑器或者集成开发工具更是从来就不支持。并且”//”或者”/*”的两个字符中间都是可以用”\”续行的,并且我没有发现任何一个编辑器可以对这种情况语法高亮,三联符和二联符更是如此。

  3. 以前有个人甚至对我做出一个小工具说,对于C/C++语言的解析做到100%正确有什么意义呢,软件公司内部都有强制的编码规约,谁会写出那样的代码呢。我想说,严谨是有意义的,无论是统计分析还是语法高亮,对于C/C++语言做到100%正确是非常有意义的。如果所有支持C/C++语法高亮的编辑器都能做到100%正确,那么像这种错误,对于那些没有好好学习透C/C++语言的程序员,或者学习透了也会马虎的程序员 就不会发生了。

  4. 那个 bug 真正的愚蠢在于:任何一个靠谱的,支持语法着色的编辑器都应当能够显示出后面一段被当做注释。因此那个 bug 在靠谱的编辑器中不可能存在。

    至于博主的问题,哪怕是用 vim 替代 vi ,都不会出现。

    当然,在没有语法着色的史前时代,人们 也可以解决博主遇到的这个问题,方法是:用编码规范规定所有二元操作符前后必须加空格。因此除法的除号前后肯定有空格。

  5. @bob

    bob :
    并且我没有发现任何一个编辑器可以对这种情况语法高亮,三联符和二联符更是如此。

    据我所知 emacs / vim 都可以实现对这种情况的语法着色,并且据我所知很少见到靠谱的编辑器不支持这种情况下的着色。

  6. poet :
    据我所知 emacs / vim 都可以实现对这种情况的语法着色,并且据我所知很少见到靠谱的编辑器不支持这种情况下的着色。

    ”//”或者”/*”的两个字符中间都是可以用”\”续行的,这个有编辑器可以支持。我又用了一下emacs,我这里怎么不成呢。
    /\
    / comment

    /\
    * comment */

    无论是vim还是emacs,能给我截个图吗,我对这个很感兴趣。

  7. @poet
    你用的是什么版本啊?我用的是:
    GNU Emacs 23.1.1
    VIM 7.2

    都不支持。我用过的编辑器大大小小有三四十个左右吧,对 C、C++的续行符也是研究了一阵儿,真的是没见过一个能对我说的那种情况语法高亮的。除了这个之外,C++的关键字也是可以被续行符隔开的,比如:
    in\
    t a =0;

    我也没见过有任何一个编辑器能语法高亮的。

    能快点儿回复我不?我真的想赶紧知道,难道我的vim和emacs的设置不对?

  8. 读 k&r 的书,使用标准的代码格式确实不会犯这种错误啊。第一个错误就是只用/**/才有的优越感

  9. @poet
    如果你说的是普通的二联符和三联符的高亮,那么我也调查了一下:
    #include
    %:include
    ??=include

    VS2008、VS2010的编辑器不支持,Emacs不支持,VIM仅支持二联符。

    是有什么特殊设置,还是你看错我说的话了,咱能给个结论吗,你回复了一下就没下文了,弄得我这个感兴趣的人在这儿干着急,不太像话吧。

  10. 对后一个例子,打括号总是个好习惯。对于前一个例子,说明C编译器是先处理的‘\’,再处理的注释。这有点奇怪。

  11. bob :我很喜欢这篇文章,让我想起了我写的一个小工具遇到的问题,以及那篇”21天教你学会C++”。抛开良好的编码规约不谈。除了钻研算法、专业知识、业务知识等等,程序员更应该认真的去好好学习你用到的编程语言,“工欲善其事,必先利其器”,语言作为工具,全面的了解你用到的语言是非常重要的,尤其是C和C++这种有很多历史遗留问题的语言。这些基本的东西了解之后,今后这些代码摆在你面前,你可以轻松看出来。自己喜欢关注细节,C++的二联符和三联符早在初学时就在C++之父的TC++PL一书的附录上看到了。C/C++支持的续行符更是研究的比较多,许多这样的问题依赖于编辑器的语法高亮显然是不靠谱的,就我所知,VS2008的编辑器(VS2005没用过)开始才开始支持“单行注释接续行符”的语法高亮,而许多其他的编辑器或者集成开发工具更是从来就不支持。并且”//”或者”/*”的两个字符中间都是可以用”\”续行的,并且我没有发现任何一个编辑器可以对这种情况语法高亮,三联符和二联符更是如此。

    晕 这都行。
    以前还没注意过

  12. 有一次我发现有一个循环语句无论如何却只执行一次,找了老半天,后来发现,在for语句的括号后面有一个分号 ;
    各种囧。。。。

  13. 我现在写程序太依赖 VAX 了,,不过它也非常的强大,基本程序写完是不可能出现语法的问题,只有逻辑的问题。。

  14. /\
    / comment

    /\
    * comment */
    这部分在emacs会被认为是错的,会用红下划线标识,用的插件是emacs-23.3自带的cedet.

  15. g = fname ? fopen(fname, “w+”) : tmpfile();
    这句在我的emacs中标识为不是注释。。。。。
    但// Microsoft’s version of tmpfile() creates the file in C:\ 的\符号会被后移几个空格(对齐).

  16. fname是一个字符串指针,用fname?来判断而不是用 NULL != fname来进行分支判断是有风险的,因为C语言规范上并没有严格保证NULL就一定是0指针。

  17. 你的bug就不要秀了,谁让你不爱写空格的,活该了
    最讨厌看那种没空格、没空行、所有代码都挤在一起的代码

  18. 大麦头 :
    fname是一个字符串指针,用fname?来判断而不是用 NULL != fname来进行分支判断是有风险的,因为C语言规范上并没有严格保证NULL就一定是0指针。

    c语言就没有NULL

  19. 实是编辑工具没提示好。而且即使作者不是初学者,他遇到这个问题的重要原因之一就是他之前没有遇到这种问题。对于‘\ ’可能在宏定义的时候经常写。另一个重要的原因是他估计是在terminal或者记事本下编写的代码。这问题在一个支持C语法查检的文本编辑器中都被反应出来。

发表回复

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