15.5 作用域
作用域 (Scope) 描述“一个标识符在源代码中可见的区域”。它是静态语义规则,与对象是否已经创建是两个概念。理解作用域可以直接避免命名冲突、阴影覆盖和接口泄露。
1. 四种常见作用域
四种作用域及其区间
文件作用域从声明点持续到翻译单元结尾;块作用域从声明点持续到当前复合语句结束;函数原型作用域只在函数声明的形参列表内有效;标号作用域覆盖整个函数体。它们共同决定同名标识符何时表示同一个实体,何时表示不同实体。
2. 块作用域与阴影覆盖
#include <stdio.h>
int main(void) {
int x = 1;
{
int x = 2;
printf("inner x = %d\n", x);
}
printf("outer x = %d\n", x);
return 0;
}2
3
4
5
6
7
8
9
10
11
12
13
输出:
inner x = 2 outer x = 1
3. 函数原型作用域
函数声明中的形参名只在该原型内部有效,它们主要用于可读性和文档化,不会在原型外形成可见名字。
int add(int left, int right); /* left/right 仅在这个声明里有效 */
int add(int a, int b) {
return a + b;
}2
3
4
5
定义阶段可以换用不同的形参名,编译器只关心类型是否匹配。
4. 标号作用域补充
label: 的可见范围覆盖整个函数体,因此 goto 可以跨块跳转到该标号,但目标仍必须位于同一函数内。它不受普通块作用域限制。
int f(int x) {
if (x < 0) {
goto fail;
}
return 0;
fail:
return -1;
}2
3
4
5
6
7
8
9
这类写法常用于统一失败出口,但仍应控制在清晰、可验证的范围内。
5. 声明点的时间边界
块作用域从声明点开始
块作用域标识符从声明点之后才可见;声明点之前,外层的同名对象仍然可见。
int x = 1;
{
/* 此处仍是外层 x 可见 */
int x = 2; /* 从这里开始,内层 x 可见 */
}2
3
4
5
6. 习题
6.1 巩固
阅读下面的程序,写出输出,并回答问题。
#include <stdio.h>
int limit = 100;
int main(void) {
printf("A %d\n", limit);
{
printf("B %d\n", limit);
int limit = 5;
printf("C %d\n", limit);
for (int limit = 0; limit < 2; limit++) {
printf("D %d\n", limit);
}
printf("E %d\n", limit);
}
printf("F %d\n", limit);
return 0;
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
(1) 写出六行输出。
(2) B 与 C 在同一个块里,打印的值却不同。用声明点解释。
(3) 把 int limit = 5; 移到 printf("B %d\n", limit); 之前,输出有什么变化?
(4) 用 gcc -std=c17 -Wall -Wextra 编译原程序没有任何警告,加上 -Wshadow 后出现两条警告。抄下这两条警告,指出各自对应哪一处遮蔽,并说明 -Wall -Wextra 为什么不报。
下面四个片段都与原型作用域和名字空间有关。把每个片段单独存成一个 .c 文件,用 gcc -std=c17 -Wall -Wextra -c 编译(只编译不链接,避免缺少函数定义干扰诊断),抄下诊断,然后写出能通过编译的最小修改,并解释原因。
(1)
int add(int left, int right);
int main(void) {
return left + right;
}2
3
4
5
(2)
int add(int n, int n);(3)
void show(struct point *p);
struct point {
int x;
int y;
};
void show(struct point *p) {
(void)p;
}2
3
4
5
6
7
8
9
10
(4)
int scale(int factor, int value);
int scale(int n, int v) {
return n * v + factor;
}2
3
4
5
第 (4) 个片段里的 factor 在函数体内不可见。修好之后,原型里的名字 factor、value 还有什么作用?把它们改成 int scale(int, int); 会不会影响调用方?
6.2 编程
补全下面的程序。matrix_find 在 rows 行 4 列的矩阵里按行优先查找 target,找到时把行列写进 row_out、col_out 并返回 1,找不到返回 0。要求只保留一个 return 1 出口:双重循环命中后执行 goto found;,跳到函数末尾的 found: 标号再返回。
#include <stdio.h>
int matrix_find(const int m[][4], int rows, int target, int *row_out, int *col_out) {
/* 在这里补全 */
}
int main(void) {
int m[3][4] = {
{1, 2, 3, 4},
{5, 6, 7, 8},
{9, 10, 11, 12},
};
int row = -1;
int col = -1;
int ok = matrix_find(m, 3, 7, &row, &col);
printf("7 -> %d %d %d\n", ok, row, col);
row = -1;
col = -1;
ok = matrix_find(m, 3, 20, &row, &col);
printf("20 -> %d %d %d\n", ok, row, col);
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
验收:输出两行 7 -> 1 1 2 与 20 -> 0 -1 -1,-Wall -Wextra 无警告。
然后回答:
- 把
found:挪到另一个函数里,goto found;那一行编译报什么错? - 在同一个函数里另外定义一个名为
found的int对象,程序还能编译吗?标号与普通标识符共用名字空间吗? - 如果
found:后面直接就是复合语句的},在-std=c17 -pedantic-errors与-std=c2x -pedantic-errors下分别得到什么结果?
下面的程序有三处与作用域有关的缺陷,输出也是错的。规格如下:sum_above 返回所有大于 threshold 的元素之和,并把同样的量累加到全局 total(每个元素只加一次);clamp_sum 返回 1 + 2 + ... + n,若超过 2 * n 就取 2 * n;scale 在 factor > 0 时返回 factor * value,否则返回 value。
#include <stdio.h>
int total = 0;
static int sum_above(const int *a, int n, int threshold) {
int sum = 0;
for (int i = 0; i < n; i++) {
if (a[i] > threshold) {
{
int sum = sum + a[i];
total += sum;
}
}
}
return sum;
}
static int clamp_sum(int n) {
int sum = 0;
goto cap;
{
int ceiling = n * 2;
cap:
for (int i = 1; i <= n; i++) {
sum += i;
}
if (sum > ceiling) {
sum = ceiling;
}
}
return sum;
}
static int scale(int factor, int value) {
if (factor > 0) {
int value = factor * 2;
return value;
}
return value;
}
int main(void) {
int a[] = {1, -2, 3, 4, -5};
int sum = sum_above(a, 5, 0);
printf("sum = %d, total = %d\n", sum, total);
printf("clamped = %d\n", clamp_sum(4));
printf("scaled = %d\n", scale(2, 5));
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
48
49
50
51
52
(1) 先按原样编译运行,记录输出。sum_above 里 int sum = sum + a[i]; 右边的 sum 指的是哪个对象?它的值从哪来?按规格,正确的 total 应该是多少?
(2) 用 -Wall -Wextra -Wshadow -Wjump-misses-init 编译,抄下与这三处缺陷对应的警告,说明每条警告指向哪条作用域规则。
(3) 修好三处缺陷。验收:gcc -std=c17 -Wall -Wextra -Wshadow -Wjump-misses-init 无警告,输出为 sum = 8, total = 8、clamped = 8、scaled = 10。
(4) 解释 sum_above 里 int sum = sum + a[i]; 中右边的 sum 为什么不是外层那个 sum,并说明 clamp_sum 里 goto 跳过了什么。
6.3 综合与挑战
下面的模块在 -std=c17 -Wall -Wextra 下能编译运行,但输出与规格不符。规格如下:
summarize(1, a, n)打印a中正数的个数,并把这个数加到全局total。record(bad)打印本次调用新增的错误数(bad非零记 1,否则记 0),同时让全局errors累计。classify(1)返回 10,classify(2)返回 11,classify(3)返回 0。clamp_sum(4)返回 8: 超过 ,取上限。
#include <stdio.h>
int total = 0;
int errors = 0;
static int count_positive(const int *a, int n) {
int count = 0;
for (int i = 0; i < n; i++) {
if (a[i] > 0) {
count++;
}
}
return count;
}
static int classify(int mode) {
switch (mode) {
case 1:
int adjusted = 10;
return adjusted;
case 2:
return adjusted + 1;
default:
return 0;
}
}
static void record(int bad) {
int errors = 0;
if (bad) {
{
extern int errors;
errors++;
}
}
printf("call errors = %d\n", errors);
}
void summarize(int mode, const int *a, int n) {
switch (mode) {
case 1:
int count = count_positive(a, n);
{
int count = count;
total += count;
}
printf("positive = %d\n", count);
break;
default:
printf("mode %d ignored\n", mode);
break;
}
}
static int clamp_sum(int n) {
int sum = 0;
goto cap;
{
int ceiling = n * 2;
cap:
for (int i = 1; i <= n; i++) {
sum += i;
}
if (sum > ceiling) {
sum = ceiling;
}
}
return sum;
}
int main(void) {
int samples[] = {3, -1, 4, 0, 5, -9, 2};
summarize(1, samples, 7);
summarize(1, samples, 7);
record(1);
record(0);
printf("classify = %d %d %d\n", classify(1), classify(2), classify(3));
printf("clamped = %d\n", clamp_sum(4));
printf("total = %d, errors = %d\n", total, errors);
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
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
(1) 编译运行,抄下输出,指出哪些行与规格不符。
(2) 用 -std=c17 -Wall -Wextra -Wshadow -Wjump-misses-init -Wnested-externs -pedantic-errors 编译,把诊断按「错误」和「警告」分开抄下。逐条说明它涉及哪个名字、由哪条作用域规则引起;其中与 adjusted、ceiling 有关的两条,也要说明它们分别与 case 标号的作用范围和 goto 跳过声明有关。
(3) 改写程序,使规格得到满足,并且上述严格选项在 -std=c17 与 -std=c2x 下都无诊断。验收输出:
positive = 4
positive = 4
call errors = 1
call errors = 0
classify = 10 11 0
clamped = 8
total = 8, errors = 12
3
4
5
6
7
(4) 用 -std=c2x 重编原程序,比较 (2) 里的错误列表,说明少了哪一类错误、C23 为此放宽了什么规则。
(5) record 里块内的 extern int errors; 看起来是在「重新声明」,实际指向哪一个对象?如果要让函数内的局部名字与全局对象并存,应该怎么写?