C++编译原理

参考:《编译原理》
编译过程1
编译过程2
编译过程3
编译过程4

C++编译的过程:预处理、编译、汇编、链接

答:

一、预编译(预处理)

预编译程序所完成的基本上是对源程序的“替代”工作。经过此种替代,生成一个没有宏定义、没有条件编译指令、没有特殊符号的输出文件。这个文件的含义同没有经过预处理的源文件是相同的,但内容有所不同。

(1)由源文件.cpp或.c生成.i文件,这是在预编译阶段完成的;(命令:gcc -E .cpp/.c —>.i)

(2)主要功能

  1. 展开所有的宏定义,消除“#define”;
  2. 处理所有的预编译指令,比如#if、#ifdef等;
    3. 处理#include预编译指令,将包含文件插入到该预编译的位置;
  3. 删除所有的注释“/**/”、"//"等;
  4. 添加行号和文件名标识,以便于编译时编译器产生调试用的行号信息以及错误提醒;
  5. 保留所有的#program编译指令,原因是编译器要使用它们;

(3)缺点:不进行任何安全性及合法性检查

二、编译

编译过程,就是把经过预编译生成的文件,进行一系列语法分析、词法分析、语义分析优化后生成相应的汇编代码文件。

(1)由.i文件生成.s文件,这是在编译阶段完成的;(命令:gcc -S .i —>.s)

(2)主要功能

  1. 词法分析:
    将源代码文件的字符序列划分为一系列的记号。
    一般词法分析产生的记号有:标识符、关键字、数字、字符串、特殊符号(加号、等号);
    在识别记号的同时也将标识符放入符号表、将数字、字符放入到文字表等;
    有一个lex程序可以实现词法扫描,会按照之前定义好的词法规则将输入的字符串分割成记号,所以编译器不需要独立的词法扫描器;
    ( 如果存在括号不匹配或者表达式错误,编译器就会报告语法分析阶段的错误)

  2. 语法分析:
    语法分析器将对产生的记号进行语法分析,产生语法树----就是以表达式尾节点的树,一步步判断如何执行表达式操作。

  3. 语义分析:
    由语法阶段完成分析的并没有赋予表达式或者其他实际的意义,比如乘法、加法、减法,必须经过语义阶段才能赋予其真正的意义;
    语义分析主要分为静态语义和动态语义两种;
    静态语义通常包括声明和类型的匹配、类型的转换。比如当一个浮点型的表达式赋值给一个整型的表达式时,其中隐含了一个浮点型到整型转换的过程。只要存在类型不匹配编译器会报错。经过语义分析后的语法树的所有表达式都有了类型。
    动态语义分析只有在运行阶段才能确定。

  4. 优化后生成相应的汇编代码文件

  5. 汇总所有符号

三、汇编

生成可重定位的二进制文件(.obj文件)

(1)由.s文件生成的.obj文件;(命令:gcc -c .s–>.o)

(2)此文件中生成符号表,能够产生符号的有:所有数据都要产生符号、指令只产生一个符号(函数名);

四、链接

链接阶段完成后生成可执行文件.exe,链接阶段主要分为两部分:

(1)合并所有“.obj”文件的段,并调整段偏移和段长度(按照段的属性合并,属性可以是“可读可写”、“只读”、“可读可执行”,合并后将相同属性的组织在一个页面内,比较节省空间),合并符号表,进行符号解析完成后给符号分配地址;
其中符号解析的意思是:所有.obj符号表中对符号引用的地方都要找到该符号定义的地方。
在编译阶段,有数据的地方都是0地址,有函数的地方都是下一行指令的偏移量-4(由于指针是4字节);
可执行文件以页面对齐。

符号重定位举例:main.c extern int gdata; test.c int gdata = 10;
main.o UND gdata -------->test.o gdata //符号重定位

在进行符号解析时要注意只对global符号进行处理,对于local符号不做处理。

(2)符号的重定位(链接核心):将符号分配的虚拟地址写回原先未分配正确地址的地方。

对于数据符号会存准确地址,对于函数符号,相对于存下一行指令的偏移量(从PC寄存器取地址,并且PC中下一行指令的地址)。

五、程序运行

这一部分主要是理解操作系统是如何管理进程和内存的。

(1)创建虚拟地址空间到物理空间的映射(创建内核地址映射结构体),创建页目录和页表。

(2)加载代码段和数据段。

(3)把可执行文件的入口地址写到CPU的PC寄存器里。

总结

C++编译过程主要分为四个阶段,分别为预编译、编译、汇编、链接。

预编译阶段:处理宏定义指令#define、头文件包含指令#include、条件编译指令#ifdef等,完成对源程序中伪指令和特殊符号的“替换”。

编译阶段:主要是进行语法检查,并生成汇编代码。

汇编阶段:主要是把汇编代码翻译成机器语言,生成目标文件.obj。

链接阶段:主要是把目标代码(obj文件)与调用的库函数代码或自定义文件代码连接起来,形成对应的可执行文件(exe文件)。

在这里插入图片描述

注:
1.编译器会将一个工程里的所有.cpp文件以分离的方式编译完毕后,再由链接器进行链接成为一个可执行文件。
2.在编译过程中头文件不参与编译,预编译时进行各种替换以后,头文件就完成了其光荣使命,不再具有任何作用。
3.头文件的作用是保证当前源文件编译不会出错,头文件本身不参与编译过程。

几个常识

1. #include“animal.h”和#include <animal.h>的区别?

.答:
<>和“”表示编译器搜索头文件的顺序不同。

<>表示从系统目录下开始搜索,然后再搜索PATH环境变量所列出的目录,不会搜索当前目录。

""表示先从当前目录搜索,然后是在系统目录和PATH环境变量所列出的目录下搜索。
.
因此,如果我们知道头文件在系统目录或者环境变量目录下时,可以用<>来加快搜索速度。

2. #include的作用?

答:
在编译.cpp文件时,该文件中的#include<xxx.h>会被xxx.h文件替换。

3.头文件如何来关联其对应的源文件?

答:
参考:https://blog.csdn.net/sinat_36053757/article/details/64444556

Q:
已知头文件“a.h”声明了一系列函数,“a.cpp”中实现了这些函数,那么如果我想在“b.cpp”中使用“a.h”中声明的这些在“a.cpp”中实现的函数,通常都是在“b.cpp”中使用#include“a.h”,但是“a.h”并没有包含“a.cpp”,一般是“a.cpp”中包含“a.h”,那么b.cpp是怎样找到a.cpp中的实现呢?

A:
编译的时候,会编译project下所有.cpp文件,但是在编译b.cpp文件时,并不会去找a.cpp文件中的函数实现,只有在link的时候才进行这个工作。
我们在a.cpp或b.cpp中用#include“a.h”实际上是引入相关声明,使得编译可以通过,程序并不关心实现是在哪里,是怎么实现的。
源文件编译后成生了目标文件(.obj文件),目标文件中,这些函数和变量就视作一个个符号。在link的时候,需要在makefile里面说明需要连接哪个.obj文件(在这里是a.cpp生成的.obj文件),此时,连接器会去这个.obj文件中找在a.cpp中实现的函数,再把他们build到makefile中指定的那个可以执行文件exe中。

在VC中,一帮情况下不需要自己写makefile,只需要将需要的文件都包括在project中,VC会自动帮你把makefile写好。
这也是为什么b.cpp只包含a.h,而不用包含a.cpp,且a.h也不用包含a.cpp,b.cpp文件中包含的a.h是为了保证b.cpp文件在编译时不会出错,a.h本身不会被编译。编译器在链接时,会将a.cpp生成的目标文件链接到可执行文件exe中,这是通过Makefile完成的,并不是a.h来完成

总结
1.头文件的作用是保证当前源文件编译不会出错,头文件本身不参与编译。
2.头文件相对应的cpp文件,由于在工程文件夹下,在编译阶段,编译器也会将其生成目标文件。在链接阶段时,其会靠编译器来自动链接到当前可执行文件中,而不是靠头文件。(主要把头文件对应的cpp文件放在project下,编译器在链接阶段就会自动链接)

4.C++中的.cpp文件和.h文件

答:
参考:https://www.cnblogs.com/fenghuan/p/4794514.html
头文件.h:写类的声明(包括类里面的成员和方法的声明)、函数原型、#define常数等,但一般来说不写出具体的实现。
源文件.cpp:主要写实现头文件中已经声明的那些函数的具体代码。
需要注意的是,开头必须#include一下实现的头文件,以及要用到的头文件。那么当你需要用到自己写的头文件中的类时,只需要#include进来就行了。

1).h叫做头文件,它是不能被编译的。“#include”叫做编译预处理指令,可以简单理解成,在1.cpp中的#include"1.h"指令把1.h中的代码在编译前添加到了1.cpp的头部。每个.cpp文件会被编译,生成一个.obj文件,然后所有的.obj文件链接起来你的可执行程序就算生成了。

好的习惯是,头文件中应只处理常量、变量、函数以及类等的声明,变量的定义和函数的实现等都应该在源文件.cpp中进行。

至于.h和.cpp具有同样的主文件名的情况呢,对编译器来讲是没有什么意义的,编译器不会去匹配二者的主文件名,相反它很傻,只认#include等语句。但是这样写是一种约定俗成的编程风格,一个类的名字作为其头文件和源文件的主文件名比如Class1.h和Class1.cpp,这个类的声明在Class1.h中,实现在Class1.cpp中,我们人类看起来比较整齐,读起来方便,也很有利于模块化和源代码的重用。

为什么这个风格会约定俗成?有一句著名的话,叫“程序是为程序员写的”。

2)声明不会分配内存,定义才会被分配内存。在h文件中声明Declare,而在cpp文件中定义Define。 “声明”向计算机介绍名字,它说,“这个名字是什么意思”。而“定义”为这个名字分配存储空间。无论涉及到变量时还是函数时含义都一样。无论在哪种情况下,编译器都在“定义”处分配存储空间。对于变量,编译器确定这个变量占多少存储单元,并在内存中产生存放它们的空间。对于函数,编译器产生代码,并为之分配存储空间。函数的存储空间中有一个由使用不带参数表或带地址操作符的函数名产生的指针。定义也可以是声明。如果该编译器还没有看到过名字A,程序员定义int A,则编译器马上为这个名字分配存储地址。声明常常使用于extern关键字。如果我们只是声明变量而不是定义它,则要求使用extern。对于函数声明, extern是可选的,不带函数体的函数名连同参数表或返回值,自动地作为一个声明。

5. #ifdef、#else、#endif

答:
参考:
参考1
参考2

作用:用在源文件中,选择指定代码进行编译。

一般情况下,源程序中所有的行都参加编译。但是有时希望对其中一部分内容只在满足一定条件才进行编译,也就是对一部分内容指定编译的条件,这就是“条件编译”。当满足某条件时对一组语句进行编译,而当条件不满足时则编译另一组语句。

条件编译命令最常见的形式为:

#ifdef 标识符 
程序段1 
#else 
程序段2 
#endif

它的作用是:当标识符已经被定义过(一般是用#define命令定义),则对程序段1进行编译,否则编译程序段2。

其中#else部分也可以没有,即:

#ifdef 
程序段1 
#endif

6. #ifndef、/#define、#endif

答:
参考:参考1

作用:用在头文件中,防止该头文件被重复引用。

被重复引用:是指一个头文件在同一个cpp文件中被include了多次,这种错误常常是由于include嵌套造成的。
比如:a.h文件中有#include “c.h”,而此时b.cpp文件导入了#include “a.h” 和#include “c.h”,那么此时就会造成c.h重复引用,因为b.cpp文件中包含了两次c.h文件。

头文件被重复引用引起的后果:有些头文件重复引用只是增加了编译工作的工作量,不会引起太大的问题,仅仅是编译效率低一些,但是对于大工程而言,编译效率低下也会需要避免的。有些头文件重复包含,会引起错误。

使用:
#ifndef // #ifndef A_H意思是"if not define A_H " 如果不存在A_H ,就#define A_H
#define A_H
#endif //否则,不执行#ifndef下面的语句,直接跳到 #endif

含义:当a.h是第一次被调用时, #ifndef A_H成立,便定义A_H,同时头文件a.h会被调用。
当第二次调用a.h文件时,由于 #ifndef A_H不成立,直接跳到 #endif ,那么头文件a.h就不会被调用。

建议:所有头文件前后都加上ifndef/define/endif

  • 5
    点赞
  • 45
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值