逗号运算符

一个二元运算符

在C和C ++编程语言中,逗号运算符,)是一个二元运算符,使用形式如a, b。它计算其第一个操作数并丢弃结果,然后计算第二个操作数并返回。这些计算之间有一个顺序点

逗号运算符的用途不同于逗号在函数调用和定义,变量声明,枚举声明以及类似结构中的使用,逗号在这些例子中的作用是作为分隔符英语Delimiter

这是“示例”一节的测试代码,为了使其能在一个文件里编译进行了部分修改。

句法

 
这是代码的运行结果,与原文中结果相符。

逗号运算符在C/C++中作为顺序点的显式标记,同时具有最低的优先级[1]

示例

在这些例子中,第二组和第三组之间的行为不同是由于逗号运算符的优先级低于赋值运算。最后一个示例与其他例子不同,因为在函数在返回前必须对返回的表达式进行完全求值。

/** 
 * 逗号在此行中充当分隔符,而不是运算符。
 * 结果:a = 1,b = 2,c = 3,i = 0 
 */ 
int a=1, b=2, c=3, i=0;

/** 
 * 将b的值赋给i。
 * 逗号在第一行中充当分隔符,在第二行中充当运算符。
 * 结果:a = 1,b = 2,c = 3,i = 2 
 */ 
int a=1, b=2, c=3;              
int i = (a, b);           
                      
/** 
 * 将a的值赋给i。
 * 逗号在第一行中充当分隔符。
 * 在第二行中作为运算符。而逗号运算符的优先级最低,所以等效于:int(i = a),b; 
 * 也可以这样理解:在第二行中逗号作为分隔符,使"int i = a"和"b"分离。
 * 第二行的大括号可避免在同一作用域中声明变量
 * 结果:a = 1,b = 2,c = 3,i = 1 
 */ 
int a=1, b=2, c=3;                                
{ int i = a, b; }

/** 
* 将a的值增加2,然后将a + b的值分配给i。
* 逗号在第一行中充当分隔符,在第二行中充当运算符。
* 结果:a = 3,b = 2,c = 3,i = 5 
*/ 
int a=1, b=2, c=3;
int i = (a += 2, a + b);
          
/** 
* 将a的值增加2,然后将a的值存储到i,并丢弃a+b的值。
* 等效于:(i =(a + = 2)),a + b; 
* 逗号在第一行中充当分隔符,在第三行中充当运算符。
* 结果:a = 3,b = 2,c = 3,i = 3 
*/ 
int a=1, b=2, c=3;
int i;
i = a += 2, a + b;

/**
 * 同第三组。
 *  结果: a=1, b=2, c=3, i=1
 */
int a=1, b=2, c=3;
{ int i = a, b, c; }

/** 
* 逗号在第一行中充当分隔符,在第二行中充当运算符。
* 将c的值赋给i,丢弃未使用的a和b值。
* 结果:a = 1,b = 2,c = 3,i = 3 
*/ 
int a=1, b=2, c=3;
int i = (a, b, c);

/**
 * 返回6,而不是4,因为关键字return之后的逗号运算符序列点被认为是单个表达式。
 */
return a=4, b=5, c=6;

/**
 * 同理,返回3
 */
return 1, 2, 3;

/**
 * 同上,返回3。但因为return不是一个函数而是一个关键字,因此1的括号没有任何实际作用。
 */
return(1), 2, 3;

用法

逗号运算符具有相对有限的用处。因为它会丢弃其第一个操作数,所以通常仅在第一个操作数具有的副作用必须在第二个操作数之前进行的情况下才有用。此外,由于它很少在特定的习惯用法之外使用,并且很容易与其他逗号或分号混淆,因此它可能会造成混淆并且容易出错。但是,在某些情况下通常会使用它,特别是在for循环和SFINAE中。[2] 对于可能没有完整调试功能的嵌入式系统,可以将逗号运算符与宏结合使用,以实现无缝覆盖函数调用,从而在函数调用之前插入代码。

循环

最常见的用法是允许使用多个赋值语句而不使用块语句,主要用在for循环的初始化和增量表达式中。这是逗号运算符在基础C编程中唯一的惯用用法。在以下示例中,循环中第一部分赋值的顺序很重要:

void rev(char *s, size_t len)
{
    char *first;
    for (first = s, s += len; s >= first; --s) {
        putchar(*s);
    }
}

其他语言中可以使用另一种方法来解决这个问题:并行赋值,它允许在单个语句中进行多个赋值。它也使用逗号,但语法和语义不同。一个例子:Go语言的for循环。[3]

在循环体内部通常也会使用逗号运算符,比如在教学用程序和商用程序的循环末尾,通常会进行如下示例第二行的操作以更新变量的值;这也可以用第一行所示形式来实现:

++p, ++q;
++p; ++q;

事实上,上面第一行中的操作有严格的顺序要求:必须先完成++p再完成++q,而在使用类似第二行的形式是则没有这个要求——现代电脑大多数在遇到类似代码时会使用并行计算的方式。在某些特殊情况下这种差异可能会导致一些问题(如与线程进程有关的代码)。

逗号运算符可以在预处理器宏中使用,以在单个表达式内执行多个操作。

一种常见用法是在断言失败时显示自定义错误消息。这是通过将带括号的表达式列表传递给assert宏来完成的,其中第一个表达式是自定义的错误字符串,第二个表达式是断言条件。当断言失败时,assert会逐字输出所提供的全部参数。以下是一个示例:


#include <stdio.h>
#include <assert.h>

int main ( void )
{
    int i;
    for (i=0; i<=9; i++)
    {
        assert( ( "i is too big!", i <= 4 ) );
        printf("i = %i\n", i);
    }
    return 0;
}

输出:

i = 0
i = 1
i = 2
i = 3
i = 4
assert: assert.c:6: test_assert: Assertion `( "i is too big!", i <= 4 )' failed.
Aborted

这种行为是由于assert的定义实现的。下面是GNU的assert.h节选,其中包括了在C++中assert宏的定义:

...

# if defined __cplusplus
#  define assert(expr)							\
     (static_cast <bool> (expr)						\
      ? void (0)							\
      : __assert_fail (#expr, __FILE__, __LINE__, __ASSERT_FUNCTION))

...

可以看到,assert宏的参数被包裹在括号中,随后转换为bool形式,若判断失败则调用__assert_fail函数。正常情况下,将一个字符串转换为bool会出错[4],然而在这里,示例程序使用了逗号运算符的性质,使得在第三行执行时抛弃第一个值(即字符串),只测试第二个值;而在第5行中,宏参数expr被完整地传递到__assert_fail函数中,因此实现了自定义错误资讯的功能。另外,这里使用了一些预处理器的特殊用法。

断言通常在生产环境中被禁用,因此只应该在调试时使用。

判断语句

逗号可以在循环语句判断语句(if,while,do while或for)内使用,以辅助计算,尤其是在调用函数和使用函数返回值时。变量具有块作用域::

if (y = f(x), y > x) { 
    ... // statements involving x and y
}

//等价于:
y = f(x);
if (y > x ) {
    ...
}

Go语言中的if语句存在类似的用法,然而讨论它偏离了本文的主题。关于其更多资讯见参考资料[5]

复合返回值

逗号可以在return语句中使用,以使结构更加紧凑。表明两个操作是一个整体。但通常不建议这么做。因为任何一个这种操作都可以转换为几个更加清晰的结构。如下面这个例子所示:

if (failure)
    return (errno = EINVAL, -1);

也可以写成:

if (failure) {
    errno = EINVAL;
    return -1;
}

如果写成第一种形式,很有可能会被认为是返回了一个“元组”或是被认为返回一组数据,没有第二种方案清晰。

代替块语句

为了简便书写,可以使用逗号代替块语句。

if (x == 1) y = 2, z = 3;
if (x == 1)
    y = 2, z = 3;

使用块语句的版本:

if (x == 1) {y = 2; z = 3;}
if (x == 1) {
    y = 2; z = 3;
}

其它语言

OCamlRuby中,分号(“;”)和这里的逗号用处相同。JavaScript[6] 和Perl [7]中的逗号 和 C / C ++中的作用相同。在Java中,逗号是分隔符,用于在各种上下文中隔开列表中的元素[8]。它不是运算符,不会对任何数据求值[9]

另请参见

参考资料

  1. ^ ISO/IEC 9899:2018 (PDF). [2020-06-10]. (原始内容 (PDF)存档于2020-07-22). 
  2. ^ SFINAE - cppreference.com. en.cppreference.com. [2020-07-12]. (原始内容存档于2021-05-06). 
  3. ^ Effective Go页面存档备份,存于互联网档案馆): for页面存档备份,存于互联网档案馆), "Finally, Go has no comma operator and ++ and -- are statements not expressions. Thus if you want to run multiple variables in a for you should use parallel assignment (although that precludes ++ and --)."
  4. ^ ISO/IEC 14882:2017. [2020-07-12]. (原始内容存档于2017-12-09). 
  5. ^ The Go Programming Language Specification - The Go Programming Language. golang.org. [2020-07-12]. (原始内容存档于2021-05-13). 
  6. ^ Comma operator - JavaScript | MDN. web.archive.org. 2014-07-12 [2020-07-12]. 原始内容存档于2014-07-12. 
  7. ^ perlop - perldoc.perl.org. perldoc.perl.org. [2020-07-12]. (原始内容存档于2020-07-11). 
  8. ^ Chapter 2. Grammars. web.archive.org. 2019-07-22 [2020-07-12]. 原始内容存档于2019-07-22. 
  9. ^ Is comma (,) operator or separator in Java?. Stack Overflow. [2020-07-12]. (原始内容存档于2019-04-10). 

参考书目

  • Ramajaran, V., Computer Programming in C, New Delhi: Prentice Hall of India, 1994 
  • Dixit, J.B, Fundamentals of computers and programming in C, New Delhi: Laxmi Publications, 2005 
  • Kernighan, Brian W.; Ritchie, Dennis M., The C Programming Language 2nd, Englewood Cliffs, NJ: Prentice Hall, 1988 

外部链接