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

57

在许多编程竞赛中,我看到人们写这种类型的for循环。

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

除非我漏掉了什么,否则这就相当于

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

为什么要使用版本(1 << 7)
每次计算条件不是多余的开销吗?


12
C语言有一个称为“似乎规则”的东西(确切的说是C++有这个规则,而C有等效的文本但没有用这个特定的名称):重要的是程序产生与你代码输出相同的结果。(“输出”包括对易失变量的访问和库函数的调用)。除此之外,编译器可以任意进行优化。如果你写了一个生成素数的程序,编译器可能会检测到并在可执行文件中硬编码一个素数列表。在你的代码中,所有的编译器都会直接硬编码128而不是在运行时进行移位。 - M.M
4
也许不是所有的人都能够熟记在2^0和2^32之间的2的幂次方。(尽管,可以承认,我认为程序员至少应该熟记2的幂次方达到2^16,并且还需知道类似于2^20、2^24、2^30以及2^32本身的“插值点”……) - Marco13
13
128会让它看起来像是一个不可解释的“魔法常数”。写(1 << 7)会让人立刻明白这个常数代表什么以及它来自哪里。最好采用这种方式。 - AnT stands with Russia
11
两个版本都不好。应该写成 for(i = 0; i < NUMBER_OF_STEPS; i++),不要使用魔法数字。 - glampert
5
@LukaHorvat: 那么就写成 for(i = 0; i < ONE_HUNDRED_TWENTY_EIGHT; i++)。我称之为 _魔法常量_!。 - rodrigo
显示剩余8条评论
7个回答

76

是的,它们在行为上是等价的。

那么人们为什么使用(1 << 7)版本?

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

每次计算条件肯定会带来额外的开销!我找不到背后的原因!

实际上不会有开销,任何正常的编译器都会将1 << 7替换为128,因此两个循环的性能相同。

(C11, 6.6p2)"常量表达式可以在编译时而非运行时进行评估,并且因此可能在任何可以使用常量的地方使用。"


20
请注意,C语言“要求”编译器能够在编译时评估常量表达式,因为它们可以在各种上下文中使用,在这些上下文中必须知道实际值,以确定约束违规(例如负数组大小或无效位域宽度)或表达式类型(由于表达式是否是空指针常量取决于值,并且结果可以通过?:运算符以非常强大的方式传播)。因此,在编译时完全评估所有常量表达式没有不好的理由。 - R.. GitHub STOP HELPING ICE
其实,我认为预处理器总是在编译时评估常量表达式。因此,只要它们比常量更具信息性,就保证在使用表达式时没有任何开销。 - Echelon
1
@Echelon:预处理器只能识别预处理器标记并执行字符串替换。除此之外,它对C/C++的语法和语义没有任何了解。 - user1196549
1
请注意,预处理器常量表达式是不同类别的常量表达式。预处理器算术运算使用目标平台上最大的整数类型 (intmax_tuintmax_t)。控制常量表达式的 #if 必须在预处理时(更具体地说是在第四个翻译阶段)进行评估(例如,在 #if 1 << 7 == 128 中)。@YvesDaoust @Echelon - ouah
1
@Echelon:我刚刚查看了一个中间预处理器输出(Visual Studio编译器),在源代码中没有任何常量被替换。一个预处理器能否在不知道整个语言语法的情况下执行有效的字面常量替换? - user1196549
显示剩余3条评论

31

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

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

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

实际上,假设编译器足够好,即使汇编代码也应该是相同的。

因此,第一种选项本质上只是用来“表明立场”的。

你完全可以使用第二个选项并在上面添加注释。


1
更好的语句应该是一个更合适的名称来代替 ii 倾向于表示“我正在处理的索引”,但程序员想要表达的是“我正在处理的模式”。像 bitCombo 这样的名称可以在没有注释的情况下清晰地表达意思。比赛往往强调简短和巧妙而不是清晰,所以也许他们坚持使用 i 来保持神秘感。 - Mirinth

19

1 << 7 是一个常量表达式,编译器会将其视为 128,运行时没有任何开销。

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


3
谢谢,是的,我看到的for循环使用了7位!谢谢! - harrythomas
2
是的。如果有助于理解,我会这样做。我称它们为“biterators” :) - Martin James

14

那么为什么人们使用 (1 << 7) 版本?

这是一种文档形式,不是一个神奇数字,而是对于编写代码的人来说有意义的2的7次方(two to the seventh power)。现代优化编译器应该为两个示例生成完全相同的代码,因此使用这种形式没有成本,并且增加了上下文信息。

使用 godbolt 我们可以验证这确实是事实,至少对于几个版本的gcc、clang和icc。使用一个具有副作用的简单示例以确保代码不会被完全优化掉:

#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 ) ;
  }
}

代码的相关部分显示它们都生成了以下内容 查看实时演示
cmpl    $128, %ebx

我们所拥有的是C11标准草案第6.6节“常量表达式”中定义的“整数常量表达式”,该部分规定:“整数常量表达式117)应具有整数类型,并且只能具有整数常量、枚举常量、字符常量、其结果为整数常量的sizeof表达式等操作数[...]”。此外,它还指出:“常量表达式不得包含赋值、递增、递减、函数调用或逗号运算符,除非它们包含在不被评估的子表达式中115)。” 我们可以看到,常量表达式允许在翻译期间进行评估: “常量表达式可以在翻译时评估,因此可以在任何可以使用常量的地方使用。”

@YvesDaoust 好的,楼主确实说了这是为了一个编程比赛。 - Shafik Yaghmour
1
那么它很可能是(1 << ((1 << (1 << 1)) - 1)) - 1的值,其中1是一个神奇数字。 - user1196549

5

对于循环for(i = 0; i < (1 << 7); i++)

对于循环for(i = 0; i < 128; i++)

在性能上没有区别,但是当开发者在循环中使用for(i = 0; i < (1 << 7); i++)时,可以获得巨大的优势。

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

}

现在它处于内部循环的上限即(1 << k)以2的幂变化的状态。但是只有当你的算法需要这个逻辑时才适用。


2
(1 << k)是一个循环不变式,因此编译器应该每个外部循环迭代只计算一次。实际上,它可能执行归纳变量消除,将其转换为for (int j = 1; j <= 128; j <<= 1) for (i = 0; i < j; i++) { ... } - zwol

3
编译器对这两种情况输出相同的代码。你可能想要使用不同的形式,具体取决于上下文。
  1. 当它是算法中的常量部分或设计选择时,您可以使用NUM_STEPSNUM_ELEMENTS_IN_NETWORK_PACKET以清晰地表达。
  2. 或者您可以写128来表明它是一个常量。
  3. 如果您参加比赛并且测试说类似于“运行2^7次”,则可以写1 << 7

或者,您可以炫耀自己知道位操作!

在我看来,编程就像为两个人编写信件,编译器和将要阅读它的人。您的意思应该对两者都清楚。


从来没有一份程序是只被人而不是计算机所理解的。一个程序应该始终以可读性为其他人编写。无论如何,计算机总是能够理解它(当然,前提是它是有效的语法)。 - hashier

-3

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

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


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