4.2 复合语句
复合语句 (Compound Statement) 是用花括号包裹的一组声明与语句,也常被称为“块”。它不仅是排版工具,更是作用域和存储期的边界。
1. 基本语法
c
{
声明序列
语句序列
}1
2
3
4
2
3
4
进入块时,块内自动存储期对象开始生效;离开块时,它们的生命周期结束。
2. 复合语句与作用域
c
#include <stdio.h>
int main(void) {
int x = 1;
{
int y = 2;
printf("%d %d\n", x, y);
}
printf("%d\n", x);
return 0;
}1
2
3
4
5
6
7
8
9
10
11
12
13
2
3
4
5
6
7
8
9
10
11
12
13
输出:
bash
1 2 1
y 的作用域到内层块的 } 为止,外层的第二个 printf 不能使用 y。把只在局部用到的对象声明在内层块中,可以缩小它的作用域和生存期。
3. 空复合语句
{} 也是合法语句。它常用于占位、宏展开兼容或分支对齐。虽然语义简单,但若频繁出现空块,通常说明控制流可以重构得更清晰。
4. 用复合语句作为分支体
即使控制分支当前只有一条语句,也优先使用复合语句。这样在后续扩展时不会破坏结构,也能避免 4.3.1 中介绍的悬垂 if 等陷阱。
5. 声明与语句交错
现代 C 允许在复合语句中把声明和普通语句交错书写。这样可以把对象声明放在首次使用附近,减少“声明处离使用处过远”的阅读负担。
c
{
step1();
int code = step2();
if (code != 0) {
handle(code);
}
}1
2
3
4
5
6
7
2
3
4
5
6
7
当对象生命周期应尽量短时,这种写法通常更清晰。
6. 习题
6.1 巩固
#10413
⚡2⏳2
把下面三个片段各自补上一行 #include <stdio.h>,分别存成 a.c、b.c、c.c,用 gcc -std=c17 -Wall -Wextra 编译。
c
/* a.c:块外使用块内对象 */
int main(void) {
{
int value = 3;
printf("a: %d\n", value);
}
printf("c: %d\n", value);
return 0;
}1
2
3
4
5
6
7
8
9
2
3
4
5
6
7
8
9
c
/* b.c:同一块内重复声明 */
int main(void) {
int value = 3;
int value = 7;
printf("%d\n", value);
return 0;
}1
2
3
4
5
6
7
2
3
4
5
6
7
c
/* c.c:两个并列的块各声明同名对象 */
int main(void) {
{
int value = 3;
printf("a: %d\n", value);
}
{
int value = 7;
printf("b: %d\n", value);
}
return 0;
}1
2
3
4
5
6
7
8
9
10
11
12
2
3
4
5
6
7
8
9
10
11
12
- 三个文件各能否编译通过?不能的抄下第一条错误。
a.c的错误是「未声明」而不是运行时读到垃圾值,这说明块结束时发生了什么?b.c与c.c的写法差别只有一对花括号,为什么一个报重定义、一个合法?重定义指的是“同一作用域内重复声明同名对象”,据此指出b.c的作用域是什么。c.c编译运行后的输出是什么?两个value是同一个对象吗?两个块都结束后,还有哪个value活着?- 如果把
c.c第二个块里的int也删掉,还能编译吗?不能的话,抄下诊断。这与你对第 4 问的回答是否一致?
#10414
⚡2⏳2
下面的程序里有四种“什么都不做”的写法。
c
#include <stdio.h>
int main(void) {
int ready = 0;
int hits = 0;
if (ready) {
} else {
}
++hits;
if (ready)
;
++hits;
if (ready) {
;
}
++hits;
for (int i = 0; i < 3; ++i) {
}
printf("%d\n", hits);
return 0;
}1
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
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
- 写出输出。
- 用
gcc -std=c17 -Wall -Wextra编译,哪一处产生警告?抄下警告文本,并说明其余三处为什么沉默。 {}、{ ; }与单独一个;在语法上分别是什么?它们各自能放在哪些位置?- 把
ready改成 1,输出和警告各有什么变化? - 4.2 节第 3 小节提到空块可用于“分支对齐”。以本题的
if/else为例,说明保留空块与删掉整个分支各有什么代价。
6.2 编程
#10415
⚡3⏳3
下面的函数把五个中间结果都放在同一层,并且把 printf 写在声明之前。
c
#include <stdio.h>
static int order_total(int price, int qty) {
printf("price = %d, qty = %d\n", price, qty);
int subtotal = price * qty;
int discount = subtotal / 10;
int total = subtotal - discount;
int shipping = total > 100 ? 20 : 10;
int grand = total + shipping;
return grand;
}
int main(void) {
printf("grand = %d\n", order_total(30, 4));
printf("grand = %d\n", order_total(10, 5));
return 0;
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
- 写出输出。
- 用
gcc -std=c17 -Wall -Wextra编译有警告吗?用gcc -std=c89 -pedantic -Wall -Wextra呢?抄下诊断,并指出它对应第 5 小节讲的哪一种写法。 - 用两个复合语句重构:第一个块计算
subtotal、discount与total,第二个块计算shipping并把它并进total,函数最后只返回最终的total。要求price、qty的打印放在第一个块之前,两次调用的输出与原来完全一致。 - 重构后在
-std=c89 -pedantic下还有诊断吗?为什么把声明搬进新块就能满足 C90 的约束? - 重构后
subtotal、discount、shipping各在哪个块里可见?在第二个块里写subtotal会得到什么诊断?total为什么必须留在函数最外层?
#10416
⚡3⏳2
下面的函数想先算出小计,再把它返回,但结果总是 0。
c
#include <stdio.h>
static int settle(int price, int qty) {
int total = 0;
{
int total = price * qty;
printf("subtotal = %d\n", total);
}
return total;
}
int main(void) {
printf("total = %d\n", settle(30, 10));
return 0;
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
- 写出输出。
- 用
-Wall -Wextra编译没有警告。加上-Wshadow呢?抄下诊断。 - 内层的
int是多余的吗?删掉它,输出变成什么?删掉之后这个函数等于哪个操作? - 如果想保留内层块(块里还要放几个只在算小计时用到的对象),应该怎么改才能把结果带出来?给出改法并验证输出。
- 这类“结果写进了内层同名对象”的缺陷,为什么
-Wall -Wextra默认不报?在工程里可以怎么防住它?
6.3 综合与挑战
#10417
⚡4⏳4
下面的工资计算能正确运行,但所有中间量都摊在同一层,而且只能在 C99 及以后编译。
c
#include <stdio.h>
/* 返回实发工资(元);hours 或 rate 为负时返回 -1 */
static int payroll(int hours, int rate) {
if (hours < 0 || rate < 0) {
printf("invalid\n");
return -1;
}
int normal_hours = hours < 40 ? hours : 40;
int extra_hours = hours > 40 ? hours - 40 : 0;
int base_wage = normal_hours * rate;
int extra_wage = extra_hours * (rate * 3 / 2);
int gross = base_wage + extra_wage;
int base = 1000;
int taxable = gross > base ? gross - base : 0;
int tax;
if (gross <= base) {
tax = gross / 10;
} else if (gross <= 3000) {
tax = 100 + taxable / 5;
} else {
tax = 500 + taxable * 3 / 10;
}
int net = gross - tax;
printf("gross=%d tax=%d net=%d\n", gross, tax, net);
return net;
}
int main(void) {
payroll(20, 15);
payroll(40, 20);
payroll(45, 20);
payroll(50, 30);
payroll(0, 20);
printf("%d\n", payroll(-1, 20));
return 0;
}1
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
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
- 写出六行输出。
- 用
gcc -std=c89 -pedantic -Wall -Wextra编译,抄下所有诊断,并指出每条对应哪个声明。 - 重构:用复合语句把计算工资与计算税额分成两个块,让每个中间量只在需要的范围内可见;函数最外层只保留最终要返回的结果以及确实跨块使用的对象。要求输出与原来逐字节相同。
- 重构后的程序必须在
-std=c89 -pedantic -Wall -Wextra与-Wshadow下都零诊断。说明你的块划分是如何同时满足这两条要求的。 - 验收:
payroll(20, 15)输出gross=300 tax=30 net=270;重构后六个调用依次输出gross=300 tax=30 net=270、gross=800 tax=80 net=720、gross=950 tax=95 net=855、gross=1650 tax=230 net=1420、gross=0 tax=0 net=0、-1。先把期望结果抄成对照表再运行,不要边看输出边改。 - 把你重构后的版本与原来的版本并排比较:块划分多出来的花括号给读者提供了什么信息?哪些对象的作用域缩小后,阅读时不再需要往上翻找?