C/C++ `!a`与`a==0`的区别

5
当我有一个整数或指针 a 时,事实证明两者都是
!a

并且

a==0

它们具有相同的行为。在非常低的层面上,计算速度方面是否存在差异?


4
请检查生成的汇编代码,尽管不应该出现问题。 - Fred Foo
4
很可能不会有。如果有的话,那又有什么关系呢? - The Paramagnetic Croissant
7
过早优化是万恶之源。 - dandan78
这主要取决于编译器,甚至更多地取决于所使用的编译器优化标志。 - Arsenii Fomin
3
@danda78,无论原始问题是什么,写那句陈词滥调都是不必要的。这个问题不一定是关于优化某些代码的,而是试图更好地理解类似(可能相同)操作的性能差异。 - Neowizard
显示剩余4条评论
7个回答

10
由于编译器了解逻辑等价性,因此性能上不太可能有差异,所以它们可以为两者发出相同的代码。这种等价性是基本的,而不是某些聪明的定理:对于标准中定义的整数类型,!a 的含义是“值等于0”(或者严格来说,正如James指出的那样,“非(值不为0)”),对于指针类型,!a 的含义是“a 是空指针”(或者严格来说,“非(值为非空指针)”)。然而,并没有要求编译器必须为两者发出相同的代码,因此C或C++标准不能保证性能相同。

同样的规则也适用于浮点类型、未作用域枚举和成员指针。基本上,对于任何具有到bool的隐式转换的类型T,该转换被定义为a != static_cast<T>(0)。(唯一的例外是std::nullptr_t,它总是转换为false;我甚至不确定static_cast<std::nullptr_t>(0)是否合法。) - James Kanze
我的意思是“表达式!E等同于(0==E)”,这样就不会有个人解释的余地了。如果编译器为每种情况生成不同的代码,导致执行速度不同,那么!和==就不相等,因此编译器不符合标准。 - Lundin
1
@Lundin:我认为你对 C 标准中“等效”一词的理解是错误的。它仅意味着抽象机器的定义行为相同。它并不意味着发出的代码是相同的或性能相同,因为这两件事都不是由标准所定义的抽象机器的一部分。 - Steve Jessop
1
就此而言,请看前面一句话 /4,“如果提升后的类型是无符号类型,表达式E等同于该类型中可表示的最大值减去E”。我不相信C标准要求这里`E *必须*使用减法操作码来实现。甚至它也不要求如果~E使用按位取反操作码,那么UINT_MAX - E`也必须使用按位取反操作码以保持两者的等价性。 - Steve Jessop
@Lundin:无论如何,我认为很明显我们在这里不能达成一致。即使我在标准中找到了清楚说明等价的表达式的示例,并且所有常见编译器都会生成不同的代码,你也会断言所有常见编译器在这方面都不符合规范。我毫无证据地断言,没有编译器作者像你这样解释标准,标准的作者也没有。我所能做的最好就是已经说过的,你似乎不同意:因为标准仅定义抽象机的行为,所以等价指的是该行为。 - Steve Jessop
显示剩余7条评论

9
按定义,!需要一个bool类型的操作数;如果它的操作数是int类型,则会有一个隐式转换(这对混淆很好,但通常应该避免使用)。按定义,将int a隐式转换为bool的方式是a != 0。因此!a就是!(a != 0)。实际上,很难想象任何编译器不会生成与a == 0完全相同的代码,因此性能肯定不应该是一个考虑因素。说你的意思,做你所说的:既然你在比较0,那就应该这样写。
编辑:
正如Steve Jessop指出的那样,如果要比较的类型不是int,则!a的正式定义是!(a != static_cast<T>( 0 ),其中Ta的类型。虽然隐式转换通常会影响可读性,但我认为没有人会坚持将0显式转换为另一种整数类型。另一方面,我系统地为浮点类型写a == 0.0,在C++中为指针类型写a == nullptr——这涉及到另一个隐式转换,从std::nullptr_t到指针类型。并非所有的隐式转换都是相等的:-)。(FWIW:我接受整数类型之间的隐式转换,以及浮点类型之间的隐式转换,只要它们不是缩小转换,如C++11 §8.5.4/7所定义的那样,除了应该使用'\0'而不是0来表示字符数据;以及指针之间的隐式转换,以及指针与std::nullptr_t之间的隐式转换。整数类型到浮点类型的隐式转换也不会让我感到困扰,尽管我永远不会写它们。但就是这些。)

3
+1 for 说到做到: 如果检查的目的是确定一个数值是否等于0,那么使用== 0是正确的。如果检查的目的是确定某个抽象条件未满足(这恰好通过数值来建模),则使用!。考虑:if(numberOfItems == 0)if(!file_handle) - ComicSansMS
1
一个文件句柄也不是抽象的真或假,所以应该是 if ( file_handle != invalid_file_handle ),其中 invalid_file_handle 是一个定义的常量。虽然有些情况下... 在 Unix 环境中编程的人会觉得 if ( fd < 0 ) 很熟悉,例如,不需要为 0 定义一些特殊的符号,如果环境将句柄定义为指针类型(作为公共接口的一部分——很多接口将使用 void* 作为接口中的魔术 cookie),那么 if ( handle != nullptr ) 就可以了。 - James Kanze
@JamesKanze 理解了,我的例子远非完美。正如您所提到的,最终一切都取决于上下文。感谢您抽出时间澄清。 - ComicSansMS
1
@Lundin 有点是有点不是。在编写C代码时,你应该像C有布尔类型一样编写它,与C++的bool几乎具有相同的行为。但是,语言层面上的正式区别是不同的。 - James Kanze
2
@ComicSansMS 是的。上下文是关键。在我发表评论后不久,我意识到我总是坚持让人们使用像while ( std::getline( input, line ) )...这样的东西,其中包含一些相当可怕的隐式转换,如果你仔细想想的话。但你不会考虑这个问题,因为std::istream是以这种方式指定的:如果有任何失败,则为false,否则为true - James Kanze
显示剩余2条评论

3
在C语言中,“!”和“==0”没有区别,两者最终都会生成相同的机器代码。区别在于风格上的差异。C标准本身就明确写道(C11 6.5.3.3):“表达式!E等效于(0 == E)。 ”应该只在实际上是布尔类型的操作数上使用“!”。不幸的是,在C语言中并不存在真正的布尔类型,一切都被视为整数。(尽管C99引入了“_Bool”类型和“bool”宏,但无论何时在表达式中使用这些类型,它们仍然会提升为整数。)因此,你应该将表达式视为存在真正布尔类型的情况。好的C程序员编写代码时要假装真的有一个真正的布尔类型存在。(有关“实际上是布尔类型”的更多信息,请参见MISRA-C:2012附录D “必要类型”。)
对于所有其他操作数,应该使用“==0”。这包括普通整数和指针。例如,编写“if(!ptr)”被认为是不好的样式,检查指针的正确方法是“if(ptr == NULL)”。 (MISRA-C:2004 12.6)
由于同样的原因,“if(ptr)”被视为松散的编码。因为“if”期望的是基本上是布尔的表达式。正确的代码使用“if(ptr!= NULL)”,这也更加可读和自述。即使生成的机器代码是相同的。(MISRA-C:2004 13.2)
C++不同,因为它有一个真正的布尔类型。对于“!”操作数,期望是布尔类型,如果不是布尔类型,则会转换为布尔类型。C将执行相反的操作,如果操作数是布尔型,则会转换为整数。尽管如此,最终仍不会影响性能,因为编译器允许将所有隐式转换优化为最合适的类型,前提是它不会改变代码的结果。
同样在C ++中,“!”和“==”的结果类型始终是布尔类型,而在C中结果始终是整数(你应该将其视为布尔类型...但它仍然是整数)。

等价并不意味着性能相同,也不能保证生成相同的机器代码。在这个例子中可能是这样的。但是说 x+y 等价于 y+x 并不能说明任何关于性能的问题。性能保证超出了语言标准的范围。 - P.P
@SteveJessop 好的,我会删除关于性能的部分,因为似乎没有正式的证据支持任何一种观点。 - Lundin
1
@Lundin所说的“导致错误的危险做法”是一种编码风格问题。如果你编写的代码实际上没有错误,那么这不是一个正确性问题。 - M.M
2
MISRA-C准确地说是一组编码风格规则和指南。我并不是在暗示编码风格只是“无害的个人偏好”。详情请参见链接。此外,不遵循MISRA标准的人并不一定写出糟糕的代码。 - M.M
1
@Lundin,这些都与本帖有什么关系呢?我从未建议不遵循编码标准,甚至没有远离。那么为什么你要用轻蔑的口气跟我说话,好像我需要教育才能理解编码标准的价值一样? - M.M
显示剩余7条评论

2

我预计对于整数和指针,!aa==0 的计算速度不会有区别。

使用最能清晰表达您意图的变量。对于整数,我总是使用 `a==0'。对于指针,这两个版本都很好。

PS:性能差异不是不可能的,但应该非常小。对于良好的优化编译器来说,差异实际上是不可能的。它将被视为错误并很快得到修复。


2
在许多情况下,它们是相同的。任何现代编译器都会根据目标机器选择最佳指令。第一个在开发人员中很受欢迎。
需要考虑在重载运算符“==”和“!”时,可能会依赖于变量类型a。(例如:自定义对象)

2
您可以执行的最佳优化是,仅考虑这两个语句,请使用0 == a而不是a == 0
在C中存在一些小差异,特别是对于不提供布尔类型的C版本,在旧版本的C中,通常将bool处理为int,因此隐式转换仅转换为整数而不是布尔值,这就是双重否定的技巧 !!a 的用处,这也是为什么这个技巧在现代语言版本中无用但在旧代码库中广泛采用的原因。
除此之外,我想不出其他实质性的区别。

4
为什么要写成0 == a而不是a == 0呢?这样会让读者感到困惑。 - James Kanze
1
@JamesKanze 我个人认为恰好相反。https://dev59.com/iW865IYBdhLWcg3wi_Q2#DZ6cEYcBWogLw_1b3EDK - user2485710
1
@JamesKanze:对于那些不知道==是对称关系的读者肯定会感到困惑;-) 不直觉理解是很自然的,因为虽然在字面上,“和…一样”是对称的,但在强调方面它却不是对称的。因此,在理解代码时重点强调往往容易让读者感到困扰,特别是当常量出现在左侧时。在不需要强调重点的情况下,它们(据我所知)并不会产生这种影响。 - Steve Jessop
2
@mc110 我觉得"最常见的打字错误"非常罕见。尽管这种情况确实会发生,但大多数编译器都会在这种情况下发出警告,并且因为它是一个众所周知的错误,在代码审查中也容易被发现。另一方面,大多数人在比较时都会将常量放在右边,并期望在阅读时看到它在那里。我不知道为什么,但事实就是这样。因此,在编写代码时,您应该这样做;任何不寻常的地方都会使读者犹豫不决,从而降低理解速度。 - James Kanze
3
答案错误将得到-1分。0==aa==0在性能和类型转换方面没有任何区别。此外,“在C语言中存在一些细微差别,对于不支持bool类型的C版本,通常会将bool作为整数处理”是毫无意义的。所有版本的C语言都将bool类型视为整数,并且C语言中的所有运算符都会将bool类型提升为整数(C++不同)。在C语言中不存在将任何内容隐式转换为bool类型的转换。 - Lundin
显示剩余7条评论

2

我在自己的代码中通常使用!a,只是因为它更短/更简单(在工作中,编码标准是0 == a)。

正如其他人所说,编译器会解决任何不同之处,并且两种情况下都会执行某种检查“值是否为零”,并从中形成布尔结果。最终结果应该非常相似。

上述内容假设a的类型是简单类型(整数/字符/布尔或浮点类型)。如果它恰好是一个结构体,那么!a版本将调用operator bool操作数,而a == 0可能会使用不同的方法,例如operator== - 注意这里的规则很复杂,一个对于另一个的影响可以是重大的。

总之,对于简单类型,这只是一种风格问题(在大多数编译器中!)。对于复杂类型,这取决于所涉及的类/结构体的设计。

如果性能真的很重要,请测量不同解决方案之间的差异,并查看哪个在该情况下表现最佳。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接