C逗号运算符的用途

93
你会在for循环语句中看到它的使用,但它在任何地方都是合法的语法。如果有的话,你还发现它还有其他用途吗?

21
我认为所有“逗号运算符”的“巧妙使用”都会降低代码的可读性。 - Kirill V. Lyadvinsky
2
我必须同意。逗号的过度使用会使您的C代码变得难以阅读。 - Obi
请参阅https://dev59.com/OnVD5IYBdhLWcg3wNIzc。 - Paul Stephenson
10
一个拥有13个赞和4个收藏的问题被认为不是一个问题而被考虑关闭,这是为什么? - jmucchiello
20个回答

105
C语言(以及C++)在历史上是两种完全不同的编程风格的混合体,可以称之为“语句编程”和“表达式编程”。正如您所知,每种过程式编程语言通常都支持诸如顺序分支(参见结构化编程)等基本结构。这些基本结构在C/C++语言中以两种形式存在:一种用于语句编程,另一种用于表达式编程。
例如,当您按照语句编程的方式编写程序时,您可能会使用由;分隔的一系列语句。当您想要进行某些分支时,您可以使用if语句。您还可以使用循环和其他类型的控制传输语句。
在表达式编程中,相同的结构也可用于您。这实际上就是,运算符发挥作用的地方。运算符,在C中只是一个连续表达式的分隔符,即在表达式编程中,运算符,的作用与语句编程中的;相同。表达式编程中的分支通过?:运算符和替代地,通过&&||运算符的短路评估属性来完成。(表达式编程没有循环。如果要用递归替换它们,您必须应用语句编程。)
例如,以下代码
a = rand();
++a;
b = rand();
c = a + b / 2;
if (a < c - 5)
  d = a;
else
  d = b;

这是传统的语句编程示例,可以通过表达式编程来重新编写

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

或者作为
a = rand(), ++a, b = rand(), c = a + b / 2, d = a < c - 5 ? a : b;

或者

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

或者

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

毋庸置疑,在实践中,语句编程通常会产生更易读的C/C++代码,因此我们通常只在非常有限的情况下使用表达式编程。但在许多情况下,表达式编程也很方便。接受何种程度的表达式编程主要取决于个人偏好以及识别和阅读已经建立的惯用语的能力。
另外需要注意的是:语言本身的设计显然是针对语句的。语句可以自由地调用表达式,但表达式不能调用语句(除了调用预定义函数)。这种情况在GCC编译器中以一种相当有趣的方式得到改变,它支持所谓的"语句表达式"作为扩展(与标准C中的"表达式语句"相称)。 "语句表达式"允许用户直接将基于语句的代码插入表达式中,就像他们可以将基于表达式的代码插入标准C语句中一样。
另一个需要注意的是:在C ++语言中,基于函数对象的编程发挥着重要作用,这可以看作是另一种形式的“表达式编程”。根据当前C ++设计的趋势,在许多情况下,它可能被认为比传统的语句编程更可取。

32

我认为通常情况下,C语言的逗号并不是一个好的编码风格,因为它太容易被忽略了 - 无论是别人试图阅读/理解/修复你的代码,还是你自己一个月后再来看。当然,在变量声明和for循环之外,它是惯用的。

你可以使用它,例如将多个语句打包到三元运算符(?:)中,如:

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

但是我的天啊,为什么?!? (我在真实的代码中看到它被用于这种方式,但不幸的是没有访问权限来展示)


11
有点棘手,我不知道这个。 C(++) 真的有太多“特性”,除了在鸡尾酒会上引起无休止的愚蠢笑声外毫无用处。 - Frerich Raabe
19
这确实很棘手,因为你把逗号运算符混合使用在声明中 xD 不管它是否合法,我不知道,但标准是知道的 :) 事实是:你需要在op?:的第三个操作数上加括号,否则绑定将是:int x = (cond ? A : B), 117; xD - Johannes Schaub - litb
11
我有点难过,因为你不想修复它。我理解你想保持答案的“零编辑”状态。但是为了保留那些会产生编译错误的代码(例如“numeric constant之前应该有未限定的id”的错误),我认为这并不好。如果你想保持代码混乱,你也可以写其他任何无法编译的代码。我认为关键在于代码既应该令人困惑又要能够编译通过。 - Johannes Schaub - litb
9
答案中存在严重的术语错误。在标准的C语言中,你不能“将多个语句打包进三元运算符”中。以上所“打包进三元运算符”的不是语句,而是由,运算符连接成较大表达式的子表达式。在C语言中,“语句”是一个重要的概念,误用该术语只会导致不必要的混淆。 - AnT stands with Russia
4
这样做的最好理由是初始化一个常量变量,因为你不能这样做 const int x; if(some_bool) { printf("WTF"); x=5; } else { fprintf(stderr, "No, really, WTF"); x=117; }。尽管如此,我同意你的例子不太美观或易读。 - Adisak
显示剩余2条评论

23

C++中的两个很厉害的逗号运算符特性:

a) 读取流直到遇到特定字符串(有助于保持代码DRY):

...

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

b) 在构造函数初始化器中编写复杂的代码:

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

1
关于a),最好使用while (cin >> str && str != ""),虽然可能还有其他类似的用法。 - UncleBens
3
@UncleBens,cin >> str返回的是iostream,当到达文件结尾时会转换为false布尔值,而不是遇到空字符串时! - P Shved
2
这难道不意味着你的代码会卡在文件的最后一行吗?cin >> str 不会覆盖 str(我想是这样的),而且 str != "" 将永远为真。 - DrPizza
@DrPizza,啊,现在我明白了。我假设与之比较的字符串将在EOF之前遇到。 - P Shved
5
你的第二个例子需要再加上一对括号,以防止逗号被当作构造函数参数分隔符。(我认为你并没有这样的意图)。 - Johannes Schaub - litb
你认为这种保持代码DRY的方式比使用无限循环并在内部检查条件更好吗? - AlwaysLearning

22

我曾经在宏中看到过这个用法,其中宏假装成函数并希望返回一个值,但需要先进行一些其他的操作。虽然这种做法总是很丑陋,而且通常看起来像是一种危险的黑科技。

以下是一个简化的示例:

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

在这里,B=SomeMacro(A)将Permute(A)的结果作为"返回值"赋值给"B"。


9
尽管我不认同它丑陋和危险的说法,但还是建议您谨慎声明宏,这样就可以避免问题了。 - qrdl

11

Boost Assignment库是一个很好的例子,展示了如何以有用、易读的方式对逗号运算符进行重载。例如:

using namespace boost::assign;

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

另一个类似的例子是Eigen的逗号初始化,虽然它并不是真正的“初始化”,只是赋值。 - Ruslan

10

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

我不能将日志消息放在派生锁构造函数的主体中,因此我必须将其放在基类构造函数的参数中,使用初始化列表中的 : baseclass( ( log( "message" ) , actual_arg ))。请注意额外的括号。

以下是类的摘录:

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__ )
    {
    }

  ....

};

8

根据C标准:

逗号运算符的左操作数作为void表达式进行求值;在它的求值之后有一个序列点。然后对右操作数进行求值; 结果具有其类型和值。(逗号运算符不会产生lvalue。)如果试图修改逗号运算符的结果或在下一个序列点之后访问它,则行为是未定义的。

简而言之,它允许您在C语言只期望一个表达式的位置指定多个表达式。但实际上它主要用于for循环。

请注意:

int a, b, c;

这里的“,”不是逗号运算符,而是声明符列表。

10
问题是关于何时使用它,而不是它做什么。 - Graphics Noob
另外为了完整性:在参数列表中使用的逗号不是逗号运算符。 - Michael Burr
2
int a, b, c; 中没有“初始化器”。在 int a, b, c; 中,逗号分隔声明符 - AnT stands with Russia
是的,你说得对,我在写那段话时想到了 int a[] = {1, 2, 3} - Nicolas Goy

5

有时它被用于宏中,例如像这样的调试宏:

#define malloc(size) (printf("malloc(%d)\n", (int)(size)), malloc((size)))

(但是看看这个可怕的失败案例,由我亲手制造,它展示了过度使用会带来什么后果。)

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


是的,我看过那个。但我认为这是一种不好的做法。用你自己的函数替换stdlib malloc内置函数要好得多,因为你可以在编译后进行操作。许多调试工具都会透明地执行此操作。这样做还可以让你测试错误代码(例如,随机返回NULL并查看是否被捕获)。 - Nicolas Goy
@Nicolas:同意。这是我的最新的malloc宏,没有任何逗号运算符:#define malloc(s) smdm_debug_malloc((s), __FILE___, __LINE__) - Thomas Padron-McCarthy
然而,即使不考虑跟踪内存分配的特定情况,逗号运算符在宏黑客中有时也很有用。逗号运算符基本上是一种hackish运算符 - 它是为了在只允许一个的地方插入两个东西。它几乎总是丑陋的,但有时这就是你所拥有的全部。我的一个同事曾经称这样的事情为“把10磅的粪便塞进5磅的袋子里”。 - Michael Burr

5

除了for循环之外,在其他地方使用逗号操作符可能会有代码异味,我唯一认为逗号操作符可以很好地被使用的地方是作为delete的一部分:

 delete p, p = 0;

与其他选项相比,唯一的优势是如果这个操作分成两行,你可能会不小心只复制/粘贴其中一半。

我也喜欢它,因为如果你养成了习惯,你就永远不会忘记零赋值。(当然,为什么 p 没有被包装在 auto_ptr、smart_ptr、shared_ptr 等类型的指针中是一个不同的问题。)


3
将指针设置为null以后再进行delete的好处,有很大争议…… - AnT stands with Russia
1
好吧,当删除成员指针时,我不会在析构函数中这样做。但我不认为防御性编程是一个坏主意。更好的点在于我的回答:如果你正在进行防御性编程,为什么不使用auto_ptr/unique_ptr/shared_ptr等防御性指针编程家族之一呢? - jmucchiello

5
你可以在这个问题有“C ++”标签的情况下进行重载。我曾经看到一些代码,其中重载逗号用于生成矩阵。或者向量,我不太记得了。这不是很漂亮(虽然有点困惑):
我的向量 foo = 2,3,4,5,6;

5
重要提示:如果您过度使用逗号运算符,那么您将失去序列点,即您将获得函数调用语义,并且运算符的操作数将按照未指定的顺序进行评估。 - Richard Corden
我曾经使用过这个,但是对于没有代数运算的类(比如小部件)来说,我使用了 / 作为操作符。 - fa.
1
以下是一个例子: http://www.boost.org/doc/libs/1_40_0/libs/assign/doc/index.html - Bill Lynch
请参考Eigen库,这是一个很好的实践例子:http://eigen.tuxfamily.org/dox-devel/TutorialAdvancedInitialization.html - Adam

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