词法元素
章节大纲
-
源代码词法单元
标准 C 要求在将源输入解析为词法单元时,必须形成尽可能长的有效词法单元序列。对于任何特定的语法结构,其含义必须不存在歧义。例如,对于文本
a+++++b
必须生成语法错误,因为被识别的词法单元依次为:
a, ++, ++, +, b
其中(后置)第二个
++
操作符的操作数不是一个左值。注意,表达式a++ + ++b
是合法的,因为空白字符使得词法单元被解析为:
a, ++, +, ++, b
同样,
a+++ ++b
亦然。
旧式做法
在 C89 之前,一些预处理器允许通过组合其他词法单元来创建新的词法单元。例如:
#define PASTE(a,b) a/**/b PASTE(total, cost)
这里的意图是宏展开后生成单一词法单元
totalcost
,而不是两个独立的词法单元total
与cost
。这一做法依赖于非标准 C 的方式,即将宏定义中的注释替换为“无”(而非一个空格)。标准 C 引入了预处理器的词法单元粘贴运算符##
,作为实现所期望行为的跨平台解决方案。在 C89 之前,一些预处理器还允许在预处理过程中生成字符串字面量词法单元。标准 C 添加了预处理器字符串化运算符
#
,作为实现该行为的跨平台解决方案。建议:避免利用遵循与标准 C 定义的词法规则不一致的预处理器的特殊行为。
关键字
以下词法单元被标准 C 定义为关键字:
auto break case char const /* C89 */ continue default do double else enum extern float for goto if inline /* C99 */ int long register restrict /* C99 */ return short signed /* C89 */ sizeof static struct switch typedef union unsigned void volatile /* C89 */ while _Alignas /* C11 */ _Alignof /* C11 */ _Atomic /* C11 */ _Bool /* C99 */ _Complex /* C99 */ _Generic /* C11 */ _Imaginary/* C99 */ _Noreturn /* C11 */ _Static_assert /* C11 */ _Thread_local /* C11 */
旧式:虽然 K&R 中未定义
enum
和void
,但在 C89 之前的多个编译器中已对其提供了支持。标准 C 不定义或保留在 K&R 以及某些旧版 C 编译器中曾保留的关键字
entry
。关于 C++ 的考虑:标准 C++ 不定义或保留关键字
restrict
;同样,也不定义那些以下划线开头且后跟大写字母的关键字。(不过,对于部分情况,它提供了替代拼写,例如alignas
、alignof
、bool
和thread_local
。)许多编译器支持扩展关键字,有的以一个或两个下划线开头,或处于程序员标识符命名空间中的名称。
标识符
拼写规则
K&R 和 C89 允许标识符中包含下划线、英文大小写字母以及十进制数字。
在某些较旧环境中,允许的外部名称集可能不包括下划线,且可能不区分大小写,这种情况下,外部名称中的某些字符可能会被映射为其他字符。
C99 添加了预定义标识符
__func__
。此外,C99 还支持在标识符中使用通用字符名(参见通用字符名),以及任意数量的实现定义扩展字符。关于 C++ 的考虑:标准 C++ 定义了以下关键字:
alignas alignof and and_eq asm bitand bitor bool catch char8_t char16_t char32_t class compl concept consteval constexpr constinit const_cast co_await co_return co_yield decltype delete dynamic_cast explicit export false friend mutable namespace new noexcept not not_eq nullptr operator or or_eq private protected public reinterpret_cast requires static_assert static_cast template this thread_local throw true try typeid typename using virtual wchar_t xor xor_eq
其中一些名称在标准 C 的头文件(例如
<stdalign.h>
中的alignas
)中定义为宏,这将在其他地方讨论。关于 C++ 的考虑:标准 C++ 对以下标识符赋予特殊意义:
final
、import
、module
与override
。建议:如果你的 C 代码有可能经过 C++ 编译器处理,请避免使用标准 C++ 定义为关键字或具有特殊意义的标识符。
关于 C++ 的考虑:根据标准 C++,“每个包含双下划线__
或以一个下划线后跟大写字母开头的标识符均保留给实现使用”,以及“每个以单个下划线开头的标识符均保留给实现用作全局命名空间中的名称”。
长度和有效字符限制
虽然标准 C 对标识符的总长度没有限制,但被视为有效(有意义)的字符数可能有限。具体地说,外部名称的长度限制可能比内部名称更严格(通常由于链接器的考虑)。标识符中被认为是有效的字符数是实现定义的。标准 C 要求实现至少能够区分外部标识符的前 31 个字符,以及内部标识符的前 63 个字符。
命名空间
K&R 定义了两大不相交的标识符类别:一类是普通变量相关的标识符,另一类是结构体和共用体成员以及标签。
标准 C 添加了几个新的标识符命名空间。完整的集合包括:
-
标签
-
结构体、共用体和枚举标签
-
结构体和共用体的成员(每个结构体和共用体有其独立的命名空间)
-
其他所有标识符,统称为普通标识符
在标准 C 函数原型中允许的标识符拥有其独立的命名空间,其作用域从标识符开始到该原型声明结束。因此,相同的标识符可以在不同原型中使用,但在同一原型中不能重复使用。
K&R 中有一句话:“两个结构体可以共享一组共同的初始成员;也就是说,如果两个结构体的前面所有成员相同,并且后面的相同成员类型也一致,则同一成员可以出现在两个结构体中。(实际上,编译器只检查两个结构体中同一名称的成员在类型和偏移上是否一致,但如果前面的成员不同,则这种构造在移植性上存在问题。)”标准 C 通过采用每个结构体有独立成员命名空间的方式,消除了这一限制。
通用字符名
C99 添加了对通用字符名的支持,其形式为
\uXXXX
和\UXXXXXXXX
,其中 X 为十六进制数字。它们可以出现在标识符、字符常量和字符串字面量中。
常量
数值常量
标准 C 要求实现处理常量表达式时至少使用目标执行环境所具有的精度,它可以使用更高的精度。
整数常量
-
C89 为支持无符号常量提供了后缀
U
(或u
),此后缀可用于十进制、八进制和十六进制常量。长整型常量可加后缀L
(或l
)。 -
C99 添加了类型
long long int
,其常量可加后缀ll
(或LL
);同时添加了无符号长长整型,其常量后缀为ull
(或ULL
)。 -
在 K&R 中,八进制常量允许出现数字 8 和 9(其八进制值分别为 10 和 11)。标准 C 不允许在八进制常量中使用 8 和 9。
整数常量的类型取决于其数值、基数和后缀的存在,这可能会引起问题。例如,考虑一个 int 为 16 位、采用二进制补码表示的机器,最小 int 值为 -32768。但表达式
-32768
的类型实际上是 long 而非 int!因为并不存在负整数常量,实际上这是两个词法单元:整数常量32768
与一元减号。由于32768
无法放入 16 位,因此其类型为 long,然后值再取负。因此,在没有函数原型的情况下调用f(-32768)
可能会导致参数类型不匹配。如果查看<limits.h>
中INT_MIN
的定义,通常会发现类似:#define INT_MIN (-32767 - 1)
这满足了要求该宏的类型为 int 的要求。
关于基数,在此 16 位机器上,
0xFFFF
的类型为 unsigned int,而-32768
的类型为 long。同样,在 32 位、二进制补码的机器上,对于最小值
-2147483648
,其类型可能是 long 或 long long 而非 int,这取决于类型映射。建议:当整数常量的类型非常重要时(例如作为函数调用参数或用于
sizeof
),请明确指定其类型,或进行适当的强制类型转换。类似的问题也存在于将零常量传递给一个期望指针的函数时——你可能意图表达“空指针”,但在没有函数原型时,零的类型为 int,其大小或格式可能与参数指针类型不匹配。此外,对于指针与整数格式差异较大的机器,不会自动进行隐式转换以补偿这一差异。
正确的做法是使用库宏
NULL
,它通常定义为以下之一:#define NULL 0 #define NULL 0L #define NULL ((void *)0)
旧式:在 C89 之前,不同编译器对整数常量的类型处理各有不同。K&R 要求:“一个十进制常量,其值超过机器最大带符号整数时,被视为 long;同理,超过机器最大无符号整数的八进制或十六进制常量也被视为 long。”
标准 C 对整数常量的类型规定如下:“一个整数常量的类型是其可以表示的对应列表中的第一个。对于无后缀的十进制常量:依次为 int, long int, unsigned long int;对于无后缀的八进制或十六进制常量:依次为 int, unsigned int, long int, unsigned long int;带有 U (或 u) 后缀的:unsigned int, unsigned long int;带有 L (或 l) 后缀的:long int, unsigned long int;同时带有 U (或 u) 和 L (或 l) 后缀的:unsigned long int。” C99 对
long long
与unsigned long long
增加了相应规定。有些编译器支持用二进制(基数 2)表示整数常量;另一些则允许在所有进制中使用分隔符(如下划线)分隔数字。这些功能均不属于标准 C 的范畴。
以 0 开头的整数常量被视为八进制。预处理指令
#line
的形式为:# line digit-sequence new-line
请注意,该语法不涉及“整数常量”,而
digit-sequence
即使有前导零,也被解释为十进制整数。浮点常量
浮点常量默认类型为 double。C89 增加了对long double
类型的支持,并提供了常量后缀 F(或 f)用于 float 常量,以及 L(或 l)用于 long double 常量。建议:当浮点常量的类型很重要时(例如作为函数调用参数或用于
sizeof
),请明确指定类型或进行强制类型转换。C99 增加了使用十六进制表示浮点常量的支持。
C99 同时新增了
<float.h>
中的宏FLT_EVAL_METHOD
,其值可能允许浮点常量以超出规定范围和精度的格式进行求值。例如,编译器有可能(悄然)将3.14f
视作3.14
或甚至3.14L
来处理。枚举常量
枚举中定义的值的名称是整数常量,标准 C 定义它们为 int 类型。K&R 中未包含枚举。
关于 C++ 的考虑:在 C++ 中,枚举常量具有所属枚举类型,该类型为可以表示该枚举中所有常量值的某种整型。
字符常量
-
源字符集到执行字符集的字符映射为实现定义。
-
对于包含在执行字符集中未表示的字符或转义序列的字符常量,其值为实现定义。
-
对于字符常量或字符串字面量中未指定转义序列(除反斜杠后跟小写字母外)的含义为实现定义。注意,保留给标准 C 未来使用的小写字母后的未指定转义序列,其含义不应由实现自由定义。这意味着一个符合标准的实现可以为例如
'\E'
(比如表示 ASCII 的 Escape 字符)提供语义,但不应为'\e'
提供。
建议:避免在字符常量中使用非标准转义序列。
-
包含多个字符的字符常量的值为实现定义。在 32 位机器上,可以将四个字符打包进一个 word,如
int i = 'abcd';
;在 16 位机器上,类似int i = 'ab';
可能被允许。
建议:避免使用多字符常量,因为其内部表示是实现定义的。
-
标准 C 支持早期流行的十六进制形式的字符常量,其通常形式为
'\xh'
或'\xhh'
,其中 h 是十六进制数字。 -
K&R 声明如果反斜杠后跟的字符不在规定范围内,则反斜杠将被忽略。标准 C 则规定这种行为是未定义的。
由于标准 C 不允许在八进制常量中使用数字 8 和 9,之前支持的类似
'\078'
的字符常量将获得新的含义。为避免与形如
??x
的三字符序列混淆,C89 定义了字符常量'\?'
。一个已经存在的'\?'
形式的常量将因此具有不同的含义。建议:由于不同字符集可能有所差异,使用字符的图形表示形式而非其内部表示更为合适。例如,在 ASCII 环境中,建议直接使用
'A'
而非'\101'
。有些实现可能允许使用
''
表示空字符,但标准 C 不允许这种表示。K&R 未定义常量
'\"'
,尽管在字面字符串中显然是必要的。在标准 C 中,字符'"'
与转义后的\"
等价。C89 增加了宽字符常量的概念,其写法与字符常量相同,但前置一个
L
(例如,L"abc"
用于宽字符串)。C99 增加了在字符常量中支持通用字符名。参见通用字符名。
C11 增加了对带前缀
u
或U
的宽字符常量的支持。标准 C 要求整数类型的字符常量的类型为 int。
关于 C++ 的考虑:标准 C++ 要求字符常量的类型为 char。
字符串字面量
标准 C 允许具有相同字面表示的字符串字面量被共享,但并不要求必须共享。
在某些系统中,字符串字面量可能存储在可读写内存中,而在其他系统则存储于只读内存。标准 C 指出,如果程序试图修改字符串字面量,行为将是未定义的。
建议:即使实现允许,也不要修改字符串字面量,因为这违背程序员的直观预期。也不要依赖于相同字面量会被共享。如果代码需要修改字符串,请改为使用字符数组来初始化该字符串,再对数组进行修改。这不仅避免了对字面量的修改,还使得你可以显式共享相同字符串。
建议:常见写法如
char *pMessage = "some text";
在使用 C89 或更高版本编译器时,应改为声明为
const char *
,以便任何修改底层字符串的尝试都能被诊断出来。关于 C++ 的考虑:标准 C++ 要求字符串字面量默认具有 const 限定,因此下面这种在 C 中常用的写法在 C++ 中无效:
char *message = "…";
必须写为
const char *message = "…";
字面字符串的最大长度是实现定义的,但标准 C 要求至少支持 509 个字符。
由于标准 C 不允许在八进制常量中使用数字 8 与 9,之前支持的类似
"\078"
的字符串字面量将获得新的含义。K&R 以及标准 C 允许使用反斜杠/换行符续行的方式将字符串字面量延续到多行,如下:
static char text[] = "a string \ of text";
但这要求续行必须从第一列开始。另一种方法是利用 C89(以及某些早期编译器)所支持的字符串连接功能,如下:
static char text[] = "a string " "of text";
C89 增加了宽字符串字面量的概念,其写法与普通字符串字面量相同,但前置一个
L
(例如L"abc"
)。C99 增加了在字符串字面量中支持通用字符名。参见通用字符名。
C11 增加了带前缀
u
(或U
)的宽字符字符串,以及使用前缀u8
的 UTF–8 字符串字面量。
标点符号
旧式:在 K&R 之前,复合赋值运算符写作
=op
;而在 K&R 及标准 C 中,写作op=
。例如,将s =* 10
变更为s *= 10
。C89 增加了省略号标点符号
...
,作为函数声明与定义增强语法的一部分。它还增加了预处理器专用的标点符号#
与##
。C95 增加了以下双字符标点符号(digraphs):
<:
,:>
,<%
,%>
,%:
, 以及%:%:
。
头文件名
标准 C 定义了头文件名的语法。若在形如
<…>
的#include
指令中出现字符, \, "
或/*
,其行为为未定义。同样地,对于形式为"…"
的#include
指令中,出现这些字符也是未定义的。在使用
#include "…"
形式时,文本"…"
并不被视为字符串字面量。在使用分层文件系统的环境中,需要用\
指示不同文件夹/目录层级,此反斜杠不被认为是转义序列的开始,因此也不需要进行转义。
注释
C99 增加了支持以
//
开头的单行注释。C99 之前,部分实现作为扩展支持这种注释方式。K&R 以及标准 C 均不支持嵌套注释,尽管有的实现确实支持。嵌套注释的需求主要源于希望能禁用一段包含注释的代码块,如下:
/* int i = 10; /* ... */ */
可以通过以下方式达到同样效果:
#if 0 int i = 10; /* ... */ #endif
标准 C 要求,在词法解析阶段,一个注释应被替换为一个空格。有的实现则将其替换为“无”,从而允许一些巧妙的词法单元粘贴行为(参见前文的“源代码词法单元”示例)。
-