编译器的相关知识(入门时著)

作用:将高级语言(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 定义的宏。

对象式宏:简单的文本替换。
cs 复制代码
// 源代码
#define PI 3.14159
#define BUFFER_SIZE 1024

double circumference = 2 * PI * radius;
char buffer[BUFFER_SIZE];

// 预处理后
double circumference = 2 * 3.14159 * radius; // PI被替换
char buffer[1024]; // BUFFER_SIZE被替换
函数式宏:带参数的宏。
cs 复制代码
// 源代码
#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**:根据条件判断。
cs 复制代码
#define VERSION 2

#if VERSION == 1
    printf("Running version 1\n");
#elif VERSION == 2
    printf("Running version 2\n"); // 只有这部分代码会被保留
#else
    printf("Running unknown version\n");
#endif
②**#ifdef, #ifndef**:检查宏是否被定义(常用于头文件保护符和功能开关)。
cs 复制代码
// 头文件保护符:防止头文件被多次包含
#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.处理预定义宏

编译器本身会预定义一些宏,它们在预处理时会被展开为特定的值,非常有用。

①常见预定义宏
cs 复制代码
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.删除注释

预处理器的首要任务之一就是移除所有注释,因为注释对编译器毫无意义。

cs 复制代码
// 这是一个单行注释
int a = 10; /* 这是一个
              多行注释 */
int b = 20; // 另一个注释

int a = 10; 
int b = 20; 

5. 处理其他编译器指令

①**#error**:强制产生一个编译错误并输出消息。
cs 复制代码
#ifndef REQUIRED_MACRO
#error "REQUIRED_MACRO is not defined! Please define it." // 编译将在此处停止
#endif
②**#pragma**:向编译器传递实现特定的指令。
cs 复制代码
#pragma once // 非标准但广泛支持的头文件保护符,作用类似#ifndef/#define
#pragma message("Compiling this file...") // 在编译输出中显示一条消息
#pragma warning(disable: 4996) // (MSVC) 禁用特定编号的警告
③**#line** :改变 __LINE____FILE__ 宏的值。
cs 复制代码
//改变 __LINE__ 
#include <stdio.h>

int main() {
    printf("This is line %d in file %s\n", __LINE__, __FILE__);
    
#line 100 // 从这里开始,行号被重置为100
    printf("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之后就会立刻执行,然后就报错停止运行。

相关推荐
Lecxcy_Kastreain1 天前
解决VSCode默认F5配置无法启动调试器的问题
ide·vscode·编辑器
一骑红尘荔枝来1 天前
转载:VSCODE 关闭文件和资源管理器关联
ide·vscode·编辑器
2501_915918411 天前
Web 前端可视化开发工具对比 低代码平台、可视化搭建工具、前端可视化编辑器与在线可视化开发环境的实战分析
前端·低代码·ios·小程序·uni-app·编辑器·iphone
乐吾乐科技2 天前
乐吾乐大屏可视化组态软件【SQL数据源】
物联网·信息可视化·编辑器·数据可视化·大屏端
数字冰雹2 天前
“图观”端渲染场景编辑器
人工智能·编辑器
云梦谭2 天前
Cursor 编辑器:面向 AI 编程的新一代 IDE
ide·人工智能·编辑器
ONLYOFFICE3 天前
【技术教程】如何将ONLYOFFICE文档集成到使用Spring Boot框架编写的Java Web应用程序中
java·spring boot·编辑器
Blue桃之夭夭3 天前
Visual Studio Code设置个性化背景教程
ide·vscode·编辑器
EQ-雪梨蛋花汤3 天前
【Unity笔记】Unity 编辑器扩展:打造一个可切换 Config.assets 的顶部菜单插件
unity·编辑器·游戏引擎