C语言程序环境与预处理—从源文件到执行程序,这里面有怎么的工序?绝对0基础!

news2025/1/23 5:20:16

正文开始前,我们简单聊上一聊!

众所周知!编译器的功能非常强大的,我们在编译软件上敲的每一行代码,点击执行,就会输出结果,从代码-->输出结果,这中间经历了怎样的一个过程?编译器难道直接读你的代码吗?看完此篇,相信你会感到惊讶!

我会一步步详细解说!放心食用!安全无毒!

目录

程序的翻译环境和执行环境

翻译环境详解

运行环境详解

以下为预处理详解

                  预处理符号

预处理指令#define

带副作用的宏参数

宏和函数的对比

#undef 移除宏定义

条件编译 

文件包含


程序的翻译环境和执行环境

注:以下介绍的内容属于C语言知识范畴(这些掌握了也是不错的!)在我们介绍的各个环节里面,实际会更加复杂

在C语言标准(ANSI  C)中,任何一种实现,都存在2个环境:翻译环境  执行环境

翻译环境:将源码转化为可执行的机器指令

执行环境:实际执行代码

翻译环境详解

组成一个程序的每个源文件通过编译过程分别转换成目标代码

每个目标文件由链接器捆绑在一起,形成一个单一且完整的可执行程序

链接器同时也会引入标准C语言库函数中任一被该程序用到的函数,而且它可以搜索程序员个人的程序库,将其需要的函数也链接到程序中

在下面这个图中我们可以看到,多个源文件经过编译器处理得到对应的目标文件,最后再经过链接器加工,得到可执行程序(下面会拆分编译环节进行讲解!)

 我们把编译分为以下3个阶段:       预编译      编译     汇编

下面是画图演示:

                                                            编译
预编译编译汇编

编译特点:从左到右,依次执行各个阶段

下面我们将每个阶段进行讲解! 

                                                                      预编译 

在预编译阶段,主要包含以下几个操作:

1:宏的替换。预处理阶段会处理#define定义的宏。比如#define  MAX  100,那么在接下来出现MAX的地方,都会被替换成100。此外,预处理还会处理条件编译指令,比如:#ifdef,#ifndef,#endif等等

2:注释的删除。因为注释不影响程序的逻辑,因此在编译前将其删除可以减少编译时间并减少最终生成的目标文件大小

3:头文件的包含。预处理阶段会处理源代码中的#include指令,将指定的头文件内容插入源文件中。比如:某个源文件中包含#include<stdio.h>,预处理程序会将stdio.h头文件的内容插入源文件中,再进行后续处理

       

                                                                      编译

编译阶段,主要进行以下几个操作:

1:语法分析。将代码分解成一个个单词或者符号,然后检查这些单词或者符号是否符合C语言的语法规则

2:语义分析。在语法分析的基础上,检查变量类型、函数调用等是否符合C语言的语义规则,来确保代码的含义是正确的,为后续代码生成做准备

3:中间代码生成。编译器将源代码转化为中间表示,通常是一种与机器无关的代码形式,这步是为了简化后续的编译过程,提高编译效率

4:优化。编译器对中间代码进行优化,以提高运行效率,优化可以通过消除冗余的代码、优化循环等方式实现,这一步可以人为选择是否进行

5:目标代码生成。最后编译器将中间代码,转化为汇编代码,这一步完成了源代码到可执行代码的转换(源代码翻译成了汇编语言)

                                                                   汇编

主要任务是将:汇编语言转化为目标文件的过程(将汇编语言转化为二进制存放到目标文件中)

解释:汇编器将汇编代码转化为机器语言,并生成目标文件,目标文件包含了可执行代码、符号和调试信息.......

汇编语言主要组成:

数据段:定义程序中使用的数据

代码段:包含程序的指令

堆栈段:管理函数调用和局部变量

编译阶段我们逐个解释完了!最后我们来看看链接做些什么!

(下面是对  编译  跟  链接  的细分图:)

编译    链接
预编译编译汇编链接                 

                                                                         链接

1:符号解析。链接器需要找到每个符号(比如函数变量)的定义位置。如果某个符号在多个文件中定义,链接器需要解决冲突,确保每一个符号只有一个定义

2:重定位。链接器调整目标文件中的代码和数据的位置,确保它们在可执行文件中的正确布局。链接器将不同的目标文件和库函数中的代码段、数据等合并成一个完整的可执行文件

运行环境详解

下面看程序运行环境的过程:

1:程序必须载入内存中。在有操作系统的环境中,一般由两个操作系统完成。在独立的环境中,程序的载入必须由手工安排,也可能是通过可执行代码置入只读内存来完成

2:调用main函数,程序开始运行。

3:开始执行代码。这时程序将使用一个运行时堆栈,存储的函数的局部变量和返回地址。程序同时也可以使用静态内存,存储于静态内存中的变量在程序的整个执行过程中一直保留它们的值

4:终止程序。分为正常终止和意外终止(比如程序崩溃)

以下为预处理详解

下面我们对编译时的预处理阶段各个指令以及标识符进行讲解!

预处理符号

                                                                    预处理符号

预处理符号都是语言内置的:

__FILE__                    进行编译的源文件

__LINE__                   文件当前的行号

__DATE__                  文件被编译的日期

__TIME__                   文件被编译的时间

__STDC__                  如果编译器遵循 ANSI  C,其值为1,否则未定义

我们可以打印出来这些预处理符号的效果:

预处理指令#define

                                                                  #define定义标识符

我们知道#define可以用来定义标识符,让接下来出现这个标识符的地方都被替代!比如:

问:在定义标识符的最后要不要加上“  ; ” ? 

答:最好不要加上分号,不然容易出现问题,比如对比下面两幅图:

                                                                       

                                                                 #define定义宏

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

下面是宏的申明方式:

#define   name(parament-list) stuff

(其中,parament-list可以理解为一个参数符号,可能替换掉stuff中的某些符号)

 注意:参数的左括号必须与name紧邻

            如果两者之间出现空格,那么编辑器就将宏定义理解成了定义标识符

            宏的替换不会出现其它的操作,只会原方不动的被替换(下面第2个例子会解释)

下面看宏的使用例子:

 解释:这里宏接收到了一个参数5,也就是MAX(5)

            紧接着MAX(5)替换成了 5*5

 好,那么下面我们来看一个代码,大家先猜猜  的值会是多少?

 我们知道,宏定义的替换只会原原本本的替换,不会按预想的操作进行,那么可以这么解释上面代码:     MAX(5)*  3  被替换成了  5+5 * 3,只会原原本本的替换

 如果你想按照预想的进行,咋办?对X+X添个括号就行了!比如:

 千万要注意括号的位置,比如下面这种种情况,括号位置的不同输出结果截然不同:

                                                               #宏参数的替换

我们先来看以下代码:

我们刚刚学了宏定义,那么如果使用宏定义来打印,是不是更加灵活?比如:

解释:#插入参数到字符串,这个很好理解吧,就是把有 x 的地方替换成了括号里面的内容! 

那么我们来看有2个参数的#define替换:

 解释:1:我们知道#define里面的参数是会发生替换的,那么不难理解format直接替换成“%f”

            2:#参数会把对应的宏参数变成对应的字符串,下面我们进行演示:

 总结:#参数需要格外注意,它是将x传过去的参数变成字符串,再进行替换 

                                                           ##左右两边进行合成

作用:连接左右两边

这个跟直接替换参数很像,就是在宏定义里面使用了##,然后在接下来运用的时候直接去掉##

 比如,我们训练一题:遵循:先直接替换,再去掉##

带副作用的宏参数

如果宏参数本身存在一定副作用,那么这个副作用随着程序的进行,这个作用会一直延伸,无法预测,我们来看下面一段代码:

解释:作为宏的参数, 直接传参,不进行任何处理,然后a++ > b++吗?也就是5小于6吗?然后ab自加1,接着走b++,最后结果也就是6+1=7b自身也变成了8

这种宏的使用有很大的副作用,随着调用参数,我们本是简单比较两个值的大小,但是最后结果出乎意料!

宏和函数的对比

我们知道函数可以实现上面的代码,比如:

 采用函数的形式虽然也可以,但是从调用函数到返回值,这期间的时间可能远远比宏使用更长!我们看下面的函数调用反汇编代码

                                                              函数刚准备调用结束:

                                                   从反汇编我们可以看到做了很多准备 :

 

 我们实际只需要几句代码!但是编译器本身却做了这么多准备!所以函数调用的时间相较于宏是很长的!

如果使用宏来实现比较大小,在预处理阶段就替换成宏的对应代码的了,因此:使用宏实现的话是无法调试的,因为我们调试的是可执行程序,而宏在预处理阶段就替换了!

所以宏的缺点:

                        1:每次使用宏的时候,将一段代码直接插入程序中,除非宏比较短,否则大幅度增加代码的长度

                        2: 宏无法参与调试

                        3:宏由于是直接替换,因此与类型无关,不够严谨

                        4:宏可能带来算数优先级问题,导致bug出现

那么我们什么时候使用宏来实现?什么时候使用函数呢!

如果实现一些小功能,可以使用宏,而相较于一些稍大的工程,最好是使用函数 

#undef 移除宏定义

比如,我现在创建了一个宏定义:

如果我们在写程序的使用,不想使用这个宏定义了,那么可以在中间移除它,比如:

条件编译 

在编译一条语句或者一组语句时,我们可以选择这条语句或者这组语句是否参加编译!因为我们有条件编译指令,下面我们来学习学习!

                                                                  单个选择性编译

理解:根据宏是否被定义,判断是否执行语句 (只要被定义即可,不论参数内容)

比如:

我们看代码:

 

                                                              多个分支的条件编译 

理解:一次性实现多个单分支

比如:

看代码实现:

  

                                                                   判断是否被定义 

理解:就是判断这个宏是否被定义

比如:

我们看代码实现:

 

                                                                     嵌套指令

 理解:就是从外到内进行嵌套!套娃哈哈哈!

比如:

我们看代码实现:

 

 

好了!下面我们做一下总结:

1:如果遇到#if 要格外注意,它判断的是内容

2:除了  #if  之外,其它一律判断宏是否被定义,不管什么内容

3:一般一个分支的头和尾分别是  #.....  跟  #endif ,根据这个来进行划分,遇到这种条件编译的,找头和尾来作为一个分支 

文件包含

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

那么编译器怎么替换呢?

预处理时先删除这条指令,并用包含文件的内容替换

那么如果出现了10个这样的头文件,是不是就被替换了10次?

是的,因此在写代码时要考虑这个问题

接下来我们看看#include<.........>    跟     #include“..........”   的区别!

                                               #include<......>(比如:#include<stdio.h>)

如果头文件的形式是上面这样

查找策略:那么它的意思是这个包含文件的内容在库里,编译器直接从库里面寻找这个头文件对应库函数的声明

如果编译器在库里没有找到,那么就会发生报错

                                              #include“...........”(比如:#include“text.h”)

查找策略:编译器直接去找对应文件里面去寻找这个自定义函数的定义,就比如:告诉编译器这个自定义函数在哪里有声明

如果编译器没有找到,同样发生报错

如果我们使用库函数时,用这种头文件形式,就无法区分是本地文件还是库里面的文件

那么以上两种方式都直接告诉了编译器这个函数在哪里声明,如果我们使用了重复的头文件呢?

 如果A程序员使用了一次这个头文件,B程序员又写了一个,C程序员又写了一个,那么就会让编译器重复查找,效率很低,我们有以下2中解决办法:这种办法只针对非库里的头文件,也就是#include“.......”类型,原因:库里面的头文件开发者已经设置好了,不会出现这种问题)

 比如:(这两种方法都可以避免头文件的重复使用)

我们直接看代码实现:(假如我们的自定义函数声明在text.h这个文件里面)

 

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

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

相关文章

第35天:安全开发-JavaEE应用原生反序列化重写方法链条分析触发类类加载

时间轴&#xff1a; 序列化与反序列化图解&#xff1a; 演示案例&#xff1a; Java-原生使用-序列化&反序列化 Java-安全问题-重写方法&触发方法 Java-安全问题-可控其他类重写方法 Java-原生使用-序列化&反序列化 1.为什么进行序列化和反序列化&#xff1…

硬件作品3----STM32F103RCT6最小系统板MCU配置

参考文章&#xff1a;对stm32F103RCT6原理图解析&#xff08;详细&#xff09;-CSDN博客 本想绘制稍微复杂一些的电路&#xff0c;但是出现很多问题&#xff0c;因此先绘制一块最小系统板进行原理、绘制方法的验证。 设计难度&#xff1a;★ 适合人群&#xff1a;初学者 一、…

SparkSQL数据源与数据存储综合实践

文章目录 1. 打开项目2. 查看数据集2.1 查看JSON格式数据2.2 查看CSV格式数据2.3 查看TXT格式数据 3. 添加单元测试依赖4. 创建数据加载与保存对象4.1 创建Spark会话对象4.2 创建加载JSON数据方法4.3 创建加载CSV数据方法4.4 创建加载Text数据方法4.5 创建加载JSON数据扩展方法…

【回忆迷宫——处理方法+DFS】

题目 代码 #include <bits/stdc.h> using namespace std; const int N 250; int g[N][N]; bool vis[N][N]; int dx[4] {0, 0, -1, 1}; int dy[4] {-1, 1, 0, 0}; int nx 999, ny 999, mx, my; int x 101, y 101; //0墙 (1空地 2远方) bool jud(int x, int y) {if…

项目中使用的是 FastJSON(com.alibaba:fastjson)JSON库

从你的 pom.xml 文件中可以看到&#xff0c;项目明确依赖了以下 JSON 库&#xff1a; FastJSON&#xff1a; <dependency><groupId>com.alibaba</groupId><artifactId>fastjson</artifactId><version>1.2.47</version> </depende…

高效安全文件传输新选择!群晖NAS如何实现无公网IP下的SFTP远程连接

文章目录 前言1. 开启群晖SFTP连接2. 群晖安装Cpolar工具3. 创建SFTP公网地址4. 群晖SFTP远程连接5. 固定SFTP公网地址6. SFTP固定地址连接 前言 随着远程办公和数据共享成为新常态&#xff0c;如何高效且安全地管理和传输文件成为了许多人的痛点。如果你正在寻找一个解决方案…

Windows第一次上手鸿蒙周边

端云一体所需装备 很重要&#xff1a;C/D/E/F盘要有二三十G的可用空间&#xff01; 硬件&#xff1a;华为鸿蒙实验箱&#xff08;基础版&#xff09;》飞机板核心板环境监测板 软件&#xff1a;Visual Studio Code写代码 终端编译 Hiburn烧录到开发板 MobaXterm &#xff08…

使用AI生成金融时间序列数据:解决股市场的数据稀缺问题并提升信噪比

“GENERATIVE MODELS FOR FINANCIAL TIME SERIES DATA: ENHANCING SIGNAL-TO-NOISE RATIO AND ADDRESSING DATA SCARCITY IN A-SHARE MARKET” 论文地址&#xff1a;https://arxiv.org/pdf/2501.00063 摘要 金融领域面临的数据稀缺与低信噪比问题&#xff0c;限制了深度学习在…

【Qt】05-菜单栏

做菜单 前言一、创建文件二、菜单栏 QMenuBar2.1 示例代码2.2 运行结果 三、工具栏 QToolBar3.1 运行代码3.2 结果分析 四、状态栏 QStatusBar4.1 运行代码4.2 运行结果 五、文本编辑框 QTextEdit5.1 运行代码5.2 运行结果 六、浮动窗口 addDockWidget6.1 运行代码6.2 运行结果…

细说STM32F407单片机电源低功耗StandbyMode待机模式及应用示例

目录 一、待机模式基础知识 1、进入待机模式 2、待机模式的状态 3、退出待机模式 二、待机模式应用示例 1、示例功能和CubeMX项目设置 &#xff08;1&#xff09; 时钟 &#xff08;2&#xff09; DEBUG、LED1、KeyRight、USART6、CodeGenerator &#xff08;3&#x…

中国综合算力指数(2024年)报告汇总PDF洞察(附原数据表)

原文链接&#xff1a; https://tecdat.cn/?p39061 在全球算力因数字化技术发展而竞争加剧&#xff0c;我国积极推进算力发展并将综合算力作为数字经济核心驱动力的背景下&#xff0c;该报告对我国综合算力进行研究。 中国算力大会发布的《中国综合算力指数&#xff08;2024年…

w-form-select.vue(自定义下拉框组件)(与后端字段直接相关性)

文章目录 1、w-form-select.vue 组件中每个属性的含义2、实例3、源代码 1、w-form-select.vue 组件中每个属性的含义 好的&#xff0c;我们来详细解释 w-form-select.vue 组件中每个属性的含义&#xff0c;并用表格列出它们是否与后端字段直接相关&#xff1a; 属性解释表格&…

前沿技术趋势洞察:2024年技术的崭新篇章与未来走向!

引言 时光飞逝&#xff0c;2024年已经来临&#xff0c;回顾过去一年&#xff0c;科技的迅猛进步简直让人目不暇接。 在人工智能&#xff08;AI&#xff09;越来越强大的今天&#xff0c;我们不再停留在幻想阶段&#xff0c;量子计算的雏形开始展示它的无穷潜力&#xff0c;Web …

消息队列篇--原理篇--RabbmitMQ(Exchange,消息转换器、docker部署,绑定和确认机制等)

RabbitMQ是一个基于AMQP协议的消息队列系统&#xff0c;支持多种消息传递模式&#xff0c;包括点对点&#xff08;P2P&#xff09;、发布/订阅&#xff08;Pub/Sub&#xff09;和路由模式。RabbitMQ 的设计目标是提供高可用性、可扩展性和可靠性&#xff0c;适用于企业级应用集…

C++入门 详细版

欢迎来到干货小仓库&#xff01;&#xff01; 一分耕耘一分收获&#xff0c;离自己的目标越来越近。 passion&#xff01;passion&#xff01;&#xff01;passion&#xff01;&#xff01;&#xff01; 1.命名空间 由于C语言无法避免名字或者函数重复等问题&#xff0c;当有多…

一文大白话讲清楚webpack基本使用——9——预加载之prefetch和preload以及webpackChunkName的使用

文章目录 一文大白话讲清楚webpack基本使用——9——预加载之prefetch和preload1. 建议按文章顺序从头看&#xff0c;一看到底&#xff0c;豁然开朗2. preload和prefetch的区别2. prefetch的使用3. preload的使用4. webpackChunkName 一文大白话讲清楚webpack基本使用——9——…

Android AutoMotive --CarService

1、AAOS概述 Android AutoMotive OS是谷歌针对车机使用场景打造的操作系统&#xff0c;它是基于现有Android系统的基础上增加了新特性&#xff0c;最主要的就是增加了CarService&#xff08;汽车服务&#xff09;模块。我们很容易把Android AutoMotive和Android Auto搞混&…

(三)线性代数之二阶和三阶行列式详解

在前端开发中&#xff0c;尤其是在WebGL、图形渲染、或是与地图、模型计算相关的应用场景里&#xff0c;行列式的概念常常在计算变换矩阵、进行坐标变换或进行图形学算法时被使用。理解二阶和三阶行列式对于理解矩阵运算、旋转、平移等操作至关重要。下面&#xff0c;我将结合具…

Linux Bash 中使用重定向运算符的 5 种方法

注&#xff1a;机翻&#xff0c;未校。 Five ways to use redirect operators in Bash Posted: January 22, 2021 | by Damon Garn Redirect operators are a basic but essential part of working at the Bash command line. See how to safely redirect input and output t…

蓝桥杯算法日常|c\c++常用竞赛函数总结备用

一、字符处理相关函数 大小写判断函数 islower和isupper&#xff1a;是C标准库中的字符分类函数&#xff0c;用于检查一个字符是否为小写字母或大写字母&#xff0c;需包含头文件cctype.h&#xff08;也可用万能头文件包含&#xff09;。返回布尔类型值。例如&#xff1a; #…