在《C语言编程魔法书》中的14章第6节已经详细讨论了C语言的六种语句:标签语句(labeled statement)、复合语句(compound statement)、表达式语句(expression statement)、选择语句(selection statement)、迭代语句(iteration statement)以及跳转语句(jump statement)。这里,我将对其中的表达式语句中的一种特殊形式的语句再做讨论——即空语句(null statement)。空语句在《C语言编程魔法书》中没怎么提到,这里正好做个补充。
空语句属于一种特殊的表达式语句,其表达式完全为空,即就一个分号( ; )所构成的一条表达式语句。这里我们需要注意,别把void表达式语句、空语句、空复合语句搞混了,以下先展示这三种不同的语句概念:
(void)0; // 这是一条void表达式语句
; // 这是一条空语句
{ } // 这是一条空的复合语句
空语句可以出现在所有语句所能出现的地方,一般来说它没有特殊语义。但是这里各位需要注意的是,C语言中分号可作为一条表达式语句、声明和定义的结束符,也能表示一条空语句,这是分号的两种不同的语义。此外,C语言标准中其实把声明/定义同语句是分开的,因此声明和定义不属于语句。如果我们把“声明”写作为“声明语句”就显得不够专业了~😄 另外,复合语句的 } 可作为结束符;而在声明和定义中,只有定义函数时,} 才能作为结束符,而其他场合则不行。
下面我们举个例子:
int a = 0; // 这里的分号表示声明的结束
a = 1; ; // 这里有两个分号,左边的表示表达式语句的结束,右边的则表示一条空语句。因此这里有两条语句。
C语言中除了表达式语句和跳转语句,其他类型的语句都需要跟一条其他语句来构成一条完整语句的。比如说:
int a = 0;
LABEL: a = 1; // 这是一条标签语句,在标签LABEL后面跟了一条表达式语句
if(a > 0) a = -a; // 这是一条选择语句,其后面伴随的是一条表达式语句作为一条完整的语句
for(;;); // 这是一条迭代语句,其后面伴随的是一条空语句构成一条完整的语句
while(a > 0) { --a; } // 这是一条迭代语句,其后面伴随一条复合语句构成一条完整的语句
if(a < 0) a++;
else { --a; } // 这里从if开始到最后 } 结束是一条完整的选择语句
以上这些概念请务必牢记,后面提到的容易触碰的“陷阱”其实就与语句的性质与条数有关。
我们上面已经提到了,标签语句、选择语句和迭代语句需要伴随一条这6种语句中的任何一条作为完整语句,而且无论是哪种情况均可跟任何一种语句。为了方便起见,我们可以把标签语句的LABEL:
,选择语句的if(condition)
和switch(selection)
,迭代语句的while(condition)
和for(sub-clauses)
看作为“头”(header),然后后面再跟一条6种语句中的任何一种。因此,我们现在可以放开脑洞,看看以下代码,这些代码都是合法有效的,完全能通过编译的:
static void ctest(void)
{
int a = 10;
// 这里的选择语句头后面跟着一条标签语句
if(a > 0)
LABEL:
++a;
// 这里的选择语句头后面跟着一条跳转语句
if(a < 0)
goto LABEL;
// 这里的标签语句头后面跟着一条选择语句
LABEL2:
if(a == 0);
// 这里的选择语句头后面跟着一条空语句
switch(a);
// 这里的选择语句头后面跟着一条标签语句
switch(a)
LABEL3:
++a; // 这里的 ++a; 将永远执行不到,除非显式使用goto跳转语句跳到LABEL3
// 这里的选择语句头后面跟着一条标签语句
switch (a)
LABEL4:
{
case 11:
puts("case 11 reached!!");
break;
case 12:
puts("case 12 reached!!");
break;
default:
puts("default case reached!!");
break;
}
if(a == 11)
{
++a;
goto LABEL4;
}
// 这里选择语句头后面跟着一条标签语句:case 12: --a;
switch(a)
case 12:
--a; // 这里的 --a; 将会被执行到。
// 这里后面不允许添加 break; 跳转语句,因为到 --a; 为止已经是一条完整的选择语句了。
}
当你执行ctest
函数之后,控制台将会打印出两行“ case 11 reached!! ”。看到这里,各位不要被C语言的灵活性吓到😅~正因如此,C语言在语法体系的设计方面是相当完备的。这里要详细讨论的是“LABEL4”这块。“LABEL4”所引出的标签语句与选择语句头switch(a)
一起组成了一条完整的选择语句。这里与上面“LABEL3”处的选择语句不同,由于LABEL4标签语句是由一条带有case标签组的复合语句构成,因此这里完全可以跟switch配对上。因而,当执行到“LABEL4”上面的switch(a)
之后,下面的case语句将会被选择执行。
此外,由于“LABEL4”所引出的是一条标签语句,因此这里变量a
的上下文将会被保持住,后续对a的修改不影响这里的case判断。也就是说,在LABEL4处的a
的值已经被hold住不变了,我们可以认为这里的a被赋值给了一个隐藏的临时常量,而后面的case标签语句将会根据此隐藏的临时常量的值进行选择执行。因此,这个函数被执行后将会两次都输出“ case 11 reached!! ”。
了解了上述语句的特性之后,我们以后在写选择语句、迭代语句的时候就要务必小心了,不要很悠哉地乱加分号,以免引起很难发现的bug。对于一般的表达式语句,后面跟多少分号都没问题,比如:
int a = 0;
a++;;; // 这里其实有三条语句😄
但以下代码将是一个bug:
int a = 0;
for(int i = 0; i < 10; i++); // 这里来了个分号……😅
++a;
大家猜猜,上述代码执行后,a的值是几?😏
有了上述对C语言选择语句和迭代语句潜在的陷阱之后,我们下面就要聊聊一些更高级的主题了——当一个宏定义遇上选择语句或迭代语句之后,会发生神马化学反应~
我们有时为了封装一些代码块,会使用宏函数。不过在使用宏函数的时候也很有可能会引发一些意向不到的问题,尤其是该宏函数与选择语句、循环语句连用的场合,比如以下代码:
#define MY_MACRO(n) if((n) > 0) { printf("n = %d\n", n); puts("PASS!"); }
这句代码初步看起来没啥问题吧?然而,我们再看看以下代码会发生神马:
int n = 100;
if(n >= 10)
MY_MACRO(10);
else // 发生编译错误:Expected expression
{
puts("n below ")
}
我们看到,这个看似朴素的宏函数“MY_MACRO”上面加了if语句之后,这里就出问题了!问题原因在哪儿?上面已经详细讨论过了。由于MY_MACRO在预处理时进行宏展开后,实际上是一条完整的选择语句,然而这里MY_MACRO(10)
后面又跟了一个分号,导致了该分号作为一条空语句的形式出现,从而与else无法配上。这里,MY_MACROz(10)宏展开后如以下代码所示:
int n = 100;
if(n >= 10)
if((n) > 0)
{ printf("n = %d\n", n); puts("PASS!"); } ;
else // 发生编译错误:Expected expression
{
puts("n below 10!");
}
所以,我们如果写成以下形式则可避免宏展开后发生编译错误:
int n = 100;
if(n >= 10)
{
MY_MACRO(10);
}
else // 发生编译错误:Expected expression
{
puts("n below 10!");
}
当然,如果我们强迫第三方开发者一定要在选择、迭代语句后要用复合语句,那么似乎也属于要求比较苛刻了,那么我们自己定义的宏函数用哪些技巧可以使得调用起来更加灵活,不加如此约束呢?我们在市面上看到比较常用的是使用do-while
迭代语句。do-while有何好处呢?while()在最后并不是以 } 符号结尾的,因此后面所跟的分号将作为整条迭代语句的结束符使用,而不会变成空语句。比如:
#define MY_MACRO(n) do { \
if((n) > 0) { printf("n = %d\n", n); puts("PASS!"); } \
} while(0) // 注意,这里可不能加分号
int main(int argc, const char * argv[])
{
int x = 100;
if(x >= 10)
MY_MACRO(10);
else
{
puts("x below 10!");
}
}
这么一来,我们可以看到上述代码编译、运行都不会有任何问题。当然,除了do-while之外还可以用其他技巧,总之,我们的目标就是要把宏函数调用后面的分号作为语句的结束符,而不是空语句。比如,我这里“别出心裁”地使用if-else选择语句,当然不能说这种解决方案完全完美,但还是能解决问题😊
#define MY_MACRO(n) if((n) > 0) { printf("n = %d\n", n); puts("PASS!"); } \
else
int main(int argc, const char * argv[])
{
int x = 100;
if(x >= 10)
MY_MACRO(10);
else
{
puts("x below 10!");
}
}
这段代码编译运行也不会有任何问题。而且宏定义部分也比之前的do-while版本更简洁清爽。
我这里也属于抛砖引玉,大家可以随之打开脑洞,设计出更有创造力的表达方式。
网友评论