鹏哥C语言复习——程序的编译、链接和预处理

目录

可执行程序的生成:

预处理(预编译):

预定义符号:

#define(重难点):

第一种的讲解(定义常量):

第二种的讲解(定义宏):

#和# #:

#undef:

命名约定:

条件编译:

编译:

词法分析:

语法分析:

语义分析:

汇编:

链接:

运行环境:

头文件的包含:

头文件的嵌套包含:


 

可执行程序的生成:

电脑不能直接执行C语言代码,计算机能够执行二进制指令;而编译器就是把C语言代码翻译成二进制指令,所以编译器就是完成翻译官的工作

28025b45d6514f479e517ba4c34e074b.jpg

注:可执行程序中包含了二进制指令,翻译环境一般是指编译器,运行环境一般是指操作系统

对于常见的编写代码的软件:vscode2022来说,它是一个集成开发环境(包含了编译器、编辑器、链接器、调试器)

c1.exe --- 编译器

link.exe --- 链接器

e252afbdca1440df90a70f88e1bcf55e.jpg

注:上图是在windows环境下的编译与链接(目标文件以 .obj 为后缀)

注意事项:

  • 多个 .c 文件单独经过编译器,编译处理生成对应的目标文件
  • 多个目标文件和链接库一起经过链接器处理生成最终的可执行程序
  • 链接库是指运行时库(它是支持程序运行的基本函数集合)或者第三方库

 

编译又可以分成:预处理(有些书也叫做预编译)、编译、汇编三个过程;Windows环境下编译原理与在linux环境下一致

0f19b7a30fa246ea923600d2baf06100.jpg

注:上图是在linux环境下的编译与链接(目标文件以 .o 为后缀)

 

预处理(预编译):

在C语言中,使用gcc来进行编译,预处理使用到的指令应该是 -E 选项,得到的是 .i 文件;可以通过 -o 来生成文件;预处理会进行以下操作

操作事项:

  • 将所有的 #define 删除,并展开所有的宏定义
  • 处理所有的条件编译指令,如:#if、#ifdef、#elif、#else、#endif
  • 处理#include预编译指令,将包含的头文件的内容插入到该预编译指令的位置,这个过程是递归进行的,也就是说被包含的头文件也可能包含其他头文件
  • 删除所有的注释
  • 添加行号和文件名标识,方便后续编译器生成调试信息等
  • 保留所有的#pragma的编译器指令,编译器后续会使用

经过预处理后的 .i 文件中不再包含宏定义,因为宏已经被展开。并且包含的头文件都被插入到 .i 文件中,所以当我们无法知道宏定义或者头文件是否包含正确的时候,可以查看预处理后的 .i 文件来确认

 

预定义符号:

C语言设置了一些预定义符号,可以直接使用,预定义符号也是在预处理阶段处理的。

1 __FILE__ //进行编译的源文件
2 __LINE__ //文件当前的行号
3 __DATE__ //文件被编译的日期
4 __TIME__ //文件被编译的时间
5 __STDC__ //如果编译器遵循ANSI C(C语言标准),其值为1,否则未定义

1是为了找到文件所在位置,2是指使用__LINE__语句的行号,不是所有编译器都遵循ANSI C(例如vscode2022)

经过预处理以后,预定义符号已经替换成当前数据了

 

 

#define(重难点):

分为两种定义

1. #define 定义常量

2. #define 定义宏

 

第一种的讲解(定义常量):

#define MAX 1000

 在经过预处理以后,整个文件当中只要出现MAX的地方都换成1000,#define消失

在C语言当中,#define还有以下常见定义格式:

e6e7b077ed8542198659527f8911739d.jpg

由上图不难看出,#define也可以是定义一个循环语句或者打印语句(诸如此类的C语言代码)

那么 #define MAX 1000 与  #define MAX 1000; 有区别吗?

前者在经历预处理后,任何的MAX都会变成1000;而后者在经历预处理后,任何的MAX都会变成1000;

第二种的讲解(定义宏):

#define机制包括了一个规定,允许把参数替换到文本中,这种实现通常称为宏或者宏定义。

下面是宏的申明方式:

#define name( parament_list) stuff

 其中的parament_list是一个由逗号隔开的符号表,它们可能出现在stuff中。(即parament_list是name这个符号表的参数)

注意:参数列表的左括号必须与name紧邻,如果两者之间有任何空白存在,参数列表就会被解释为stuff的一部分(就是定义了一个符号name,name后的所有代码都是name的内容)。

例如下述代码,就是创建了一个函数,这个函数是完成了x^2的操作

#define SQUARE(x) x*x

那么如果给一个变量 int a = 5,那么SQUARE(a) 就是把a放入宏当中,然后宏再返回一个表达式预处理过后将#define删除 

那么按照上面的思路来推理SQUARE(a+1)还是不是36?

答:不是,最后是11。宏的参数是直接替换进去的,例如我们假设的a+1的例子,传参之后,得到的表达式应该是a+1*a+1(并没有括号,根据运算符的先后,最后结果应该是5+5+1 == 11)。因此如果我们想要得到36,就需要  ((x)*(x)) 这样来定义宏。

以上告诉我们,在使用宏时不要吝啬括号

宏替换的注意事项:

  1. 宏参数和#define定义中可以出现其他#define定义的符号。但是对于宏,不能出现递归(在宏中调用宏本身)
  2. 当预处理器搜索#define定义的符号的时候,字符串常量的内容并不被搜索。 

宏定义相较于一般自定义函数的优点:

  1. 一般函数需要经过传参(函数调用)、执行运算和return操作(函数返回),而宏函数简化成只有执行运算的操作(在预处理后,计算机已经把所有宏定义的符号替换成宏的内容了)。所以宏比函数在程序规模和速度方面更胜一筹
  2. 函数参数声明只能一种类型,而宏的参数类型是任意的
  3. 宏有时可以完成一些函数做不到的事情

比如:宏的参数可以出现类型,但函数不能 

比如我们在进行动态内存开辟的时候,每次使用malloc函数都需要:

(void*)p = (void*)malloc(10 * sizeof(void));
//次数void是指任意类型,而不是空

 那么在这种情况下,如果我们想要将他简化,可以创造一个自定义函数Malloc,让他等于赋值等号后面一串内容;这样写起来即为Malloc(10,void)(还是以上述代码为例)

可是函数参数不能是类型,因此我们需要用到宏定义来解决这个问题,就像如下定义

#define Malloc(n,type) (type*)malloc(n*sizeof(type))

 

带有副作用的宏参数:

  1. 每次使用宏,一份宏定义的代码将插入到程序当中。除非宏比较短,否则程序代码长度会很长。而一般函数代码只需要出现一次,后续直接进行调用操作即可,代码相对简短很多
  2. 宏的调试是不够清晰明了的
  3. 宏由于类型未定义,不够严谨
  4. 宏可能会因为运算符优先级问题,导致程序出错
  5. 如果宏中参数多次出现同一个,那么假设使用该宏(名字为test)的时候 ,test(a++) 中的参数a会执行多次a++的运算,这不同于函数调用只会在传参前 +1

宏和函数对比总结:

271d2f42ea0444f499789a66830d2496.jpg

 

#和# #:

#运算符:

在讲解该运算符以前,我们先得了解以下两种代码

	printf("helloworld");
	printf("hello" "world");

以上两种代码的输出是相同的,这也告诉了我们,在C语言当中,使用printf函数时打印内容可以由多个字符串组合而成 

#运算符是将宏的一个参数转换为字符串字面量。它仅允许出现在带参数的宏的替换列表中

因此,#运算符操作可以理解为“字符串化”

当我们有一个变量int a = 10 的时候,如果想要打印出:the value of a is 10

就可以写: 

#define PRINT(n) printf("the value of "#n" if %d",n);
//
int a = 10;
……;
//
PRINT(a);

 上述代码详解:
首先是#define,进行了宏定义(符号表为PRINTF(n)),参数为n;后续对该宏的展开是指printf一个内容,这个内容里面包含了数据的数值打印以及#n;而 #n 完成的操作即是后续程序输入什么变量名称,他就会在 #n 处打印什么名称,最后可以得到 the value of a is 10;换言之,如果没有#运算符,最后打印结果就会变成 the value of n is 10。

然后在后续代码使用该宏即可。

但以上代码只能完成整型打印,所以如果想要优化代码,我们可以将宏改为PRINT(n,type),type指的是要打印的数据在printf函数里的表现形式。在这以后,还是以上述代码为例,我们就可以是 PRINT(a,"%d") 了

 

# # 运算符:

# # 运算符可以把位于它两端的符号合并成一个符号,它允许宏定义从分离的文本片段创建标识符。该操作符被称为记号粘合,这样的连接必须产生一个合法的标识符,否则其结果就是未定义的。

假设现在我们要比较两个数据的大小,并且两个数据的类型是任意的(但两数据类型相同) ,那么在解决这一问题时就需要多次声明比较大小的函数,比较麻烦

b0ea87b9cd544da1a86589c10a548ed9.jpg

因此我们就可以通过宏定义来解决这个问题,并在宏定义时使用# # 运算符

e848e763a41148b8b10fc50271f8d709.jpg

上述代码解释:

\ 是连接符,由于宏定义需要写在一行内,而如果在实现宏定义内容时过长,可以考虑通过 \ 把内容放在好几行,计算机在运行时会将其视为一行,增加代码可读性。

上述代码中,定义了一个符号表GENERIC_MAX(type),并在后续加上了一个自定义函数,这就表明宏的内容可以是自定义函数也可以库函数(限制小);其中,自定义函数返回类型和参数都是type(也就是我们输入计算机的类型)

而函数名我们想让它以 类型_max 的形式出现,就可以通过 type##_max 的取名方式;此处 type##_max 中的type##就是直接以传入的类型来去命名我们的函数

因此,通过了宏定义(内容为函数),多次调用宏,即可实现多个代码大致相同的自定义函数的创建,一步到位;并在后续代码中直接使用由宏定义来定义的函数

# 、# #的不同:

#是对某一个字符(例如 n 和 #n)的修改;# #是对字符串(例如函数名)中的某部分的修改

 

#undef:

该指令是用来移除一个宏定义的

 

命名约定:

由于函数和宏的使用语法很相似,所以仅仅通过代码语言无法区分两者。

因此我们平时有个习惯

宏名全大写

函数名不要全部大写

 

条件编译:

有时候,我们会写一些调试性代码,这种代码删除可惜,保留又碍事(代码可读性受损),此时我们可以选择性地进行编译,即称条件编译。以下有几种条件编译的种类(全部都是以宏为基础而存在的):

668836c00d6a4718bc992f5bc7e2fb48.jpg

种类一:

        #if 常量表达式 是判断由#define定义的常量是否满足某一个关系,就比如M(随意假设的一个由define定义的常量)>0,M < 10 等;#endif 是到这里结束的含义,即如果M的表达式为真,就开始执行从 #if 开始到 #endif 语句结束的代码;通过这样的方式,我们不想执行某一块的代码只需将 #define M num 中的num更改掉,改为表达式为假的情况即可

种类二:
        基于种类一的全部特点之下,可以有选择地挑选自己想要执行的语句,就像是else-if语句中的 else if语句 else语句 if语句 

fca6445553f34a65bbe0f8deed6d4495.jpg

种类三:

        #if defined(symbol):如果某个符号被用 #define 定义了为真,未被定义为假

        #ifdef symbol:和#if defined(symbol) 相同用法

        e.g #define MAX 10                #ifdef MAX 

        由于已经定义了MAX这个符号,因此 #ifdef MAX 为真


        #if !defined(symbol):与 #if defined(symbol) 相对,某个符号未被定义为真,被定义了为假

        #ifndef symbol:和#if !defined(symbol)相同用法,即if not defined 的含义

种类四:

        该种类即为判断某个符号是否有被定义的分支语句

 

编译:

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

用到的是 -S 指令,是针对 .i文件进行操作,生成的文件是 .s 文件(里面存储了汇编代码)

即是将 C语言代码变成汇编代码

 

词法分析:

将源代码程序被输入扫描器,扫描器就是将代码中的字符分割成一系列的记号(关键字、标识符、字面量、特殊字符等)

例如下面的代码:

array[index] = (index+4)*(2+6)

 会进行以下拆分

5caf5a6ff04b4a6fb65c82facfbdbf96.jpg

语法分析:

接下来语法分析器,将对扫描产生的记号进行语法分析,从而产生语法树。这些语法树是以表达式为节点的树

814bbf8b62a14fe7a4cf1e776e2c20e6.jpg

语义分析:

语义分析器来完成语义分析,即对表达式的语法层面分析。编译器所能做的分析是语义的静态分析。静态语义分析通常包含声明和类型的匹配,类型的转化等。这个阶段会报告错误的语法信息。

5f28248aa3a0418eb05db70c228ca743.jpg

以上只是笼统地将编译器的工作原理概括了出来,具体内容还请搜寻《编译原理》这门课程或者相关资料

 

汇编:

用到的是 -c 指令,是针对 .s 文件进行操作,生成的文件是 .o 文件(目标文件)

即是把汇编代码转换成了机器指令

 

链接:

链接是一个复杂的过程,链接的时候需要把一堆文件链接在一起才生成可执行程序文件。

链接过程主要包括:地址和空间分配,符号决议和重定位等等

链接解决的是一个项目中多文件、多模块之间相互调用的问题。(在一个文件中只需要声明外部还有一个文件,另外一个文件中的内容也可以在该文件中使用,如下图的两个 .c 文件,以及一个文件中通过 extern int Add(int,int))

bd54913c267a4bd58f9d12b31de40406.jpg

符号表指的是由某个函数符号及其函数符号所存放的地址构成的表格,如下图所示

ab014d0cf4e548a796391ea677b75bab.jpg

 在使用链接操作合并多个 .c 文件时,会将符号表一同合并,那么到底是保留位于 0x1000 的Add函数,还是保留 0x0000 的Add函数呢?(Add函数所位于的位置都是假设的)

由于后一个Add函数地址无效,所以只保留前一个Add函数。

部分全局符号表,在合并时将有效地址留下来,无效的地址舍去,这一过程叫做重定位

 

运行环境:

  1. 程序必须载入内存中。在有操作系统的环境中:一般由操作系统来完成。在独立环境中,程序的载入必须手工安排,也可能是通过可执行代码置入只读内存来完成。
  2. 程序的执行便开始,接着便调用main函数。
  3. 开始执行程序代码。这个时候程序将使用一个运行时栈堆(函数栈帧内容),存储函数的局部变量和返回地址。程序同时也可以使用静态(static)内存或者动态开辟内存(malloc,realloc)
  4. 终止程序。正常终止也可能意外终止(电脑死机、断电等)

 

头文件的包含:

"文件名":该种包含方式叫做本地文件包含,是先在源文件所在目录下查找,如果该头文件未被找到,编译器就会像找库函数头文件一样在标准库里查找头文件

<文件名>:该种包含方式叫做库文件包含,是直接去标准库里查找,如果找不到就提示编译错误

所以,库文件也可以通过 "文件名" 的方式来查找,只是这样做查找的效率降低,同时不容易区分库文件还是本地文件

 

头文件的嵌套包含:

#include 指令可以使另外一个文件被编译,就像它实际出现在了 #include 指令的地方一样

替换方式:预处理器先删除这条指令,并用包含文件的内容替换。

一个头文件被包含10次,那就编译10次,因此被重复包含对编译压力比较大。

而在开发程序写代码的时候,极大可能会出现多次包含的情况

就比如有个 test.c 文件,temp1.c 文件和 temp2.c 文件都因某种需要包含了它

与此同时,又有一个 end.c 文件包含了 temp1.c 文件和 temp2.c 文件

这时候,end.c 文件就包含了两次 test.c 文件,重复编译两次

 那么像上面这种情况应该怎么办呢?

方法1(条件编译):

每个头文件的开头写

#ifndef __TEST_H__
#define __TEST_H__
	//头文件的内容
#endif 
//__TEST_H__可以改成其他标识符,例如M,N……

方法2(用编译器自带的指令):

#pragma once

就可以避免头文件的重复引用

 

  • 15
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
代码下载:完整代码,可直接运行 ;运行版本:2022a或2019b或2014a;若运行有问题,可私信博主; **仿真咨询 1 各类智能优化算法改进及应用** 生产调度、经济调度、装配线调度、充电优化、车间调度、发车优化、水库调度、三维装箱、物流选址、货位优化、公交排班优化、充电桩布局优化、车间布局优化、集装箱船配载优化、水泵组合优化、解医疗资源分配优化、设施布局优化、可视域基站和无人机选址优化 **2 机器学习和深度学习方面** 卷积神经网络(CNN)、LSTM、支持向量机(SVM)、最小二乘支持向量机(LSSVM)、极限学习机(ELM)、核极限学习机(KELM)、BP、RBF、宽度学习、DBN、RF、RBF、DELM、XGBOOST、TCN实现风电预测、光伏预测、电池寿命预测、辐射源识别、交通流预测、负荷预测、股价预测、PM2.5浓度预测、电池健康状态预测、水体光学参数反演、NLOS信号识别、地铁停车精准预测、变压器故障诊断 **3 图像处理方面** 图像识别、图像分割、图像检测、图像隐藏、图像配准、图像拼接、图像融合、图像增强、图像压缩感知 **4 路径规划方面** 旅行商问题(TSP)、车辆路径问题(VRP、MVRP、CVRP、VRPTW等)、无人机三维路径规划、无人机协同、无人机编队、机器人路径规划、栅格地图路径规划、多式联运运输问题、车辆协同无人机路径规划、天线线性阵列分布优化、车间布局优化 **5 无人机应用方面** 无人机路径规划、无人机控制、无人机编队、无人机协同、无人机任务分配 **6 无线传感器定位及布局方面** 传感器部署优化、通信协议优化、路由优化、目标定位优化、Dv-Hop定位优化、Leach协议优化、WSN覆盖优化、组播优化、RSSI定位优化 **7 信号处理方面** 信号识别、信号加密、信号去噪、信号增强、雷达信号处理、信号水印嵌入提取、肌电信号、脑电信号、信号配时优化 **8 电力系统方面** 微电网优化、无功优化、配电网重构、储能配置 **9 元胞自动机方面** 交通流 人群疏散 病毒扩散 晶体生长 **10 雷达方面** 卡尔曼滤波跟踪、航迹关联、航迹融合

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值