2.5 保留标识符
C 标准把一部分标识符保留给实现和标准库使用。用户代码若声明或定义这些名字,轻则触发诊断,重则产生未定义行为。
1. 常见保留规则
以一个下划线加大写字母开头(如 _IOBUF)或以两个下划线开头(如 __size)的名字保留。以单个下划线开头的其他名字在文件作用域中保留,不能用作全局对象、函数、typedef 名或结构体标记。除此之外,标准库头文件公开的外部标识符同样不能被用户重新定义。
2. 一个对比例子
/* 不推荐:未定义行为 */
int __count = 0;
int _Engine = 0;2
3
/* 推荐:普通项目命名 */
int engine_count = 0;
int mdr_engine_state = 0;2
3
3. 习题
判断下列名字在文件作用域和块作用域中是否保留,填一张 2×7 的表:
_count
_Count
__count
_1
count_
mdr_count
_temp2
3
4
5
6
7
要求:
- 每一格写「保留」或「可用」,并在表后写一句判断依据。以单下划线开头、后随小写字母或数字的名字,两种作用域的结论不同,要写清差别在哪里。
- 把结论写成代码验证:文件作用域一组、块作用域一组,用
gcc -std=c17 -Wall -Wextra编译。编译器对这些名字有诊断吗?把结论与「标准说保留」这件事对照,说明为什么不能拿「编过了」当合规的证据。
验收:块作用域里只有 _Count、__count 两个保留;文件作用域里凡是以下划线开头的都保留,共五个。
下面的程序里,三个以下划线开头的名字不一定都在违规。
#include <stdio.h>
int __count = 0;
int _Engine = 0;
int main(void) {
int _local = 2;
__count = __count + _Engine + _local;
printf("%d\n", __count);
return 0;
}2
3
4
5
6
7
8
9
10
11
要求:
- 用
gcc -std=c17 -Wall -Wextra编译,记录输出与全部警告。 - 逐个说明
_Engine、__count、_local各自违反哪条保留规则。三者中哪一个在块作用域里其实不违规?它在什么条件下才违规? - 程序编译通过了,是否说明这些名字没问题?把这一点与「未定义行为」的定义对上:标准允许编译器对违规名字做什么,又不保证什么?
- 改写程序:保留名字换成小写下划线风格、带
mdr_前缀的名字,输出不变。改写后-Wall -Wextra必须无输出。
验收:改写前输出 2,警告为零;第 2 问要指出 _local 在块作用域可用但不该用;改写后输出仍为 2。
标准库头文件公开的外部标识符也保留。这条规则和编译器的内建函数(builtin)有直接关系。
#include <stdio.h>
size_t strlen(const char *s) {
(void)s;
return 99;
}
int main(void) {
printf("%d\n", (int)strlen("abcd"));
return 0;
}2
3
4
5
6
7
8
9
10
11
要求:
- 用
gcc -std=c17 -Wall -Wextra编译运行,写出输出。输出不是99,说明调用被换成了内建实现。 - 用
gcc -std=c17 -Wall -Wextra -fno-builtin重新编译运行,输出变成什么?-fno-builtin关掉的是什么? - 用
gcc -c生成目标文件,再用nm -g --defined-only列出符号。符号表里有strlen,为什么调用没走到这里? - 把函数名改成
mdr_strlen,去掉-fno-builtin重编译,输出是多少? - 综合 3、4 两问,说明「标准库外部标识符保留」这条规则除了避免重名之外的实际意义。
- 如果一定要自己实现长度函数,应该怎么命名?写出改法。
验收:第 1 步 4,第 2 步 99,第 4 步 99;第 5 问要答出编译器可以假定标准库名字的含义,并据此替换、内联或优化调用。
审查下面这个模块。它给一个小工具做状态记录,编译通过,但名字全踩在保留标识符的边界上。
#include <stdio.h>
#define __BUF 4
#define MDR_LIMIT 8
typedef int _Count;
int __total = 0;
int _engine = 0;
static int _helper(int x) {
return x * 2;
}
struct _state {
int state;
int upper;
};
int main(void) {
struct _state s = {3, 5};
_Count n = _helper(s.state) + s.upper;
__total = n + __BUF + MDR_LIMIT;
printf("%d %d %d\n", n, __total, _engine);
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
(1) [3.2] 逐个列出模块里所有以下划线开头的标识符,指出每一个按标准是否保留。MDR_LIMIT 和 _engine 的结论不同,要写清依据。
(2) [2.2] 运行程序,记下输出。再用 gcc -std=c17 -Wall -Wextra -pedantic 编译一次,有新的诊断吗?据此说明为什么这类违规在生产代码里容易长期存在。
(3) [4.3] 全部改名后重写:宏用全大写加项目前缀,类型、对象、函数、结构体标记用全小写加项目前缀,结构体成员不用前缀。要求编译无警告,输出与改写前逐字相同。
(4) [5.3*] 这条规则在跨文件时更危险。标准只保证外部标识符的前 31 个字符有意义。写两个翻译单元,各定义一个前 31 个字符相同、只在后面几个字符上不同的外部对象,用 nm 查看两份目标文件的符号,再链接成一个程序,说明本机为什么能区分、而只保证 31 个字符的实现可能把它们当成同一个名字。写出一种不会落到这条保证边缘的命名改法。