4.1 表达式语句
表达式语句 (expression statement) 是最常见、也最容易被忽略的一类语句:它的形式是“表达式后接分号”。程序会先对表达式求值,再丢弃求得的值;写表达式语句是为了它的副作用。
1. 基本形式
count++; /* 修改对象状态 */
sum += value; /* 复合赋值 */
printf("ok\n"); /* 函数调用 */2
3
这三行都是表达式语句。它们的作用都来自副作用:前两行修改对象的值,第三行通过函数调用写出输出。
2. 空表达式语句
只有一个分号 ; 也是合法语句,称为空语句 (null statement)。它常见于循环体故意留空的场景。
while (*p++ != '\0') {
;
}2
3
空语句本身不执行任何操作;上例中,循环的全部工作都写在条件 *p++ != '\0' 里。如果代码意图不够清楚,建议加注释或改成显式块。
3. 常见误用
把条件判断后面误写一个分号,会导致控制语句失效。
if (ready) {
do_work();
}2
3
if (ready);
{
do_work();
}2
3
4
第二段中 if 实际控制的是空语句,后面的块会无条件执行。这类问题在代码评审中很常见,务必通过格式化和告警规则提前拦截。
4. 有意忽略返回值时的写法
有些函数返回值在当前语境下确实可以忽略。此时可以显式写 (void) 转换,表达“这里是有意丢弃”,同时避免和遗漏检查混在一起。
#include <stdio.h>
int main(void) {
(void)puts("hello");
return 0;
}2
3
4
5
6
输出:
hello
这仍然是表达式语句,但意图更清楚:这次调用只关心副作用,不关心返回值。
5. 赋值与条件
赋值表达式加上分号就构成表达式语句。赋值表达式也可以出现在 if、while 的条件中,但这样容易与比较运算符 == 混淆。若只是要更新对象,应写成单独的表达式语句;若确实需要在条件中赋值,宜写成 if ((x = f()) != 0) 这样的形式,使赋值和比较各自明确。
6. 完整表达式
每条表达式语句中的表达式都是一个完整表达式。前一条表达式语句中的副作用会在下一条开始前完成。把复杂逻辑拆成多条表达式语句,不仅更易读,也不必再依赖同一个表达式内部的求值顺序。
7. 与 for 三段式的区分
for (init; cond; step) 中的 init、cond、step 都不是普通的“表达式语句 + 分号”(init 还可以是声明)。step 部分常写成逗号表达式,见 5.4 逗号运算符;若 step 逻辑过重,拆回循环体往往更清楚。
8. 习题
8.1 巩固
编译并运行下面的程序。
#include <stdio.h>
int main(void) {
int total = 0;
total = 10;
total + 1;
total += 1;
printf("%d\n", total);
total == 10;
;
return 0;
}2
3
4
5
6
7
8
9
10
11
12
13
- 逐行指出哪些是表达式语句,哪些是别的语句。
int total = 0;算不算语句? - 程序的输出是什么?
- 用
gcc -std=c17 -Wall -Wextra编译,哪两行触发了警告?把警告文本抄下来。 - 这两行的值都被丢弃了,编译器为什么还要专门提示?如果本意是让
total再加 1,应该怎么改?改完重新编译运行,输出变成什么? - 单独一个
;在这一段里起什么作用?删掉它,程序有什么变化?
下面的四个循环都没有实际循环体。
#include <stdio.h>
int main(void) {
int i = 0;
int j;
int k = 3;
while (i++ < 3)
;
for (j = 0; j < 3; ++j)
;
do
;
while (k-- > 0);
for (j = 0; j < 3; ++j) {
}
printf("%d %d %d\n", i, j, k);
return 0;
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
- 写出输出。每个循环的条件各被求值了几次?
- 用
-Wall -Wextra编译,哪一处产生-Wempty-body?其余三处为什么不报? - 四个“空体”里,哪些是空语句,哪些是空复合语句?
- 把
while (k-- > 0)改成while (--k > 0),输出变成什么?说明k--与--k出现在条件位置时的差别。 - 把第一个循环改写成
while (i++ < 3);(分号紧跟在条件后面),再在它下面接一个与while同缩进的块,块里打印一行。编译会多出哪条警告?这条警告想提醒读者什么?如果把分号放回下一行,警告为什么又消失了?
下面的程序用 (void) 转换标出几处“有意丢弃结果”,其中一部分写法与不加转换完全相同。
#include <stdio.h>
static int compute(int x) {
return x * 2;
}
int main(void) {
int x = 3;
int unused = 10;
x + 1;
(void)(x + 1);
unused;
(void)unused;
compute(x);
(void)compute(x);
return 0;
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
- 用
gcc -std=c17 -Wall -Wextra编译,哪些行有警告?警告文本是什么? (void)转换让哪些警告消失了?compute(x);本来就不报警告,那么给它加上(void)有什么用?(void)unused;对unused做了什么?它和unused;的区别在哪里?- 如果
compute的返回值在这个语境下确实可以丢掉,(void)compute(x);与compute(x);生成的代码有区别吗?读者能从写法上看出什么? (void)转换的结果类型是void。写int y = (void)compute(x);,编译会发生什么?由此说明(void)适合放在什么位置、不能放在什么位置。
8.2 编程
下面的程序把赋值写进了条件,三处条件的本意各不相同。
#include <stdio.h>
/* 成功返回 0,失败返回负的错误码 */
static int open_sensor(void) {
return 0;
}
/* 失败返回 -1,成功返回 0~100 的读数 */
static int read_level(void) {
return 0;
}
static int remaining = 2;
/* 取走一个样本;没有样本时返回 0 */
static int take(void) {
if (remaining == 0) {
return 0;
}
--remaining;
return 100;
}
int main(void) {
int status;
int level;
int value;
int total = 0;
if (status = open_sensor()) {
printf("open failed: %d\n", status);
} else {
printf("open ok\n");
}
if (level = read_level()) {
printf("level = %d\n", level);
} else {
printf("level unavailable\n");
}
while (value = take()) {
total += value;
}
printf("total = %d\n", total);
return 0;
}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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
- 编译运行,写出输出。三处条件各触发了哪条警告?
- 逐个说明每处条件的本意与实际判断的内容。为什么读数 0 会被判成“读取失败”?
- 按 4.1 节第 5 小节的建议改写三处条件,使赋值与比较各自明确。注意
open_sensor用 0 表示成功,read_level用 -1 表示失败,两处的判断条件不能照抄。改写后的输出是什么? while循环把“没有样本”和“样本值为 0”混为一谈。如果传感器确实可能读到 0,这个循环该怎样改?不用指针,改成两个函数(一个报告是否还有样本,一个取出样本)也可以。- 编译器对
if (status = open_sensor())只给警告而不报错,原因是什么?
#include <stdio.h>
int main(void) {
int i;
int sum;
for (i = 0, sum = 0; i < 10; ++i, sum += i) {
;
}
printf("i = %d, sum = %d\n", i, sum);
return 0;
}2
3
4
5
6
7
8
9
10
11
12
- 写出输出。
for的头部出现了几处逗号。哪些是逗号运算符,哪些只是声明里的分隔符?把init改写成int i = 0, sum = 0之后,这一处的逗号还是运算符吗?- 把
sum += i移进循环体,其余不变,输出是什么?两者为什么不同?提示:步进表达式在自增之后执行,最后一次执行时i已经变成多少? - 把
init写成int i = 0, sum = 0(在for里声明两个对象),循环之后再打印sum,编译会报什么?据此说明init中声明的作用域边界。 - 如果步进里还要再加一项(例如统计轮数),写成三项逗号表达式合适吗?4.1 节第 7 小节建议怎么处理?
8.3 综合与挑战
#include <stdio.h>
static int trace(int id) {
printf("call %d\n", id);
return id;
}
int main(void) {
int r = trace(2) * 10 + trace(1);
printf("r = %d\n", r);
return 0;
}2
3
4
5
6
7
8
9
10
11
12
- 在 GCC 上运行,写出输出。
r的值是否由标准保证?两次trace的打印先后呢? - 把两个乘加项对调,写成
trace(1) * 10 + trace(2),r与打印顺序各是什么? - 用三条表达式语句改写,使打印顺序固定为
call 2、call 1,r仍为 21。 - 4.1 节第 6 小节说,把复杂逻辑拆成多条表达式语句就不必再依赖同一个表达式内部的求值顺序。结合本题说明:拆分之后,先后关系由什么保证?
- 如果把
trace换成写日志的函数,这种“一个表达式里两次调用”的写法会让人对日志顺序产生什么误解?给出一种消除歧义的做法。