宏(预编译)详解
目录
一、程序的编译环境
二、运行环境
三、预编译详解
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函数,也肯能是意外终止。

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

如图所示,翻译环境 可以继续细分为编译和链接,编译还可以继续细分为预处理,编译,汇编,其中在翻译过程中首先进行的是预处理过程,在预处理过程中首先会把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;
}
可以发现,打印出来的结果跟预期一样,由(__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函数只能处理单行内容,如果想换行必须在每一行的末尾加上'\'转义字符才能把换行表示成字符来处理,否则会报错。
代码执行结果如下:

注意:在#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;
}

注意:这里替换文本的时候,参数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;
}
结果为:
发现了打印的值和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指令的地方一样。
1)本地文件包含:
#include"filename"
查找方式:
先在源文件所在目录下查找,如果该头文件未找到,编译器就像查找库函数头文件一样在标准位置查找头文件。 如果找不到就提示编译错误。
2)Linux环境的标准头文件的路径:
为/usr/include
3)库文件包含:
#include<filename.h>

相关文章:
宏(预编译)详解
目录 一、程序的编译环境 二、运行环境 三、预编译详解 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指令 一、命名约定: …...
hue实现对hiveserver2 的负载均衡
如果你使用的是CDH集群那就很是方便的 在Cloudera Manager中,进入HDFS Service 进入Instances标签页面,点击Add Role Instances按钮,如下图所示 点击Continue按钮,如下图所示 返回Instances页面,选择HttpFS角色…...
SkyWalking 告警规则配置说明
Skywalking告警功能是在6.x版本新增的,其核心由一组规则驱动,这些规则定义在config/alarm-settings.yml 文件中。告警规则定义分为两部分: 1、告警规则:它们定义了应该如何触发度量警报,应该考虑什么条件 2、webhook(网络钩子):定义当告警触发时,哪些服务终端需要被…...
HTML 表单笔记/练习
表单 概述 表单用于收集用户信息,用户填写表单提交到服务器 一般传参方式: GETPOSTCookie 传参要素 传参方式 GETPOST 参数的名字目标页面内容的数据类型(只有在上传文件的时候) 提示信息 一个表单中通常还包含一些说明性的文…...
关于Java Integer和Long使用equals直接比较
Integer和Long不能直接equals比较会返回False Long.class源码 public boolean equals(Object obj) {if (obj instanceof Long) {return this.value (Long)obj;} else {return false;} }Integer.class源码 public boolean equals(Object obj) {if (obj instanceof Integer) {…...
nodejs+vue衣服穿搭推荐系统-计算机毕业设计
模块包括主界面,系统首页、个人中心、用户管理、风格标签管理、衣服分类管理、衣服穿搭管理、服装信息管理、我的搭配管理、用户反馈、系统管理等进行相应的操作。无论是日常生活,还是特定场景,诸如面试、约会等,人们都有展现自我…...
Java并发面试题:(七)ThreadLocal原理和内存泄漏
ThreadLocal是什么? ThreadLocal是线程本地存储机制,可以将数据缓存在线程内部。ThreadLocal存储的变量在线程内共享的,在线程间又是隔离的。 ThreadLocal实现原理? ThreadLocal的底层是ThreadLocalMap,每个Thread都…...
香港服务器在国内访问太慢怎么能提高?
一直以来,全球化业务需求的增长是跟随着蓬勃向上的互联网而发展的。有了网络,海外贸易就在鼠标的轻点中完成。而IDC市场中的香港服务器也因为免备案政策的特性,开始逐渐成为企业想要跨越地域壁垒而考虑的对象。但在使用过程中ÿ…...
使用Proxyman抓取Android的https请求
使用Proxyman抓取Android的https请求 有时,您可能需要测试您的移动应用程序并检查与其关联的所有网络请求。在网络上,此任务非常简单,只需按Ctrl Shift I打开开发人员工具即可。从那里,您可以导航到网络选项卡并检查与网页相关的…...
基础MySQL的语法练习
基础MySQL的语法练习 create table DEPT(DEPTNO int(2) not null,DNAME VARCHAR(14),LOC VARCHAR(13) );alter table DEPTadd constraint PK_DEPT primary key (DEPTNO);create table EMP (EMPNO int(4) primary key,ENAME VARCHAR(10),JOB VARCHAR(9),MGR …...
RAID和LVM配置指南:创建、扩容和管理RAID设备和逻辑卷的方法
文章目录 1. 简介1.1 什么是RAID和LVM1.2 RAID和LVM的作用和优势 2. RAID配置命令:mdadm2.1 安装mdadm2.2 创建RAID设备2.2.1 RAID 02.2.2 RAID 12.2.3 RAID 52.2.4 RAID 10 2.3 添加磁盘到RAID设备2.4 删除磁盘从RAID设备2.5 查看和管理RAID设备2.6 故障处理与恢复…...
MapStruct使用方法
一、用途 1.1 优势 与动态映射框架相比,MapStruct 具有以下优势: (1)通过使用普通方法getter、setter调用,而不是反射来快速执行,效率很高。 (2)编译时类型安全:只能映…...
【LeetCode】50. Pow(x, n)
1 问题 实现 pow(x, n) ,即计算 x 的整数 n 次幂函数(即, x n x^n xn )。 示例 1: 输入:x 2.00000, n 10 输出:1024.00000 示例 2: 输入:x 2.10000, n 3 输出&a…...
vue2技能树(2)-模板语法、vue的工具链、渐进式框架
目录 Vue2技能树Vue 2 简单的模板语法详解插值绑定属性指令v-if 和 v-elsev-forv-on 计算属性过滤器插槽 Vue 2 生态系统详解1. Vue Router2. Vuex3. Vue CLI4. Axios5. Vue Devtools6. Element UI、Vuetify、Quasar等UI框架7. Nuxt.js8. Vue Apollo、Vue Router、Vue Fire等插…...
【Git系列教程-目录大纲】
《Git系列教程-目录大纲》 完完全全从零开始深入学习Git,教程配图200张,其中包括包括Git基本命令、命令原理、Git底层命令、分支、分支的原理、Git代码冲突原理/解决、tag标签、Git存储状态、分支合并原理、典型合并、快进合并、同轴开发、非同轴开发、…...
【高等数学】导数与微分
文章目录 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、基本初…...
springboot之quartz动态可控定时任务
Quartz Quartz是一个开源的任务调度框架,可以用来实现定时任务的调度,如定时发送邮件、定时备份数据等。Quartz具有很高的可靠性和灵活性,支持集群部署和分布式调度,并且提供了丰富的API和插件,可以轻松实现复杂的调度…...
什么是CSS的外边距重叠?
区块的上下外边距有时会合并(折叠)为单个边距,其大小为两个边距中的最大值(或如果它们相等,则仅为其中一个),这种行为称为外边距折叠。注意:有设定浮动和绝对定位的元素不会发生外边…...
设计模式之抽象工厂模式
前言 工厂模式一般指的是简单工厂模式、工厂方法模式、抽象工厂模式,这是三种工厂模式的最后一篇,其他两种的文章链接如下: 设计模式之简单工厂模式-CSDN博客 设计模式之工厂方法模式-CSDN博客 建议三种模式放在一起对比学习,…...
Compose预处理组件大比拼:性能、应用场景和可视化对比总结
在机器学习的世界里,预处理组件就像是厨师的烹饪工具。选择合适的工具不仅可以让整个烹饪过程更加顺畅,还能确保最终的菜肴更加美味。 本文将深入探讨四种“烹饪工具”:TransformedTargetRegressor、make_column_transformer、make_column_selector和ColumnTransformer。通…...
日语学习-日语知识点小记-构建基础-JLPT-N4阶段(33):にする
日语学习-日语知识点小记-构建基础-JLPT-N4阶段(33):にする 1、前言(1)情况说明(2)工程师的信仰2、知识点(1) にする1,接续:名词+にする2,接续:疑问词+にする3,(A)は(B)にする。(2)復習:(1)复习句子(2)ために & ように(3)そう(4)にする3、…...
如何在看板中体现优先级变化
在看板中有效体现优先级变化的关键措施包括:采用颜色或标签标识优先级、设置任务排序规则、使用独立的优先级列或泳道、结合自动化规则同步优先级变化、建立定期的优先级审查流程。其中,设置任务排序规则尤其重要,因为它让看板视觉上直观地体…...
Qt Widget类解析与代码注释
#include "widget.h" #include "ui_widget.h"Widget::Widget(QWidget *parent): QWidget(parent), ui(new Ui::Widget) {ui->setupUi(this); }Widget::~Widget() {delete ui; }//解释这串代码,写上注释 当然可以!这段代码是 Qt …...
MVC 数据库
MVC 数据库 引言 在软件开发领域,Model-View-Controller(MVC)是一种流行的软件架构模式,它将应用程序分为三个核心组件:模型(Model)、视图(View)和控制器(Controller)。这种模式有助于提高代码的可维护性和可扩展性。本文将深入探讨MVC架构与数据库之间的关系,以…...
Nginx server_name 配置说明
Nginx 是一个高性能的反向代理和负载均衡服务器,其核心配置之一是 server 块中的 server_name 指令。server_name 决定了 Nginx 如何根据客户端请求的 Host 头匹配对应的虚拟主机(Virtual Host)。 1. 简介 Nginx 使用 server_name 指令来确定…...
新能源汽车智慧充电桩管理方案:新能源充电桩散热问题及消防安全监管方案
随着新能源汽车的快速普及,充电桩作为核心配套设施,其安全性与可靠性备受关注。然而,在高温、高负荷运行环境下,充电桩的散热问题与消防安全隐患日益凸显,成为制约行业发展的关键瓶颈。 如何通过智慧化管理手段优化散…...
Axios请求超时重发机制
Axios 超时重新请求实现方案 在 Axios 中实现超时重新请求可以通过以下几种方式: 1. 使用拦截器实现自动重试 import axios from axios;// 创建axios实例 const instance axios.create();// 设置超时时间 instance.defaults.timeout 5000;// 最大重试次数 cons…...
C++ 求圆面积的程序(Program to find area of a circle)
给定半径r,求圆的面积。圆的面积应精确到小数点后5位。 例子: 输入:r 5 输出:78.53982 解释:由于面积 PI * r * r 3.14159265358979323846 * 5 * 5 78.53982,因为我们只保留小数点后 5 位数字。 输…...
C++八股 —— 单例模式
文章目录 1. 基本概念2. 设计要点3. 实现方式4. 详解懒汉模式 1. 基本概念 线程安全(Thread Safety) 线程安全是指在多线程环境下,某个函数、类或代码片段能够被多个线程同时调用时,仍能保证数据的一致性和逻辑的正确性…...
GitFlow 工作模式(详解)
今天再学项目的过程中遇到使用gitflow模式管理代码,因此进行学习并且发布关于gitflow的一些思考 Git与GitFlow模式 我们在写代码的时候通常会进行网上保存,无论是github还是gittee,都是一种基于git去保存代码的形式,这样保存代码…...
