关于c ++:C逗号运算符的使用

Uses of C comma operator

本问题已经有最佳答案,请猛点这里访问。

您可以在for循环语句中看到它,但它在任何地方都是合法的语法。如果有的话,你在别处发现它有什么用途?


C语言(以及C++)在历史上是两种完全不同的编程风格的混合体,可以称之为"语句编程"和"表达式编程"。如您所知,每个过程编程语言通常都支持诸如排序和分支之类的基本构造(参见结构化编程)。这些基本构造在C/C++语言中有两种形式:一种用于语句编程,另一种用于表达式编程。

例如,当您以语句的形式编写程序时,可能会使用由;分隔的语句序列。当您想进行一些分支时,可以使用if语句。您还可以使用循环和其他类型的控制转移语句。

在表达式编程中,您也可以使用相同的构造。这正是,运算符发挥作用的地方。运算符,只不过是C中顺序表达式的分隔符,即表达式编程中的运算符,与语句编程中的运算符;具有相同的作用。表达式编程中的分支是通过?:操作符实现的,或者通过&&||操作符的短路评估特性实现的。(表达式编程没有循环。为了用递归替换它们,您必须应用语句编程。)

例如,以下代码

1
2
3
4
5
6
7
8
a = rand();
++a;
b = rand();
c = a + b / 2;
if (a < c - 5)
  d = a;
else
  d = b;

这是传统语句编程的一个例子,可以用表达式编程重新编写为

1
a = rand(), ++a, b = rand(), c = a + b / 2, a < c - 5 ? d = a : d = b;

或作为

1
a = rand(), ++a, b = rand(), c = a + b / 2, d = a < c - 5 ? a : b;

1
d = (a = rand(), ++a, b = rand(), c = a + b / 2, a < c - 5 ? a : b);

1
a = rand(), ++a, b = rand(), c = a + b / 2, (a < c - 5 && (d = a, 1)) || (d = b);

不用说,在实践中,语句编程通常会产生更多可读的C/C++代码,所以我们通常使用表达式编程非常精确和有限的量。但在很多情况下,它都很方便。可接受与不可接受之间的界限在很大程度上取决于个人偏好以及识别和阅读既定习语的能力。

作为补充说明:语言的设计显然是针对语句而定制的。语句可以自由调用表达式,但表达式不能调用语句(除了调用预定义的函数)。这种情况在GCC编译器中以一种相当有趣的方式发生了变化,它支持所谓的"语句表达式"作为扩展(在标准C中对称于"表达式语句")。语句表达式"允许用户直接将基于语句的代码插入表达式,就像他们可以将基于表达式的代码插入标准C中的语句一样。

另一个补充说明:在C++语言中,基于函数的程序设计起着重要的作用,可以被看作是另一种形式的表达式编程。根据当前C++设计的发展趋势,在许多情况下,它可能被认为比传统的语句编程更可取。


我认为一般来说,C逗号不是一种好的使用方式,因为它很容易被遗漏——要么是别人试图阅读/理解/修复您的代码,要么是您自己一个月的时间。当然,在变量声明和for循环之外,它是惯用的。

例如,您可以使用它将多个语句打包成一个三元运算符(?:),阿拉巴马州:

1
int x = some_bool ? printf("WTF"), 5 : fprintf(stderr,"No, really, WTF"), 117;

但是我的上帝,为什么????(我见过它在实际代码中以这种方式使用,但不幸的是,没有访问权来显示它)


我见过它在宏中使用,在宏中,宏假装是一个函数,想要返回一个值,但需要先做一些其他工作。它总是很难看,而且经常看起来像一个危险的黑客。

简化示例:

1
#define SomeMacro(A) ( DoWork(A), Permute(A) )

这里,EDOCX1[0]返回permute(a)的结果,并将其赋给"b"。


C++中两个杀手逗号运算符特性

a)从流中读取直到遇到特定的字符串(有助于保持代码干燥):

1
2
3
 while (cin >> str, str !="STOP") {
   //process str
 }

b)在构造函数初始值设定项中编写复杂代码:

1
2
3
class X : public A {
  X() : A( (global_function(), global_result) ) {};
};


我必须使用逗号调试互斥锁,以便在锁开始等待之前放置消息。

我只能将派生锁构造函数主体中的日志消息放入基类构造函数的参数中,使用初始化列表中的:base class((log("message"),actual_arg))。注意附加的括号。

以下是这些类的摘录:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
class NamedMutex : public boost::timed_mutex
{
public:
    ...

private:
    std::string name_ ;
};

void log( NamedMutex & ref__ , std::string const& name__ )
{
    LOG( name__ <<" waits for" << ref__.name_ );
}

class NamedUniqueLock : public boost::unique_lock< NamedMutex >
{
public:

    NamedUniqueLock::NamedUniqueLock(
        NamedMutex & ref__ ,
        std::string const& name__ ,
        size_t const& nbmilliseconds )
    :
        boost::unique_lock< NamedMutex >( ( log( ref__ , name__ ) , ref__ ) ,
            boost::get_system_time() + boost::posix_time::milliseconds( nbmilliseconds ) ),
            ref_( ref__ ),
            name_( name__ )
    {
    }

  ....

};

boost赋值库是一个很好的例子,它以一种有用的、可读的方式重载逗号运算符。例如:

1
2
3
4
using namespace boost::assign;

vector<int> v;
v += 1,2,3,4,5,6,7,8,9;


根据C标准:

The left operand of a comma operator is evaluated as a void expression; there is a sequence point after its evaluation. Then the right operand is evaluated; the result has its type and value. (A comma operator does not yield an lvalue.)) If an attempt is made to modify the result of a comma operator or to access it after the next sequence point, the behavior is undefined.

简而言之,它允许您指定多个表达式,其中C只需要一个表达式。但实际上,它主要用于for循环。

注意:

1
int a, b, c;

不是逗号运算符,而是声明符列表。


你可以重载它(只要这个问题有一个"C++"标签)。我见过一些代码,其中重载的逗号用于生成矩阵。或者向量,我记不清了。不是很漂亮吗(虽然有点混乱):

myvector foo=2,3,4,5,6;


它有时用于宏,例如调试如下宏:

1
2
#define malloc(size) (printf("malloc(%d)
", (int)(size)), malloc((size)))

(但看看这可怕的失败,真正的失败是你的,因为当你做得太过分的时候会发生什么。)

但是,除非您真的需要它,或者您确信它使代码更易于阅读和维护,否则我建议不要使用逗号运算符。


在for循环之外,即使存在可以散发出代码气味的气味,我认为唯一适合逗号运算符的地方是作为删除的一部分:

1
 delete p, p = 0;

唯一的替代值是,如果此操作位于两行上,则只能意外地复制/粘贴此操作的一半。

我也喜欢它,因为如果你是出于习惯,你永远不会忘记零任务。(当然,为什么p不在某个类型的auto-ptr、smart-ptr、shared-ptr等包装中是另一个问题。)


给定@nicolas goy从标准中引用的话,那么听起来您可以为循环编写一个行程序,比如:

1
2
3
int a, b, c;
for(a = 0, b = 10; c += 2*a+b, a <= b; a++, b--);
printf("%d", c);

但是上帝啊,伙计,你真的想让你的C代码更模糊吗?


一般来说,我避免使用逗号运算符,因为它只会降低代码的可读性。在几乎所有情况下,只做两个陈述会更简单和清楚。像:

1
foo=bar*2, plugh=hoo+7;

没有明显的优势:

1
2
foo=bar*2;
plugh=hoo+7;

除了循环之外的一个地方,我在if/else结构中使用了它,比如:

1
2
3
4
if (a==1)
... do something ...
else if (function_with_side_effects_including_setting_b(), b==2)
... do something that relies on the side effects ...

您可以将函数放在if之前,但如果函数运行时间较长,则可能希望在不需要时避免执行该函数,并且如果函数不应该执行,除非a!=1,那么这不是一个选项。另一种选择是将if嵌套在一个额外的层中。这就是我通常做的,因为上面的代码有点神秘。但我不时地用逗号来做,因为嵌套也是神秘的。


ASSERT宏中添加一些注释非常有用:

1
ASSERT(("This value must be true.", x));

由于大多数断言样式的宏将输出其参数的整个文本,因此这会在断言中添加额外的有用信息。


对于我来说,在C语言中使用逗号的一个真正有用的例子是使用它们有条件地执行某些操作。

1
  if (something) dothis(), dothat(), x++;

这相当于

1
  if (something) { dothis(); dothat(); x++; }

这不是"少打字",只是有时看起来很清楚。

同样,循环也是这样的:

1
while(true) x++, y += 5;

当然,只有当循环的条件部分或可执行部分非常小时,这两个部分才有用,即两个三操作。


我经常使用它在一些cpp文件中运行静态初始值设定项函数,以避免经典的单例初始化出现延迟问题:

1
2
3
4
5
6
7
8
9
10
11
12
13
void* s_static_pointer = 0;

void init() {
    configureLib();
    s_static_pointer = calculateFancyStuff(x,y,z);
    regptr(s_static_pointer);
}

bool s_init = init(), true; // just run init() before anything else

Foo::Foo() {
  s_static_pointer->doStuff(); // works properly
}


我唯一一次看到在for循环之外使用的,运算符是在三元语句中执行赋值。那是很久以前的事了,所以我记不清确切的说法,但它是这样的:

1
int ans = isRunning() ? total += 10, newAnswer(total) : 0;

显然,没有一个理智的人会写这样的代码,但作者是一个邪恶的天才,他根据生成的汇编程序代码而不是可读性来构造C语句。例如,他有时使用循环而不是if语句,因为他更喜欢它生成的汇编程序。

他的代码非常快,但是无法维护,我很高兴我不再需要使用它了。


这对于"编码高尔夫"来说非常方便:

代码高尔夫:打方块

if(i>0)t=i,i=0;中的,保存两个字符。


QEMU有一些代码在for循环的条件部分使用逗号运算符(请参见qemu queue.h中的qtailq_foreach_safe)。他们所做的归结为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include <stdio.h>

int main( int argc, char* argv[] ){
  int x = 0, y = 0;

  for( x = 0; x < 3 && (y = x+1,1); x = y ){
    printf("%d, %d
"
, x, y );
  }

  printf("
%d, %d

"
, x, y );

  for( x = 0, y = x+1; x < 3; x = y, y = x+1 ){
    printf("%d, %d
"
, x, y );
  }

  printf("
%d, %d
"
, x, y );
  return 0;
}

…具有以下输出:

1
2
3
4
5
6
7
8
9
10
11
0, 1
1, 2
2, 3

3, 3

0, 1
1, 2
2, 3

3, 4

此循环的第一个版本具有以下效果:

  • 它避免执行两个分配,因此减少了代码不同步的机会
  • 因为它使用了&&,所以在最后一次迭代之后不会对赋值进行评估。
  • 由于未对分配进行评估,因此在队列结束时,它不会尝试取消引用队列中的下一个元素(在qemu的代码中,而不是上面的代码中)。
  • 在循环中,您可以访问当前和下一个元素

在数组初始化中找到它:

在C语言中,如果我使用()来初始化二维数组而不是,会发生什么?

初始化数组a[][]时:

1
int a[2][5]={(8,9,7,67,11),(7,8,9,199,89)};

然后显示数组元素。

我得到:

1
2
11 89 0 0 0
0 0 0 0 0


我将它用于宏"将任何类型的值赋给char*指向的输出缓冲区,然后将指针增加所需的字节数",如下所示:

1
#define ASSIGN_INCR(p, val, type)  ((*((type) *)(p) = (val)), (p) += sizeof(type))

使用逗号运算符意味着宏可以在表达式中使用,也可以根据需要用作语句:

1
2
3
4
5
6
if (need_to_output_short)
    ASSIGN_INCR(ptr, short_value, short);

latest_pos = ASSIGN_INCR(ptr, int_value, int);

send_buff(outbuff, (int)(ASSIGN_INCR(ptr, last_value, int) - outbuff));

它减少了一些重复的打字,但你必须小心,不要让它变得太不可读。

请看我对这个答案的超长版本。