2.4 标识符
1. 标识符
- 标识符是字符序列 (Character Sequence),可以指代一个实体(对象,函数,宏等)。
- 翻译限度(不需要记,实际往往支持更多):
- 实现至少应支持:内部标识符或宏名中 63 个有意义的初始字符,外部标识符中 31 个有意义的初始字符;
- 一个翻译单元中最多 4095 个外部标识符;
- 一个块中最多声明 511 个拥有块作用域的标识符;
- 一个预处理翻译单元中最多同时定义 4095 个宏标识符。
保留标识符与未定义行为
有些标识符叫保留标识符,用户代码声明这类标识符属于未定义行为:翻译阶段 7 中会被识别为关键字的预处理记号;以一个下划线开始,并出现在文件作用域普通名字空间或标记名字空间中的标识符;所有以一个下划线后随一个大写字母或另一下划线开始的标识符,如 _Mdr、__mdr;标准库所定义的所有外部标识符。
翻译限度只是最低保证
上面列出的数字表示标准规定的最低支持规模。真实工具链通常支持更多,可移植讨论只应依赖标准保证或目标实现文档。
2. 标识符、实体与名字空间
名字空间区分同名标识符
同一个字符序列在不同上下文中可能属于不同名字空间。C 中至少要区分普通标识符、标记名(struct、union、enum 的标签)、成员名和标号名等类别。结构体成员名只在对应结构体或联合体内部解释。
struct item {
int value;
};
int item = 1; /* 普通标识符,与 struct item 的标记名不冲突 */2
3
4
5
3. 关键字
- 类型:
_Bool(bool)chardoublefloatintlongshortsignedunsignedvoid_Complex_Decimal32_Decimal64_Decimal128_Imaginary_BitInt(n) - 指定符和限定符:
constvolatileinlinerestrict_Noreturn_Alignas(alignas)_Atomic_Generic - 常量:
truefalsenullptr - 控制语句 相关:
breakcasecontinuedefaultdoelseforgotoifreturnswitchwhile - 存储类 说明符:
autoexternregisterstaticconstexpr_Thread_local(thread_local) - 运算符:
sizeoftypeoftypeof_unqual_Alignof(alignof) - 类型的声明:
enumstructunion - 类型推断:
auto - 其他:
_Static_assert(static_assert)typedef
上面的列表按 C23 列出。括号中的 bool、alignas、thread_local、alignof、static_assert,以及 true、false,在 C23 之前是标准头中定义的宏,C23 起成为关键字;nullptr、constexpr、typeof、typeof_unqual、_BitInt、_Decimal32 等以及 auto 的类型推断用法是 C23 新增的。
关键字在语法中有固定用途。到了翻译阶段 7,能够被识别为关键字的记号会按关键字处理。因此,用户代码不能把 int、return、struct 这类词重新声明成自己的名字。
4. 习题
4.1 巩固
下面八个写法,哪些能在函数体内用作普通对象名,哪些不能?逐个给出判断和理由,再用编译器核对。
count_
2nd
Int
double
my-var
_ok
mdr_count
case2
3
4
5
6
7
8
要求:
- 判断分三类:合法、关键字、词法上不成立(根本不是一个标识符记号)。
- 把判断为合法的名字写进一个程序,各声明一个
int对象并打印它们的值。用gcc -std=c17 -Wall -Wextra编译,编译器不能给出任何诊断。 - 把不合法的名字逐个单独写进一条
int声明,编译,抄下第一条 error,说明编译器把它当成了什么。 - 说明
Int与int的关系,以及为什么关键字不能靠改名的大小写绕开。
验收:合法的一组恰好四个;2nd 的 error 与整数后缀有关,my-var 的 error 说明它被切成了三个记号。
先读下面的程序,写出输出,再回答后面的问题。
#include <stdio.h>
struct item {
int item;
int value;
};
int item = 1;
int value = 4;
int main(void) {
struct item value = {2, 3};
int total = item + value.item;
goto value;
value:
printf("%d %d %d %d\n", item, value.item, value.value, total);
return 0;
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
- 写出输出,并说明
total是怎么算出来的。 item在这个程序里出现了三次:结构体标记、成员名、文件作用域对象。它们分别属于哪个名字空间?struct item与int item为什么能共存?value同时是文件作用域对象、自动对象和标号。value.item里的value指哪个对象?goto value用的是哪个名字空间?- 删掉
int value = 4;这一行,程序还能编译吗?输出是否改变? - 在
main开头加一行int item = 10;,重新写出输出,并说明item + value.item里的item换成了谁。
验收:原程序输出 1 2 3 3;第 5 问输出 10 2 3 12。先写答案再编译。
4.2 编程
本节列出的 511、4095 是翻译限度,不是编译器的实际上限。用实验核对这句话。
要求:
- 写一个生成器(C、Python 或 shell 均可),生成
many_vars.c:在main的同一个块里声明 600 个对象,名字从v0到v599,初值等于编号,再逐个累加进total并打印。用gcc -std=c17 -Wall -Wextra编译,要求没有警告,运行输出179700。 - 生成
many_macros.c:连续写 5000 个宏定义,编号从 0 到 4999,末尾用#if M4999 != 4999配#error自检。编译并运行,确认通过。 - 回答:600 超过了哪一个限度?程序通过说明了什么,又不能说明什么?判断一段代码是否可移植时应该依据什么?
- 把对象数改成 6000 再编译一次,记录结果(不要求通过)。
验收:第 1、2 步编译时没有任何输出,第 1 步运行打印 179700;第 3 问要写清「标准保证的下限」与「本机工具链的实际上限」的区别,并说明不能拿本机结果当作可移植性依据。
4.3 综合与挑战
下面这段程序在 C17 下不加任何头文件时能编译运行:
#include <stdio.h>
int bool = 1;
int true = 2;
int alignas = 3;
int static_assert = 4;
int nullptr = 5;
int constexpr = 6;
int main(void) {
printf("%d %d %d %d %d %d\n", bool, true, alignas, static_assert, nullptr,
constexpr);
return 0;
}2
3
4
5
6
7
8
9
10
11
12
13
14
要求:
- 用
gcc -std=c17 -Wall -Wextra编译运行,记录输出。 - 在
#include <stdio.h>后面依次加上<stdbool.h>、<stdalign.h>、<assert.h>,每加一个重新编译一次,记录哪些名字从这一步开始报错,以及报错把它当成了什么。 - 改用
gcc -std=c2x编译原程序。GCC 13 不认识-std=c23这个写法,C23 模式要写c2x。记录哪些名字在这个模式下变成关键字。 - 列出六个名字在三种条件下的可用性:C17 无头文件、C17 带上述头文件、C23。
- 改写程序,使它在
-std=c17和-std=c2x下都能无警告通过,输出不变。改写后的名字要符合工程习惯。
验收:第 2 步中 bool、true、alignas、static_assert 报错,nullptr、constexpr 不报错;第 3 步六个名字全部报错;第 5 步两种标准下 -Wall -Wextra 均无输出。
本章内容对应 cppref 链接如下: