编译原理
一、预处理(预编译)阶段
1.头文件的包含
2.清除注释
3.宏的替换
4.处理所有的条件编译指令,如#ifdef #ifndef #endif等,也就是带#那些
5.保留#pargma指令
6.添加行号和文件名标识,方便调试使用
此时源程序还是文本文件。这个过程不会检查错误,生成预处理文件xxx.ii。
在C语言中,预编译将.h文件进行文本级的扩展与.c 文件一起编译成.i中间文件。
在C++中,预编译将.cpp和.hpp文件编译成.ii中间文件
二、编译阶段
把预处理完的文件xxx.ii进行词法分析、语法分析、语义分析、优化,生成汇编代码文件xxx.s。汇编语言还是文本文件,但CPU无法理解文本文件。
这个过程会检查语法错误,也可以通过参数来屏蔽某些编译告警。
模板(template)和内联(inline)在大多数编译器中都是在编译阶段进行处理。template在编译阶段完成具现化。inline在编译阶段将函数调用替换为函数本体,从而减少函数调用的开销,(需要注意一般较短且不包含switch、while等复杂结构控制语句的函数会被展开,若内联的代码过长inline关键字会被编译器忽略,不予展开)。
(一)词法分析:
词法分析是编译过程的第一个阶段,这个阶段的任务可以看成是从左到右一个字符一个字符地读入源程序,从中识别出一个个单词符号,即对构成源程序的字符流进行扫描然后根据构词规则识别单词(也称单词符号或符号)。上述读入源程序的过程和识别符号的任务通过词法分析程序实现,词法分析整个过程依据的是语言的词法规则。词法分析程序的输出通常是一个二元组,即单词种别和单词自身的值。词法分析程序可以使用lex等工具自动生成。
(二)语法分析:
语法分析是编译过程的一个逻辑阶段,此阶段的任务是在词法分析的基础上将单词序列组合成各类语法短语,如“程序”,“语句”,“表达式”等等。语法分析程序判断源程序在结构上是否正确。
(三)语义分析
语义分析是编译过程的一个逻辑阶段,语义是解释控制信息每个部分的意义,它规定了需要发出何种控制信息,以及完成的动作与做出什么样的响应,此阶段的任务是对结构上正确的源程序进行上下文有关性质的审查, 进行类型审查,语义分析将审查类型并报告错误。也就是说,语义分析结合上下文推导出语句真正的含义。
三、汇编阶段
编译后的 xxx.s 文件经汇编器 (as) 将其中的每条汇编代码转变成机器可执行的二进制指令并生成一个可重定位目标文件 (relocatable object files) xxx.o。
window平台上是.obj文件,linux是.o文件,为可重定位的目标文件,这种文件类型是链接过程的直接输入。
汇编器仅仅是按照汇编指令和机器指令的对照表将汇编代码进行翻译。一般认为汇编和机器码是一一对应的。当然也有例外,现代汇编中的伪指令和复杂的分支循环结构不能与机器码一一对应。
之所以要经过预处理、编译、汇编这么一系列步骤才生成目标文件,是因为在每一阶段都有相应的优化技术,只有在每个阶段分别优化并生成最为高效的机器指令才能达到最大的优化效果,如果一步到位直接从源程序生成目标文件,可能就会失去很多代码优化的机会。
四、链接阶段
链接是将多个目标文件以及所需的库文件(静态库为XXX.a或XXX.lib,动态库为XXX.so或XXX.dll)链接成最终的可执行文件。
(一)静态链接
在链接期,将静态链接库中的内容直接装填到可执行程序中。
在程序执行时,这些代码都会被装入该进程的虚拟地址空间中。
1.优点
运行速度快并且不依赖外部环境:因为在可执行程序中已经具备了所有执行程序所需要的任何东西,在执行的时候运行速度快。
2.缺点
浪费空间:因为每个可执行程序中对所有需要的目标文件都要有一份副本,如果运行多个程序并且这些程序都对同一个目标文件有依赖,那么目标文件在内存中就会存在多个副本;
更新困难:因为每当一个依赖文件的代码修改了,这个时候就需要全部重新编译链接形成新的可执行程序。
注意:链接器在链接静态链接库的时候是以目标文件为单位的。比如引用了静态库中的printf()函数,那么链接器就会把库中包含printf()函数的那个目标文件链接进来,如果很多函数都放在一个目标文件中,很可能很多没用的函数都被一起链接进了输出结果中。由于运行库有成百上千个函数,数量非常庞大,每个函数独立地放在一个目标文件中可以尽量减少空间的浪费,那些没有被用到的目标文件就不要链接到最终的输出文件中。
(二)动态链接
在链接期,只在可执行程序中记录与动态链接库中共享对象的映射信息。
在程序执行时,动态链接库的全部内容被映射到该进程的虚拟地址空间。其本质就是将链接的过程推迟到运行时处理
1.优点
节约内存: 即使需要每个程序都依赖同一个库,但是该库不会像静态链接那样在内存中存在多分,副本,而是这多个程序在执行时共享同一份副本;
更新方便: 更新时只需要替换原来的目标文件,而无需将所有的程序再重新链接一遍。当程序下一次运行时,新版本的目标文件会被自动加载到内存并且链接起来,程序就完成了升级的目标。
2.缺点
性能略差: 因为把链接推迟到了程序运行时,所以每次执行程序都需要进行链接,所以性能会有一定损失。
依赖外部环境: 因为把链接推迟到了程序运行时,所以要保证程序运行时外部的库存在且内容正确无误。
五、生成exe
经过这四个步骤以后,一个完整的可执行程序exe就生成了。
六、注意点
(一)防止多重包含
在预处理阶段,你会经常看到#ifndef #define #endif
。这是为了防止重复包含。在预处理器第一次处理这个头文件时,会#define一个宏,当其他文件再次包含这个头文件时,预处理器检测到这个宏,会直接跳过这段代码,这样就不会出现重复代码
(二)cpp到obj再到exe
在编译成.obj文件时,一个.h和对应的.cpp会形成一个.obj文件,你的工程中如果有多个.h和.cpp文件,就会生成多个.obj文件,例如一个C_Object类的.h和.cpp文件将会生成一个.obj文件,此时已经成为机器语言的.obj文件,将会在链接时将会链接到一起生成可执行文件。
上述过程分为两步:
(1)每个cpp先生成各自对应的obj即编译单元;
(2)最后通过链接器把所有obj链接成一个exe形成一个程序。
而重定义在这两步之中都可能发生。
第一步中,如果一个cpp内不小心定义了多份(>=2)相同数据,在生成obj,还没链接前就已经报错了。
第二步也是最常出现重定义的地方。你可能不小心地在两个cpp中定义了相同的数据,各自生成obj的时候并不会报错,但是当链接的时候就会出现重定义。或者你的项目的多份cpp都include了同一份.h文件,而这个.h文件存在定义,一样的,在各自生成obj的时候不会报错(如果没有其他的重定义的话),但是当链接的时候就会报错。(注意:是要.h中有定义,只是声明不会报错)
(三)例子
//a.h #ifndef A_H //防止多重包含 #define A_H class C_A { pulic: void funA(); }; #endif//A_H
//a.cpp #include<iostream> #include"a.h" #include "global.h" void C_A::funA() { i=0; std::cout <<i <<std::endl; }
//b.h #ifndef B_H #define B_H class C_B { public: void funB(); }; #endif // B_H
//b.cpp #include<iostream> #include"global.h" #include"b.h" void C_B::funB() { i=1; std::cout << i << std::endl; }
//global.h #ifndef GLOBAL_H #define GLOBAL_H extern int i; #endif //GLOBAL_H
//global.cpp int i ;//申请内存
//main.cpp #include"a.h" #include"b.h" void main() { C_A a; C_B b; a.funA(); b.funB(); }
程序很简单,但是如果要注释掉extern int i;IDE会报什么样的错误呢?
首先在预处理阶段不会报错,三个cpp文件会将他们包含的头文件的代码部分在分别的.cpp文件展开,成为新的文本文件。
在编译阶段也不会出错,因为没有语法错误,新的源文件会被编译成汇编语言,此时还是一个文本文件,此时应该还定义了程序中的变量应该申请多少内存,在程序运行时向系统索要内存。
在汇编阶段将会转化为a.obj、b.obj、gobal.obj,main.obj,此时为二进制,为机器语言。
链接阶段,应该为将四个文件链接到一起,a.obj中包括一个整形的i被定义,b.obj中也有一个i被定义,global.obj也有一个i被定义,链接到一起时检测到多个相同名称的变量存在,出错!报错为找到一个或多个多重符号的定义。所以要在global.h文件中声明i,这样连接器检测到i为一个声明,在不同的obj文件中进行赋值。通过编译。
那在global.h文件中声明一个函数,在.cpp文件实现,编译器会不会说函数多重定义呢?答案是不会,因为函数声明和定义的方式不一样,而变量的声明和定义如果不加extern关键字则会一样,无法辨别变量是声明还是定义,所以要有extern的存在。
若没有extern上述程序问题为:
“i”的多重定义
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 记一次.NET内存居高不下排查解决与启示
· DeepSeek 开源周回顾「GitHub 热点速览」
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了