这个 bug 是在 Miranda NG 的项目中发现的。代码中包含的错误被 PVS-Studio 分析器诊断为: V502 可能 '?:' 操作的结果和预期的有差。 '?:' 的优先级低于 '|' 。
解释
我们有看到过很多例子,即使代码逻辑不对也能运行。这一次,我想提出一个不一样的、发人深思的话题来讨论。有时我们会看到完全不正确的代码很偶然的,即使困难重重,还是运行了。现在,对于有经验的程序员来说,这没什么好惊讶的(这是另一个故事了),但对那些 C/C++ 的初学者来说,这就有点令人感到困惑了。所以,今天我们来看看这样的例子。
在上面给出的代码中,我们需要调用带有一定标志的 CheckMenuItem() ; 而且,猛地一看我们会觉得,如果 bShowAvatar 是 true ,那就做或运算 MF_BYCOMMAND | MF_CHECKED ,相反的,如果是 false ,就是 MF_BYCOMMAND | MF_UNCHECKED 。很简单。
在上面给出的代码中,程序员选用了最意料之中的三元运算符来表达这个意思(这个表达式是 if-then-else 的简便版本):
但问题是, ‘ |’ 的优先级高于 ‘ ?: ’ 的(参看 C/C++ 中的运算优先级 )。这样就导致了一下子有两个错误。
第一个错误是,条件改变了。它不再是 —— 像某人可能理解的 —— "dat->bShowAvatar" ,而是 "MF_BYCOMMAND | dat->bShowAvatar" 。
第二个错误 —— 只剩下一个标志可以选择 —— 不是 MF_CHECKED 就是 MF_UNCHECKED. 标志 MF_BYCOMMAND 已经缺失了。
尽管有这些错误,代码还是正确运行了!原因 —— 踩了狗屎运。那个程序员很幸运,因为标志 MF_BYCOMMAND 等于 0x00000000L 。而因为 MF_BYCOMMAND 等于 0, 所以最后没有影响到代码。可能一些有经验的程序员已经知道我想要表达的意思了,但我还是再做写解释吧,以便初学者理解。
首先,让我们看一眼加了括号的正确表达式。
然后用具体的值来代替宏:
如果 ‘ |’ 的其中一个操作数是 0, 那么我们就可以简化上面的表达式,得到:
现在我们再来看原先那个不正确的表达式:
把具体的值代入:
在子表达式 "0x00000000L | dat->bShowAvatar" 中,其中一个操作数是 0 。简化可得:
最后,我们得到了相同的表达式,这就是为什么有错误的代码还是能正确运行了。又一个编程奇迹发生了。
正确代码
有很多种方法来修正这段代码。其中一种就是加括号,另一种 —— 加一个中间变量。还有比较老的 if 运算也是可以的:
我其实并不坚持让你用这个方法来修正代码。这样看上去是更易读了,但是有点长。所以,这更多的是偏好的问题。
建议
我的建议很简单 —— 避免使用太复杂的表达式,尤其是三元运算符。还有,别忘了加括号。
就像在第四章已经说过的, ‘ ?: ’ 这个运算真的很危险。有的时候一疏忽你就忘了它的优先级非常的低,然后你就会写一个不正确的表达式。人们会在他们想要阻塞一个字符串的时候用到它,所以,你别这么做。
读了那么长一篇由静态代码分析器开发者所写的文章,却没有看到关于使用它的建议,是不是很奇怪。所以我们现在就讲这个。
下面的代码选自 Haiku 项目( BeOS 的继承者)。代码中包含的错误被 PVS-Studio 诊断为: V501. '<' 表达式左右两边的操作数是一样的: lJack->m_jackType < lJack→m_jackType
解释
这是一个很常见的拼写错误。在右边应该是 rJack ,然后被错写成了 lJack 。
这个拼写错误实际上是很简单的一种,但这种情况真的蛮复杂的。因为无论是编程风格,或者是其他的方法,在这里都无济于事。有的时候就是在拼写的时候犯了个错误,对此,你能做什么呢?
要特别强调说这不是某个人或者项目的问题。无疑,世人皆会出错,即使是在重大项目中的专业人员也会。 这 是关于我这个言论的证明。你可以看到最简单的拼写错误,诸如 A == A, 在 Notepad++, WinMerge, Chromium, Qt, Clang, OpenCV, TortoiseSVN, LibreOffice, CoreCLR, Unreal Engine 4 等等这些项目中出现。
所以这个问题是真实存在的,而且这不是学生的实验课作业。当有人跟我说,有经验的程序员是不会犯这样的错误的时候,我一般都会把这个 链接 甩给他们。
正确代码
建议
首先,让我们先来讲几个不那么有用的小贴士。
在编程的时候要特别小心,不要让错误溜进你的代码。(话是挺漂亮的,然而并没有什么用)。
用好的编程风格。(没有哪一种编程风格能帮你避免在变量名上出错。)
那么,有用的建议呢?
代码审查
单元测试(测试驱动开发「 TDD 」)
静态代码分析
我应该说,每一种方法都有各自的强处和弱处。这就是为什么要写出最高效可靠的代码是把它们都用起来。
代码审查 能帮我们找到大量的,不同的错误,而且最重要的是,它可以帮我们提高代码的可读性。但不幸的是,分享式阅读( Shared reading )代码有点昂贵、无聊而且不会保证正确性。很难一直保持警惕,然后在阅读这种代码的时候发现拼写错误:
理论上, 单元测试 应该能帮到我们。但仅仅是理论上。在实际中,检查所有可能的运行路径是不现实的。而且,测试本身 也会 有一些错误 :)
静态代码分析仅仅是个程序,而非人工智能。分析器可能会跳过一些错误,有时还会误报,就是其实代码是正确的,但它弹出了错误信息。尽管有这些缺点,它还是一个很有用的工具的。在编写代码的早期,它可以检测到很多错误。
静态代码分析器也可以用做便宜版本的代码审查。让代码分析器而不是程序员来检查代码,也可以让它更全面的检查某一代码片段。
当然,我会推荐使用 PVS-Studio 静态代码分析器,这是我们开发的。额,这世上并非只有这一款。还有很多免费或付费的工具可以使用。比如说,你可以看看免费开放的 Cppcheck 分析器。维基百科上也给出了一系列静态代码分析器的单子: List of tools for static code analysis .
注意:
如果不正确使用静态代码分析器,可能会让你有点头疼。最典型的错误之一就是 “ 把检查模式选项设为最大值,然后就陷入大量的提示信息之中。 ” 我能给出的众多建议中的一个就是,为获得更多选项 ,去看看 A , B 会有用的。
静态分析器的使用应该有个界限,不是一直使用,或者当遇到问题的时候使用。参看 C , D 的解释。
真的,尝试着去使用静态代码分析器。你会喜欢它的。它是一个非常好的工具。
最后,我推荐阅读 John Carmack 的文章: 静态代码分析 .
假设你要在你的项目中实现 X 功能。软件开发理论家会说,你要用已经存在的库 Y 来实现你需要的功能。事实上,这是软件开发中一个典型的方法 —— 重用你自己或者其他人之前已经创建好的库(第三方库)。然后大多数程序员也用这个方法。
但是,在一些文章或书籍中那些理论家忘了说,用某些第三方库近 10 年会有多悲剧。
我非常建议避免在项目中增加新的库。但是也别误解我。我不是让你丝毫都不用库,自己去实现所有的功能。这当然很没必要啊。但有时有些程序员心血来潮,想要往项目里加点 ‘ cool’ 的特性,然后就引入了新的库。难的不是往项目里加新的库,而是从此以后整个项目要不得不带上它。
追溯几个大项目的演化,我看到了很多有第三方库引起的问题。我可能只会列举其中的一些,但这个单子已经能够引发我们的思考了:
引入新的库会立即增加项目的大小。在我们这个有快速互联网和大型 SSD 驱动的时代,这当然不是什么大问题。但当从版本控制系统下载时间从 1 变成 10 的时候,这就令人有点不快了。
即使你只用到了库功能的 1% ,你还是得把它整个都导入到项目中。结果就是,如果这些库是用是在编译模块使用的(比如, DLL ),分布大小会很快增长。如果你把库当作源代码使用,那编译时间会大大增长。
跟项目的编译( compilation )连接的设备也会变得更复杂。有些库需要额外的组件。一个简单的例子:我们需要用 Python 来编译链接成目标文件( building )。结果就是,有时你需要很多额外的程序才能创建这个项目。而且有些地方会出错的几率也会上升。这很难解释,你需要自己去经历。在大项目中,有的地方就是一直运行不起来,你就得花很大的精力去解决它,让所有的一切都能正常编译运行。
如果你有担心漏洞,你要时常更新第三方库。那些违反者( violator )应该对这个很有兴趣,研究代码库寻找漏洞。首先,很多库都是开源的,其次,如果发现了其中一个库的弱点,你可以写一个 exploit 去利用那些用到这个库的应用程序。
那些库有可能突然就改变了许可证类型。第一,你要将这件事时刻放在心上,还要保持跟进。第二,如果真的发生了,你要做什么其实也不太清楚。比如说,有一次,广泛使用的 softfloat 库从个人协议移到了 BSD 。
在升级编译器的版本的时候你会遇到困难。肯定会有一些库没有跟新的编译器兼容,那你只能等了,或者你自己连接那个库。
在移到不同的编译器上的时候,你也会遇到问题。比如说,你以前用的是 Visual C++ ,现在打算用 Intel C++ 。可定会有一些库出错。
在移到不同平台的时候,也会遇到问题。有的时候甚至不是一个完全不同的版本。比如说,你打算把一个 Win32 应用程序移到 Win64 上。你就会遇到同样的问题。最可能的是,有些库没准备好,你要想要怎么处理他们。最令人不悦的就是,有些库根本就不开发了,也没有更新它了。
[招生]科锐逆向工程师培训(2024年11月15日实地,远程教学同时开班, 第51期)
上传的附件: