1. C99
已经是上个世纪的最后一版 C 语言,但由于谭浩强当年没有跟上,导致我们很多人也没跟上。
1.1. 语法级
1.1.1. 基本概念
1.1.1.1. 注释
C99 起支持 C++ 式的双斜杠单行注释。
1.1.1.2. 标识符
标识符中可使用由 \u
和 \U
转义的 Unicode 字符。\u
后面为 4 位 16 进制数,\U
后面为 8 位 16 进制数,该 16 进制数表示一个 Unicode 码点,如:
int \u6211 = 42;
1.1.2. 预处理
1.1.2.1. 宏
1.1.2.1.1. 宏定义
#define
指令的形参列表尾部可使用变长参数列表 ...
,使用 __VA_ARGS__
访问变长参数,如:
#define print1(s, ...) printf(s, 1, __VA_ARGS__)
print1("%d %d %d\n", 2, 3); // printf("%d %d %d\n", 1, 2, 3);
使用 #__VA_ARGS__
将变长参数放入一对双引号中,并用一个逗号与空格隔开(无论代码如何排版),如:
#define print2(a, b, ...) printf(#__VA_ARGS__, a, b)
print2(1, 2, %d,
%d); // printf("%d, %d", 1, 2);
1.1.2.1.2. 预定义宏
__STDC_VERSION__
的值为 199901L
。
新增宏 __STDC_HOSTED__
,当目标环境为操作系统之下则为 1
,目标环境无操作系统则为 0
。
1.1.3. 表达式
1.1.3.1. 字面量
新增整型字面量后缀 ll
或 LL
,表示 long long
类型。新增整型字面量后缀 ull
或 Ull
或 uLL
或 ULL
,表示 unsigned long long
类型。
新增字符字面量转义符 \u
和 \U
。\u
后面为 4 位 16 进制数,\U
后面为 8 位 16 进制数,该 16 进制数表示一个 Unicode 码点,如 '\u6211'
。
新增复合字面量用以表达数组、结构体、联合体,其形式为 (类型){初始化列表}
,如:
int i = (int[]){ 1, 2 }[1];
1.1.3.2. 初始化
初始化列表中的项,新增一种可用形式 指派器列表=初始化器
。其中指派器列表的形式为数组的下标表达式 [常量表达式]
或结构体、联合体的成员访问表达式 .标识符
。
对于数组的初始化,可使用指派器列表指定下标,后续的下标会在此指定值的基础上依次加 1,如:
int ar[5] = { 1, [4] = 5, [1] = 2, 3 }; // { 1, 2, 3, 0, 5 }
按照初始化列表的顺序,后面相同下标的赋值会覆盖前面,如:
int ar[5] = { [3] = 5, 5, [1] = 2, 3, 4 }; // { 0, 2, 3, 4, 5 }
可用于嵌套数组,此时指派器列表可以是嵌套的数组下标表达式,用于指定当前嵌套深度下的数组下标,如:
int ar[3][3] = { [0] = { 1, [1] = 2, 3 }, { [1] = 5, 6 }, [2][1] = 8, 9 }; // { { 1, 2, 3 }, { 0, 5, 6 }, { 0, 8, 9 } }
对于结构体的初始化,可使用指派器列表指定成员,后续的成员会依照结构体中成员声明的顺序,后面相同成员的赋值会覆盖前面,如:
typedef struct S {
int a, b, c, d, e;
} S;
S s = { 1, 1, .e = 5, .b = 2, 3 }; // { 1, 2, 3, 0, 5 }
可用于嵌套结构体,此时指派器列表可以是嵌套的成员访问表达式,用于指定当前嵌套深度下的成员,如:
typedef struct S {
int a, b, c;
} S;
typedef struct R {
S x, y, z;
} R;
R r = { .x = { 1, .b = 2, 3 }, { .b = 5, 6 }, .z.b = 8, 9 }; // { { 1, 2, 3 }, { 0, 5, 6 }, { 0, 8, 9 } }
1.1.4. 声明
声明不再限定于只能出现在块的开头。这是 C99 中谭浩强错过的最重要的一点,以致于到了二十一世纪我们许多人还遵守着这个限制。
1.1.4.1. 限定符
在函数声明中,限定符可用于数组形参的方括号内,数组形参会转化为指针形参,限定符转而修饰该指针,如:
int f(int[const]); // int f(int* const)
int g(const int[]); // int g(const int*)
1.1.4.1.1. restrict
新增限定符关键字 restrict
以修饰一个指针类型的左值,提示编译器在该指针的作用域内,如果经由该指针所访问的内存会被修改,则该内存仅经由该指针访问。根据此限定符,编译器能作出更大胆的优化。
1.1.5. 语句
1.1.5.1. for
for
语句的初始化子句可以是一个声明,该声明的作用域为整个循环体,包括循环体的条件表达式和迭代表达式,如:
for (int i = 0; i < 42; i++) {
1.1.6. 类型
1.1.6.1. 标量
新增至少 64 位的有符号整型 long long
和无符号整型 unsigned long long
。
新增算术类型关键字 _Bool
,该类型的值只能是 0
或 1
。通常使用头文件 stdbool.h
中定义的宏 bool
、true
、false
。所有非零值的标量转化为 _Bool
类型后其值为 1
,零值的标量转化为 _Bool
类型后其值为 0
,注意这与转化为其他某种整型时的区别,特别是将其他某种整型用作布尔类型时,如:
#include <stdbool.h>
bool b1 = false; // _Bool b1 = 0;
bool b2 = NULL; // 0
bool b3 = 0.5; // 1
#define Bool unsigned char
Bool b4 = 0.5; // 0
1.1.6.2. 数组
新增变长数组。声明一个数组时如果长度不是一个常量,则声明为变长数组,如:
void f(int i) {
int ar[i];
变长数组的声明不能使用初始化列表。变长数组的长度在运行时确定,内存在运行时分配,长度在其生命周期内不可变。变长数组的生命周期只能在函数块内部,不能作为全局变量、结构体和联合体的成员。
变长数组作为函数形参时,在函数原型声明中,使用 *
作为数组长度,该数组同样会转化为元素类型的指针,如:
void f(int[*]); // void f(int*)
变长组数 T[n]
的 sizeof
操作仍是计算整个数组的长度,即 sizeof(T) * n
,但是在运行时计算,因此不再是常量。
1.1.6.3. 枚举
允许枚举声明中的最后一项的后面出现逗号。
1.1.6.4. 结构体
新增结构体柔性数组成员。柔性数组成员只能作为不完整数组类型而声明在最后,如:
typedef struct S {
char c;
int ar[];
} S;
S *s = (S*)malloc(sizeof(S) + sizeof(int) * 3);
s->ar[2] = 42;
结构体的初始化列表、sizeof
操作符、赋值操作符会忽略柔性数组成员。包含柔性数组成员的结构体不能作为数组元素和结构体成员。
结构体位域字段可使用 _Bool
类型,宽度只能为 1,如:
struct S {
_Bool b: 1;
};
1.1.7. 函数
新增函数域内的预定义静态局部变量 static const char __func__[]
,内容为当前函数名。
1.2. 标准库级
1.2.1. 类型
头文件
<stdbool.h>
定义了宏 bool
、true
、false
。true
和 false
是整型常量 1
和 0
。
头文件
<stdint.h>
定义了确定长度的整型 typedef int8_t
、int16_t
、int32_t
、int64_t
、intptr_t
、uint8_t
、uint16_t
、uint32_t
、uint64_t
、uintptr_t
等。
1.2.2. 数值计算
新增大量数值计算函数。
2. C11
和 C++ 同年推出的新世纪第一个 C 语言新标准,谭浩强更加跟不上了。
2.1. 语法级
2.1.1. 基本概念
2.1.1.1. 对齐
新增操作符关键字 _Alignof
查询类型的对齐,返回类型为 size_t
,返回值为常量,如:
typedef struct S {
int i;
char c;
} S;
_Alignof(S) // 4;
新增关键字 _Alignas
以声明对齐,其形式为 _Alignas(整型常量表达式或类型)
,对齐为整型常量表达式的值或类型的 _Alignof
的值,如:
typedef struct S {
_Alignas(16) char s[42];
};
2.1.2. 预处理
2.1.2.1. 宏
__STDC_VERSION__
的值为 201112L
。
新增宏 __STDC_UTF_16__
,当 char16_t
使用 UTF-16 编码则为 1
。
新增宏 __STDC_UTF_32__
,当 char32_t
使用 UTF-32 编码则为 1
。
新增宏 __STDC_ANALYZABLE__
,当编译器支持可分析性则为 1
。
新增宏 __STDC_LIB_EXT1__
,当标准库包含带边界检查的特定 API 则为 201112L
。
新增宏 __STDC_NO_ATOMICS__
,当编译器不支持原子类型且标准库不包含原子类型 API 则为 1
。
新增宏 __STDC_NO_COMPLEX__
,当编译器不支持复数类型且标准库不包含复数类型 API 则为 1
。
新增宏 __STDC_NO_VLA__
,当编译器不支持变长数组则为 1
。
2.1.3. 表达式
2.1.3.1. 字面量
新增字符字面量前缀 u
,字符类型为 char16_t
,通常是 UTF-16 编码。如 u'我'
即 (char16_t)0x6211
,若字符的 Unicode 码点对应的 UTF-16 编码超出单个编码单元,则依照编译器的具体实现。
新增字符字面量前缀 U
,字符类型为 char32_t
,通常是 UTF-32 编码。如 U'我'
即 (char32_t)0x00006211
,若字符的 Unicode 码点对应的 UTF-32 编码超出单个编码单元(这都能超过?),则依照编译器的具体实现。
新增字符串字面量前缀 u8
,字符类型为 char
,UTF-8 编码。如 u"是我"
即 (char[]){ 0xe6, 0x98, 0xaf, 0xe6, 0x88, 0x91, 0 }
。
新增字符串字面量前缀 u
,字符类型为 char16_t
,通常是 UTF-16 编码。如 u"是我"
即 (char16_t[]){ 0x662f, 0x6211, 0 }
。
新增字符串字面量前缀 U
,字符类型为 char32_t
,通常是 UTF-32 编码。如 U"是我"
即 (char32_t[]){ 0x0000662f, 0x00006211, 0 }
。
2.1.3.2. 泛型选择表达式
新增关键字 _Generic
以提供泛型选择表达式,其形式为 _Generic(控制表达式, 关联列表)
。关联列表中的关联项由逗号分隔,关联项的形式为 类型名: 表达式
或 default: 表达式
,各表达式不能为逗号表达式。控制表达式会经历左值转换(去掉顶层的类型限定符、数组到指针、函数到指针)但不会求值,转换后的类型与各关联项中的类型名进行匹配,返回匹配到的关联项中的表达式,若无 default
项且无匹配项则编译出错。如:
int i = 42;
j = _Generic(i,
char: i + 1,
int: i + 2,
default: i + 3
); // 44
2.1.3.3. 静态断言
新增关键字 _Static_assert
以提供静态断言,其形式为 _Static_assert(整型常量表达式, 字符串字面量)
,静态断言在编译时进行,当整型常量表达式的值为 0 则发生编译错误,字符串字面量会出现在错误信息中。
2.1.4. 声明
2.1.4.1. 存储期说明符
新增关键字 _Thread_local
以声明变量的存储期为新增的线程存储期。线程存储期为该线程的整个执行过程,变量在线程启动时初始化。不能用于函数声明。用于在块作用域中声明时,必须与 static
或 extern
存储期组合。
2.1.4.2. 原子类型
新增关键字 _Atomic
以声明变量为原子类型。
不能用于数组和函数声明,如:
typedef int pair[2];
// _Atomic pair p;
但可声明元素为原子类型的数组,如:
_Atomic int p[2];
2.1.5. 类型
2.1.5.1. 标量
新增 16 位和 32 位无符号字符类型 char16_t
和 char32_t
,通常是 typedef。
2.2. 标准库级
2.2.1. 线程
新增线程支持库
头文件
<threads.h>
新增线程、线程局部存储和同步原语操作函数。
头文件
<stdatomic.h>
新增原子操作函数。