编译器的相关知识(入门时著)
作用:将高级语言(c#、python)程序翻译成机器码(二进制数据)。
工作流程:预处理——>编译(词法、语法、语义分析,生成中间码/优化)——>汇编——>链接
我做一个便于理解的比喻,有兴趣请先移步下方阅读秋鳞小故事
https://blog.csdn.net/QL_SD/article/details/151440212?fromshare=blogdetail&sharetype=blogdetail&sharerId=151440212&sharerefer=PC&sharesource=QL_SD&sharefrom=from_linkhttps://blog.csdn.net/QL_SD/article/details/151440212?fromshare=blogdetail&sharetype=blogdetail&sharerId=151440212&sharerefer=PC&sharesource=QL_SD&sharefrom=from_link
=========================================================================
一、具体操作
(一)预处理(宏展开、条件编译、处理预定义宏、删除注释、处理编译器指令)
1.宏展开
这是预处理器的核心功能之一。它处理所有以 #define
定义的宏。
①对象式宏:简单的文本替换。
// 源代码
#define PI 3.14159
#define BUFFER_SIZE 1024double circumference = 2 * PI * radius;
char buffer[BUFFER_SIZE];// 预处理后
double circumference = 2 * 3.14159 * radius; // PI被替换
char buffer[1024]; // BUFFER_SIZE被替换
②函数式宏:带参数的宏。
// 源代码
#define MAX(a, b) ((a) > (b) ? (a) : (b))
#define SQUARE(x) ((x) * (x))int x = 10, y = 20;
int z = MAX(x, y);
int sq = SQUARE(5);// 预处理后
int x = 10, y = 20;
int z = ((x) > (y) ? (x) : (y)); // 宏被展开,参数被替换
int sq = ((5) * (5)); // 宏被展开,参数被替换
2.条件编译
根据条件决定哪些代码块被包含在编译中,哪些被忽略。这是实现跨平台、调试、功能开关的关键。
①#if, #elif, #else, #endif:根据条件判断。
#define VERSION 2#if VERSION == 1printf("Running version 1\n");
#elif VERSION == 2printf("Running version 2\n"); // 只有这部分代码会被保留
#elseprintf("Running unknown version\n");
#endif
②#ifdef, #ifndef:检查宏是否被定义(常用于头文件保护符和功能开关)。
// 头文件保护符:防止头文件被多次包含
#ifndef MY_HEADER_H // 如果MY_HEADER_H未定义
#define MY_HEADER_H // 则定义它,并包含以下内容// 头文件的真实内容...
void some_function();#endif // MY_HEADER_H// 功能开关
#define DEBUG_MODE // 注释掉这行即可关闭调试信息#ifdef DEBUG_MODE#define DEBUG_PRINT(msg) printf("DEBUG: %s\n", msg)
#else#define DEBUG_PRINT(msg) // 定义为空,编译后什么也不做
#endif
3.处理预定义宏
编译器本身会预定义一些宏,它们在预处理时会被展开为特定的值,非常有用。
①常见预定义宏
printf("File: %s\n", __FILE__); // 当前源代码文件名
printf("Line: %d\n", __LINE__); // 当前行号
printf("Date: %s\n", __DATE__); // 编译日期 (格式 "MMM DD YYYY")
printf("Time: %s\n", __TIME__); // 编译时间 (格式 "HH:MM:SS")
printf("Function: %s\n", __func__); // 当前函数名 (C99标准)
// 检查操作系统
#ifdef __linux__// Linux-specific code
#elif defined(_WIN32)// Windows-specific code
#endif
4.删除注释
预处理器的首要任务之一就是移除所有注释,因为注释对编译器毫无意义。
// 这是一个单行注释
int a = 10; /* 这是一个多行注释 */
int b = 20; // 另一个注释int a = 10;
int b = 20;
5. 处理其他编译器指令
①#error:强制产生一个编译错误并输出消息。
#ifndef REQUIRED_MACRO
#error "REQUIRED_MACRO is not defined! Please define it." // 编译将在此处停止
#endif
②#pragma:向编译器传递实现特定的指令。
#pragma once // 非标准但广泛支持的头文件保护符,作用类似#ifndef/#define
#pragma message("Compiling this file...") // 在编译输出中显示一条消息
#pragma warning(disable: 4996) // (MSVC) 禁用特定编号的警告
③#line:改变 __LINE__
和 __FILE__
宏的值。
//改变 __LINE__
#include <stdio.h>int main() {printf("This is line %d in file %s\n", __LINE__, __FILE__);#line 100 // 从这里开始,行号被重置为100printf("This is line %d in file %s\n", __LINE__, __FILE__);printf("This is line %d in file %s\n", __LINE__, __FILE__);return 0;
}//打印
This is line 4 in file example.c
This is line 100 in file example.c
This is line 101 in file example.c//=====================================================
//同时改变行号和文件名
1: #include <stdio.h>
2:
3: int main() {
4: printf("Error location: %s:%d\n", __FILE__, __LINE__);
5:
6: #line 1 "fake_file.h" // 指令生效!
7: printf("Error location: %s:%d\n", __FILE__, __LINE__);
8:
9: int x = 5;
10: printf("Error location: %s:%d\n", __FILE__, __LINE__);
11:
12: int y = x / 0; // 除零错误!
13:
14: return 0;
15: }//错误发生在原始代码的第12行,但因为我们用 #line 设置了文件名和行号。
//编译器报告错误时,会显示 fake_file.h(5),而不是真正的文件名和行号。//编译时的错误信息:
fake_file.h(5): error C2124: divide or mod by zero/*
报错信息分解:
①fake_file.h:编译器报告的发生错误的文件名。在代码中使用了 #line 1 "fake_file.h" 指令。这条指令强制编译器将它之后处理的所有代码都“当作”是来自一个名为 fake_file.h 的虚拟文件。所以,当错误发生时,编译器诚实地报告了它“认为”的当前文件名。
②(5):编译器报告的发生错误的行号。为什么是5:这是由 #line 1 "fake_file.h" 指令设置的。该指令将下一行(第一个 printf)的逻辑行号设置为 1。下一行(int x = 5;)的逻辑行号就是 2。再下一行(第二个 printf)的逻辑行号是 3。注释行 // 这行代码会产生... 的逻辑行号是 4。出错的行 int y = x / 0; 的逻辑行号就是 5。
③error C2124: divide or mod by zero:错误的类型和描述。这是Microsoft Visual C++编译器的特定错误代码(C2124),表示在编译时检测到了除数为零的运算(除法/或取模%)。这是一个致命错误,会导致编译失败。
*/
结果:
输出一个“纯净”的、没有注释和宏定义的文本文件(通常为.i
文件)。
(二)编译
①词法分析(输出Token流):将每句代码拆分成基础词法单元。
将“int a = 10 + 5;”拆分成" int ","a "," = "," 10 "," + "," 5 " " ; "。如果出现词法错误就会报错
②语法分析(输出AST(抽象语法树)):检查“单词”是否符合语法规则,并生成一棵“语法树”。
检查“int a = 10 + 5;”符不符合C语言的语法(类型 变量名 = 表达式;)不符合的话就会语法报错。(下图就是“语法树”)
Declaration/ | \type name initializer(int) (a) |BinaryExpr/ | \op lhs rhs(+) (10) (5)
③语义分析(装饰AST:确认类型等):检查语句是否有意义。
检查“int a = b + c”,会去检查“a”、“b”是否已经声明,类型是否正确。如果类型不能进行相关运算或者未声明,会语义报错。
④生成中间表示/优化:生成非常接近机器码,但是又不依赖具体PCU的中间表示(中间表示不是),并且将进行各种优化。
" int a = 10 + 5 "直接优化成"int a = 15",再之后给人看的变量也会不存在,而是直接给立即数“15”分配一个地址空间,之后程序访问“a”值就是直接去立即数“15”的地址
⑤代码生成:
将优化后的中间表示转换为目标相关的汇编代码(转换成不同系统架构适用的汇编代码:ARM、X86)
结果:输出汇编代码(.s文件)
(三)汇编
将经过编译的代码翻译成机器码(二进制指令)
结果:输出目标文件(通常为.o
或.obj
文件),里面已经是机器码了,但还不完整。
(四)链接
链接各种文件做集成:
一个程序通常由多个源文件(.c
)编译成多个目标文件(.o
),并且会用到标准库(如printf
函数)。链接器的工作就是把所有这些零散的目标文件拼凑在一起。解决它们之间的相互引用问题(比如你在main.c
里调用了function.c
里的一个函数,链接器负责把这个调用关系连接上)。把标准库的代码也“链接”进来。
输出:输出:最终的可执行文件(如Windows的.exe
,Linux的elf
,单片机的.hex
或.bin
)
二、C和python的编译器的不同
(1)C的流程:
C 源代码
-> C#编译器 -> IL 中间代码
-> (运行时) -> JIT 编译器 -> 本地机器码
-> CPU 执行
c是编译完所有代码之后才会执行。所以编译的时候会比代码运行时的报错少,比如10÷0可以被编译通过,只会在代码运行到对应位置,才会因为语法错误,导致程序停止运行
(2)python的流程:
Python 源代码
-> Python 编译器 -> 字节码
-> (运行时) -> PVM 解释器 -> C 函数 -> CPU 执行
python是编译一句执行一句,所以当编译10÷0之后就会立刻执行,然后就报错停止运行。