您会看到它在 for 循环语句中使用,但它在任何地方都是合法的语法。如果有的话,你在其他地方发现了它的什么用途?
20 回答
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) ) {};
};
Boost Assignment库是以一种有用的、易读的方式重载逗号运算符的一个很好的例子。例如:
using namespace boost::assign;
vector<int> v;
v += 1,2,3,4,5,6,7,8,9;
我不得不使用逗号来调试互斥锁,以便在锁开始等待之前放置一条消息。
我不能只在派生锁构造函数的主体中记录日志消息,所以我不得不将它放在基类构造函数的参数中,使用初始化列表中的 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__ )
{
}
....
};
来自 C 标准:
逗号运算符的左操作数被评估为 void 表达式;在其评估之后有一个序列点。然后对右操作数求值;结果有它的类型和值。(逗号运算符不会产生左值。))如果尝试修改逗号运算符的结果或在下一个序列点之后访问它,则行为未定义。
简而言之,它允许您指定多个表达式,而 C 只需要一个表达式。但实际上它主要用于 for 循环。
注意:
int a, b, c;
不是逗号运算符,它是声明符列表。
你可以重载它(只要这个问题有一个“C++”标签)。我看过一些代码,其中重载的逗号用于生成矩阵。或者向量,我记不太清了。是不是很漂亮(虽然有点混乱):
MyVector foo = 2, 3, 4, 5, 6;
它有时用在宏中,例如像这样的调试宏:
#define malloc(size) (printf("malloc(%d)\n", (int)(size)), malloc((size)))
(但是看看这个可怕的失败,真的,当你过度失败时会发生什么。)
但除非你真的需要它,或者你确信它会使代码更易读和维护,否则我建议不要使用逗号操作符。
在 for 循环之外,即使有可能有一股代码气味,我认为逗号运算符的唯一用途是作为删除的一部分:
delete p, p = 0;
替代方案的唯一价值是,如果它位于两行上,您可能会意外地仅复制/粘贴该操作的一半。
我也喜欢它,因为如果您出于习惯这样做,您将永远不会忘记零分配。(当然,为什么 p 不在某种 auto_ptr、smart_ptr、shared_ptr 等包装器中是另一个问题。)
鉴于@Nicolas Goy 对标准的引用,听起来您可以编写单行 for 循环,例如:
int a, b, c;
for(a = 0, b = 10; c += 2*a+b, a <= b; a++, b--);
printf("%d", c);
但是天哪,伙计,你真的想用这种方式让你的 C 代码更加晦涩难懂吗?
在ASSERT
宏中添加一些注释非常有用:
ASSERT(("This value must be true.", x));
由于大多数断言样式宏将输出其参数的整个文本,因此这会在断言中添加额外的有用信息。
一般来说,我避免使用逗号运算符,因为它只会降低代码的可读性。在几乎所有情况下,只做两个陈述会更简单、更清楚。像:
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 嵌套一个额外的层。这实际上是我通常做的,因为上面的代码有点神秘。但我时不时地用逗号来做,因为嵌套也很神秘。
我经常使用它在一些 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;
当然,只有当循环的条件部分或可执行部分非常小,两三个操作时,两者才有用。
我见过的唯一一次在循环,
外使用的运算符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 循环的条件部分使用逗号运算符(参见qemu- queue.h中的 QTAILQ_FOREACH_SAFE)。他们所做的归结为以下几点:
#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