在本文中,我们邀请您尝试从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 );
检查给定字符是否为按当前安装的C语言环境分类的空格字符。 在默认语言环境中,空格字符如下:
- 空格(0x20,'');
- 换页(0x0c,'\ f');
- 换行符LF(0x0a,'\ n');
- 回车CR(0x0d,'\ r');
- 水平制表符(0x09,'\ t');
- 垂直制表符(0x0b,'\ v')。
返回值 如果字符是空白字符,则为非零值;否则为0。 否则为零。
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')被视为空白字符的条件。
这意味着,除了空格和水平制表符之外,他们还计划跳过换页和垂直制表符。
更值得注意的是,他们也希望跳过回车符(0x0d,'\ r')。 但是不会发生-遇到此字符时循环终止。 如果换行符由CR + LF序列表示,则该程序最终会出现意外行为,CR + LF序列是某些非UNIX系统(例如Microsoft Windows)中使用的类型。
有关使用LF或CR + LF作为换行符的历史原因的更多详细信息,请参见Wikipedia页面“
换行符 ”。
EatWhitespace函数旨在以相同的方式处理文件,无论它们使用LF还是CR + LF作为换行符。 但是在CR + LF的情况下失败。 换句话说,如果您的文件来自Windows世界,那么您就有麻烦了:)。
尽管这可能不是一个严重的错误,尤其是考虑到在类似UNIX的操作系统中使用了GNU Midnight Commander,而LF(0x0a,'\ n')被用作换行符,但是像这样的琐事仍然容易让人讨厌在Linux和Windows上准备的数据的兼容性问题。
使这个错误变得有趣的是,您几乎可以肯定在执行标准代码审查时会忽略它。 宏的实现细节很容易忘记,有些项目作者可能根本不了解它们。 这是一个非常生动的例子,说明了静态代码分析如何有助于代码审查和其他错误检测技术。
覆盖标准功能是一个坏习惯。 顺便说一下,我们在最近的文章“
赞赏静态代码分析 ”中讨论了
#define sprintf std :: printf宏的类似情况。
更好的解决方案是为宏指定一个唯一的名称,例如
is_space_or_tab 。 这本来可以避免所有的混乱。
也许标准的
isspace函数太慢了,程序员创建了一个更快的版本,足以满足他们的需求。 但是他们仍然不应该那样做。 一个更安全的解决方案是定义
isspace,以便您获得不可编译的代码,而所需的功能可以实现为具有唯一名称的宏。
感谢您的阅读。 不要犹豫,
下载 PVS-Studio并在您的项目中尝试一下。 提醒一下,我们现在也支持Java。