15. 声明、名称与生存期
前面的章节已经在具体场景中使用了声明和局部可见性。本章把这些规则整理成完整模型:
1. 习题
#11522
⚡3⏳2
先不编译,逐行推出下面程序的输出:
c
#include <stdio.h>
int x = 1;
static int next(void) {
static int n;
return ++n;
}
int main(void) {
printf("%d %d\n", x, next());
{
printf("%d %d\n", x, next());
int x = 2;
printf("%d %d\n", x, next());
}
printf("%d %d\n", x, next());
return 0;
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
- 写出四行输出,并指出每一行里的
x指的是哪个对象,next()的返回值又来自哪个对象。 - 编译运行核对结果。
- 把
int x = 2;提到块的第一行(第一条内层printf之前),其余不动,输出变成什么?这一改动用的是块作用域的哪条规则?
#11523
⚡4⏳3
某模块用下面的函数把温度转成文本:
c
#include <stdio.h>
static const char *format_temp(int celsius) {
static char buf[32];
snprintf(buf, sizeof buf, "%d C", celsius);
return buf;
}
int main(void) {
printf("%s -> %s\n", format_temp(20), format_temp(30));
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
本机输出为 20 C -> 20 C。
- 解释两个位置为什么打印出同一个字符串。分成两种写法讨论:若先分别调用
format_temp(20)、format_temp(30)把结果存进两个指针,最后一起打印,输出是30 C -> 30 C,说明原因;若像原程序那样把两次调用写在同一个printf里,输出是20 C -> 20 C,说明实参求值顺序在这里起了什么作用,以及为什么这个结果不能当作可靠行为。 - 改成调用方提供缓冲区的接口
void format_temp(int celsius, char *out, size_t size);(size_t需要<stddef.h>),重写调用点,使输出为20 C -> 30 C。 - 编译器对“两次调用共用一块缓冲区”无话可说,却能对“返回局部数组”给出
-Wreturn-local-addr警告。说明编译器在两种情况下各自掌握了哪些信息,为什么后者能静态判定、前者不能。
#11524
⚡5⏳3拓展
下面的模块把计数放在文件内部,主程序却声明了同名的外部对象。两个文件分开编译、一起链接(这种组织方式在第二部分展开,这里只需照着命令做):
c
#ifndef COUNTER_H
#define COUNTER_H
int counter_bump(void);
#endif1
2
3
4
5
6
2
3
4
5
6
c
#include "counter.h"
static int counter = 0;
int counter_bump(void) {
return ++counter;
}1
2
3
4
5
6
7
2
3
4
5
6
7
c
#include <stdio.h>
#include "counter.h"
int counter;
int main(void) {
counter_bump();
counter_bump();
printf("counter = %d\n", counter);
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
用 gcc -std=c17 -Wall -Wextra main.c counter.c -o app 编译,一条警告也没有,运行却输出 counter = 0。把两个文件各编成一个目标文件后,符号表工具对 counter 的记录如下(第二列的字母:大写表示外部链接,小写表示内部链接;B/b 指未初始化的数据段,U 指未定义引用):
text
counter.o: 0000000000000000 b counter
main.o: 0000000000000000 B counter
0000000000000000 U counter_bump1
2
3
2
3
- 解释这两条记录分别对应哪个对象、哪种链接。链接器把重定位指向哪一个?主程序打印的是哪一个对象的值?
- 给出两种修正,都要求输出
counter = 2:一种让两个翻译单元共享同一个对象;另一种让计数只属于模块,主程序通过函数读取。 - 编译器和链接器都没有报错。另写一个测试程序,同样声明
int counter;,调用counter_bump()三次后断言counter == 3,把它和counter.c一起链接运行。断言在哪一行失败?据此说明为什么“能编过、能链接、能跑完”不能证明两个翻译单元对同一个名字的理解一致。
#11525
⚡6⏳4
写一个单文件的统计数据收集器:调用方逐个送入整数,模块累加出个数与总和。全部代码放在一个 .c 文件里,但要按模块的思路组织,状态集中在一处,对外只有下面几个函数:
c
void stats_reset(void); /* 清空计数与总和 */
void stats_add(int value); /* 未启用时忽略 */
void stats_enable(void); /* 允许累加 */
int stats_count(void); /* 已收集的个数 */
long stats_sum(void); /* 总和 */
const char *stats_label(void); /* 返回 "stats" */1
2
3
4
5
6
2
3
4
5
6
要求:
- 计数、总和、启用标志放在文件作用域的对象里,都用
static修饰;函数之外的辅助函数也用static。 - 启用标志写成暂定定义,不写初始化器。说明它为什么不需要初始化器、初值是什么;如果同一翻译单元里再写一次同样不带初始化器的同名声明,最终有几个定义、值不变吗?
stats_label返回"stats",保存在函数内的静态数组里,每次调用返回同一个地址。说明它为什么不能改成返回指向自动数组的指针,并用一段错误实现验证编译器的反应。- 两个查询函数在实现支持时加上
[[nodiscard]],用__has_c_attribute判断,使同一份源码在 C17 与 C23 下都能编译。 - 用
_Static_assert(C11 起)约束sizeof(long) >= sizeof(int),并说明这条约束的依据。 - 写一个
main验证:未启用时stats_add无效;启用后count与sum正确;stats_reset之后归零;丢弃一次stats_count()的返回值时编译出现-Wunused-result警告。
分别在 -std=c17 与 -std=c2x 下用 -Wall -Wextra 编译,除第 6 条故意丢弃返回值的那一处外不应出现其他警告。本机期望输出:
text
disabled: count=0 sum=0
enabled: count=5 sum=15 label=stats
reset: count=0 sum=01
2
3
2
3