题
这个问题在这里已经有答案了:
- 逗号运算符 , 的作用是什么? 9 个答案
您会看到它在 for 循环语句中使用,但它在任何地方都是合法的语法。您在其他地方发现了它的哪些用途(如果有的话)?
解决方案
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++ 设计的趋势,在许多情况下它可能比传统的语句编程更可取。
其他提示
我认为一般来说,C 的逗号并不是一种很好的使用风格,因为它非常非常容易被错过——要么是其他人试图阅读/理解/修复你的代码,要么是你自己一个月后就错过了。当然,在变量声明和 for 循环之外,这是惯用的。
例如,您可以使用它将多个语句打包到三元运算符 (?:) 中,ala:
int x = some_bool ? printf("WTF"), 5 : fprintf(stderr, "No, really, WTF"), 117;
但天哪,为什么?!?(我已经在实际代码中看到它以这种方式使用,但不幸的是无法访问它来显示)
我见过它在宏中使用,其中宏假装是一个函数并想要返回一个值,但需要先做一些其他工作。它总是很丑陋,而且常常看起来像一个危险的黑客行为。
简化示例:
#define SomeMacro(A) ( DoWork(A), Permute(A) )
这里 B=SomeMacro(A)
“返回”Permute(A) 的结果并将其分配给“B”。
C++ 中两个杀手级逗号运算符功能:
a) 从流中读取直到遇到特定字符串(有助于保持代码干燥):
while (cin >> str, str != "STOP") {
//process str
}
b) 在构造函数初始值设定项中编写复杂的代码:
class X : public A {
X() : A( (global_function(), global_result) ) {};
};
我必须使用逗号来调试互斥锁才能放置消息 前 锁开始等待。
我只能将日志消息放在派生锁构造函数的主体中,因此我必须使用以下方法将其放入基类构造函数的参数中:初始化列表中的 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__ )
{
}
....
};
这 提升分配 库是以有用、可读的方式重载逗号运算符的一个很好的例子。例如:
using namespace boost::assign;
vector<int> v;
v += 1,2,3,4,5,6,7,8,9;
从C标准来看:
逗号运算符的左操作数被计算为 void 表达式;评估后有一个序列点。然后计算正确的操作数;结果有其类型和值。(逗号运算符不会产生左值。))如果尝试修改逗号运算符的结果或在下一个序列点之后访问它,则行为未定义。
简而言之,它允许您指定多个表达式,而 C 只需要一个表达式。但实际上它主要用于 for 循环。
注意:
int a, b, c;
不是逗号运算符,它是声明符列表。
它有时用在宏中,例如这样的调试宏:
#define malloc(size) (printf("malloc(%d)\n", (int)(size)), malloc((size)))
(但是看看 这可怕的失败, ,真诚地,因为当你做得太过分时会发生什么。)
但除非您确实需要它,或者您确定它使代码更具可读性和可维护性,否则我建议不要使用逗号运算符。
您可以重载它(只要这个问题有“C++”标签)。我看过一些代码,其中使用重载的逗号来生成矩阵。或者向量,我不太记得了。是不是很漂亮(虽然有点令人困惑):
MyVector foo = 2, 3, 4, 5, 6;
在 for 循环之外,即使有可能有一股代码味道,我认为逗号运算符唯一有用的地方是作为删除的一部分:
delete p, p = 0;
替代方案的唯一价值是,如果该操作位于两行,您可能会意外地仅复制/粘贴该操作的一半。
我还喜欢它,因为如果您出于习惯这样做,您将永远不会忘记零分配。(当然,为什么 p 不在某种 auto_ptr、smart_ptr、shared_ptr 等包装器中是另一个问题。)
鉴于@Nicolas Goy 对标准的引用,听起来您可以为循环编写一行代码,如下所示:
int a, b, c;
for(a = 0, b = 10; c += 2*a+b, a <= b; a++, b--);
printf("%d", c);
但是天哪,伙计,你真的想编写你的 C 代码吗? 更多的 就这么晦涩难懂?
一般来说,我避免使用逗号运算符,因为它只会降低代码的可读性。几乎在所有情况下,只做两个陈述会更简单、更清楚。喜欢:
foo=bar*2, plugh=hoo+7;
与以下方面相比没有明显的优势:
foo=bar*2;
plugh=hoo+7;
除了循环之外,我在 if/else 结构中使用了它,例如:
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
宏:
ASSERT(("This value must be true.", x));
由于大多数断言样式宏将输出其参数的整个文本,因此这会在断言中添加一些额外的有用信息。
我经常使用它在一些 cpp 文件中运行静态初始化函数,以避免经典单例的延迟初始化问题:
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
}
对我来说,C 语言中逗号真正有用的一个例子是使用它们来有条件地执行某些操作。
if (something) dothis(), dothat(), x++;
这相当于
if (something) { dothis(); dothat(); x++; }
这并不是说“少打字”,只是有时看起来很清晰。
循环也是这样的:
while(true) x++, y += 5;
当然,只有当循环的条件部分或可执行部分非常小(两到三个操作)时,两者才有用。
我唯一一次看到 ,
在 a 之外使用的运算符 for
循环是在三元语句中执行赋值。那是很久以前的事了,所以我不记得确切的说法,但它是这样的:
int ans = isRunning() ? total += 10, newAnswer(total) : 0;
显然没有一个理智的人会写这样的代码,但作者是一个邪恶的天才,他根据生成的汇编代码而不是可读性来构造c语句。例如,他有时使用循环而不是 if 语句,因为他更喜欢它生成的汇编程序。
他的代码非常快但难以维护,我很高兴我不必再使用它了。
我将它用于宏“将任何类型的值分配给 char* 指向的输出缓冲区,然后将指针增加所需的字节数”,如下所示:
#define ASSIGN_INCR(p, val, type) ((*((type) *)(p) = (val)), (p) += sizeof(type))
使用逗号运算符意味着宏可以根据需要在表达式中或作为语句使用:
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));
它减少了一些重复输入,但您必须小心它不会变得太难读。
请参阅我的这个答案的过长版本 这里.
qemu 有一些在 for 循环的条件部分中使用逗号运算符的代码(请参阅 QTAILQ_FOREACH_SAFE 在 qemu-queue.h 中)。他们所做的归结为以下几点:
#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\n", x, y );
}
printf( "\n%d, %d\n\n", x, y );
for( x = 0, y = x+1; x < 3; x = y, y = x+1 ){
printf( "%d, %d\n", x, y );
}
printf( "\n%d, %d\n", x, y );
return 0;
}
...输出如下:
0, 1
1, 2
2, 3
3, 3
0, 1
1, 2
2, 3
3, 4
该循环的第一个版本具有以下效果:
- 它避免了进行两次分配,因此减少了代码不同步的机会
- 由于它使用
&&
, ,最后一次迭代后不评估赋值 - 由于不评估分配,因此当队列中的下一个元素位于末尾时(在 qemu 的代码中,而不是上面的代码中),它不会尝试取消引用队列中的下一个元素。
- 在循环内部,您可以访问当前和下一个元素
在数组初始化中发现:
在 C 语言中,如果我使用 () 而不是 {} 来初始化双维数组,到底会发生什么?
当我初始化一个数组时 a[][]
:
int a[2][5]={(8,9,7,67,11),(7,8,9,199,89)};
然后显示数组元素。
我得到:
11 89 0 0 0
0 0 0 0 0