找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

355

积分

0

好友

47

主题
发表于 9 小时前 | 查看: 0| 回复: 0

本文编译自外文博客《The most stupid C bug ever》,讲述了一个因C语言转义字符和预处理器特性导致的、令人啼笑皆非的跨平台开发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();
}

但这段代码在 Windows 平台上出现了问题。原因是 Microsoft 对标准 C 库函数 tmpfile() 的实现,默认将临时文件创建在 C:\ 根目录下。这对于没有管理员权限的用户来说会失败,甚至在 Windows 7 下,即便有管理员权限也可能出问题。

所以,在 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
}

但他很快意识到,这个写法很糟糕,会导致函数名冲突,让代码变得难以理解。

于是,他采用了更常见的跨平台做法:实现一个自己的函数 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();
}

你看出问题所在了吗?也许聪明的你一开始就发现了,但作者当时没有。所有问题的根源,都出在那行注释上:

//    Microsoft's version of tmpfile() creates the file in C:\

关键在于最后的 C:\。在 C 语言的字符串和字符常量中,反斜杠 \ 是转义字符。但在注释里呢?实际上,在 // 单行注释中,反斜杠同样具有特殊含义:它表示“续行”。编译器会将下一行物理代码也当作注释的一部分!

于是,原本的代码在预处理阶段实际上变成了:

//    Microsoft's version of tmpfile() creates the file in C:    g = fname ? fopen(fname, "w+") : tmpfile();

整个赋值语句都被注释掉了!宏定义 #define tmpfile w32_tmpfile 自然也就没有机会应用到已经被注释掉的 tmpfile() 上。这涉及到 C 语言预处理器对源代码的处理顺序。

而为什么改成 if-else 就能工作?因为作者当时恰好把旧的三元运算符语句注释掉了(//g = ...),新的 if-else 块不在那个“续行注释”的范围内,所以宏替换正常生效了。

我相信,当作者最终发现这个 BUG 的真相时,内心一定是崩溃的。这个由转义字符在注释中引发的“惨案”,肯定耗费了他大量的调试时间。

这个案例也提醒我们,在处理文件路径、正则表达式等涉及大量反斜杠的场景时,要格外小心。无独有偶,笔者也曾犯过一个“愚蠢程度”不相上下的错误。

我写了一个小函数,需要传入一个 int* pInt 指针,并在代码中用它做除数。由于当时使用没有语法高亮的 vi 编辑器,我写出了这样的代码:

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

/*  some comments */

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

程序编译通过了,但运行时行为极其诡异。用 GDB 调试时,发现有些语句似乎被直接跳过了。花费大量时间后,我终于找到原因:缺少空格

我们用有语法高亮的视角来看这段代码:

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

/*  some comments */

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

在 C 语言中,/* 被识别为块注释的开始。因此,第一行实际上变成了:

float result = num

而从 /*pInt 开始,直到 */ 结束的整个区域(包括中间的 .... 和那行独立的注释)都被当作注释处理了。最终,编译器“看到”的代码是:

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

这完全扭曲了原本的意图!这个错误深刻地提醒我们,C/C++的语法细节和书写规范何其重要,一个空格的有无可能彻底改变程序逻辑。

这两个故事,都堪称是 开发者 在特定情境下因语言特性而踩中的“经典深坑”。它们告诉我们,在编程时,尤其是在处理看似简单的字符串、注释和宏时,保持警惕和遵循良好的编码规范是多么关键。

原文链接:https://coolshell.cn/articles/5388.html
原始出处:https://www.elpauer.org/2011/08/the-most-stupid-c-bug-ever/




上一篇:基于树莓派Zero W与RFID,Flokk打造15美元级农场牲畜智能管理系统
下一篇:MySQL执行计划详解:为什么加了索引反而更慢?精准SQL性能调优指南
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-1-18 16:28 , Processed in 0.223507 second(s), 43 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表