在while循环条件中赋值

18

我在维基百科上发现了这段代码。

#include <stdio.h>

int main(void)
{
  int c;

  while (c = getchar(), c != EOF && c != 'x')
  {
    switch (c)
      {
      case '\n':
      case '\r':
        printf ("Newline\n");
        break;
      default:
        printf ("%c",c);
      }
  }
  return 0;
}

我对在while循环中用作条件的表达式很好奇:

while (c = getchar(), c != EOF && c != 'x')

它的作用非常明显,但我以前从未见过这种结构。这是专门针对 while 循环的吗?如果不是,解析器/编译器如何确定逗号分隔表达式的哪一侧返回布尔值给 while 循环?


不影响你的问题,只是标题而已,但是:在 while 循环中 c 被赋值了,但没有初始化。 - Steve Jessop
5个回答

23

逗号运算符是一种二元运算符,它评估其第一个操作数并丢弃结果,然后评估第二个操作数并返回该值。

它还是一个“序列点”,这意味着所有副作用将在执行代码的下一部分之前计算。


3
补充一下,因为这可能不是很明显——在这个上下文中,“=”运算符更新变量c的值被视为“副作用”。 - caf
正确 - 因为逗号运算符具有最低的优先级。 - Paul Dixon

12
逗号运算符在你理解之前是一种奇怪的东西,而且它不仅限于while
该表达式为:
exp1, exp2

评估exp1然后评估exp2并返回exp2

你经常看到它,尽管你可能意识不到:

for (i = j = 0; i < 100; i++, j += 2)
你实际上并没有使用从"i ++,j + = 2"返回的值,但它仍然存在。逗号运算符评估两个位以修改ij

你几乎可以在任何普通表达式可以使用的地方使用它(例如,在函数调用中的逗号不是逗号运算符),如果你喜欢编写紧凑的源代码,它非常有用。在这种情况下,它是允许像这样的东西的一部分:

while ((c= getchar()) != EOF) {...}
i = j = k = 0;

等等。

针对您的具体示例:

while (c = getchar(), c != EOF && c != 'x')

以下情况发生:

  • 完全执行c = getchar()(逗号运算符是一个序列点)。
  • 执行c != EOF && c != 'x'
  • 逗号运算符丢弃第一个值(c),并“返回”第二个值。
  • while使用该返回值来控制循环。

如果我想在一个表达式中链接多个逗号运算符,是否可以不使用括号来实现? - Josip
是的,逗号运算符具有最低的优先级。 - caf
@Josip,我会小心一点,逗号运算符的优先级非常低,这应该是可以的,但你可能会发现自己得到意想不到的结果。虽然我认为你可以在任何允许表达式的地方使用它,但这并不一定意味着你应该这样做。 - paxdiablo

4
在许多语言中,逗号是一个运算符,总是返回第二个操作数的值。操作数从左到右顺序求值。
伪代码:
a = 10
print a = 7 + 8, a * 2

注意:print 被认为是一个不带参数的语句,因此后面的内容被视为单个表达式 a = 7 + 8, a * 2执行过程如下:
  • 第一行
    • 10 放入 a
  • 第二行
    • 计算 7 + 815
    • 将计算结果(15)放入 a
    • 计算 a * 230
    • 计算逗号运算符和操作数 1530
      • 始终返回第二个操作数的值(30
    • 打印计算结果(30

2

在其他答案的基础上,对于这段代码:

EXPRESSION_1 , EXPRESSION_2

首先计算EXPRESSION_1,然后有一个序列点,然后计算EXPRESSION_2,整个表达式的值为EXPRESSION_2的值。

操作顺序保证和序列点对于你引用的代码都很重要。它们一起意味着我们可以确信在测试变量c的值之前,getchar()函数被调用并且变量c的值已经完全更新了。


1

逗号是一个运算符。默认情况下,它返回右侧表达式的值。计算顺序保证先从左到右。

更新(回复Pax的评论):

就像大多数运算符一样,它可以被重载为用户定义类型:

#include <iostream>
#include <string>
using namespace std;

enum EntryType { Home, Cell, Address };

class AddressBookEntryReference {
public:
    AddressBookEntryReference(const string& name, const EntryType &entry)
        : Name(name), Entry(entry) { }
    string Name;
    EntryType Entry;
};

AddressBookEntryReference operator,(const string& name, const EntryType &type) {
    return AddressBookEntryReference(name, type);
}

class AddressBook {
    string test;
public:
    string& operator[](const AddressBookEntryReference item) {
        // return something based on item.Name and item.Entry.

        // just to test:
        test = item.Name;
        return test;
    }
};

int main() {
    // demo:
    AddressBook book;
    cout << book["Name", Cell]  // cool syntax! 
         << endl;
}

我对“默认情况下”这个短语很好奇 - 你并不是在暗示这种行为可以被配置,对吗? 我认为标准非常清楚地表明了它的方式,不允许任何偏差。 - paxdiablo
我的原始问题与C语言有关 - 你已经深入到了C++的领域。无论如何,重载的使用非常有趣。 - Josip
Josip:哦,我没有注意到C标签。我的原始答案适用于两者,但是我对Pax的评论的回复肯定只适用于C++。 - Mehrdad Afshari
抱歉,@Mehrdad,我没有考虑到C++的角度。 - paxdiablo

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