C语言的编译过程是将源代码转换为可执行文件的过程,主要分为四个阶段:预处理、编译、汇编和链接。
预处理阶段
在编译器开始编译 .c 或 .cpp 文件之前,会先运行预处理器,处理所有以 # 开头的指令。
预处理器负责处理以#开头的指令,输入为.c源文件,输出为.i文件。主要任务包括:
宏展开、头文件包含、条件编译、注释删除。
宏(Macro)你可以看作是编译时的文本替换工具,你使用宏是为了能更方便地看懂代码,但对于编译器不是。宏展开就是将#define定义的宏替换为具体值,方便机器编译。以下面代码为例:
#include <stdio.h>
// 宏定义
#define PI 3.14159
#define SQUARE(x) ((x)*(x))
// 全局变量
int global_var = 10;
int main() {
// 使用宏
printf("PI = %f\n", PI);
printf("5的平方 = %d\n", SQUARE(5));
// 使用全局变量
printf("全局变量初始值: %d\n", global_var);
global_var = 20;
printf("全局变量修改后: %d\n", global_var);
return 0;
}
在预处理阶段PI就会被换成3.14159
在编程中,头文件(Header File)是一种包含函数声明、宏定义、类型定义(如结构体、枚举)、类声明、变量声明等信息的文件,主要用于向编译器提供程序所需的信息,以便在编译阶段正确地理解和使用这些内容。头文件是程序的“说明书”——告诉编译器“我有哪些功能可用”,而具体的“怎么做”则在源文件里。 以上面代码为例,因为使用到了 printf函数但没有定义该函数,因此引入C语言标准库:#include <stdio.h>。预处理器会:
打开指定的头文件stdio.h(根据路径查找)
将头文件的完整内容复制到当前 #include 指令的位置
替换掉 #include 行
继续处理替换后的内容
条件编译(Conditional Compilation)是 C/C++ 预处理器提供的一种机制,它允许根据预定义的条件来决定是否编译某段代码。以下代码为例:
#ifndef __DELAY_H
#define __DELAY_H
#include "sys.h"
void delay_init(void);
void delay_ms(u16 nms);
void delay_us(u32 nus);
#endif
检查宏 __DELAY_H 是否未定义
当前 __DELAY_H 没有被定义过 → 条件成立!
进入条件编译块
第2行: #define __DELAY_H
定义宏 __DELAY_H
现在 __DELAY_H 被定义了!(值为 1)
第3行: #include "sys.h"
包含 sys.h 文件的内容(嵌套包含)
第4-6行: 函数声明
void delay_init(void);
void delay_ms(u16 nms);
void delay_us(u32 nus);
这些声明被添加到当前处理的代码中
第7行: #endif
结束条件编译块
第一次包含完成! 此时:
__DELAY_H 宏已被定义
sys.h 的内容已被包含
三个函数声明已被添加
编译阶段(不细讲)
编译阶段(通常指C编译器,如GCC的cc1程序)的主要任务是将预处理后的C代码转换成汇编语言代码。
编译器将预处理后的.i文件转换为.s汇编代码文件。主要任务包括:
词法分析:将代码分解为标识符、关键字等Token。
语法分析:检查代码是否符合C语言语法规则。
语义分析:验证变量类型、作用域等。
中间代码生成:生成与平台无关的中间表示(如三地址码)。
代码优化:删除冗余代码、常量折叠等。
目标代码生成:将优化后的中间代码转换为汇编代码。
汇编阶段(不细讲)
汇编器将.s汇编文件转换为.o目标文件,包含机器码、符号表和重定位信息。主要任务包括:
指令转换:将汇编指令逐行转换为机器码。
生成目标文件:生成包含机器码和符号表的.o文件。
汇编阶段的任务是把汇编代码(.s 文件)翻译成机器能直接运行的二进制代码(目标文件 .o)
链接阶段
链接器将多个.o目标文件和库文件合并,生成最终的可执行文件。主要任务包括:
符号解析:解决跨文件的函数或变量引用。
重定位:分配最终内存地址,修正符号表中的地址偏移。
库文件处理: 静态链接:将静态库代码嵌入可执行文件。 动态链接:记录动态库路径,运行时加载。
生成可执行文件:输出符合操作系统格式的二进制文件。
链接阶段就是把多个“半成品”(目标文件 .o)拼成一个完整的可执行程序,解决“谁调用了谁”、“函数在哪”这些问题。
在程序中:
目标文件(.o) = 各个模块(比如 main.o, delay.o, utils.o)
函数和变量 = 接口
调用关系 = “谁需要谁”
想象你要组装一辆自行车:
车架厂:生产车架 → 输出 frame.o
轮子厂:生产轮子 → 输出 wheel.o
刹车厂:生产刹车 → 输出 brake.o
但每个工厂只知道自己生产的部分,不知道别的部件在哪。
比如:
车架说:“我要装两个轮子!” —— 但不知道轮子长什么样、怎么装。
轮子说:“我准备好被安装了!” —— 但不知道装在车架哪个位置。
链接器就像总装工程师:
把所有部件拿过来
看清接口
把轮子准确地装到车架上
把刹车接到正确位置
最终 → 一辆完整能骑的自行车
以下内容源自《C Primer Plus》:
C编程的基本策略是,用程序把源代码文件转换为可执行文件(其中包含可直接运行的机器语言代码)。典型的C实现通过编译和链接两个步骤来完成这一过程。编译器把源代码转换成中间代码,链接器把中间代码和其他代码合并,生成可执行文件。C使用这种分而治之的方法方便对程序进行模块化,可以独立编译单独的模块,稍后再用链接器合并已编译的模块。通过这种方式,如果只更改某个模块,不必因此重新编译其他模块。另外,链接器还将你编写的程序和预编译的库代码合并。
中间文件有多种形式。我们在这里描述的是最普遍的一种形式,即把源代码转换为机器语言代码,并把结果放在目标代码文件 (或简称目标文件 )中(这里假设源代码只有一个文件)。虽然目标文件中包含机器语言代码,但是并不能直接运行该文件。因为目标文件中储存的是编译器翻译的源代码,这还不是一个完整的程序。
目标代码文件缺失启动代码 (startup code )。启动代码充当着程序和操作系统之间的接口。例如,可以在MS Windows或Linux系统下运行IBM PC兼容机。这两种情况所使用的硬件相同,所以目标代码相同,但是Windows和Linux所需的启动代码不同,因为这些系统处理程序的方式不同。
目标代码还缺少库函数。几乎所有的C程序都要使用C标准库中的函数。例如,concrete.c 中就使用了printf() 函数。目标代码文件并不包含该函数的代码,它只包含了使用printf() 函数的指令。printf()函数真正的代码储存在另一个被称为库 的文件中。库文件中有许多函数的目标代码。
链接器的作用是,把你编写的目标代码、系统的标准启动代码和库代码这3部分合并成一个文件,即可执行文件。对于库代码,链接器只会把程序中要用到的库函数代码提取出来(见下图)。

简而言之,目标文件和可执行文件都由机器语言指令组成的。然而,目标文件中只包含编译器为你编写的代码翻译的机器语言代码,可执行文件中还包含你编写的程序中使用的库函数和启动代码的机器代码。在有些系统中,必须分别运行编译程序和链接程序,而在另一些系统中,编译器会自动启动链接器,用户只需给出编译命令即可。
4237

被折叠的 条评论
为什么被折叠?



