宏(预编译)详解

目录

一、程序的编译环境

二、运行环境

三、预编译详解

3.1预定义符号

3.2.1 #define 定义标识符

3.2.2  #define 定义宏

3.2.3#define替换规则

 3.2.4 #和##

        2)##的作用:

3.2.5宏和函数的对比

3.2.6宏的命名约定和#undef指令

一、命名约定: 

二、#undef

 3.3条件编译

3.4文件包含

        1)本地文件包含:

        2)Linux环境的标准头文件的路径:

        3)库文件包含:


在学习预编译之前我们有必要先大致了解一下一个程序从开始到结束的过程,这样有利于我们加深对程序运行的理解。

一、程序的编译环境

        

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

1.翻译环境 : 在这个环境中源代码转换为可执行的机器指令。(把C语言的代码转化为二进制指令  即可执行程序)
2.执行环境 : 它用于执行实际的代码。(执行二进制代码)



 

二、运行环境


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

2.程序执行便开始,随后调用main函数。

3.开始执行程序代码,这时程序员将使用一个运行时堆栈(Stack即函数栈帧),存储函数的局部变量和返回地址。程序同时也可以使用静态(static)内存,存储与静态内存的变量在程序的整个执行过程中一直保留他们的值。

4.终止程序,正常终止main函数,也肯能是意外终止。

a1b49f939cd548bb881bd584a654366a.png

        如图所示,多个源文件(.c文件)单独经过编译器,进行编译生成目标文件(obj文件),这个过程为编译。多个目标文件与库函数中的链接库共同在链接器的作用下生成可执行程序(exe文件),这个过程为链接过程。

94e9b2e66c83482c99ab82951b9c3ddd.png

        如图所示,翻译环境 可以继续细分为编译和链接,编译还可以继续细分为预处理,编译,汇编,其中在翻译过程中首先进行的是预处理过程,在预处理过程中首先会把test.c源文件中的注释删除以及#include头文件包含和#define 符号的替换,在之后就会生成test.i文件为编译阶段做准备。

        到了编译阶段会进行对test.i文件的解读(包含 :语法分析,词法分析,语义分析,符号汇总)其中符号汇总为下阶段的符号表做准备,最后将test.i文件转化为汇编指令文件即test.s文件。

        接下来到了汇编阶段在linux环境下,test.s文件会被转化为存放二进制test.o的目标文件文件(在win下转化为test.obj文件),这些二进制文件是以elf(linux环境下)文件格式存放的,elf文件又把二进制文件分为不同的数据段,最后在把前面编译的符号的汇总整理成符号表

        编译阶段结束,接下来就是链接阶段了,链接阶段首先把不同文件的相同段进行合并,形成新的数据段表,其次在对不同文件的的相同符号进行合并,合并为新的符号表,值得注意的是在形成符号表的过程总中有些单独文件的虚拟地址会被分配有效地址(重定位)加入新的符号表。

        以上就是程序从开始到结束的大致过程了,如果想了解更多的编译链接过程可以参考《程序员的自我修养》


三、预编译详解

3.1预定义符号

 __FILE__    //进行编译的源文件
__LINE__    //文件当前的行号
__DATE__    //文件被编译的日期
__TIME__    //文件被编译的时间
__STDC__    //如果编译器遵循ANSI C,其值为1,否则未定义

我们不妨打印出来这些预定义符号

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

c1406723ab204981b62f75ce933b47d1.png        可以发现,打印出来的结果跟预期一样,由(__STDC__)的结果看,dev C++遵循ANSIC。


3.2.1 #define 定义标识符

         用法:#define name stuff

在有了#define预处理命令后我们可以进一步对上面的预定义符号进行更加方便的表示,在main函数外使用#define+名字+要替换的内容,就可以在全局范围内使用这个宏,例如下面的代码:

#include<stdio.h>#define DEBUG_PRINT printf("file:%s\tline:%d\tdata:%s\t \time:%s\n",__FILE__, __LINE__, \__DATE__, __TIME__)
/*换行加'\'(转义字符,转义了回车)为了消除define的影响*/int main()
{DEBUG_PRINT; return 0;
}

        值得注意的是在C语言中,#define预处理指令使用了printf函数只能处理单行内容,如果想换行必须在每一行的末尾加上'\'转义字符才能把换行表示成字符来处理,否则会报错。 

代码执行结果如下:

fb0fb2a0f9664fb88fbfaacaa25368a1.png

         注意:在#define后面最好是不要加上分号,因为这样可能会造成歧义。


3.2.2  #define 定义宏

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

        宏的申明方式:#define name(parament-list) stuff , 其中parament-list是一个由逗号隔开的符号表,他们可能出现在stuff中。
        注意:1.参数列表的左括号必须与name紧邻。2.如果两者之间有任何空白的存在,参数列表就会被解释为stuff中的一部分

看看下面的例子:

#define Add(x,y) (x+y)int main()
{int a = 3;int b = 7;int c = Add(a,b);printf(“%d”,c);return 0;
}

    035fbf95791b455e80c41b87bf53b6ea.png

        注意:这里替换文本的时候,参数x,y要格外注意,#define是整体替换,不会给你添加括号,例如(还是上面的例子,只不过c变了):
        c = a * b * Add;的时候其实是c = a * b * a + b;所以在复杂宏当中各个参数最好加上括号。


3.2.3#define替换规则

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

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

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


 3.2.4 #和##
        1)#的作用:

思考这样一个问题:如何把参数插入到字符串当中呢?

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


        例如:我想要The value a is ...  The value b is...  The value c is...这样类似的输出如果用printf函数,少量的字符串CV一下就行,但是
如果需要特别多行类似的语句printf函数是做不到的。那么宏做不做得到呢?其实宏有种方法是可以做到的,就是符号'#'。

#include<stdio.h>#define PRINT(n) printf("The value "#n" is %d\n",n)int main()
{int a = 10;PRINT(a);int b = 20;PRINT(b);return 0;} 


        把一个字符串从要替换的字符串的中点分成两个字符串,除了想要替换的字符串以外,另外两个字符串都需要完整的"",在要替换的文本前加上#,这样就可以轻松替换了。
实质上这个宏其实是PRINT(n) printf("The value ""n"" is %d\n",n),相当于在'#'后面部分的字符串改变后又被重新拼接起来形成一个新的完整的字符串。

        我们来思考另一个问题:如果两个参数的类型不一样,如何能用一条语句实现呢,比如,我想要一个a为int 型,b 为float型,这样看来printf函数还是不能实现,难道宏还可以吗,没错,宏就是能一劳永逸!我们来看下面代码:

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


        在前面代码的基础上,加上了format类型格式,把输出控制符(%d,%f...)用format代替,且format需要单独的一个双引号,这样在传参的时候只需要传数据类型和输出控制符就可以实现把不同的输出控制符插入到字符串当中,怎么样,是不是很方便呢?


        2)##的作用:

##可以把位于它两边的符号合成一个符号,它允许宏定义从分离的文本片段创建标识符。 

这句话是什么意思呢?我们先来看一下下面的代码:

#include<stdio.h>#define CRT(x,y) x##yint main()
{int DataSum = 100;printf("%d\n", CRT(Data, Sum));return 0;
}

结果为:a8fdf81c55eb414ab86e28151ef72f03.png发现了打印的值和DataSum的值相同,这也就说明了这个宏能将两个片段合并成一个片段,这就是##的作用了。 


3.2.5宏和函数的对比

        宏通常被应用于执行简单的运算,就像计算两个数的加法:

#include<stdio.h>#define Add(x,y) (x + y);int Add_Fun(int x, int y)
{return x + y;
}int main()
{int x = 3, y = 2;printf("%d\n", Add_Fun(x, y));int c = Add(x, y);printf("%d",c);return 0;
}

为什么不用函数来完成这个任务呢?

原因有两点

        1.用于调用函数和函数返回的代码可能比实际执行这个小型计算机工作所需要的时间更多,所以宏比函数在程序的规模和速度方面更胜一筹。

        2.更为重要的是,函数的参数必须声明为特定的类型。所以函数只能在类型合适的表达式上使用,反之这个宏可以适用于整形长整型浮点型等可以用于>来比较的类型。宏与类型无关。 

宏的缺点:

        1.每次使用宏的时候,一份宏定义的代码将插入到程序中。除非宏比较短,否则大幅度增加程序长度。

        2.宏是没办法调试的。

        3.宏由于类型无关,也就不够严谨。

        4.宏有时候会带来运算符优先级问题,导致程序发生错误。

所以根据不同的情况进行选择使用宏还是函数有各自的优势。

        宏和函数的对比:

       

        性

                #define定义宏

         函数     

        代

        码

        长

        度

每次使用时,宏代码都会被插入到程序中。除了非常小的宏之外,程序长度会大幅组增长。
函数代码只出现于一个地方;每
次使用这个函数时,都调用那个
地方的同一份代码

        执

        行

        速

        度

更快
存在函数的调用和返回的额外开
销,所以相对慢一些

        操

        作

        符

        优

        先

        级

宏参数的求值是在所有周围表达式的上下文环境里,
除非加上括号,否则邻近操作符的优先级可能会产生
不可预料的后果,所以建议宏在书写的时候多些括
号。
函数参数只在函数调用的时候求
值一次,它的结果值传递给函
数。表达式的求值结果更容易预
测。

        带

        有

        副

        作

        用

        的

        参

        数

参数可能被替换到宏体中的多个位置,所以带有副作
用的参数求值可能会产生不可预料的结果。
函数参数只在传参的时候求值一
次,结果更容易控制。

        参

        数

        类

        型

宏的参数与类型无关,只要对参数的操作是合法的,
它就可以使用于任何参数类型。
函数的参数是与类型有关的,如
果参数的类型不同,就需要不同
的函数,即使他们执行的任务是
相同的。

        调

        试

宏是不方便调试的
函数是可以逐语句调试的。

        递

        归

宏是不能递归的
函数是可以递归的

 


3.2.6宏的命名约定和#undef指令
一、命名约定: 

        一般来说,函数与宏的使用语法很相似,所以语言本身没办法帮我们区分二者,大部分的C程序员都遵循一个默认的习惯:

        1、把宏名全部大写。 2、函数名不要全部大写。

二、#undef

        这条语句用于移除一个宏定义。

#include<stdio.h>#define MAX(x,y) ((x) + (y))int main()
{int a = 5, b = 6;printf("%3d",MAX(a, b));
#undef MAX//printf("%3d", MAX(a, b));return 0;
}

 在使用undef后,再次打印时会发现:

已经将这个宏给删除了。


 3.3条件编译

        在编译一条语句的时候我们如果要将一条语句(一组语句)编译或者放弃是很方便的。因为我们有条件编译指令。

那么条件编译指令有哪些?

        #if 常量表达式

        //...

        #elif 常量表达式

        //...

        #else

        //...

        #endif

        多个分支条件编译,也可以只有

        #if ...#endif

         if defined()

        if !defined()

        或者

        #ifdef ...

        #ifndef ...

        判断某个宏是否被定义,与宏的值

        无关,只与宏是否被定义有关。

其中,条件编译语句在程序中只能存在一次,因为在预编译阶段就会进行宏替换,所以在程序中只能起一次的作用


3.4文件包含

        我们不论写C语言还是写C++语言,我们都会用到头文件,像<stdio.h><stdlib.h><string.h>等,其实,#include指令可以使另外一个文件被编译。就像他实际出现于#include指令的地方一样。

这种替换的方式很简单:
        预处理器先删除这条指令,并用包含文件的内容替换。
        这样一个源文件被包含10次,那就实际被编译10次。
        1)本地文件包含:
#include"filename"

查找方式:

        先在源文件所在目录下查找,如果该头文件未找到,编译器就像查找库函数头文件一样在标准位置查找头文件。 如果找不到就提示编译错误。

        2)Linux环境的标准头文件的路径:

为/usr/include

        3)库文件包含:
#include<filename.h>
查找方法:
         查找头文件直接去标准路径下去查找,如果找不到就提示编译错误。 这样是不是可以说,对于库文件也可以使用 “” 的形式包含? 答案是肯定的,虽然可以但是这样做查找的效率就低些,当然这样也不容易区分是库文件还是本地文件了。 

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

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

相关文章

学信息系统项目管理师第4版系列32_信息技术发展

1. 大型信息系统 1.1. 大型信息系统是指以信息技术和通信技术为支撑&#xff0c;规模庞大&#xff0c;分布广阔&#xff0c;采用多级 网络结构&#xff0c;跨越多个安全域&#xff1b;处理海量的&#xff0c;复杂且形式多样的数据&#xff0c;提供多种类型应用 的大系统 1.1.…

【常用图像增强技术,Python-opencv】

文章目录 常用图像增强技术调整大小灰度变换标准化随机旋转中心剪切随机裁剪高斯模糊亮度、对比度和饱和度调节水平翻转垂直翻转高斯噪声随机块中心区域 常用图像增强技术 图像增强技术是常用于数据增强的方法&#xff0c;可以帮助增加数据集中图像的多样性&#xff0c;提高深…

论文阅读:Point-to-Voxel Knowledge Distillation for LiDAR Semantic Segmentation

来源&#xff1a;CVPR 2022 链接&#xff1a;https://arxiv.org/pdf/2206.02099.pdf 0、Abstract 本文解决了将知识从大型教师模型提取到小型学生网络以进行 LiDAR 语义分割的问题。由于点云的固有挑战&#xff0c;即稀疏性、随机性和密度变化&#xff0c;直接采用以前的蒸馏…

Mapping 设计指南

Mapping 设计指南 目录概述需求&#xff1a; 设计思路实现思路分析1、properties2.fields 3.search_analyzer4.2、format1、enabled2、doc_values 参考资料和推荐阅读 Survive by day and develop by night. talk for import biz , show your perfect code,full busy&#xff0…

Vue3 + Nodejs 实战 ,文件上传项目--大文件分片上传+断点续传

目录 1.大文件上传的场景 2.前端实现 2.1 对文件进行分片 2.2 生成hash值&#xff08;唯一标识&#xff09; 2.3 发送上传文件请求 3.后端实现 3.1 接收分片数据临时存储 3.2 合并分片 4.完成段点续传 4.1修改后端 4.2 修改前端 5.测试 博客主页&#xff1a;専心_前端…

【Python】文件操作

一、文件的编码 思考:计算机只能识别:0和1&#xff0c;那么我们丰富的文本文件是如何被计算机识别&#xff0c;并存储在硬盘中呢? 答案:使用编码技术( 密码本)将内容翻译成0和1存入 编码技术即:翻译的规则&#xff0c;记录了如何将内容翻译成二进制&#xff0c;以及如何将二…

人人开源前后端分离开源项目启动流程(超详细)

renren-security是一个轻量级的&#xff0c;前后端分离的Java快速开发平台&#xff0c;能快速开发项目并交付【接私活利器】采用SpringBoot、Shiro、MyBatis-Plus、Vue3、TypeScript、Element Plus、Vue Router、Pinia、Axios、Vite框架&#xff0c;开发的一套权限系统&#xf…

【计算机网络笔记】OSI参考模型基本概念

系列文章目录 什么是计算机网络&#xff1f; 什么是网络协议&#xff1f; 计算机网络的结构 数据交换之电路交换 数据交换之报文交换和分组交换 分组交换 vs 电路交换 计算机网络性能&#xff08;1&#xff09;——速率、带宽、延迟 计算机网络性能&#xff08;2&#xff09;…

(H5轮播)vue一个轮播里显示多个内容/一屏展示两个半内容

效果图 : html: <div class"content"><van-swipeclass"my-swipe com-long-swipe-indicator":autoplay"2500"indicator-color"#00C4FF"><van-swipe-itemclass"flex-row-wrap"v-for"(items, index) in M…

【Git】升级MacOS系统,git命令无法使用

终端执行git命令报错 xcrun: error: invalid active developer path (/Library/Developer/CommandLineTools), missing xcrun at: /Library/Developer/CommandLineTools/usr/bin/xcrun安装这个东东&#xff0c;&#xff1f;需要42小时 最终解决&#xff1a; 下载安装 https…

微信小程序开发的OA会议之会议个人中心的页面搭建及模板,自定义组件的学习

目录 一.自定义组件及会议效果编写 效果显示 二.个人中心布局 编写结果 ​编辑 一.自定义组件及会议效果编写 在页面中创建一个以components命名的项目来存放组件 再在components文件夹中创建一个组件&#xff0c;名为 :tabs &#xff0c;创建操作如图所示 刚刚创建好会报…

山海鲸可视化B/S架构应用

一、什么是B/S架构 BS架构&#xff08;Browser-Server架构&#xff09;是一种常见的软件架构模式&#xff0c;其中系统的核心业务逻辑和数据处理都发生在服务器端&#xff08;Server&#xff09;&#xff0c;而客户端&#xff08;Browser&#xff09;主要负责显示和用户交互。…

客户端post请求,服务器收到{}数据解决方法

当我们发起登录请求时&#xff0c;后台接收到的为{}数据 原因&#xff1a;传送过去的对象格式不对 解决方案&#xff1a; 引入qs npm install qs 在data中格式化数据 const res await axios({url:http://127.0.0.1:3000/post,method:post,data:Qs.stringify({username:te…

阿里云服务器x86计算架构ECS规格大全

阿里云企业级服务器基于X86架构的实例规格&#xff0c;每一个vCPU都对应一个处理器核心的超线程&#xff0c;基于ARM架构的实例规格&#xff0c;每一个vCPU都对应一个处理器的物理核心&#xff0c;具有性能稳定且资源独享的特点。阿里云服务器网aliyunfuwuqi.com分享阿里云企业…

【高等数学】导数与微分

文章目录 1、导数的概念1.1、引例1.1.1、变速直线运动瞬时速度1.1.2、曲线的切线 1.2、导数的定义1.3、证明常用导数1.4、导数的几何意义1.5、可导与连续的关系 2、函数的求导法则2.1、函数的和、差、积、商的求导法则2.2、反函数的求导法则2.3、复合函数的求导法则2.4、基本初…

github: kex_exchange_identification: Connection closed by remote host

问题描述 (base) ➜ test git:(dev) git pull kex_exchange_identification: Connection closed by remote host Connection closed by 192.30.255.113 port 22 致命错误&#xff1a;无法读取远程仓库。解决方案 参照下边文档 https://docs.github.com/en/authentication/tr…

基于SSM的工资管理系统

基于SSM的工资管理系统 开发语言&#xff1a;Java数据库&#xff1a;MySQL技术&#xff1a;SpringSpringMVCMyBatisVue工具&#xff1a;IDEA/Ecilpse、Navicat、Maven 系统展示 登录界面 管理员界面 通知公告 考勤管理 工资管理 请假管理 摘要 基于SSM&#xff08;Spring、S…

【每日一题】根据规则将箱子分类

文章目录 Tag题目来源题目解读解题思路方法一&#xff1a;分类讨论 其他语言cpython3 写在最后 Tag 【分类讨论】【2023-10-20】 题目来源 2525. 根据规则将箱子分类 题目解读 题目意思明确&#xff0c;根据条件判断箱子的类别。 解题思路 方法一&#xff1a;分类讨论 根据…

【小白专用】安装Apache2.4+ 安装PHP8.2+ php与sql server 2008 r2连接测试教程

PHP安装 1、PHP下载 PHP For Windows: Binaries and sources Releases 注意&#xff1a; 1.要下载Thread Safe&#xff0c;否则没有php7apache2_4.dll这个文件 2.如果是64位系统要下载x64的&#xff0c;x86的不行 3.下载Zip 2、PHP解压安装 将Zip进行解压&#xff0c;里…

docker全家桶(基本命令、dockerhub、docker-compose)

概念 应用场景&#xff1a; Web 应用的自动化打包和发布。自动化测试和持续集成、发布。在服务型环境中部署和调整数据库或其他的后台应用。从头编译或者扩展现有的 OpenShift 或 Cloud Foundry 平台来搭建自己的 PaaS 环境。 作用&#xff1a;Docker 使您能够将应用程序与基…