编译器如何知道函数调用中的逗号不是逗号运算符?

38
考虑函数调用(调用 int sum(int, int) 函数)。
printf("%d", sum(a,b));

编译器如何确定函数调用中使用的 , 不是逗号运算符? 注意: 我并不想在函数调用中使用逗号运算符,我只是想知道编译器如何知道它不是逗号运算符。

2
你在说的是两个逗号中的哪一个... - Sazzadur Rahaman
4
为什么人们要投票关闭这个?! - haccks
22
不同意此问题被视为离题。该问题询问有关如何解释某个语法规则的微妙细节,可以通过引用相关的标准语言来得出确定性的答案。在此情况下,“努力解决问题”的努力并不适用。理解或查找标准语言并非一项轻松的任务。 - Alok Save
1
有两个函数调用,一个是 sum,另一个是 printf - Keith Thompson
3
我曾经写了一些 C 代码,因为通过指针做整型除法导致代码行为异常。也就是说,表达式是 a/*b。添加一些空格后问题得到解决:a / *b。请注意,翻译时需要使内容更加通俗易懂,但不要改变原来的意思。 - Stewart
显示剩余5条评论
6个回答

49

看一下C语言的语法。它在标准的附录A中列出了完整的语法。它的工作原理是,你可以逐个步骤地浏览C程序中的每个标记,并将其与语法中的下一个项匹配起来。在每个步骤中,你只有有限的选项,因此任何给定字符的解释都取决于它所出现的上下文环境。在语法的每个规则内部,每行都提供一个有效的程序匹配替代方案。

具体来说,如果你寻找parameter-list,你会发现它包含一个明确的逗号。因此,每当编译器的C解析器处于“parameter-list”模式时,它找到的逗号将被理解为参数分隔符,而不是逗号运算符。括号也是如此(它们也可以出现在表达式中)。

这能够实现的原因是parameter-list规则小心地使用了assignment-expression规则,而不仅仅是普通的expression规则。一个expression可以包含逗号,而一个assignment-expression不能。如果不是这样,语法将是模糊的,编译器在遇到参数列表中的逗号时就不知道该怎么做了。

然而,例如不是函数定义/调用中的开括号,或者ifwhilefor语句中的开括号,将被解释为表达式的一部分(因为没有其他选项,但仅当在那一点上表达式的开头是一个有效的选择时),然后在括号内部,expression语法规则将应用,并且这允许使用逗号运算符。


4
我曾经忘记了这个名称是一个技术术语。我只是指出任何特定的标记只能在它所出现的上下文中被理解。换句话说,我使用“context sensitive”作为形容词而非名词。但是,我怀疑唯一感到困惑的人是那些已经知道答案的人! - ams
这是一个很好的回答,但你还应该提到逗号之间的东西assignment-expression非终结符,而不是expression非终结符(如Jens的回答中所讨论的),因此禁止在parameter-list的顶层使用逗号运算符。如果标准只做了你描述的事情而没有做这个,整个语法将是有歧义的。 - zwol
@EricLippert:我认为说C语言有一个无上下文文法是没有意义的。如果你这样做,那么你也可以声称C++有一个CFG(因为,就像在C的情况下一样,它是模棱两可的,并需要一个语义通行证来拒绝无效的程序)。如果你想要非常严格,那么你也可以声称大多数编程语言没有CFGs,因为它们都需要在程序被视为有效之前进行声明和定义,这不是无上下文的。这两种定义都不是非常有用,因为它将大多数语言放在同一类别中。(续) - user541686
@EricLippert:(续)…从实际角度来看(理论方面可能不太一样),我认为有一个有用的定义即:当且仅当C具有可以无歧义地解析所有有效C程序的CFG*(假设没有未声明的标识符)时,C是无上下文语言。但是,在这种情况下,由于经典的T * T;歧义需要知道“T”是什么而不仅仅是是否声明了它,因此C不是上下文无关的(因此没有CFG)。因此,我认为说C是上下文无关的并没有意义。 - user541686
@Mehrdad:我理解你的观点,但是你使用的“语法”比通常的意义更广泛。著名的句子“无色的绿色思想狂暴地睡眠。”在英语中是语法正确的,但是毫无意义,而句子“Bob Smith是英格兰国王。”是语法正确但是错误的。C语言的语法并不打算成为一个一站式的工具来确定什么是合法的C程序,就像英语的语法决定什么是真实陈述一样。 - Eric Lippert
显示剩余2条评论

26

来自C99 6.5.17:

As indicated by the syntax, the comma operator (as described in this subclause) cannot appear in contexts where a comma is used to separate items in a list (such as arguments to functions or lists of initializers). On the other hand, it can be used within a parenthesized expression or within the second expression of a conditional operator in such contexts. In the function call

f(a, (t=3, t+2), c)

the function has three arguments, the second of which has the value 5.

另一个类似的例子是数组或结构体的初始化器列表:
int array[5] = {1, 2};
struct Foo bar = {1, 2};

如果要使用逗号运算符作为函数参数,应该像这样使用:

sum((a,b))

当然,这段代码无法编译。

5
正确,但不是问题的答案。 - bmargulies
@Yu:我不想使用逗号运算符。我只是想知道编译器如何知道它不是逗号运算符! - haccks
@sasha.sochka请看原帖评论。他想知道解析器的工作原理,而不是如何在函数调用中使用逗号。 - bmargulies
1
@haccks 懂了,我修改了我的话。在函数参数中使用逗号运算符实际上并没有太多实用性,但是知道如何使用它仍然很有趣,所以我会保留这部分内容。 - Yu Hao
@YuHao; 谢谢老铁!至少还有你的帮忙。还要感谢你对我的帖子进行编辑。 - haccks

19

问题出在C语言的语法结构上。虽然其他人似乎喜欢引用这个例子,但真正的问题在于C99标准中函数调用的短语结构语法。是的,一个函数调用由应用于后缀表达式(例如标识符)的()运算符组成:

 6.5.2 postfix-expression:
       ...
       postfix-expression ( argument-expression-list_opt )

与...一起

argument-expression-list:
       assignment-expression
       argument-expression-list , assignment-expression    <-- arglist comma

expression:
       assignment-expression
       expression , assignment-expression                  <-- comma operator

逗号运算符只能出现在表达式中,即在语法中往下。因此,编译器将函数参数列表中的逗号视为分隔“赋值表达式”的逗号,而非分隔“表达式”的逗号。


1
@haccks:一个条件表达式或一元表达式,后跟一个赋值运算符,再后跟一个赋值表达式。 - Jens
1
我没有理解你的意思,请详细说明。感谢您的配合。 - haccks
4
稍微解释一下@Jens的回答:让我们改变一下问题并简化它。不是“表达式”,而是高尔夫球(涂成黄色),还有可以打开并且在里面粘东西的大清晰塑料球:东西。语法规定,您可以拥有黄色高尔夫球,它们会自动分开。或者,您可以提供一个透明的球只要您使用了两半。透明的球作为一个整体,不能打开和分离。因此:f((a,b),g)有一个“透明球”(a,b)和一个“黄球”g,因此正好有两个球,呃,参数。 - torek
2
我已经没有足够的注释空间了,所以继续回到真正的C语法:括号允许您转义到一个“完整的”表达式,其中逗号是逗号表达式部分。但是,在您有一个“额外”的开放括号之前,您仍处于这个更有限的“赋值表达式”子语法中(就像“黄色高尔夫球”想法一样),在这种情况下,逗号根本不被允许。如果解析器在此上下文中遇到逗号,则必须停止并完成赋值表达式。这是因为()“结束”:括号结束了完整的表达式上下文。 - torek
2
嗯,我没有其他自然语言来表达这个。考虑使用 {}, []()。它们“匹配”:如果你写 a[3},显然是错误的。如果你写 a[(3],仍然很明显是错误的。( 只有与匹配的 ) 结束才能完成。这样“关闭”整个序列,使其清楚地显示哪些内容彼此对应。 - torek
显示剩余2条评论

11

现有的答案说“因为C语言规范规定逗号是列表分隔符而不是运算符”。

然而,你的问题是在问“编译器如何知道…”,这与之前的解释完全不同: 这实际上和编译器如何知道printf("Hello, world\n");中的逗号不是逗号运算符没有什么区别:编译器通过上下文环境来“知道”,也就是说逗号出现的位置以及之前的代码。

C语言可以用巴科斯-瑙尔范式(BNF)进行描述 - 基本上是一组用于扫描输入文件的编译器语法分析器使用的规则。 C语言的BNF将区分语言中不同的逗号出现情况。

关于编译器如何工作以及如何编写编译器,有许多优秀的资源可以参考,例如这里


这个问题是关于C语言的。在C++的上下文中谈论“解析器”是不必要的复杂化。 - anatolyg
@anatolyg - 已修复。我现在一定是被C++搞糊涂了。 - Roddy

6

C99标准草案指出:

如语法所示,逗号运算符(如本子句所述)不能出现在使用逗号分隔列表项的上下文中(例如函数参数或初始化器列表)。而另一方面,在括号表达式或条件运算符的第二个表达式中可以在这些上下文中使用它。在函数调用 f(a, (t=3, t+2), c) 中,该函数有三个参数,其中第二个参数的值为5。

换句话说,“因为”。


6
我的孩子们不会接受那个答案,为什么OP要接受呢?但那就是原因,因为模棱两可的情况是被禁止的。 - Grady Player

1
这个问题有多个方面。其中一个方面是定义中说的。那么,编译器如何知道逗号所处的上下文呢?这是解析器的工作。对于C语言,特别是,该语言可以由LR(1)解析器解析(http://en.wikipedia.org/wiki/Canonical_LR_parser)。
解析器生成一系列表格,构成解析器可能的状态。只有某些符号在特定状态下才有效,并且这些符号在不同状态下可能具有不同的含义。解析器通过前面的符号知道自己正在解析函数。因此,它知道可能的状态不包括逗号运算符。
我这里非常笼统,但你可以在Wiki上阅读所有细节。

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