宏(预编译)详解

news2024/11/19 0:41:52

目录

一、程序的编译环境

二、运行环境

三、预编译详解

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##y

int 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.coloradmin.cn/o/1112696.html

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

相关文章

CCC数字钥匙设计【NFC】--通过NFC进行车主配对Phase3

1、车主配对流程介绍 车主配对可以通过车内NFC进行&#xff0c;若支持UWB测距&#xff0c;也可以通过蓝牙/UWB进行。通过NFC进行车主配对总共有5个Phase。本文档主要对Phase3进行介绍。 1) Phase0&#xff1a;准备阶段&#xff1b; 2) Phase1&#xff1a;启动流程&#xff1…

C语言中的3种注释方法

C语言中的3种注释方法 2021年8月28日星期六席锦 在用C语言编程时&#xff0c;常用的注释方式有如下几种&#xff1a; (1)单行注释 // … (2)多行注释 /* … */ (3)条件编译注释 #if 0…#endif (1)(2)在入门教程中比较常见。 对于(1) 【单行注释 // …】&#xff0c;注释只能显示…

将输入对象转换为数组数组的维度大于等于1numpy.atleast_1d()

【小白从小学Python、C、Java】 【计算机等考500强证书考研】 【Python-数据分析】 将输入对象转换为数组 数组的维度大于等于1 numpy.atleast_1d() 选择题 使用numpy.atleast_1d()函数,下列正确的是&#xff1f; import numpy as np a1 1 a2 ((1,2,3),(4,5,6)) print("…

Dev-C++ 软件安装教程(附安装包下载)

Dev-C&#xff08;123网盘&#xff09;提取码:EoF8https://www.123pan.com/s/JRpSVv-dKnjv.html &#xff08;软件包下载后&#xff0c;右键解压&#xff09; 一、打开文件夹&#xff0c;双击“Dev-C” 二、软件安装&#xff0c;点击“OK” 三、点击“I Agree” 四、点击“Ne…

计算机视觉基础(5)——特征点及其描述子

前言 本文我们将学习到特征点及其描述子。在特征点检测中&#xff0c;我们将学习角点检测和SIFT关键点检测器&#xff0c;角点检测以哈里斯角点检测器为例进行说明&#xff0c;SIFT将从高斯拉普拉斯算子和高斯差分算子展开。在描述子部分&#xff0c;我们将分别学习SIFT描述子和…

49数码论坛系统设计与实现

大家好✌&#xff01;我是CZ淡陌。一名专注以理论为基础实战为主的技术博主&#xff0c;将再这里为大家分享优质的实战项目&#xff0c;本人在Java毕业设计领域有多年的经验&#xff0c;陆续会更新更多优质的Java实战项目&#xff0c;希望你能有所收获&#xff0c;少走一些弯路…

浅析限流式保护器在低压配电系统中电气火灾的应用

安科瑞 华楠 【摘要】文章分析了低压配电系统短路的类型、原因及火灾危险性&#xff0c;并提出改善措施。 【关键词】&#xff1a;低压配电系统&#xff1b; 短路&#xff1b; 措施 0引言 随着人类进入现代文明社会&#xff0c;电气的普及深入千家万户&#xff0c;广泛应用于…

npm ERR! exited with error code: 128

1.遇到的问题 报错信息&#xff1a;npm ERR! E:\tools\Gitt\Git\cmd\git.EXE ls-remote -h -t https://github.com/nhn/raphael.git npm ERR! npm ERR! fatal: unable to access https://github.com/nhn/raphael.git/: OpenSSL SSL_read: Connection was reset, errno 10054 …

FPGA project : IIC_wr_eeprom

简介&#xff1a; 简单双向二线制&#xff0c;同步串行总线。 scl&#xff1a;串行时钟线&#xff0c;用于同步通讯数据。 sda&#xff1a;双向串行数据线。 物理层&#xff1a; 1&#xff0c;支持挂载多设备。 2&#xff0c;二线制。 3&#xff0c;每个设备有其单独的地…

【已解决】java的gradle项目报错org.gradle .api.plugins .MavenPlugin

我的java的gradle项目经常报错org.gradle .api.plugins .MavenPlugin。报错这个问题是因为依赖起冲突了&#xff0c;我在网上试了很多方法都没有效果&#xff0c;折让小编我很是苦恼&#xff0c;不过还好到最后问题还是解决了。 首先要知道你的项目所使用的gradle版本&#xf…

Webpack十大缺点:当过度工程化遇上简单的静态页面

&#x1f90d; 前端开发工程师&#xff08;主业&#xff09;、技术博主&#xff08;副业&#xff09;、已过CET6 &#x1f368; 阿珊和她的猫_CSDN个人主页 &#x1f560; 牛客高级专题作者、在牛客打造高质量专栏《前端面试必备》 &#x1f35a; 蓝桥云课签约作者、已在蓝桥云…

React-Router路由

1.React Router的基本使用 安装&#xff1a;安装时&#xff0c;选择react-router-dom&#xff0c;react-router会包含一些react-native的内容&#xff0c;web开发并不需要&#xff1b; npm install react-router-dom 路径模式 BrowserRouter使用history模式&#xff1b; Hash…

【六:(mock数据)spring boot+mybatis+yml】

目录 1.1、代码编写Demo类User类启动类 APplication 1.2、配置类查询语句的配置 mysql.ymlspringboot的配置 application.yml日志的配置 logback.xml数据库的配置 mybatis-config.xml 1.3、测试&#xff1a;1.3.1、测试获取用户数1.3.2、添加用户1.3.3、数据的更新1.3.4、数据的…

【八:(调springboot+testng+mybatis+数据校验】

目录 1、代码结构configcontrollermodelspringboot启动类 2、配置资源mysql.xmlapplication.ymllogback.xmlmybatis-config.xml数据库配置 3、测试验证 1、代码结构 config package com.course.config; import org.springframework.context.annotation.Bean; import org.sprin…

Linux 进程操作

文章目录 进程的基本知识进程pid进程常用的函数 forkwait和waitpidexec函数簇system函数信号处理signal函数Linux的SIGUSR1SIGUSR2 讨论 进程的基本知识 一个程序的执行称为一个进程&#xff0c;所有的代码都是在进程中执行的&#xff0c;进程是操作系统资源分配的基本单位。 在…

在启智平台上安装anconda(启智平台中新建调试任务,选的基础镜像中有conda的,就无需安装)

安装Anaconda3-5.0.1-Linux-x86_64.sh python版本是3.6 在下面的网站上找到要下载的anaconda版本&#xff0c;把对应的.sh文件下载下来 https://mirrors.tuna.tsinghua.edu.cn/anaconda/archive/ 把sh文件压缩成.zip文件&#xff0c;拖到启智平台的调试页面 上传到平台上 un…

tlaplus-vscode插件使用记录

参考官方教程Getting Started 和油管视频A gentle intro to TLA 入门和命令 首先在vscode的扩展里面下载 然后新建一个squares.tla文件 在代码区域先输入module生成上下的分隔符&#xff0c;然后输入pluscal来调用模版&#xff0c;生成一堆预设代码 小改一下&#xff0c;编写一…

WGBS项目文章 | 在缺氮情况下,细胞自噬对植物DNA甲基化会产生怎样的影响?

发表单位&#xff1a;中国科学院江西省庐山植物园 发表日期&#xff1a;2023年9月13日 研究期刊&#xff1a;International Journal of Molecular Sciences&#xff08;IF: 5.6&#xff09; 研究材料&#xff1a;拟南芥 2023年9月13日&#xff0c;中国科学院江西省庐山植物…

【二:Spring-AOP】

目录 一 、AOP1、什么是AOP2、AOP的类型3、AOP&#xff08;底层原理&#xff09;&#xff08;1&#xff09;第一种有接口情况&#xff0c;使用JDK 动态代理&#xff08;2&#xff09;第二种没有接口情况&#xff0c;使用[CGLIB](https://so.csdn.net/so/search?qCGLIB&spm…

【MyBatis进阶】mybatis-config.xml分析以及try-catch新用法

目录 尝试在mybatis项目中书写增删改查 遇见问题&#xff1a;使用mybaties向数据库中插入数据&#xff0c;idea显示插入成功&#xff0c;但是数据库中并没有数据变化? MyBatis核心配置文件剖析 细节剖析&#xff1a; try-catch新用法 截至目前我的项目存在的问题&#xf…