摘要
本文是关于如何在编码初期避免部分错误的第二篇文章。我们第一篇文章中便已提出应尽量避免在一个表达式中整合大量计算。不过,在这里,我们会对该问题进行深入探讨。下面就让我们来看一下复杂的表达式存在哪些危险因素,以及我们可如何避免大量逻辑错误。
简介
您可以点击这里,阅读我们之前发表的第一篇文章。这次我们将列出一些来自不同著名项目的错误示例,借以强调它们的普遍性。我在这里展示的错误全部是在
PVS-Studio 分析器的帮助下发现的,覆盖的时间范围相对较广。我基本上已经将这些错误全部告知相关项目的开发人员,我希望他们能够在新的代码修订版中修复这些缺陷。之所以在简介部分提到这点,是因为我每次发表类似文章后都会收到很多来信,请我“将发现的错误告知项目开发人员”。
1. 不要在复合表达式中使用三元运算符“?:”
三元条件运算使用带“?:”运算符的 C/C++ 代码编写而成。该运算会返回第二操作数或第三操作数,具体取决于由第一操作数定义的逻辑表达式的值。例如:
int minValue = A < B ? A : B;
三元运算的优先级非常低(请参见下表),而程序员经常会忘记这一点,这也是我说三元运算非常危险的原因。
图 1 ——按照优先级从高到低排列的 C/C++ 运算符
请注意“?:”运算的优先级低于加、乘、位或运算符等。分析下方的代码:
int Z = X + (A == B) ? 1 : 2;
该代码的实际运算方式可能与表面上所呈现出的有很大不同。最可能的情况是,程序员希望根据
(A == B) 条件为 X 数值加 1 或 2,而在上方的代码中,“X + (A == B)”表达式才是条件,结果,项目中编写的代码是:
int Z = (X + (A == B)) ? 1 : 2;
但是,程序员的本意是:
int Z = X + (A == B ? 1 : 2);
我想您看到这里首先想到便是掌握运算的优先级很重要。然而,虽然程序员通常很清楚运算优先级,但这个三元运算太过狡猾了!不只是新手,一些经验丰富的程序员也会犯这种错误。即使在质量最出色的代码中,您也很容易找到与三元运算相关的错误。下方列出了几个示例。
V502 Perhaps the '?:' operator works
in a different way than it was expected. The '?:' operator
has a lower priority than the '*' operator. physics
dgminkowskiconv.cpp 1061
dgInt32 CalculateConvexShapeIntersection (...)
{
...
den = dgFloat32 (1.0e-24f) *
(den > dgFloat32 (0.0f)) ?
dgFloat32 (1.0f) : dgFloat32 (-1.0f);
...
}
V502 Perhaps the '?:' operator works
in a different way than it was expected. The '?:' operator
has a lower priority than the '-' operator. views custom_frame_view.cc
400
static const int kClientEdgeThickness;
int height() const;
bool ShouldShowClientEdge() const;
void CustomFrameView::PaintMaximizedFrameBorder(gfx::Canvas* canvas) {
...
int edge_height = titlebar_bottom->height() -
ShouldShowClientEdge() ? kClientEdgeThickness : 0;
...
}
V502 Perhaps the '?:' operator works
in a different way than it was expected. The '?:' operator
has a lower priority than the '|' operator. vm vm_file_win.c
393
#define FILE_ATTRIBUTE_NORMAL 0x00000080
#define FILE_FLAG_NO_BUFFERING 0x20000000
vm_file* vm_file_fopen(...)
{
...
mds[3] = FILE_ATTRIBUTE_NORMAL |
(islog == 0) ? 0 : FILE_FLAG_NO_BUFFERING;
...
}
从上面的示例中可以看出,这种类型的错误非常值得关注,这也是我将它们单独拿出来进行详细讨论的原因。此类错误出现的范围相当广泛,我可以列举出更多的例子,不过全部大同小异。
如果您能够放弃尝试在一个代码行中整合多个运算,便能避免此类错误。或者,如果您还坚持这样做,那么请不要吝啬圆括号的使用。我稍后会专门讲到圆括号问题。接下来,让我们分析下使用“?:”时如何避免潜在错误。
毫无疑问,“?:”运算符是一个语法糖,大多数情况下,您可以使用“if”来代替它,只有极少数情况是例外,例如执行参考初始化等任务时:
MyObject &ref = X ? A : B;
MyObject *tmpPtr;
If (X)
tmpPtr = &A;
else
tmpPtr = &B;
MyObject &ref = *tmpPtr;
所以说,我们不应拒绝使用“?:”运算符,但使用它时又很可能会出错。考虑到这种情况,我为自己制定了一项准则:务必将“?:”运算符的计算结果即刻存储至指定位置,不可将它与任何其它行为相混合。换句话说,“?:”运算符条件的左侧必须有一个赋值运算。下面重新回到刚刚列出的示例:
int Z = X + (A == B) ? 1 : 2;
我建议使用以下方式编写代码:
int Z = X;
Z += A == B ? 1 : 2;
如果是一个 IPP 范例代码示例,我会这样写:
mds[3] = FILE_ATTRIBUTE_NORMAL;
mds[3] |= (islog == 0) ? 0 : FILE_FLAG_NO_BUFFERING;
您可能并不认同我的建议,对此我不会多做解释。例如,就我自己而言,如果能够使用一行代码解决问题,我也不喜欢采用两行或多行代码。另一种有效的替代方法是使用圆括号将“?:”运算符括起来。我的主要任务是指出错误模式,至于具体采用哪种错误保护模式则取决于程序员的个人喜好。
2. 大胆地使用圆括号
现在,出于某些原因,在 C/C++ 编程中使用额外的圆括号被视作一件很丢人的事情。或许是因为面试时经常被问及运算符优先级问题,人们开始下意识地尝试在各种情况下使用优先级机制,如果有人添加了额外的圆括号,其他人会认为他是一个新手,不是真正的编程高手。
我甚至在互联网上看到过一些讨论,当中有些人非常教条,认为使用额外的圆括号是一种坏习惯,如果对表达式的计算方式不确定,就应该再去学习,而不要编写程序。很可惜,我后来没有找到当时看到的内容,不过,我自身并不同意这种观点。当然,您必须了解运算优先级,不过如果您在一个表达式中使用了不同类运算,那么最好使用圆括号来保护自己不会犯错误。这不仅能够帮助您避免出错,还可以确保其他开发人员也能读懂代码。
不仅新手程序员,技能娴熟的老程序员也会因为搞混优先级而犯错。表达式不一定非常复杂、非常长,即使是相对较简单的表达式也可能会出错。请参阅以下示例:
V564 The '&' operator is applied
to bool type value. You've probably forgotten to include
parentheses or intended to use the '&&' operator.
game g_client.c 1534
#define SVF_CASTAI 0x00000010
char *ClientConnect(...) {
...
if ( !ent->r.svFlags & SVF_CASTAI ) {
...
}
V564 The '&' operator is applied
to bool type value. You've probably forgotten to include
parentheses or intended to use the '&&' operator.
dosbox sdlmain.cpp 519
static SDL_Surface * GFX_SetupSurfaceScaled(Bit32u sdl_flags,
Bit32u bpp) {
...
if (!sdl.blit.surface || (!sdl.blit.surface->flags&SDL_HWSURFACE)) {
...
}
以下为来自Chromium的另一个示例:
V564 The '&' operator is applied
to bool type value. You've probably forgotten to include
parentheses or intended to use the '&&' operator.
base platform_file_win.cc 216
#define FILE_ATTRIBUTE_DIRECTORY 0x00000010
bool GetPlatformFileInfo(PlatformFile file, PlatformFileInfo* info) {
...
info->is_directory =
file_info.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY != 0;
...
}
表达式很简单,开发人员也很出色,但还是出现了错误。总之我认为,在模糊不清的代码片段中使用圆括号并非多此一举。
我认为最好的解决方式是:如果运算简单、平常,您不需要额外添加圆括号。例如:
if (A == B && X != Y)
if (A - B < Foo() * 2)
但是,如果您使用了不太常见的运算符(~、^、&、|、<<、>>、?:),最好还是添加明确的圆括号。圆括号会令代码更加清晰,确保您不会出错。例如:
If ( ! (A & B))
x = A | B | (z < 1 ? 2 : 3);
使用不常见的运算符时,添加圆括号还能帮助您正确使用之前所说的“?:”运算符。如何处理“?:”取决于个人喜好。我自身比较倾向于选择简单的方法。
总结
尽量编写简单、清晰的代码。当然,将较长、较复杂的表达式拆分为多个字符串之后,您的代码长度会增加。但是,这样一来,代码更加清晰,阅读和理解起来更加轻松。而且,使用这种代码出错的可能性更低。请不要害怕创建附加变量,编译器将能够有效地优化代码。
在含有罕见运算符或者位和逻辑运算同时出现的表达式中,请不要吝啬圆括号的使用。
将来,编程员看到您含有圆括号的代码时,只会心存感激。 |