逗號運算符
在C和C ++編程語言中,逗號運算符( ,
)是一個二元運算符,使用形式如a, b
。它計算其第一個操作數並丟棄結果,然後計算第二個操作數並返回。這些計算之間有一個順序點。
逗號運算符的用途不同於逗號在函數調用和定義,變量聲明,枚舉聲明以及類似結構中的使用,逗號在這些例子中的作用是作為分隔符。
句法
逗號運算符在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;
}
其它語言
在OCaml和Ruby中,分號(「;」)和這裡的逗號用處相同。JavaScript[6] 和Perl [7]中的逗號 和 C / C ++中的作用相同。在Java中,逗號是分隔符,用於在各種上下文中隔開列表中的元素[8]。它不是運算符,不會對任何數據求值[9]。
另請參見
參考資料
- ^ ISO/IEC 9899:2018 (PDF). [2020-06-10]. (原始內容 (PDF)存檔於2020-07-22).
- ^ SFINAE - cppreference.com. en.cppreference.com. [2020-07-12]. (原始內容存檔於2021-05-06).
- ^ 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 --)."
- ^ ISO/IEC 14882:2017. [2020-07-12]. (原始內容存檔於2017-12-09).
- ^ The Go Programming Language Specification - The Go Programming Language. golang.org. [2020-07-12]. (原始內容存檔於2021-05-13).
- ^ Comma operator - JavaScript | MDN. web.archive.org. 2014-07-12 [2020-07-12]. 原始內容存檔於2014-07-12.
- ^ perlop - perldoc.perl.org. perldoc.perl.org. [2020-07-12]. (原始內容存檔於2020-07-11).
- ^ Chapter 2. Grammars. web.archive.org. 2019-07-22 [2020-07-12]. 原始內容存檔於2019-07-22.
- ^ 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
外部連結
- "Effect of using a comma instead of a semi-colon in C and C++ (頁面存檔備份,存於網際網路檔案館)", Stack Overflow
- Boost.Assignment (頁面存檔備份,存於網際網路檔案館) – Boost庫中的工具,通過重載逗號運算符可以使填充容器變得更加輕鬆。