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

news2024/9/24 23:26:04

目录

可执行程序的生成:

预处理(预编译):

预定义符号:

#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

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

 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1701298.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

怎么添加微信留言板功能

在这个信息爆炸的时代&#xff0c;如何让自己的微信公众号或朋友圈内容脱颖而出&#xff0c;成为每位内容创作者思考的问题。今天&#xff0c;我将为您揭示一种新颖且实用的功能——微信留言板&#xff0c;并带您探讨如何通过巧妙设置&#xff0c;将其打造成独一无二的主题&…

RuntimeError: CUDA out of memory. Tried to allocate 1.77 GiB?如何解决

&#x1f3c6;本文收录于「Bug调优」专栏&#xff0c;主要记录项目实战过程中的Bug之前因后果及提供真实有效的解决方案&#xff0c;希望能够助你一臂之力&#xff0c;帮你早日登顶实现财富自由&#x1f680;&#xff1b;同时&#xff0c;欢迎大家关注&&收藏&&…

JavaWeb-JS

目录 学习重点 什么是 JavaScript? Web标准 JS的引入方式 JS的基本语法 JS的函数 JS的对象 JS事件监听 学习重点 js 引入方式 js 基础语法 js 函数 js 对象 js 事件监听 什么是 JavaScript? Web标准 Web 标准也称为网页标准 &#xff0c;由一系列的标准组成&#xff0…

Spring - Spring Cache 缓存注解这样用,实在是太香了!

作者最近在开发公司项目时使用到 Redis 缓存&#xff0c;并在翻看前人代码时&#xff0c;看到了一种关于 Cacheable 注解的自定义缓存有效期的解决方案&#xff0c;感觉比较实用&#xff0c;因此作者自己拓展完善了一番后分享给各位。 Spring 缓存常规配置 Spring Cache 框架给…

GpuMall智算云:AUTOMATIC1111/stable-diffusion-webui/stable-diffusion-webui-v1.8.0

配置环境介绍 目前平台集成了 Stable Diffusion WebUI 的官方镜像&#xff0c;该镜像中整合如下资源&#xff1a; GpuMall智算云 | 省钱、好用、弹性。租GPU就上GpuMall,面向AI开发者的GPU云平台 Stable Diffusion WebUI版本&#xff1a;v1.8.0 Python版本&#xff1a;3.10.…

运维专题.Docker功能权限(Capabilities)管理和查看

运维专题 Docker功能权限&#xff08;Capabilities&#xff09; - 文章信息 - Author: 李俊才 (jcLee95) Visit me at CSDN: https://jclee95.blog.csdn.netMy WebSite&#xff1a;http://thispage.tech/Email: 291148484163.com. Shenzhen ChinaAddress of this article:htt…

Python深度学习基于Tensorflow(12)实战生成式模型

文章目录 Deep Dream风格迁移参考资料 Deep Dream DeepDream 是一项将神经网络学习模式予以可视化展现的实验。与孩子们观察云朵并尝试解释随机形状相类似&#xff0c;DeepDream 会过度解释并增强其在图像中看到的图案。 DeepDream为了说明CNN学习到的各特征的意义&#xff0c…

SQL使用函数给多个分表添加同一字段

数据库中分表时&#xff0c;往往需要向多个分表中添加同一个字段&#xff0c;可以定义一个函数&#xff0c;每次调用这个函数向多个份表中添加同意字段。 1、创建函数示例&#xff1a; 在PostgreSQL中创建一个简单的函数 以下是一个在PostgreSQL中创建函数的简单示例&#x…

PXE + Kickstart 无人值守装机

目录 一、简介 二、PXE工作流程 三、搭建 PXE 远程安装服务器 1.安装包准备 2.环境准备 3.搭建PXE远程安装服务器 &#xff08;1&#xff09;关闭 firewalld 和 selinux &#xff08;2&#xff09;配置双网卡 1&#xff09;添加硬件网卡 2&#xff09;编辑网卡配置文…

强化学习,第 2 部分:政策评估和改进

目录 一、介绍 二、关于此文章 三、求解贝尔曼方程 四、策略评估 4.1 更新变体 4.2 例描述 五、策略改进 5.1 V函数描述 5.2 政策改进定理 六、策略迭代 七、值迭代 7.1 算法描述 7.2 异步值迭代 八、广义策略迭代 九、结论 一、介绍 R强化学习是机器学习中的一…

一屏万象,场景无限:蓝牙墨水屏标签多功能多场景应用带您领略未来

在数字化浪潮汹涌澎湃的今天&#xff0c;智能科技产品层出不穷&#xff0c;它们不仅极大地改变了我们的生活方式&#xff0c;更在无形中拓宽了我们的视野。而今&#xff0c;一款融合了创新技术与实用性于一体的蓝牙墨水屏标签&#xff0c;正以其多功能多场景应用的特性&#xf…

【C++/STL】vector(常见接口、模拟实现、迭代器失效)

&#x1f308;个人主页&#xff1a;秦jh_-CSDN博客&#x1f525; 系列专栏&#xff1a; https://blog.csdn.net/qinjh_/category_12575764.html?spm1001.2014.3001.5482 目录 简单使用 常见接口 find insert vector模板 模拟实现 尾插 构造 迭代器失效 使用memcpy拷贝问…

Web漏洞:网络安全的隐形杀手

随着互联网的深入发展&#xff0c;Web应用程序已成为企业和个人生活中不可或缺的一部分。然而&#xff0c;随着Web应用的普及&#xff0c;网络安全问题也日益凸显&#xff0c;其中Web漏洞是网络安全领域面临的重大挑战之一。本文将详细探讨一些常见的Web漏洞及其潜在的危害&…

使用 Azure DevOps 和 Azure Web Apps 进行 .NET Core 应用的 CI/CD

概览 在现代软件开发中&#xff0c;快速部署和高效的版本控制系统是非常关键的。通过利用 Azure DevOps 和 Azure Web Apps&#xff0c;开发团队可以实现自动化的持续集成和持续部署&#xff08;CI/CD&#xff09;&#xff0c;从而加快从开发到生产的过程。接下来我们一步步来…

深入探索MySQL SELECT查询:从基础到高级,解锁数据宝藏的密钥

系列文章目录 更新ing... MySQL操作全攻略&#xff1a;库、表、数据、事务全面指南深入探索MySQL SELECT查询&#xff1a;从基础到高级&#xff0c;解锁数据宝藏的密钥MySQL SELECT查询实战&#xff1a;练习题精选&#xff0c;提升你的数据库查询技能PyMySQL&#xff1a;连接P…

灯塔工厂产业数字化平台解决方案(50页PPT)

方案介绍&#xff1a; 随着工业4.0和智能制造的快速发展&#xff0c;传统工厂正面临着转型升级的迫切需求。为了提升生产效率、优化资源配置、增强市场竞争力&#xff0c;我们推出了灯塔工厂产业数字化平台解决方案。该方案旨在通过先进的信息技术手段&#xff0c;将传统工厂转…

感谢信∣高成长型动力电池供应商『华鼎国联』采购管理平台项目上线,企企通SRM加速新能源汽车发展新质生产力

近日&#xff0c;企企通收到来自华鼎国联四川动力电池有限公司&#xff08;以下简称“华鼎国联”&#xff09;的感谢信&#xff0c;对企企通团队在采购数字化项目实施中所付出的努力表示感谢。 华鼎国联在感谢信中特别指出&#xff0c;回首披荆斩棘的2023年&#xff0c;企企通的…

通过css实现------简单边框流动特效

效果展示 代码部分 <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><meta http-equiv"X-UA-Compatible" content"IEedge"><meta name"viewport" content"widthdevice…

阿里云产品DTU评测报告(三)

这阿里云产品DTU评测报告&#xff08;三&#xff09; 连接物联网平台创建项目 连接物联网平台 在开始连接物联网平台之前&#xff0c;首先需要下载开发工具Visual Studio Code 开发工具&#xff0c;开发工具可以到https://code.visualstudio.com/ 去查找下载&#xff0c;下载完…

9.Redis之list类型

list相当于链表、数据表 1.list类型基本介绍 列表中的元素是有序的"有序"的含义,要根据上下文区分~~有的时候,谈到有序,指的是"升序","降序”有的时候,谈到的有序,指的是, 顺序很关键~~如果把元素位置颠倒,顺序调换.此时得到的新的 List 和之前的 Li…