逗號運算子
在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庫中的工具,通過多載逗號運算子可以使填充容器變得更加輕鬆。