为啥在 C for 循环的条件中使用表达式而不是常量?

Posted

技术标签:

【中文标题】为啥在 C for 循环的条件中使用表达式而不是常量?【英文标题】:Why an expression instead of a constant, in a C for-loop's conditional?为什么在 C for 循环的条件中使用表达式而不是常量? 【发布时间】:2014-10-24 06:45:18 【问题描述】:

在很多编程比赛中我看到有人写这种for-loop

for(i = 0; i < (1 << 7); i++)

除非我遗漏了什么,否则是一样的

for(i = 0; i < 128; i++)

为什么使用(1 &lt;&lt; 7) 版本? 每次计算条件不是不必要的开销吗?

【问题讨论】:

C 有一个叫做“as-if 规则”的东西(嗯 - 不完全是;C++ 有这个,C 有相同的文本,但不叫它那个特定的名称):重要的是该程序产生与您的代码输出相同的输出。 (“输出”包括对 volatile 变量的访问和对库函数的调用)。除此之外,它可以为所欲为。如果您编写了一个程序来生成素数,编译器可以检测到这一点,并在可执行文件中硬编码一个素数列表。在您的代码中,所有编译器都会对 128 进行硬编码,而不是在运行时进行移位。 也许并不是所有的人都熟记 2^0 到 2^32 之间的 2 的幂。 (虽然,诚然,我认为程序员至少应该知道它们高达 2^16,以及一些“插值点”,如 2^20、2^24、2^30 和 2^32 本身......)跨度> @harrythomas:写128 会让它看起来像是一个来源不明的“魔法常数”。写(1 &lt;&lt; 7) 可能会立即清楚该常量代表什么以及它来自何处。最好这样做。 两个版本都不好。应该是for(i = 0; i &lt; NUMBER_OF_STEPS; i++)。不要使用幻数。 @LukaHorvat:然后写for(i = 0; i &lt; ONE_HUNDRED_TWENTY_EIGHT; i++)。我称之为魔法常数!。 【参考方案1】:

是的,它们在行为上是相同的。

那为什么人们使用 (1

我猜,他们用它来记录它是 2 的幂。

每次都计算条件一定是开销!我找不到这背后的原因!

并非如此,任何普通编译器都会将1 &lt;&lt; 7 替换为128,因此两个循环将具有相同的性能。

(C11, 6.6p2) “一个常量表达式可以在翻译期间而不是运行时进行计算,因此可以在常量可能存在的任何地方使用。”

【讨论】:

请注意,C 语言要求编译器能够在编译时评估常量表达式,因为它们可用于各种用途必须知道实际值的上下文,以确定违反约束(例如,负数组大小或无效位域宽度)或表达式类型(由于表达式是否为空指针常量取决于值和结果可以通过?: 运算符以非常强大的方式传播)。因此,没有充分的理由不在编译时全面评估所有常量表达式。 其实我以为是预处理器在编译时总是对常量表达式求值。因此,这保证了使用表达式的开销为零 - 只要它们比常量提供更多信息。 @Echelon:预处理器只能识别预处理器标记并执行字符串替换。除此之外,它对 C/C++ 语法和语义一无所知。 @YvesDaoust @Echelon 请注意,预处理器常量表达式是不同类的常量表达式。预处理器算术使用目标中的最大整数类型(intmax_tuintmax_t)完成。 #if 控制常量表达式(例如,在 #if 1 &lt;&lt; 7 == 128 中)需要在预处理器时进行评估(更具体地说,在翻译阶段 4)。 @Echelon:我刚刚看了一个中间预处理器输出(Visual Studio 编译器),源代码中没有替换任何常量。预处理器可以在不了解整个语言语法的情况下执行有效的文字常量替换吗?【参考方案2】:

让我们将这些选项中的每一个都翻译成简单的英语:

for(i = 0; i < (1 << 7); i++) // For every possible combination of 7 bits
for(i = 0; i < 128; i++)      // For every number between 0 and 127

两种情况下的运行时行为应该相同。

事实上,假设一个像样的编译器,即使是汇编代码也应该是相同的。

所以第一个选项本质上只是为了“发表声明”。

您也可以使用第二个选项并在上方添加评论。

【讨论】:

更好的说法是为i 取一个更合适的名称。 i 倾向于表示“我正在处理的索引”之类的东西,但程序员试图说“我正在处理的模式”。像bitCombo 这样的东西会在没有评论的情况下说清楚。不过,比赛往往强调简短而聪明,而不是清晰,所以也许他们坚持使用i 来保持神秘感。【参考方案3】:

1 &lt;&lt; 7 是一个常量表达式,编译器将其视为128,运行时没有开销。

没有循环体,很难说作者为什么要用它。可能它是一个循环迭代与 7 位相关的东西,但这只是我的猜测。

【讨论】:

谢谢..是的,我正在查看的 for 循环使用 7 位!谢谢! 是的。如果它有助于理解,我会这样做。我称他们为“咬人者”:)【参考方案4】:

那为什么人们使用 (1

它是一种文档形式,它不是一个幻数,而是2^7(2 的七次方),这对于编写代码的人来说都是有意义的。现代优化编译器应该为两个示例生成完全相同的代码,因此使用这种形式没有成本,并且添加上下文有好处。

使用godbolt,我们可以验证确实如此,至少对于gccclangicc的几个版本。使用带有副作用的简单示例来确保代码没有被完全优化掉:

#include <stdio.h>

void forLoopShift()

  for(int i = 0; i < (1 << 7); i++)
  
    printf("%d ", i ) ;
  


void forLoopNoShift()

  for(int i = 0; i < 128; i++)
  
        printf("%d ", i ) ;
  

对于代码的相关部分,我们可以看到它们都生成了以下see it live:

cmpl    $128, %ebx

我们拥有的是一个整型常量表达式,如 C11 标准草案 6.6常量表达式 中定义的那样:

整数常量表达式117) 应该是整数类型并且应该只有操作数 即整数常量、枚举常量、字符常量、sizeof 结果是整数常量的表达式,[...]

和:

常量表达式不应包含赋值、递增、递减、函数调用、 或逗号运算符,除非它们包含在不属于 评估。115)

我们可以看到在翻译过程中允许对常量表达式求值:

可以在翻译期间而不是运行时评估常量表达式,并且 因此,可以在常数可能存在的任何地方使用。

【讨论】:

@YvesDaoust 好吧,OP 确实说这是为了编程竞赛 那么很可能是 (1 【参考方案5】:

for(i = 0; i

for(i = 0; i

提供相同的性能,但开发人员可以在循环中使用 for(i = 0; i

for(int k = 0; k < 8; k++)

  for(int i = 0; i < (1 << k); i++)
   
    //your code
    


现在它处于内部循环上限,即 (1

【讨论】:

(1 &lt;&lt; k) 是一个loop invariant,因此编译器应该在每个外部循环迭代中只计算一次。实际上,它可能会执行induction variable elimination,将其转换为for (int j = 1; j &lt;= 128; j &lt;&lt;= 1) for (i = 0; i &lt; j; i++) ... 【参考方案6】:

编译器为这两种情况输出相同的代码。您可能希望根据上下文使用不同的形式。

    如果您想明确表示算法中的常量部分或设计选择,则可以使用 NUM_STEPSNUM_ELEMENTS_IN_NETWORK_PACKET。 或者你可以写128,明确是128,一个常数。 如果您参加比赛并且测试显示类似 “运行 2^7 次”,请写信 1 &lt;&lt; 7

或者,你可以炫耀你知道位操作!

在我看来,编程就像为两个人写一封信,编译器和必须阅读它的人。你的意思应该对双方都清楚。

【讨论】:

从来没有人能理解而不是计算机能理解的程序。程序应该始终对其他人(人)可读。无论如何,计算机总会理解它(当然,它是有效的语法)【参考方案7】:

它由预处理器评估,因为两个操作数都是常数。

但如果你要使用数字而不是位移,它不应该是 0x0100 吗?

【讨论】:

以上是关于为啥在 C for 循环的条件中使用表达式而不是常量?的主要内容,如果未能解决你的问题,请参考以下文章

golang for循环取值为啥不按顺序输出?

为啥我应该在循环中使用 foreach 而不是 for (int i=0; i<length; i++) ?

c语言for语句中的条件问题

C语言中for循环中条件表达式的小于和小于等于问题

为啥这段代码在 for 循环中只使用 x 而不是 x 和 y? [关闭]

Verilog循环语句