首页 > 其他分享 >【C语言】预处理(预编译)详解(上)(C语言最终篇)

【C语言】预处理(预编译)详解(上)(C语言最终篇)

时间:2024-10-26 23:18:37浏览次数:3  
标签:定义 ++ MAX 预处理 详解 参数 C语言 我们 define

在这里插入图片描述

文章目录

一、预定义符号

   学习本篇文章的内容推荐先去看前面的编译和链接,才能更好地理解和吸收,文章链接:【C语言】编译和链接(编译环境和运行环境)
   C语⾔设置了⼀些预定义符号,可以直接使⽤,预定义符号也是在预处理期间处理的,如下:

_ _FILE_ _ 
_ _LINE_ _ 
_ _DATE_ _ 
_ _TIME_ _ 
_ _STDC_ _ 

   我们需要注意的是,使用这些预定义符号的时候,下面的两个短下划不能少,并且两个短下划线之间是没有间隙的,由于写文章时两个短下划线会进行合并,所以这里我加上了空格,但是在实际使用时,下面的两个短下划线之间没有间隙
   接下来我们来再详细介绍一下它们(在描述时我会省略短下划线,但是我们要注意,那些下划线也属于预定义符号的一部分,使用时必须加上):

  1. FILE代表当前进行编译的源文件,在打印时,需要使用占位符%s,它不仅会打印文件名,还会打印文件的完整路径
  2. LINE代表出现了这个预定义符号的行号,比如这个预定义符号出现在第6行时,那么它就代表6,所以需要使用%d进行打印
  3. DATE代表文件被编译时的日期,打印时需要使用占位符%s
  4. TIME代表文件被编译时的具体时间,具体到时分秒,打印时也是使用占位符%s
  5. STDC就与编译文件的编译器有关了,如果编译当前文件的编译器完全遵守了ANSI C标准,那么它将会被定义,并且值为1,打印时需要使用%d,如果该编译器不完全遵守ANSI C标准,那么STDC这个预定义符号就没有被定义过,如果使用它就会报错

   接着我们就来使用一下这几个预定义符号,首先我们来使用前4个预定义符号,来打印我们源文件在编译时的各种信息,如下:

#include <stdio.h>

int main()
{
	printf("FILE: %s\n", __FILE__);
	printf("LINE: %d\n", __LINE__);
	printf("DATE: %s\n", __DATE__);
	printf("TIME: %s\n", __TIME__);
	return 0;
}

   我们来看看代码运行结果:
在这里插入图片描述
   我们来看看运行结果是否如同我们上面说的那样,首先FILE会打印源文件的完整路径,LINE会打印它出现时的行号,可以看到LINE确实是在第8行出现,DATE打印的就是文件被编译的日期,是2024年10月26日,也没有问题,最后就是TIME,也确实打印了文件编译时的时分秒
   接着我们就可以使用STDC这个预定义符号,来判断我们的编译器是否完全遵循ANSI C,如下:

#include <stdio.h>

int main()
{
	printf("STDC: %d", __STDC__);
	return 0;
}

   接着我们在VS2022这个IDE上面运行一下,结果如图:
在这里插入图片描述
   可以看到VS2022在运行时报错了,不认识这个标识符,说明我们的VS2022并没有严格遵守ANSI C标准,可能遵守了%99,但是就是没有完全遵守

二、#define定义常量

   #define定义常量的基本语法如下:

#define name stuff

   其中的name就是我们定义的常量的名称,stuff就是我们定义的常量的值,可以是整型,可以是字符串,也可以是字符等等
   接着我们就使用#define来定义各种类型的常量,我们要注意的一点是,在取名时我们的常量名最好全部大写,这是我们编程的一种习惯,如下:

#include <stdio.h>

#define MAX 100
#define STR "I am Sam!"
#define CH 'x'

int main()
{
	printf("MAX: %d\n",MAX);
	printf("STR: %s\n",STR);
	printf("CH : %c\n", CH);
	return 0;
}

   我们来看看运行结果:
在这里插入图片描述   可以看到这些常量都可以正常使用
   接着我们思考一个问题,在#define定义常量时,后面是否要加分号?比如:

 #define MAX 100;
 #define MAX 100

   我们首先要知道#define定义常量时是怎么工作的,它会直接把常量名替换为对应的值,在第一条语句中,MAX就代表了100; ,而在第二条语句中,MAX就只代表100
   所以很明显我们在使用#define定义常量时,最好不要在后面加上分号,那么为什么有时候加上分号也没有问题呢?如下:

#define MAX 100;

int a = MAX;

   当我们运行这条语句时,发现不会出错,这是为什么呢?我们只需要把它替换一下就知道了:

int a = 100;;

   现在相当于就是语句后多了一条分号,前一个分号就是这条语句的结束标志了,第二个分号相当于是一个空语句,什么也没有做,所以这句话就相当于了两条语句,第二条语句是空语句,什么也没有做,所以执行起来没有问题
   但是这种情况也不是我们使用#define定义常量的初衷,我们只是想要使用MAX表示100而已,并不想要带上那个分号,并且加上分号后,有很多情况会出错,所以我们使用时就最好不要在#define定义常量时在后面加上分号

三.、#define定义宏

   #define 机制包括了⼀个规定,允许把参数替换到⽂本中,这种实现通常称为宏(macro)或定义宏(definemacro),下面是宏的声明方式:

#define name( parament-list ) stuff

   其中的parament-list 是⼀个由逗号隔开的符号表,它们可能出现在stuff中,要注意的是:参数列表的左括号必须与name紧邻,如果两者之间有任何空⽩存在,参数列表就会被解释为stuff的⼀部分
   是不是有点难懂,我们可以看如下的例子:

#define SQUARE( x )  x * x

   它的形式有点类似于函数,前面就相当于函数名,括号中就是宏的参数,后面是这个宏的计算方式,比如使用SQUARE(5),那么预处理后,就会把这条语句转化成5*5
   其中SQUARE和第一个小括号要紧紧贴在一起,如果两者之间有任何空⽩存在,那么(x)就会成为后面的一部分,就会出错
   那么我们上面写的这个宏是否就完全正确了呢?其实它还存在一个问题,比如我们来看一个例子:

#include <stdio.h>

#define SQUARE( x ) x * x

int main()
{
	int a = 5;
	printf("%d\n", SQUARE(a + 1));
	return 0;
}

   我们预期的结果是它帮我们算出6的平方36,那么它最后能否得到这个结果呢?我们来看看它的运行结果:
在这里插入图片描述
   我们可以惊奇的发现,程序运行的结果不应该是a+1,也就是6的平方36吗?为什么结果变成了11?这就要涉及到我们上面谈到过的,#define定义的内容是直接替换的,不会有任何的变化
   其中的x会直接被a+1替换,那么SQUARE(x)经过替换过后应该是如下的样子:

a + 1 * a + 1
//带入a=5
5 + 1 * 5 + 1

   这个时候就可以发现问题了,由于运算符的优先级,中间的1 * 5会优先计算,变成5,然后就是5+5+1,最后结果为11
   那么怎么解决运算符导致的错误呢?我们可以在定义宏的时候,把参数使用小括号括起来,让每个参数成为一个整体,无论怎么样都是参数内部先计算,最后再进行宏定义的运算,如下:

#define SQUARE( x ) (x) * (x)

我们将宏定义改成这样再来看看代码运行结果:
在这里插入图片描述
   那么这样是否就一定不会出错了呢?这里就不卖关子了,这样还是不能确保得到我们预期的结果,为什么呢?我们接着看一个例子:

#include <stdio.h>

#define DOUBLE( x ) (x) + (x)

int main()
{
	int a = 5;
	printf("%d\n", 10 * DOUBLE(a));
	return 0;
}

   按照我们的预期,宏DOUBLE会帮我们计算出一个数的2倍,那么这里5的2倍是10,乘以10过后就变成了100,那么我们来看最后的结果是否是100:
在这里插入图片描述
   可以看到结果又与我们预期的不一样了,这还是我们在预处理阶段出现的问题,还是因为#define使用宏的时候,会直接替换内容,上面的那条语句经过替换后如下:

10 * (a) + (a)
//将a替换成5之后
10 * (5) + (5)

   这个时候就可以看出来,由于*的优先级更高,所以10和前面那个5结合变成了50,然后+5变成了55,这就是55的由来,所以我们可以看出,光给每个参数加上()还不够,我们还最好把整个式子括起来,表示它们是一个整体,如下:

#define DOUBLE( x ) ((x) + (x))

   接着我们就拿这个宏定义来试试答案是否会变成我们预想的100,如图:
在这里插入图片描述
   可以看到最后结果就正确了,所以总结一下,在我们使用宏定义的时候,我们要使用()将每个参数括起来,保证每个参数是一个整体,最后我们还要使用()将整个式子括起来,保证整个式子是一个整体

四、带有副作用的宏参数

   宏参数还有副作用,是不是基本上没有听过这种说法,为什么会这么说呢?我们一起来学习一下:
   带有副作用的宏参数就是:当宏参数在宏的定义中出现超过⼀次的时候,如果参数带有副作⽤,那么你在使⽤这个宏的时候就可能出现危险,导致不可预测的后果,其中副作⽤就是表达式求值的时候出现的永久性效果
   我们可以举一个例子,如下:

//不带副作⽤
x+1;
//带有副作⽤
x++;

   乍一看这两者不是一样的吗?但其实并不一样,因为x++对x造成了永久性的效果,就是对x自增了一个1,而x+1这个表达式对x并没有影响
   接着我们来看一个例子来更好的理解,我们来定义一个宏,它的功能就是帮我们找到两个数中的最大数:

#include <stdio.h>

#define MAX(x,y) ( (x) > (y) ? (x) : (y) ) 

int main()
{
	int a = 5;
	int b = 2;
	int ret = MAX(a++, b++);
	printf("a = %d b = %d ret = %d\n", a, b, ret);
	return 0;
}  

   这个例子的运行结果是什么呢?我们预期的结果是a变成6,b变成3,ret则是5,因为传参的时候使用的是后置++,所以是先使用a和b的值,也就是把5和2作为参数传过去后,然后a和b再++,所以a变成了6,b变成了3,ret还是5
   那么最后结果是否是我们预期的结果呢?如图:
在这里插入图片描述
   可以看到,最后结果和我们的预期又不一样,而且还相差的很远,这是为什么呢?这其实就是我们所说的带副作用的宏参数,那么引起它的本质是什么呢?没错,还是因为宏定义时的#define替换规则
   由于在预处理阶段,会将宏直接替换过来,所以上面的语句就变成如下语句:

( (a++) > (b++) ? (a++) : (b++) ) 

   在执行这条语句时,首先会执行(a++) > (b++),此时这里是后置++,所以a和b先使用再自增1,由于a是5,b是2,a>b成立了,然后对a和b进行自增1,a就变成了6,b就变成了3
   由于(a++) > (b++)的结果为真,所以最后整个三目表达式返回的就是a++的结果,由于这里还是后置++,所以返回的就是6,然后对a自增1变成7,所以最后ret的值就是6,a的值为7,b的值为3
   所以我们在使用宏的时候最好不要使用带副作用的宏参数,也就是使用后会对原本的参数造成永久性效果的表达式,例如++和- -操作

五、宏替换的规则

   在程序中扩展#define定义符号和宏时,需要涉及以下⼏个步骤,我们简单地了解一下:

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

注意:

  1. 宏参数和#define定义中可以出现其他#define定义的符号,比如先使用#define定义一个常量N,值为100,那么这个N就可以在另一个#define中出现,但是对于宏,不能出现递归
  2. 当预处理器搜索#define定义的符号的时候,字符串常量的内容并不被搜索

六、宏和函数的对比

1.宏的优势

   宏通常被应⽤于执⾏简单的运算,而函数则可以应用于较为复杂的场面,⽐如在两个数中找出较⼤的⼀个时,写成下⾯的宏,更有优势⼀些

#define MAX(x,y) ( (x) > (y) ? (x) : (y) ) 

   那为什么不⽤函数来完成这个任务?原因有2点:

  1. ⽤于调⽤函数和从函数返回的代码可能⽐实际执⾏这个⼩型计算⼯作所需要的时间更多,因为函数还要开辟自己的栈帧,进行返回等等操作,所以宏⽐函数在程序的规模和速度⽅⾯更胜⼀筹
  2. 更为重要的是函数的参数必须声明为特定的类型,所以函数只能在类型合适的表达式上使⽤,反之这个宏怎可以适⽤于整形、⻓整型、浮点型等可以⽤于>来比较的类型,宏的参数是类型⽆关的,比如上面我们定义的MAX宏,不仅可以比较整型,同时也可以比较浮点型和长整型等等,而一个函数只能比较单个数据类型

2.函数的优势

   对于宏来说,函数也有它的优势,它们没有一定的哪一个好,只有哪一个更适合我们的需求,那么对比宏,函数的优势如下:

  1. 每次使⽤宏的时候,⼀份宏定义的代码将插⼊到程序中,除⾮宏⽐较短,否则可能⼤幅度增加程序的⻓度
  2. 宏是没法调试的,而函数可以一步一步调试,查看bug出现的原因
  3. 宏由于类型⽆关,也就不够严谨,这在上面成为了它的优势,但是在某些场景导致它的不够严谨,这个时候就要使用函数
  4. 宏可能会带来运算符优先级的问题,导致程序容易出错,比如忘记对参数加上(),或者忘了给整个式子加上()都可能出现预期以外的结果

3.宏和函数的命名约定

   ⼀般来讲函数的宏的使⽤语法很相似,并且语⾔本⾝没法帮我们区分⼆者,所以我们平时就通过命名来简单区分它们,接下来我们来看看它们的命名约定:

  1. 宏名全部大写
  2. 函数名不要全部大写,一般是多个单词中,每个单词的首字母大写

   今天的C语言知识分享就到这里啦,也是我们的最终篇(上),下一篇我们会讲到条件编译,又是一个硬知识,最后希望大家能在我的博客能够学习到知识,如果有疑问欢迎提出来
   bye~

标签:定义,++,MAX,预处理,详解,参数,C语言,我们,define
From: https://blog.csdn.net/hdxbufcbjuhg/article/details/143235296

相关文章

  • Matplotlib保姆级详解
    1.概念Matplotlib库:是一款用于数据可视化的Python软件包,支持跨平台运行,它能够根据NumPyndarray数组来绘制2D图像,它使用简单、代码清晰易懂2.安装pipinstallmatplotlib3.应用场景数据可视化主要有以下应用场景:企业领域:利用直观多样的图表展示数据,从而为企业......
  • 【Linux探索学习】第八弹——Linux工具篇(三):Linux 中的编译器 GCC 的编译原理和使用详
    #1024程序员节|征文#Linux下的vim编辑器:【Linux探索学习】第七弹——Linux的工具(二):Linux下vim编辑器的使用详解-CSDN博客前言:在上一篇我们学习了如何在Linux环境下直接用vim编辑器来进行编辑代码,今天我们来学习如何运行我们所编辑的代码,运行代码就需要编译器,也就是我们下......
  • c语言之正负整数在内存中的存储本质
    int、short、long、longlong是如何定义变量的        我们先从最为我们所知的定义变量入手,当我们用int定义一个变量的时候,这个变量是整型,长度是4个字节,不同的操作系统下由int定义的变量长度有可能不同,当然对于short、long、longlong也是同样如此,因此为了使大家更清......
  • C语言经典20例(输入数组元素,将其反转并输出)
    1.定义数组:首先定义一个数组来存储输入的元素。2.输入元素:使用循环结构(如for循环)来从用户那里获取数组元素。3.反转数组:通过交换数组两端的元素来实现反转,这通常需要一个循环,该循环从数组的两端开始,向中间移动。4.输出反转后的数组:再次使用循环结构来打印反转后的数组。......
  • C语言——数组、指针、函数
    目录1、数组、指针、函数2、数组指针及指针数组2.1、数组指针2.2、指针数组2.3、区别3、指针函数与函数指针3.1、指针函数3.2、函数指针3.3、区别4、所有组合1、数组、指针、函数    在前面我们已经学习了数组、指针以及函数,看起来都没有难的地方,我自认......
  • sed 命令详解及示例
    sed是一种流编辑器,能高效地完成各种替换、删除、插入等操作,按照文件数据行顺序,重复处理满足条件的每一行数据,然后把结果展示打印,且不会改变原文件内容。sed会逐行扫描输入的数据,并将读取的数据内容复制到临时缓冲区中,称为“模式空间”(patternspace),然后拿模式空间中的数据与给......
  • 如何利用递归和迭代构建二叉树?详解题解
    文章目录根据二叉树创建字符串思路代码二叉树的层序遍历思路代码二叉树的最近公共祖先思路代码二叉搜索树与双向链表思路代码从前序与中序遍历序列构造二叉树思路代码总结根据二叉树创建字符串题目:样例:可以看见,唯一特殊的就是左子树,当右子树存在的时候左......
  • 实验3 c语言函数应用编程
    实验任务1task1.c1#include<stdio.h>23charscore_to_grade(intscore);//函数声明45intmain(){6intscore;7chargrade;89while(scanf("%d",&score)!=EOF){10grade=score_to_grade(score);//......
  • 《面试最爱问的Spring》- IOC启动流程,实战详解
    简介Spring作为一款经典框架,并且作为Spring家族的老大哥,也是SpringBoot,SpringCloud的一个基石,在我们工作中使用频率非常高,所以深入了解Spring的实现就很有必要。IoC(或DI)是Spring框架的核心功能之一,是Spring生态系统的基础。此处有一个很重要的容器,容器的作用:用来存储对象,Bea......
  • 【Linux】线程池详解及其基本架构与单例模式实现
    目录1.关于线程池的基本理论     1.1.线程池是什么?1.2.线程池的应用场景:2.线程池的基本架构2.1.线程容器2.2.任务队列2.3.线程函数(HandlerTask)2.4.线程唤醒机制3.添加单例模式3.1.单例模式是什么?3.2.饿汉实现方式和懒汉实现方式饿汉式单例模式:懒汉式单例......