
我们邀请您尝试从GNU Midnight Commander项目的一个非常简单的函数中查找错误。 怎么了 就这样 这很有趣。 虽然没有,但我们撒了谎。 我们再次想证明一个人在代码审查过程中发现的错误,但是很容易找到PVS-Studio静态代码分析器。
最近,我们收到一封信,询问分析仪为什么会在
EatWhitespace函数上生成警告,其代码如下。 其实问题不是那么简单。 尝试自己弄清楚这段代码有什么问题。
static int EatWhitespace (FILE * InFile) { int c; for (c = getc (InFile); isspace (c) && ('\n' != c); c = getc (InFile)) ; return (c); }
如您所见,
EatWhitespace函数非常小。 甚至对函数的注释也比函数本身的主体占用更多的空间:)。 现在一些细节。
Getc函数说明:
int getc ( FILE * stream );
该函数返回由指定流的文件位置的内部指示符指向的字符。 然后指示器转到下一个字符。 如果在调用流时已到达文件结尾,则该函数返回
EOF并为此流设置文件结尾指示符。 如果发生读取错误,该函数将返回EOF值并为给定的流设置错误指示符(错误)。
isspace函数的描述:
int isspace( int ch );
该函数根据当前语言环境的分类检查字符是否为空格。 在标准语言环境中,以下字符为空格:
- 空格(0x20,``);
- 页面更改(0x0c,'\ f');
- 换行符LF(0x0a,'\ n');
- 回车CR(0x0d,'\ r');
- 水平制表符(0x09,'\ t');
- 垂直制表符(0x0b,'\ v')。
返回值 非零值,如果字符为空格,则为零。
EatWhitespace函数应跳过所有被视为空格的字符,但换行符'\ n'除外。 停止从文件读取的另一个原因可能是到达文件末尾(EOF)。
而现在,知道了这一切之后,尝试找到一个错误!
为防止读者偶然不立即看到答案,请添加几个等待中的独角兽。

图1.找出错误的时间。 独角兽将等待。还是看不到错误?
问题是,我们欺骗了有关
isspace的读者。 哈哈 这根本不是标准功能,而是自制宏。 是的,我们没有责备,让您感到困惑。

图2.独角兽给读者关于isspace是什么的错误印象。实际上,当然,我们和我们的独角兽不应该受到指责。 GNU Midnight Commander项目的作者决定在
charset.h文件中创建自己的
isspace实现,从而
加剧了
混乱 :
#ifdef isspace #undef isspace #endif .... #define isspace(c) ((c)==' ' || (c) == '\t')
通过创建这样的宏,一些开发人员将其他开发人员弄糊涂了。 编写代码的前提是
isspace是将回车符(0x0d,'\ r')视为空白字符之一的标准函数。
实现的宏仅将空格和制表符视为空白字符。 让我们替换宏,看看会发生什么。
for (c = getc (InFile); ((c)==' ' || (c) == '\t') && ('\n' != c); c = getc (InFile))
子表达式('\ n'!= C)是冗余的(冗余的),因为其结果将始终为true。 PVS-Studio分析仪对此进行警告,并发出警告:
V560条件表达式的一部分始终为true:('\ n'!= C)。 c。第136章
为了清楚起见,让我们分析事件开发的3个选项:
- 到达文件末尾。 文件结尾(EOF)不能为空格或制表符。 由于短路评估,未计算子表达式('\ n'!= C)。 循环停止。
- 读取不是空格或制表符的任何字符。 由于短路评估,未计算子表达式('\ n'!= C)。 循环停止。
- 阅读空格字符或水平制表符。 计算子表达式('\ n'!= C),但其结果始终为true。
换句话说,所检查的代码与此等效:
for (c = getc (InFile); c==' ' || c == '\t'; c = getc (InFile))
我们发现该代码无法正常工作。 现在让我们看看这有什么后果。
在
EatWhitespace函数的主体中编写
isspace调用的程序员希望可以调用标准函数。 这就是为什么他添加了换行符LF('\ n')不应被视为空白字符的条件。
因此,程序员计划除了空格和水平制表符之外,还将跳过诸如页面更改和垂直制表符之类的字符。
值得注意的是,还计划跳过CR回车符(0x0d,'\ r')。 这不会发生,并且在遇到该符号时循环会停止。 如果文件中的行分隔符是某些非UNIX系统(例如Microsoft Windows)上使用的CR + LF序列,这将导致不愉快的意外。
对于那些想了解更多使用LF或CR + LF作为行分隔符的历史原因的人,这里是Wikipedia文章“
换行 ”。
EatWhitespace函数
应该以相同的方式处理文件,其中LF和CR + LF都用作分隔符。 对于CR + LF,情况并非如此。 换句话说,如果您的文件来自Windows世界,那么您就不走运了:)。
也许这不是一个严重的错误,特别是因为GNU Midnight Commander在类似UNIX的操作系统上很常见,在该操作系统上,LF字符(0x0a,'\ n')用于转换行。 然而,由于这些琐事,在Linux和Windows系统中准备的数据不兼容的各种烦人的问题出现了。
所描述的错误很有趣,因为用经典的代码审查几乎不可能检测到。 并非所有的项目开发人员都能知道宏的复杂性,而忘记它们很容易。 这是一个很好的示例,其中静态代码分析补充了代码审查和其他错误查找技术。
覆盖标准功能是不好的做法。 顺便说一下,最近在“
爱静态代码分析 ”一文中,使用
#define sprintf std :: printf宏考虑了类似的情况。
更好的解决方案是为宏指定唯一的名称,例如
is_space_or_tab 。 这样就不会造成混乱。
创建宏的原因可能是标准
isspace函数的运行缓慢
,程序员创建了一个更快的版本,足以解决所有必要的任务。 但是,这个决定是错误的。 定义
isspace以便获得未编译的代码会更加
可靠 。 并在具有唯一名称的宏中实现必要的功能。
谢谢您的关注。 我们邀请您
下载并尝试使用PVS-Studio分析仪来测试您的项目。 另外,我们提醒您,分析器最近增加了对Java语言的支持。

如果您想与说英语的读者分享这篇文章,请使用以下链接:Andrey Karpov。
想扮演侦探吗? 在Midnight Commander中找到函数中的错误 。