预处理、编译、链接

由多个源文件组成的C程序,经过编辑、预处理、编译、链接等阶段会生成最终的可执行程序
1.编辑:也就是编写C/C++程序。 .C
2.预处理:相当于根据预处理指令组装新的C/C++程序。经过预处理,会产生一个没有宏定义,没有条件编译指令,没有特殊符号的输出文件,这个文件的含义同原本的文件无异,只是内容上有所不同。
3.编译:将预处理完的文件进行一系列词法分析、语法分析、语义分析及优化后,产生相应的汇编代码文件。.s 在经过转换为机器码成为目标文件 .OBJ (debug模式下,会在obj文件内加入调试信息)
4.链接:通过链接器将一个个目标文件(或许还会有库文件)链接在一起生成一个完整的可执行程序。 链接程序的主要工作就是将有关的目标文件彼此相连接,也就是将在一个文件中引用的符号同该符号在另一个文外件中的定义连接起来,使得所有的这些目标文件成为一个能够被操作系统装入执行的统一整体。

编译时是对该标识符进行确定类型储存大小,链接时进行确定全局内存地址和函数的重定位,执行时分配内存空间

下面的内容仅是对于编译链接过程的语法介绍,目的是减少编程错误,而对应做了什么?,怎么工作后面?

一、预处理

预处理 将编写的源代码转换为预编译文件
1、由预处理器对程序代码作必要的转换处理,相当于根据预处理指令组装新的C/C++程序。经过预处理,会产生一个没有宏定义,没有条件编译指令,没有特殊符号的输出文件,这个文件的含义同原本的文件无异,只是内容上有所不同。
2、预处理命令:预处理命令改进了程序设计环境,提高了编程效率,但其不是C语言本身的组成部分,不能对它们进行编译(因为编译程序不能识别它们)。必须在对程序进行通常的编译(包括词法和语法分析,代码生成,优化等)之前,先对程序中这些特殊的命令进行“预处理”,即根据预处理命令对程序作相应的处理(例如,若程序中用#define 命令定义了一个符号常量A,则在预处理时将程序中所有的A都置换为指定的字符串。若程序中用#include命令包含一个文件“stdio. h”,则在预处理时将stdio.h文件中的实际内容代替该命令)。
PS:C语言与其他高级语言的一个重要区别是可以使用预处理命令和具有预处理的功能。
C分别用宏定义命令、文件包含命令.条件编译命令来实现对应功能。为了与一般C语句相区别,这些命令以符号“#”开头。

1.1 宏定义

1.2 “ 文件包括”处理

所谓“文件包含”处理是指一个源文件可以将另外一个源文件的全部内容包含进来,即将另外的文件包含到本文件之中。C语言提供了#include命令用来实现“文件包含”的操作。其一般形式为 #include<文件名> 或#include “文件名”
图9-2表示“文件包含”的含义。图9-2(a)为文件 file1.c,它有一个#include≤<file2.c>命令,然后还有其他内容(以A表示)。图9-2(b)为另一文件 file2.c,文件内容以B表示。在编译预处理时,要对#include命令进行“文件包含”处理:将file2.c的全部内容复制插入到#include ≤file2.c>命令处,即file2.c被包含到file1.c中,得到图9-2©所示的结果。在编译时,对将经编译预处理的 file1.c(即图9-2©所示)作为一个源文件单位进行编译。
在这里插入图片描述
“文件包含”命令是很有用的,它可以节省程序设计人员的重复劳动。例如,某单位的人员往往使用一组固定的符号常量(如g=9.81, pi=3.1415926,e=2.718,c=……),可以把这些宏定义命令组成一个头文件,然后每个人都可以用#include命令将这些符号常量包含到自己所写的源文件中。这样每个人就可以不必重复定义这些符号常量,相当于工业上的标准零件,拿来就用。
PS:
1.在编译时并不是对两个文件(存在文件包括关系的两个文件,即源文件和头文件)分别进行编译,然后再将它们的目标程序连接的,而是在经过编译预处理后将头文件 包含到主文件中,得到一个新的源程序,然后对这个文件进行编译,得到一个目标(.obj)文件。被包含的文件成为新的源文件的一部分,而单独转成目标文件。
2.如果需要修改程序中常用的一些参数,可以不必修改每个程序,只需把这些参数放在一个头文件中,在需要时修改头文件即可。但是应当注意﹐被包含文件修改后v凡包含此文件的所有文件都要全部重新编译。
3.C语言头文件一般包括了函数原型、宏定义、结构体类型定义和全局变量定义等。
4.如果文件1包含文件2,而在文件2中要用到文件3的内容,则可在文件1中用两个include命令分别包含文件2和文件3,而且文件3应出现在文件2之前,即在filel.c中定义:
#include “file3. h”
#include “file2.h”
这样,filel和file2都可以用file3的内容。在file2中不必再用#include " file3.h"了
(以上是假设file2.h在本程序中只被filel.c包含,而不出现在其他场合)。
5.在一个被包含文件中又可以包含另一个被包含文件,即文件包含是可以嵌套的。
例如,上面的问题也可以这样处理,见图9-3。它的作用与图9-4所示相同。

在这里插入图片描述
6.尖括号(如<stdio. h>形式),系统到存放C库函数头文件的目录中寻找要包含的文件,这称为标准方式。用双撇号(即"file2.h"形式)时,系统先在用户当前目录中寻找要包含的文件,若找不到,再按标准方式查找(即再按尖括号的方式查找)。一般来说,如果为调用库函数而用#include命令来包含相关的头文件,则用尖括号,以节省查找时间。如果要包含的是用户自己编写的文件(这种文件一般都在用户当前目录中),一般用双撇号。若文件不在当前目录中,在双撤号内应给出文件路径(如井include"C: l wang\ file2.h")。

1.3 条件编译

一般情况下,源程序中所有行都参加编译。但是有时希望程序中一部分内容只在满足一定条件时才进行编译,也就是对这一部分内容指定编译的条件,这就是“条件编译”。有时,希望在满足某条件时对某一组语句进行编译,而当条件不满足时则编译另一组语句。
条件编译命令有以下几种形式:
(1)#ifdef 标识符
程序段1
else
程序段2
#endif
它的作用是若所指定的标识符已经被#define命令定义过,则在程序编译阶段编译程序段1;否则编译程序段2。
其中else部分 可以没有即
#ifdef 标识符
程序段1(即可以是语句组,也可以是命令行)
#endif
作用:提高了C源程序的通用性。(如果一个C源程序在不同计算机系统上运行,而不同的计算机又有一定的差异(例如,有的机器以16位(2个字节)来存放一个整数,而有的则以32位存放一个整数),这样往往需要对源程序作必要的修改,这就降低了程序的通用性。可以用以下的条件编译来处理:

(2)
#ifndef标识符
程序段1
#else
程序段2
#endif
它的作用是若标识符未被定义过则编译程序段l;否则编译程序段2。这种形式与第一种形式的作用相反。

//如头文件里(防止头文件重复包含的两种方法)
#pragma once //编译阶段处理  第一种  只会被执行一次
#ifndef _LISH_H//看该宏是否已经存在,若存在则不执行  预编译时处理  第二种 
#define _LISH_H//该宏不存在,则创建一个宏。避免文件重复包含


#endif

(3)
#if 表达式
程序段1
#else
程序段2
#endif
当指定的表达式值为真(非零)时就编译程序段1;否则编译程序段2。可以事先给定条件,使程序在不同的条件下执行不同的功能。
作用:若不进行条件编译则目标程序长(因为所有语句都编译),运行时间长(因为在程序运行时对if语句进测试)。而采用条件编译,可以减少被编译的语句,从而减少目标程序的长度,减少运行间。当条件编译段比较多时,目标程序长度可以大大减少。有人会觉得其优越性不太明显,但是如果程序比较复杂善于使用条件编译,其优越性是比较明显的。

define与defined的区别

defined用来检测常量有没有被定义,若常量存在,则返回true,否则返回 false 是一个表达式,可以结合!,&&等表达式
define 是定义一个标识符

二、编译

编译(同一段代码用不同的编译选项、不同的编译器编译链接后,得到的可执行文件会有很多不同).
将预处理完的文件进行一系列词法分析语法分析语义分析及优化后,产生相应的汇编代码文件。

2.1 词法分析(将标识符记号放在符号表)

从较低的层次考察,程序是由序列所组成的,将程序分解成符号的过程就是词法分析。
术语“记号”(token)指的是程序的一个基本组成单元,其作用相当于一个句子中的单词。
PS:如字符‘-'和字符‘>‘组成的字符序列->,在不同的上下文环境中,一个代表->运算符,一个代表字符串"->"。
编译器中负责将程序分解为一个一个记号的部分,一般称为 “词法分析器
如:if (x > big) big = x;
可以分解成一个个记号:

if
(
x
>
big
)
big
=
x
;

而将标识符,作为符号。

词法分析中的“”“贪心法”

C语言的某些记号,例如/、、和=,只有一个字符长,称为单字符记号。而C语言中的其他记号,例如/ * 和==,以及标识符,包括了多个字符,称为多字符记号。
当C编译器读入一个字符’/‘后又跟了一个字符’
’,那么编译器就必须做出判断:是将其作为两个分别的记号对待,还是合起来作为一个记号对待。C语言对这个问题的解决方案可以归纳为一个很简单的规则:每一个记号应该包含尽可能多的字符。也就是说,编译器将程序分解成记号的方法是,从左到右一个字符一个字符地读入,如果该字符可能组成一个记号,那么再读入下一个字符,判断已经读入的两个字符组成的字符串是否可能是一个记号的组成部分;如果可能,继续读入下一个字符,重复上述判断,直到读入的字符组成的字符串已不再可能组成一个有意义的符号。这个处理策略有时被称为“贪心法”,或者,更口语化一点,称为“大嘴法”。Kernighan与 Ritchie对这个方法的表述如下,“如果(编译器的)输入流截止至某个字符之前都已经被分解为一个个符号,那么下一个符号将包括从该字符之后可能组成一个符号的最长字符串。” (剑指OFFER上不是有题类似题目,哈哈,后面贴上来)
PS:
需要注意的是,除了字符串与字符常量,符号的中间不能嵌有空白(空格符、
制表符和换行符)。
例如,1.== 是单个符号,而= =则是两个符号的含义不同。
2.如果/是为判断下一个符号而读入的第一个字符,而/之后
紧接着*,那么无论上下文如何,这两个字符都将被当作一个符号/*,表示一段注
释的开始。
y = x/p / p指向除数, ,其本意是用x除以p所指向的值,然后把所得的商再赋给y */; 这已经不是运算符优先级的问题了,而是词法分析问题,扫描时的问题。
而实际上,/被编译器理解为一段注释的开始,编译器将不断地读入字符,直到/出现为止。也就是说,该语句直接将x的值赋给y, 根本不会顾及到后面出现的p。将上面的语句重写如下:
y=x/ p/ p指向除数 /; (加空格)
或者更加清楚一点,写作:
y = x/ (p)/ p指向除数
/;
这样得到的实际效果才是语句注释所表示的原意。

2.2 语法分析(确定运算符号的含义和优先级)

理解函数声明

词法分析器将程序分成一个一个符号,但各个符号又组合成了声明、表达式、语句和程序。
对于像如下语句

(*(void(*)( ))0) ();

构造这类表达式其实只有一条简单的规则:按照使用的方式来声明
任何C变量的声明都由两部分组成:
类型以及一组类似表达式的声明符( declarator)。声明符从表面上看与表达式有些类似,对它求值应该返回一个声明中给定类型的结果。
即声明=: 类型+声明符号
最简单的声明符就是单个变量,如:

float f,g;

这个声明的含义是:当对其求值时,表达式f和g的类型为浮点数类型(float)。
函数和指针类型的声明同理,如:

float ff();

这个声明的含义是:表达式ff()求值结果是一个浮点数,也就是说,ff是一个返回值为浮点类型的函数。类似地,

int a[3];

这个声明的含义是:a是一个拥有三个整型元素的数组

struct {
			int p[4];
			double x;
			}b[17];
			

这个声明的含义是:b是一个拥有17个元素的数组,其中每一个元素都是一个结构,该结构中包括了一个拥有4个整型元素的数组和一个双精度类型的变量

int calendar[12][31];//int (*p)[31] *p是一个整形数组,p是指向数组的指针

这个语句声明了calendar是一个数组,该数组拥有12个数组类型的元素,其中每个元素都是一个拥有31个整型元素的数组
所以sizeof(calendar)=3112sizeof(int)

float * pf:

这个声明的含义是: *pf是一个浮点数,也就是说,pf是一个指向浮点数的指针
以上声明还可以组合,如:

float *g()(*h) () ;

表示 g()与(h)()是浮点表达式。由于()的结合优先级高于 g()也就是 *(g()): g是一个函数,该函数的返回值类型为指向浮点数的指针。(*h)() *h是一个函数,该函数的返回值为浮点数,即h是一个指向返回值为浮点数的函数的指针。
看到这里我们对所声明的符号的理解过程应该有了大致了解。
而我们知道了声明一个给定类型的变量,那么该类型的类型转换符就容易得到了:
只要把声明中变量名和声明末尾的分号去掉,再将剩余的部分用一个括号整个“封装”起来即可。

float (*h) () ;

表示h是一个指向返回值为浮点类型的函数的指针,因此,

(float (*)());

表示一个“指向返回值为浮点类型的函数的指针”的类型转换符
那么我们可以分两步来分析以下表达式:

(*(void(*)( ))0) ();

1.首先,一个指针函数的调用格式:

(*fp)();//调用一个fp所指向的函数

因为fp是一个函数指针,所以*fp就是该指针所指向的函数,所以(*fp)()就是调用该函数的方式。ANSIC 标准允许简写为fp(),但仅仅是简写;

*fp()// 相当于 *(fp())   将其当做*((*fp)())的简写   

2.找一个恰当的表达式来替换fp

*0)();

是不能生效的,运算符必须要一个指针来做操作数。而且,这个指针还应该是一个函数指针,这样经运算符作用后的结果才能作为函数被调用。因此,在上式中必须对0作类型转换,转换后的类型可以大致描述为:“指向返回值为void类型的函数的指针”。
如果fp是一个指向返回值为void类型的函数的指针,那么(fp)()的值为void,即fp的声明如下:
void (fp)();
那么将其改造成类型转换符 为:
(void (
)()),那么将常数0转型为“指向返回值为void的函数的指针”
(void (
)())0; // 使得0为指针类型,那么对其的调用就是如下

(*(void(*)( ))0) ();

typedef
typedef 将一切合法的变量定义转变为类声明型,即将声明符号转换成了其含义类型

tpyedef void (* funcptr)();
(*(funcptr)0)();

编译器怎么区分’*’:

int a=4;
int b=5;
int * p=&a;//'*'声明指针
int c=a*b;//*是一个乘法运算符
int c=*p;//* 是解引用

程序通过扫描器被分割成一系列记号后,类型与变量之间的* ,变量之间的*,赋值运算符和变量之间的*

具体做了什么

确定运算符优先级和含义。
语法分析阶段对这些内容进行区分。如果出现了表达式不合法,比如括号不匹配、表达式中缺少操作数等,编译器就会报告语法分析阶段的错误。但仅对表达式的语法层面进行分析,并不了解这个语句的真正含义,如/0(当然这个是在运行时才能判断的),两个指针做乘法在语法分析是检测不出来错误的,同样,指针和浮点数做乘除也检测不出来。

2.3 静态语义分析(静态时就能确定的语句)(类型转换) 静态

数组是包含给定类型的一组数据,即一组相同类型元素的集合。
1.C语言中只有一维数组,而且数组的大小必须在编译期就作为一个常数确定下来。然而,C语言中数组的元素可以是任何类型的对象,当然也可以是另外一个数组。这样,要“仿真”出一个多维数组就不是一件难事。
2.对于一个数组,我们只能够做两件事:确定该数组的大小,以及获得指向该数组下标为0的元素的指针。其他有关数组的操作,哪怕它们乍看上去是以数组下标进行运算的,实际上都是通过指针进行的。换句话说,任何一个数组下标运算都等同于一个对应的指针运算,因此我们完全可以依据指针行为定义数组下标的行为。(数组运算与指针运算融会贯通)

虽然对数组的操作都是通过指针进行的,但指针与数组也有区别,它们的储存方式不同

具体做了什么

(静态)语义分析器对符号表里的符号类型进行了类型标识(类型转换)

如:如当一个浮点数的表达式赋值给一个整形时,其中隐含一个浮点型到整形转换的过程(语义分析需要完成这个步骤),当一个float 的表达式赋值给一个指针,语义分析程序会发现这个类型不匹配,编译器将会报错,因为float不会隐式转换为指针类型

bool a=45//编译阶段将45根据类型转换为true

(动态)语义一般指在运行期出现的语义相关的问题,比如将0作为除数是一个运行期语义错误(但不是编译器的事情了)

经过语义分析,其整个语法树的表达式才被标识了类型,每个表达式(包括符号和数字都被标识了类型),语言分析器还对符号表里符号类型也做了更新。

如两个指针相乘,因为乘法类型也成为了指针类型,说明此时乘法错误,报错。

编译器尽量保证内存对齐,以此来使硬件有更好了效率

三、链接(一个.c文件生成一个.o文件,即是单独编译形成的.o文件)

编译时,各个文件的全局变量不相互透明,即可见域限制在文件内部。连接阶段,各个文件的内容被合并在一起,因而定义于文件内的全局变量在连接完成后,它的可见域被扩大到整个程序,即需要进行一系列命名冲突,如extern 是提醒编译器该符号为声明的,是未被定义的符号(需要重定位)、static,连接器的一个重要工作就是处理命名冲突
C编译器一般与连接器分离,连接器不了解C语言诸多细节,但连接器可以理解机器语言和内部布局。(连接器把目标模块看成是由一组外部对象组成。每个外部对象代表着机器内存中的某个部分,并通过一个外部名称来识别,因此程序中的每个函数和每个外部变量,如果没有被声明为static,就都是一个外部对象)链接器只处理global的符号
连接器的工作过程:
连接器的输入是一组目标模块和库文件。连接器的输出是一个载入模块。连接器读入目标模块和库文件,同时生成载入模块。对每个目标模块中的每个外部对象,连接器都要检查载入模块,看是否已有同名的强类型外部对象。如果没有,连接器就将该外部对象添加到载入模块中;如果有,连接器就要开始处理命名冲突。

3.2 外部检查类型(一般不属于编译器任务,是程序员的任务)

假定我们有一个C程序,它由两个源文件组成。一个文件中包含外部变量n的声明:
extern int n;
另一个文件中包含外部变量n的定义:
long n;//高版本可以检查此类错误(谁检测出来的呢?是链接过程还是编译过程?)
在这里插入图片描述
即,链接过程进行了符号解析。
这是一个无效的C程序,因为同一个外部变量名在两个不同的文件中被声明为不同的类型。然而,大多数C语言实现却不能检测出这种错误。编译器对这两个不同的文件分别进行处理,这两个文件的编译时间甚至可以相差好几个月。因此,编译器在编译一个文件时,并不知道另一个文件的内容。连接器可能对C语一无所知,因此它也不知道如何比较两个n的定义中的类型。
其次,在一个文件中定义:
如:char filename[]=”/etc/passwd”;
而在另一个文件中定义:
extern char * filename;
尽管在某些上下文环境,数组与指针非常类似,但他们毕竟不同。尽管在一个语句中引用filename的值将得到指向该数组起始元素的指针,但第一个filename的类型是“字符数组”,而不是“字符指针”。第二个filename被声明为一个指针。这两个对filename的声明使用的存储空间方式不同
在这里插入图片描述
在这里插入图片描述

具体做了什么

第一步 空间与地址分配
扫描所有的输入目标文件,获得它们的

  • 3
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值