【C语言】C的编译过程预处理

news2024/11/19 16:46:16

目录

  • 一、 程序的翻译环境和执行环境
    • 1、翻译环境
      • 预处理
      • 编译
      • 汇编
      • 链接
    • 2、执行环境
  • 二、预处理详解
    • 1、预定义符号
    • 2、#define
      • #define 语法
      • #define 定义宏
      • #define 替换规则
    • 3、#和##
    • 4、宏和函数对比

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

在ANSI C的任何一种实现中,存在两个不同的环境

  • 第1种是翻译环境,在这个环境中源代码被转换为可执行的机器指令
  • 第2种是执行环境,它用于实际执行代码。

我们都是在.c源文件中编写代码,是怎么形成.exe文件,又是怎么输出结果的呢?
源文件经过翻译环境的处理,最后生成.exe文件,然后通过执行环境输出结果。

在这里插入图片描述
在翻译环境中源文件又是怎样被转换成可执行的机器指令呢?

1、翻译环境

我们都知道源程序通过编译和链接最终形成可执行程序,编译和连接便是翻译环境所做的事情。
在这里插入图片描述

  • 组成一个程序的每个源文件通过编译过程分别转换成目标代码。
  • 每个目标文件由链接器(linker)捆绑在一起,形成一个单一而完整的可执行程序。
  • 链接器同时也会引入标准C函数库中任何被该程序所用到的函数,而且它可以搜索程序员个人的程序库,将其需要的函数也链接到程序中

编译又可以分成:预编译(预处理)、编译、汇编
在这里插入图片描述

我们可以通过在Linux环境的gcc编译器下观看他的编译连接的过程。

预处理

我们先创建一个test.c文件,在test.c文件下编写代码,如下:
在这里插入图片描述
然后我们输入gcc -E test.c -o test.i,这个命令就是预处理完成之后就停下来,预处理之后产生的结果放在test.i文件中。
在这里插入图片描述
在test.i文件中我们会发现有800多行代码,而且头文件也不在了,我们再通过>vim /usr/include/stdio.h进入stdio.h文件中去,查看stdio.h头文件
在这里插入图片描述
通过test.i文件与stdio.h文件对比,会发现两个文件中的内容大致是一样的。
从这里,我们就可以知道:预处理阶段一定会做的一件事就是把要包含的文件给拷贝过来用,而使用的就是#include这个指令。

但是,我们可以看到 stdio.h文件中有900多行,而test.c文件中有800多行,那预处理阶段还进行了什么操作呢?

如果我们在test.c文件改成如下呢,再进入test.i文件,查看呢?
在这里插入图片描述

我们会发现test.i中并没有注释语句,而且也没有#define语句,并且变量a的值变为100了

从这里,我们可以知道:预处理还做了注释的删除和#define符号的替换。

综上所述,预处理阶段会做的三件事有:

  1. 头文件包含
  2. 删除注释
  3. #define定义的宏进行替换

编译

我们输入gcc -S test.c,这个命令就是编译完成之后停下来,结果保存到test.s中。
在这里插入图片描述

在test.s文件中,我们可以看到他是把C语言代码转换成汇编代码。
在这个过程中,他是经过语法分析、词法分析、语义分析、符号汇总转换成汇编代码的。

汇编

我们可以对test.c的代码进行修改
在这里插入图片描述
输入 gcc -c test.c,这个命令汇编就是完成之后就停下来,结果保存在test.o中。
在这里插入图片描述
我们发现此文件是一个二进制文件,生成一个test.o文件,也就是一个目标文件。
在此,我们可以知道这个过程就是把汇编指令转换成二进制指令,这个过程中还会形成符号表。

test.o文件是一个elf文件,我们可以通过readelf指令输入readelf test.o -s可以解读test.o文件中的内容
在这里插入图片描述

链接

我们在创建一个add.c文件,在里面写一个加函数,使他生成一个add.o的目标文件,方便我们测试
在这里插入图片描述
test.c文件中的代码:
在这里插入图片描述
我们在输入gcc test.o add.o,会发现目录中出现了一个a.out文件,这个文件就是连接后生成的可执行文件。
由此,我们可以知道,这个阶段他会把我们的文件合并成一个可执行文件,也就是合并段表和符号表合并与重定位。

在这里插入图片描述

2、执行环境

程序执行的过程:

  1. 程序必须载入内存中。在有操作系统的环境中:一般这个由操作系统完成。在独立的环境中,程序的载入必须由手工安排,也可能是通过可执行代码置入只读内存来完成。
  2. 程序的执行便开始。接着便调用main函数。
  3. 开始执行程序代码。这个时候程序将使用一个运行时堆栈(stack),存储函数的局部变量和返回地址。程序同时也可以使用静态(static)内存,存储于静态内存中的变量在程序的整个执行过程一直保留他们的值。
  4. 终止程序。正常终止main函数;也有可能是意外终止。

二、预处理详解

1、预定义符号

FILE :进行编译的源文件
LINE :文件当前的行号
DATE :文件被编译的日期
TIME :文件被编译的时间
STDC :如果编译器遵循ANSI C,其值为1,否则未定义
注意:这些预定义符号都是语言内置的。

举个例子:

#include <stdio.h>
int main()
{
	printf("%s \nline:%d\n", __DATE__, __LINE__);
	return 0;
}

运行结果:
在这里插入图片描述

2、#define

#define 语法

语法: #define name stuff

#define MAX 1000
#define reg register           //为 register这个关键字,创建一个简短的名字
#define do_forever for(;;)     //用更形象的符号来替换一种实现
#define CASE break;case        //在写case语句的时候自动把 break写上。
// 如果定义的 stuff过长,可以分成几行写,
//除了最后一行外,每行的后面都加一个反斜杠(续行符)。
#define DEBUG_PRINT printf("file:%s\tline:%d\t \
                          date:%s\ttime:%s\n" ,\
                          __FILE__,__LINE__ ,  \
                          __DATE__,__TIME__ )  

注意:在define定义标识符的时候,最好不要在最后加上;
例如:
在这里插入图片描述
上面这种情况就会出现语法错误。

#define 定义宏

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

宏的声明方式:#define name(parament-list) stuff
其中的 parament-list 是一个由逗号隔开的符号表,它们可能出现在stuff中。

例如:

#include <stdio.h>
#define  MAX(x,y) (x>y?x:y)
int main()
{
	int a = 5;
	int b = 6;
	int m = MAX(a, b);
	printf("%d\n", m);//结果:6
	return 0;
}

注意:
参数列表的左括号必须与name紧邻。
如果两者之间有任何空白存在,参数列表就会被解释为stuff的一部分。

我们再来看个例子:

#include <stdio.h>
#define  SQU(X) X*X
int main()
{
	printf("%d\n", SQU(6));//结果:36
	printf("%d\n", SQU(6 + 1));//结果:13
	return 0;
}

我们不看结果,会认为SQU(6+1)的结果为36,但是,并不是,为什么呢?

替换文本时,参数X被替换成6 + 1,所以这条语句实际上变成了:
printf("%d\n", 6 + 1 * 6 + 1);

那么,如何解决这个问题呢?
我们可以在宏定义加上两个括号,便解决了,如下:

#include <stdio.h>
#define  SQU(X) (X)*(X)
int main()
{
	printf("%d\n", SQU(6));//结果:36
	printf("%d\n", SQU(6 + 1));//结果:49
	return 0;
}

我们在看一个例子:

#include <stdio.h>
#define DOU(x) (x) + (x)
int main()
{
	printf("%d\n", 10 * DOU(6));//结果:66
	printf("%d\n", DOU(6));//结果:12
	return 0;
}

我们不看结果,会认为10 * DOU(6)的结果为120,但是,并不是,为什么呢?

替换文本时,这条语句实际上变成了:
printf("%d\n", 10 * 6 + 6);

那么,如何解决这个问题呢?
我们可以在宏定义表达式两边加上一对括号,便解决了,如下:

#include <stdio.h>
#define DOU(x) ((x) + (x))
int main()
{
	printf("%d\n", 10 * DOU(6));//结果:120
	printf("%d\n", DOU(6));//结果:12
	return 0;
}

注意:所以用于对数值表达式进行求值的宏定义都应该用这种方式加上括号,避免在使用宏时由于参数中的操作符或邻近操作符之间不可预料的相互作用。

#define 替换规则

在程序中扩展#define定义符号和宏时,需要涉及以下几个步骤:

  1. 在调用宏时,首先对参数进行检查,看看是否包含任何由#define定义的符号。如果是,它们首先被替换。
  2. 替换文本随后被插入到程序中原来文本的位置。对于宏,参数名被他们的值所替换。
  3. 最后,再次对结果文件进行扫描,看看它是否包含任何由#define定义的符号。如果是,就重复上述处理过程。

注意:

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

3、#和##

我们先来看看这串代码:

#include <stdio.h>
int main()
{
	printf("hello"" world\n");
	return 0;
}

运行结果:
在这里插入图片描述
可以看出,字符串是有自动连接的特点的。
我们再来看看这串代码:

#include <stdio.h>
int main()
{
	int a = 10;
	printf("the value of a is %d\n", a);
	int b = 20;
	printf("the value of b is %d\n", b);
	float c = 3.5f;
	printf("the value of c is %f\n", c);
	return 0;
}

在这里插入图片描述

那么,我们可以用宏的方式进行求值呢?
这里我们就可以使用#,把一个宏参数变成对应的字符串,代码如下:

#include <stdio.h>
#define PRINT(format, val)  printf("the value of "#val" is "format"\n", val)
int main()
{
	int a = 10;
	PRINT("%d", a);
	int b = 20;
	PRINT("%d", b);
	float c = 3.5f;
	PRINT("%f", c);
	return 0;
}

运行结果:
在这里插入图片描述
我们在看一个例子:

#include <stdio.h>
#define ADD_SUM(S,N)  S##N
int main()
{
	int sum_num = 1010;
	printf("%d\n", ADD_SUM(sum_, num));
	return 0;
}

运行结果:
在这里插入图片描述
综上所述:

  1. #:把一个宏参数变成对应的字符串。
  2. ##:可以把位于它两边的符号合成一个符号,它允许宏定义从分离的文本片段创建标识符。

注意:使用##连接必须产生一个合法的标识符。否则其结果就是未定义的。

4、宏和函数对比

 属性  #define定义宏函数
代码长度每次使用时,宏代码都会被插入到程序中。除了非常小的宏之外,程序的长度会大幅度增长函数代码只出现于一个地方;每次使用这个函数时,都调用那个地方的同一份代码
执行速度更快存在函数的调用和返回的额外开销,所以相对慢一些
操作符优先级宏参数的求值是在所有周围表达式的上下文环境里,除非加上括号,否则邻近操作符的优先级可能会产生不可预料的后果,所以建议宏在书写的时候多些括号。函数参数只在函数调用的时候求值一次,它的结果值传递给函数。表达式的求值结果更容易预测。
带有副作用的参数参数可能被替换到宏体中的多个位置,所以带有副作用的参数求值可能会产生不可预料的结果。函数参数只在传参的时候求值一次,结果更容易控制。
参数类型宏的参数与类型无关,只要对参数的操作是合法的,它就可以使用于任何参数类型。函数的参数是与类型有关的,如果参数的类型不同,就需要不同的函数,即使他们执行的任务是相同的。
调试宏是不方便调试的函数是可以逐语句调试的
递归宏是不能递归的函数是可以递归的

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

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

相关文章

为什么我们拥有庞大的语言模型,而Vision Transformers的规模却很小?

编者按&#xff1a;本文探讨了语言模型为何会比视觉模型的参数数量大得多的原因&#xff0c;并详细介绍了传统ViT训练方法在扩展时出现不稳定性的问题。 为此&#xff0c;本文介绍了如何改进架构以实现扩展&#xff0c;并讨论了实现模型最优状态的方法。同时&#xff0c;如何在…

Docker部署skywalking9.2版本

注意使用docker部署skywalking和使用tar包部署有点不一样OAP和UI需要分别部署原因是&#xff1a; SkyWalking UI 和 OAP 是 SkyWalking 的两个主要组件&#xff0c;它们之间的关系是前端和后端的关系。SkyWalking UI 是一个 Web 应用程序&#xff0c;它提供了一个漂亮的 UI 界面…

连续降税、人民币结算,巴西潜力爆发!开发细节见内!

本文内容 /CONTENT 01/中巴贸易现状 02/主要进口类别 03/通关和贸易政策 04/市场商业环境 05/本地公司的注册程序 06/巴西的主要节日 最近巴西降低关税&#xff0c;宣布人民币结算。想转市场的朋友不妨考虑巴西。 巴西作为南美洲最大的国家&#xff0c;当地人口占53%(…

ios音频焦点

音频焦点 两个或者两个以上的app可以同时向同一输出流播放音频。系统会将所有音频流混合在一起&#xff0c;但这样会给用户带来很大的困扰。为了避免所有音乐app同时播放&#xff0c;ios引入了“音频焦点”的概念。在ios中&#xff0c;音频焦点是操作系统为了管理音频硬件而引…

uvc驱动ioctl分析上

uvc驱动ioctl分析上 文章目录 uvc驱动ioctl分析上uvc_ioctl_querycap查询设备的能力uvc_ioctl_enum_fmt处理V4L2设备的枚举格式&#xff08;enum_fmt&#xff09;的ioctl操作uvc_ioctl_enum_fmt_vid_out枚举视频输出格式uvc_ioctl_enum_fmt_vid_cap枚举视频捕获格式 uvc_v4l2_g…

低代码到底有多爽?解放双手,推荐一款C端的低代码产品

前言引入 低代码&#xff08;LowCode&#xff09;就是一种可视化搭建系统&#xff0c;从字面意思来讲&#xff0c;一是可视化&#xff1b;二是少写代码。由此可见&#xff0c;低代码的出现是为了减轻和降低开发者的负担&#xff0c;让开发者减少重复劳动&#xff0c;避免资源和…

基于Angular+Nginx+Java+Spring开发的医院信息系统(HIS)源码

基于云计算技术的SaaS服务的医院信息系统源码 云HIS系统有效实现医疗数据共享与交换&#xff0c;解决数据重复采集及信息孤岛等问题。重构管理服务流程&#xff0c;重建统一的信息架构体系&#xff0c;重造病人服务环境&#xff0c;向不同类型的医疗机构提供SaaS化HIS服务解决…

如何在不损失质量的情况下压缩优化图像大小

您是否知道在将图像上传到 WordPress 之前对其进行优化会对您的网站速度产生巨大影响&#xff1f; 在开始时&#xff0c;许多初学者只是简单地上传图片&#xff0c;而没有针对网络对其进行优化。这些大图像文件会使您的网站变慢。 您可以通过将图像优化最佳实践作为常规博客程…

如何对高压功率放大器进行初步故障排查?

在使用测试仪器仪表进行实验检测的过程中&#xff0c;可能多少都遇到过这种情况&#xff1a;仪器开不了机无法点亮、设备幅值与理论值有较大差距、输出电压波形失真、设备异常过载、设备异响异味等等.... 作为测试仪器仪表的一种&#xff0c;功率放大器也可能会出现这种情况&a…

基于Java+SpringMvc+vue+element实现高效学生社团平台管理

基于JavaSpringMvcvueelement实现高效学生社团平台管理 博主介绍&#xff1a;5年java开发经验&#xff0c;专注Java开发、定制、远程、指导等,csdn特邀作者、专注于Java技术领域 作者主页 超级帅帅吴 Java项目精品实战案例《500套》 欢迎点赞 收藏 ⭐留言 文末获取源码联系方式…

基于Java+Springmvc+vue+element实现高校心理健康系统详细设计和实现

基于JavaSpringmvcvueelement实现高校心理健康系统详细设计和实现 博主介绍&#xff1a;5年java开发经验&#xff0c;专注Java开发、定制、远程、指导等,csdn特邀作者、专注于Java技术领域 作者主页 超级帅帅吴 Java项目精品实战案例《500套》 欢迎点赞 收藏 ⭐留言 文末获取源…

将java项目导出jar包,转成在windows上的可执行文件(没有java运行环境的电脑也可以)

前言 提示&#xff1a;直接将java运行环境一起打包没有java运行环境的电脑也可以&#xff1a; 近期在做一个java串口项目&#xff0c;需要将完整的项目导出.exe文件在没有java环境的电脑上运行&#xff0c;下面是详细的操作步骤以及遇到各种问题的解决办法.。 说明&#xff1…

HTTPS加密的简单介绍

前言 假设客户端给服务器发送HTTP请求,此时的数据都是明文的,如果黑客在这个过程中截取到了数据,进行篡改是非常容易的,这样就会造成严重后果. HTTPS和HTTP一样,都是应用层协议.只不过HTTPS在HTTP的基础上又加了一个加密层,保证传输数据的安全性. 下面我们就来探讨一下HTTPS是…

档案八防设备之新型产品多合一恒湿净化一体机

档案馆档案库房用加湿除湿除尘除酸净化一体机【囊括加湿机、除湿机、消毒机、净化机功能】 集成&#xff1a;加湿、除湿、消毒、净化四合一的智能一体机 一、简介 北京盛世宏博科技有限公司档案库房系列加湿除湿除尘除酸净化一体机是档案库房恒湿、和净化式&#xff0c;HB-670…

Oracle数据库中了locked1勒索病毒,用友nchome配置文件损坏该如何解除

随着互联网技术的不断发展&#xff0c;网络安全问题也越来越受到人们的关注。其中&#xff0c;勒索病毒是一种比较常见的网络安全威胁。最近很多集团企业在使用Oracle数据库的过程中&#xff0c;遭遇到了locked1勒索病毒的攻击&#xff0c;导致企业的用友nchome配置文件损坏&am…

Word控件Aspose.Words教程:在 PowerPoint 中使用变形过渡

Aspose.Words是一种高级Word文档处理API&#xff0c;用于执行各种文档管理和操作任务。API支持生成&#xff0c;修改&#xff0c;转换&#xff0c;呈现和打印文档&#xff0c;而无需在跨平台应用程序中直接使用Microsoft Word。 Aspose API支持流行文件格式处理&#xff0c;并…

现代 CMake 模块化项目管理指南

文章目录 一、基于CMake&#xff0c;对文件/目录组织规范1.推荐的目录组织方式2.划分子项目3.根项目的 CMakeLists.txt 配置4.子项目的 CMakeLists.txt 配置5.子项目的头文件6.子项目的源文件补充&#xff1a;GLOB 和 GLOB_RECRUSE 的区别7.头文件和源文件的一一对应关系8.只有…

揭秘神秘的JS混淆加密技术

在编程的世界里&#xff0c;沉香舞动着一种强大的力量&#xff0c;就像母亲为了救子不惜一切的决心。而在JavaScript的领域中&#xff0c;我们也有一种神秘的技术&#xff0c;它能够将代码变得晦涩难懂&#xff0c;宛如沉香救母一般&#xff0c;守护着程序的安全。今天&#xf…

Anaconda 安装并使用 PyTorch(PyCharm)

文章目录 Anaconda 安装并使用 PyTorch&#xff08;PyCharm&#xff09;1. Anaconda 安装1.1 下载安装包1.2 安装1.3 测试1.4 更改镜像源 2. PyTorch 安装2.1 创建虚拟环境2.3 激活/关闭环境2.4 CUDA2.5 conda 安装 PyTorch 3. PyCharm 使用3.1 安装 PyCharm3.2 登录3.3 使用虚…

Lookup-包含查找

lookup函数有个特性&#xff1a; LOOKUP(lookup_value, lookup_vector, [result_vector]) 如果 LOOKUP 函数找不到 lookup_value&#xff0c;则该函数会与 lookup_vector 中小于或等于 lookup_value 的最大值进行匹配。 这个特性&#xff0c;我这里简称&#xff1a;包含查找…