注:本文参考自【C reference - cppreference.com】和【C 语言参考 | Microsoft Learn】,颇有点借花献佛的意味……
C 程序是一系列包含声明的文本文件(通常为头文件和源文件)的序列。它们经过转换成为可执行程序,当操作系统调用其主函数时执行(除非它本身就是操作系统或另一个独立程序,在这种情况下入口点由实现定义)。在 C 程序中,某些词具有特殊含义,即关键字。其他词可用作标识符,可用于标识对象、函数、结构体、联合体、枚举标签、它们的成员、类型定义(typedef)名称、标签或宏。每个标识符(除宏外)仅在程序的一部分(称为其作用域)内有效,并属于四种命名空间之一。一些标识符具有链接性,这使得它们在不同的作用域或翻译单元中出现时指向相同的实体。函数的定义包括语句和声明的序列,其中一些包含表达式,这些表达式指定了程序要执行的计算。声明和表达式创建、销毁、访问和操作对象。C 语言中的每个对象、函数和表达式都与一种类型相关联。
一、基本概念
Basic concepts - cppreference.com
(一)翻译阶段
Phases of translation - cppreference.com
C源文件经编译器处理,依次经历以下阶段,实际由具体实现决定
- Phase 1 的处理:将源文件字节映射到源字符集的字符,替换特定符号,处理三字符序列。
- Phase 2 的操作:处理行拼接和换行符,明确了非空源文件结束时的要求。
- Phase 3 的分解:将源文件分解为注释、空格序列和预处理令牌,并对注释进行替换处理。
- Phase 4 的包含文件处理:对 #include 引入的文件进行递归的 1 至 4 阶段处理,并在阶段结束时移除所有预处理指令。
- Phase 5 的字符转换:将字符常量和字符串字面量中的字符和转义序列从源字符集转换到执行字符集。
- Phase 6 的字符串拼接:对相邻字符串字面量进行拼接。
- Phase 7 的编译:对令牌进行语法和语义分析及翻译。
- Phase 8 的链接:收集翻译单元和库组件,形成包含执行所需信息的程序图像。
(二)标点符号
序号 | 符号 | 描述或用途 |
1 |
| 结构体或联合体定义中的声明列表,复合语句的界定,初始化中的初始化器。 |
2 |
| 下标运算符,数组声明,C99 起在初始化中引入数组元素的设计符,C23 起属性说明符的界定。 |
3 |
| 引入预处理指令,字符串化预处理运算符。 |
4 |
| 标记粘贴预处理运算符。 |
5 |
| 表达式中的分组指示,函数调用, |
6 |
| 语句结束,声明或结构声明列表的分隔。 |
7 |
| 条件运算符的一部分,标签声明,位字段成员声明中引入宽度,C23 起枚举基的引入。 |
8 |
| 函数声明中的可变参数,C99 起宏定义中的可变参数宏。 |
9 |
| 条件运算符的一部分。 |
10 |
| C23 起属性的作用域指示,预处理器前缀参数的作用域指示。 |
11 |
| 成员访问运算符,C99 起初始化中的结构体/联合体成员的设计符。 |
12 |
| 成员访问运算符。 |
13 |
| 一元补运算符(位非运算符)。 |
14 |
| 逻辑非运算符。 |
15 |
| 一元加运算符,二元加运算符。 |
16 |
| 一元减运算符,二元减运算符。 |
17 |
| 间接运算符,乘法运算符,指针运算符,C99 起函数声明中可变长数组的长度占位符。 |
18 |
| 除法运算符。 |
19 |
| 模运算符。 |
20 |
| 位异或运算符。 |
21 |
| 地址运算符,位与运算符。 |
22 | | | 位或运算符 |
23 |
| 简单赋值运算符,初始化中的对象和初始化器列表的界定,枚举定义中的枚举常量值的引入。 |
24 |
| 复合赋值运算符。 |
25 |
| 复合赋值运算符。 |
26 |
| 复合赋值运算符。 |
27 |
| 复合赋值运算符。 |
28 |
| 复合赋值运算符。 |
29 |
| 复合赋值运算符。 |
30 |
| 复合赋值运算符。 |
31 |
| 复合赋值运算符。 |
32 |
| 等式运算符。 |
33 |
| 不等式运算符。 |
34 |
| 小于运算符,C23 起 |
35 |
| 大于运算符,C23 起上述指令和表达式中头文件名的结束。 |
36 |
| 小于等于运算符。 |
37 |
| 大于等于运算符。 |
38 |
| 逻辑与运算符。 |
39 |
| 逻辑或运算符。 |
40 |
| 位左移运算符。 |
41 |
| 位右移运算符。 |
42 |
| 位左移赋值运算符。 |
43 |
| 位右移赋值运算符。 |
44 |
| 递增运算符。 |
45 |
| 递减运算符。 |
46 |
| 逗号运算符,用于分隔声明列表、初始化器列表、函数调用参数列表、枚举器列表、宏参数列表、属性列表等。 |
(三)标识符
Identifier - cppreference.com
- 标识符的定义:是由数字、下划线、大小写拉丁字母和特定的 Unicode 字符组成的任意长序列,需以非数字字符开头,且区分大小写,必须符合标准化形式 C。
- 保留标识符:如关键字、以特定格式开头的外部标识符、标准库定义的外部标识符等均为保留标识符,在程序中不能声明使用。
- 潜在保留标识符:包括函数名、typedef 名、宏名、枚举常量等多种类型,它们可能在未来被使用。
- 翻译限制:虽然标识符长度无具体限制,但早期编译器和链接器对标识符有初始字符数量和数量等方面的限制,C 语言标准规定了一些最低支持的限制。
(四)作用域
Scope - cppreference.com
在 C 程序中出现的每个标识符仅在源代码中被称为其作用域的某些可能不连续的部分可见(即可以被使用)。在一个作用域内,只有当实体处于不同的命名空间时,一个标识符才可以表示多个实体
- C 语言有四种作用域:包括块作用域、文件作用域、函数作用域、函数原型作用域。
- 块作用域的规则:在复合语句、函数体、特定表达式和语句内声明的标识符,其作用域从声明点开始到所在块或语句结束。C99 之前,选择和迭代语句自身无块作用域。默认情况下,块作用域变量无链接且自动存储。
- 文件作用域的特性:在任何块或参数列表之外声明的标识符,其作用域从声明点开始到翻译单元结束。文件作用域标识符默认具有外部链接和静态存储。
- 函数作用域:从函数定义开始,到函数块结束而结束。函数内声明的标签且只有标签在函数内任何地方、嵌套块等都有效。
- 函数原型作用域:在函数声明的参数列表中引入的名称的作用域在函数声明符结束时结束。
- 其他作用域规则:结构体、联合体和枚举标签以及枚举常量的作用域有特定的起始点。标识符的作用域通常在声明符结束和初始化器之前开始。
- 嵌套作用域:如果两个由相同标识符命名的不同实体同时处于作用域内,并且它们属于相同的命名空间,那么这些作用域就是嵌套的(不允许有其他形式的作用域重叠),并且出现在内部作用域中的声明会隐藏外部作用域中的声明。
(五)生存期
Lifetime - cppreference.com
在C语言中,对象的生命周期是指对象存在于内存中的时间段,在此期间它可以被程序访问和修改。生命周期的开始和结束时间取决于对象的存储类型。
对象的生命周期定义
- 生命周期: 在这个时间段内,对象保持其地址不变,并且保留其最后一次有效存储的值(除非该值由于某种原因变得不确定)。对于可变长数组(VLA),它还保留其大小。
- 可变长数组 (VLA): 这种类型的数组是在运行时确定其大小的数组。自C99标准以来,VLA成为C语言的一部分。
存储期限与生命周期的关系
- 自动存储期限: 这类对象通常是在函数调用时创建的局部变量。它们的生命周期从进入作用域开始,到离开作用域结束。
- 静态存储期限: 静态变量在整个程序执行期间都存在。它们在程序启动时初始化一次,并且在整个程序执行过程中保持其值。
- 线程存储期限: 这种类型的对象在多线程环境中为每个线程提供独立的存储空间。它们的生命周期也与线程的存在相关联。
- 分配存储期限: 通过内存分配函数如
malloc
或calloc
分配的对象。它们的生命周期从分配函数返回开始,到使用free
函数释放为止。
访问生命周期外的对象
- 未定义行为: 如果试图访问一个已经超出其生命周期的对象,那么结果是未定义的。这可能导致程序崩溃或其他不可预测的行为。
临时生命周期
- 结构体和联合体: 当结构体或联合体对象包含数组成员,并且这些对象是通过非左值表达式指定时,它们具有临时生命周期。
- 开始和结束: 临时生命周期从评估表达式时开始,根据C11标准,在整个表达式评估完成时结束。
例如,考虑下面的代码片段:
1struct Point {
2 int x, y;
3} point = {10, 20};
4
5// 使用非左值表达式
6(int[point.x])[0] = 42; // 此处point具有临时生命周期
在这个例子中,point
作为数组下标的一部分出现在表达式中,因此它具有临时生命周期。需要注意的是,这种用法可能会导致复杂性和潜在的错误,因为一旦表达式计算完成,对point
的引用就不再有效。
(六)命名空间
Lookup and name spaces - cppreference.com
名字空间的定义
当C程序解析标识符时,它需要确定该标识符所代表的内容。为了做到这一点,C语言为不同的标识符类别定义了不同的命名空间。每个命名空间都是独立的,这意味着可以在不同的命名空间中使用相同的标识符名称而不会产生冲突。
(七)类型
Type - cppreference.com
对象、函数和表达式具有一种称为类型的属性,该属性决定了存储在对象中或由表达式计算的二进制值的解释。
类型分类
兼容类型
在C语言中,兼容类型的概念很重要,因为它决定了不同翻译单元(Translation Units, TU)之间如何引用同一对象或函数。在 C 程序中,在不同的翻译单元中指向相同对象或函数的声明不必使用相同的类型。它们只需要使用足够相似的类型,称为兼容类型。如果两个声明引用同一个对象或函数但类型不兼容,则程序的行为是未定义的。
类型名称
类型名称在C语言中可以用于以下情况:
- 类型转换 (
cast
) - 类型大小查询 (
sizeof
)
类型名称还可以用来引入新的类型,例如通过typedef
或者在类型转换表达式中。
(八)对象
Objects and alignment - cppreference.com
C语言中的对象是指程序执行环境中的一段数据存储区域,这段区域的内容可以表示一定的值。
整数类型的字节使用
- 当整数类型占据多个字节时,这些字节的使用方式由实现定义。
- 主要有两种实现:大端和小端。
- 大端实现将最高有效字节存储在最低地址处。
- 小端实现将最低有效字节存储在最低地址处。
二、执行单元
(九)主函数
main_function - cppreference.com
C语言中的main
函数是程序的入口点,它是程序启动后第一个被调用的函数。
main
函数的形式
main
函数可以有不同的形式,常见的形式如下:
-
无参数形式:
1int main(void) { 2 // body 3}
这种形式的
main
函数没有参数,适用于不需要命令行参数的简单程序。 -
带参数形式:
1int main(int argc, char *argv[]) { 2 // body 3}
这种形式的
main
函数接收两个参数:argc
和argv
。argc
是一个整数,表示命令行参数的数量(包括程序名称)。argv
是一个指向字符串数组的指针,这些字符串是命令行参数。 -
其它扩展实现形式:
1int main(int argc, char *argv[], char *envp[]) { 2 // body 3}
这种形式的
main
函数接收三个参数:argc
、argv
和envp
。除了argc
和argv
外,envp
是一个指向环境变量字符串数组的指针。
参数说明
-
argc
:- 表示传入程序的命令行参数的数量(包括程序名称)。
- 是一个非负整数。
-
argv
:- 是一个指向字符串数组的指针,其中每个字符串都是一个命令行参数。
argv[0]
通常指向程序名称。- 如果
argv[0]
非空指针(即argc > 0
),它指向一个表示程序名称的字符串;如果程序名称无法获取,则argv[0][0]
保证为零。 argv[argc]
是空指针。
返回值
main
函数的返回值具有特殊的意义:
- 如果
main
函数的返回类型与int
兼容,那么从main
函数的初始调用返回相当于调用了exit
函数,并将main
函数返回的值作为参数传递给exit
函数。 - 返回值为0或
EXIT_SUCCESS
表示成功终止。 - 返回值为
EXIT_FAILURE
表示失败终止。
特殊性质
-
原型不可提供:
main
函数的原型不能由程序提供。
-
退出行为:
- 如果
main
函数的返回类型与int
兼容,那么从main
函数返回的行为等同于调用exit
函数,并传递main
函数返回的值作为参数。这会触发一系列标准清理操作,比如调用atexit
注册的函数、关闭和刷新所有输出流、删除通过tmpfile
创建的文件,并将控制权返回给执行环境。
- 如果
-
未定义的行为:
- 如果
main
函数执行了一个未指定返回值的return
语句,或者到达了函数结尾而没有执行return
,则返回给执行环境的终止状态是未定义的(直到C99标准)。 - 如果
main
函数的返回类型与int
不兼容(例如void main(void)
),返回给执行环境的值是未指定的。如果main
函数的返回类型与int
兼容,但是控制流到达了函数结尾而没有执行return
,则行为等同于执行return 0;
(自C99标准起)。
- 如果
(十)语句
Statements - cppreference.com
(十一)表达式
Expressions - cppreference.com
(十一)初始化、声明与定义
(十二)函数
致读者:
本文略显潦草地总结了C的一些知识点,难免有疏漏之处,本人知识有限也无法提供足够专业的知识分析,仅仅是作为一个参考,概览一些C的语法面貌。本文的目的,是作为一个小总结和再回顾。虽然是总结,但是可以发现的是,对于C的整体语法体系,这里仅仅是探明了一部分,至少本人没有去完全了解C的语法体系。即使是作为技术的纯粹者和热爱者,完全熟练一门语言也需要时间,更何况C的灵活性和变化的应用环境。所以倘若有读者能在这里得到点什么,这篇文章也算有点意义了。对于较为庞大的思维导图,本文提供了相应文件,读者可以自行修改。