C语言----预处理(详解)

        好了书接上回。我在讲编译与链接的时候写过宏和条件建议。我说会在下一篇博客中讲解,那么来了。今天我们来详细的讲讲预处理。宏与条件编译也在其中,那么我们现在就来好好会会这个预处理吧。

预定义符号

        关于预定义符号,我暂时只知道几个。并且我知道的这几个对于我们现在还在学习阶段感觉没什么帮助,哈哈。我知道的这几个预定义符号应该对于我们以后工作时写日志,应该是有帮助吧。这个有什么作用还是等大家日后自己定夺吧,这里我就先给大家引出来。

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

86254f5101e34ee2905c53b5e98a6fe8.png

       这里我是先进行执行后才将__STDC__给打出来的,不是在执行前就写出来的,大家注意一样,别误会了。然后大家可以看一下我打印出来的结果。__FILE__:详细的打印出来被编译文件的地址。__LINE__:打印了这条代码是在第几行。虽然我自己在编译器中没有标注出来,但我觉得编译器应该不会错吧,1要是真的出错我更愿意相信是自己写代码错误了。__DATE__ :打印日期,但是大家要注意顺序是几月份(英语),几号,多少年。这个顺序与我们平常的顺序是有一点不一样的,但是一下子也可以看出来,大家稍微注意一下就可以了。__TIME__:打印时间这个就没什么好说的了,一下子就可以看出来了。最后的__STDC__:因为我的编译器没有遵循ANSI C,所以就是未定义。我觉得应该是vs都是这样的吧,大家可以尝试其他的编译器看看,反正在下的编译器是没办法的了。

int main()
{
	printf("file:%s line:%d\n", __FILE__, __LINE__);
	printf("date:%s time:%s\n", __DATE__, __TIME__);
	printf("stdc:%d\n", __STDC__);
	return 0;
}

#define

       我想写过扫雷代码的朋友应该都使用过这个吧,至少大家应该都看过这个吧。#define定义的标识符常量和宏都会在在预处理阶段,对程序中所有出现的标识符,宏名都会被定义的内容替换掉。大家可以理解写这个是是我们写宏的前提。

#define name stuff

        上面只是一个基本格式,那么如何使用,我们来看看下面的代码吧:

ba31bcceb0694da3a7f59b5d83ce471b.png

         这里我只是定义了一个为100的常量和一个符号while(1),不知道大家有没有看出这个#define的作用,我们只需要在最开头的地方写出#define,然后空一个写名字,再空一个写内容。这样一个简单的定义就完成了,那是不是只能写像我上面的那些简单的定义嘞,当然不是啦。还有一些定义比如:

#define DEBUG_PRINT printf("file:%s\tline:%d\t \
 date:%s\ttime:%s\n" ,\
 __FILE__,__LINE__ , \
 __DATE__,__TIME__ )

#define CASE break;case //在写case语句的时候⾃动把 break写上。

      不知道大家是否注意到我上面的这个代码是否有一个\,那么这个\是干什么的嘞,其实这个就是方便我们观看,我们换行了。如果我不写\的话那这个代码就是这样比较长,也许这样可能还还算可以看,但是要还一个更多的代码的话,怎么搞,是吧。这样有一个\,不仅方便我们观看而且还便于我们写的时候看。

153886e3b76a427e9febe3ad130c745d.png

        然后这里再提一嘴,我们要不要在#define定义标识符后面是否需要写;嘞?其实这个写了也无所谓的,编译器并不会报错,但是这对我们的代码会有一点的影响,大家可以看看:

282ffc3d9e0241fd9da9bd302c437369.png

        大家看到这个,我们知道我们这里#define并没有报错,但是当我们使用的时候却报错了。所以在#define定义标识符后面写;并不会报错,但是当我们使用的时候需要注意,不然会出现我们意想不到的问题。所以我们建议不在#define定义后面写;。不然的话可能会造成意想不到的问题。

#define定义宏

        好了,终于到了我们上面一篇博客中提及过的知识了。关于宏的知识了。允许把参数替换到⽂本中,这种实现通常称为宏(macro)或定义宏 (define macro)。参数列表的括号必须与我们写的名字紧凑,不能分开,要是分开了的话,那就是另外一个东西了。

#define name( parament-list ) stuff

        那么我们写点代码来看看吧:

deaa3db44539440584eb946b3ea7de01.png

        在这里我写了一个wu和yh一个是判断大小一个是平方。这里就是运用了把参数替换到⽂本中的,大家可以在看一下替换后的样子是什么样的:

5f21fefee6f34469a66749a7dfe70e18.png

       好,我们看到这里大家应该看到了如何定义宏了吧。但是大家想一下下面的这个代码结果是什么嘞:

b79578c8635b43b58b420916f9a832fd.png

       大家看一下我们打印的第三个结果是多少,是11。但是我们看我们想要的结果是多少啊。我们应该想要的是36啊。怎么会是11啊。所以着我们就得考虑考虑运算符的优先级了呀。我们这里替换的话是什么样子的话:

2ef5a8123c6a43049ef979b305952979.png

       这样打大家看一下,是吧,这样替换的话,得到11的话好像也是正确的呀,但是对于我们想要的结果却是错误的啊,那么我们如何想要得到我们的结果嘞。

c49de5189c4a465898c2dc46e2c032d6.png

       是不是,我们在宏的时候加了括号后,结果就舒服了。是吧那我们再看看下面的代码结果又是什么样子的嘞:

f6e8258680894af6bd3373f2b5f67ed4.png

      我们第5个结果是55,但是大家看一下,我们写这个代码是想得到的结果是个啥嘞。我们是不是像10*10结果为100呀。但为什么我们结果是55嘞。那就是我们

2e08b41aafb449aaa4b42614b04a6a09.png

       这可以看出我们给传递参数的时候是没问题的,但是我们其实是想在传递后将计算结果后再乘以10那怎么搞嘞:

1382dbbab4414bbfa0246bd822e5d70e.png

          是吧,这不就迎刃而解了呀我们在传递过后计算后再计算其他的。经过上面这两个坑后,是不是觉得哎呀,这几个问题都是括号起来关键作用啊。没错,我们在定义宏的时候不要舍不得括号,是吧。我们这里开始就是因为没写括号所以出现了问题。

#define xi 100;
#define ha while(1)
#define wu(y,x) x>y?x:y 
#define yh(x) (x)*(x)
#define yh1(x) ((x)+(x))
//#define DEBUG_PRINT printf("file:%s\tline:%d\t date:%s\ttime:%s\n" ,__FILE__,__LINE__ ,  __DATE__,__TIME__ )

int main()
{
	int h = wu(3, 6);//int h=3>6?3:y;
	printf("%d\n", h);
	int xix = yh(6);//int xix=6*6;
	printf("%d\n", xix);
	printf("%d\n", yh(5+1));//5+1*5+1=5+5+1=11
	printf("%d\n", yh(5 + 1));
	printf("%d\n",10* yh1(5));//10*(5+5)
	//ha
	//{
	// printf("%d\n",xi);
	//	
	//}
	return 0;
}

宏定义的副作用

       大家都知道是药三分毒,既然救人的要都是有毒性的,那么我们宏有点副作用一个很正常吧。当宏参数在宏的定义中出现超过⼀次的时候,如果参数带有副作⽤,那么你在使⽤这个宏的时候就可能出现危险,导致不可预测的后果。副作⽤就是表达式求值的时候出现的永久性效果。那具体是个什么副作用嘞,我来写一个例子大家看一下:
cf59b5beef3542a8aa916c90abfded8d.png

   

        大家可以看一下,明明我传的是5++和8++。再怎么说吧,也不会搞个10出来吧,这是为什么嘞。当然出现问题先找自己的原因。编译器肯定是没用错的。想想如果我们把参数替换掉会是什么样子的嘞。6ef366c49ef140b5aacdb1434432e1d8.png

        大家看一下,我们要是替换过后结果是不是这样的,我们先替换过去后,都++了。然后当判断结束后,要返回值的时候又++了,那么最大的值是不是有两个++了。这也就表明了宏并不是无所不能的,在一定方面上看宏的使用是不如自定义函数的。我们当我们学会了使用宏定义后不要什么都是宏定义,大家要思考一下使用宏定义一点还是自定义函数。

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

int main()
{
	int x = 5;
	int y = 8;
	int z = na(x++, y++);//((5++)>(8++)?(5++):(8++))
	printf("x=%d y=%d z=%d\n", x, y, z);//输出的结果是什么?
	return 0;
}

宏替换的规则

在程序中扩展#define定义符号和宏时,需要涉及⼏个步骤。
1. 在调⽤宏时,⾸先对参数进⾏检查,看看是否包含任何由#define定义的符号。如果是,它们⾸先被替换。
2. 替换⽂本随后被插⼊到程序中原来⽂本的位置。对于宏,参数名被他们的值所替换。
3. 最后,再次对结果⽂件进⾏扫描,看看它是否包含任何由#define定义的符号。如果是,就重复上 述处理过程。
注意:
1. 宏参数和#define 定义中可以出现其他#define定义的符号。但是对于宏,不能出现递归。
2. 当预处理器搜索#define定义的符号的时候,字符串常量的内容并不被搜索。

       上面这里表明了,宏可以出现其他的#define但是不要在定义的时候就加入其他的#define不然是会像下面一样报错的。

#define na(x,y) ( (x) > (y) ? (x) : (y) )
#define xi(x) (x++)
#define ha(x,y) ( (xi(x)) > xi((y)) ? ((x)) : ((y)) )
int main()
{
	int a = 5;
	int b = 7;
	int m = ha(a, xi(b));
	printf("%d", m);
//	int x = 5;
//	int y = 8;
//	int z = na(x++, y++);//((5++)>(8++)?(5++):(8++))
//	printf("x=%d y=%d z=%d\n", x, y, z);//输出的结果是什么?
	return 0;
}

宏函数的对比

       不知道大家是否还记得我在上面提醒过大家不要学习了宏定义后就只用宏定义了。不要忘了我们还有自定义函数呀。这里我们就来简单的概述一下,宏与自定义函数的优缺点吧。

        首先有一个致命的问题就是宏不能执行太多代码的问题,尽管我们可以使用\来换行,但是大家要记得,宏的替换规则是直接替换呀,我先替换过后,再一步一步计算。但自定义函数就不一样了,我们平常大量代码都是这样写的所以大家在使用宏定义的时候可要好好斟酌啊。

       接着宏是无法直接调试的,虽然我又方法让我们看到预编译的结果,但是我们在vs上面是无法看到的呀,我们只能看到编译后的结果。这就让我们一点难受了。要是写代码出问题了,我调试都无法直接找到问题。还得我们一行一行的找问题。

     然后就是我们开头讲过的优先级问题了。当时我们说过,不要吝啬括号。如果我们一不小心少写了个括号,那么这个结果也是天差地别的了。

      再来就是每次使⽤宏的时候,⼀份宏定义的代码将插⼊到程序中。除⾮宏⽐较短,否则可能⼤幅度增加程序的长度。也就是说我们每次使用宏定义都是直接替换的,如果宏定义太长的话,替换过去,那这个代码也就太长了。

      然后说一个像有点但又像缺点的点吧。就是宏定义不吃数据类型的问题。就是因为不吃类型所以我们只管写就可以了,但是因为不管类型,那么我们写什么都可以,是不是就不太严谨啊。是吧。而且宏的参数可以出现类型,但是函数做不到。:

#define ha(x) sizeof(x)

int main()
{

	printf("%d", ha(int));
	return 0;
}

       但宏并不是一无是处不然也不会有这个东西了是吧。⽤于调⽤函数和从函数返回的代码可能⽐实际执⾏这个⼩型计算⼯作所需要的时间更多。所以宏⽐ 函数在程序的规模和速度⽅⾯更胜⼀筹。

#与##

#

      #运算符将宏的⼀个参数转换为字符串字面量。它仅允许出现在带参数的宏的替换列表中。
#运算符所执⾏的操作可以理解为”字符串化 “。 也就是说当我们像要在#define里面打印字符串面量的话用#就对了。还是来看看吧:

       我们这里是想要打印的结果是x is 6.但是结果却是a is 6这就是差别嘛。如果想要打印的字符串是我们传递的那个面量的话,#就派上用场了。

        是吧,这里我们就讲打印的变为了传递过来的字符串面量。

#define yh(a) printf("a is %d\n",a)
#define hld(a) printf(""#a" is %d",a)

int main()
{
	int x = 6;
	yh(x);
	int c = 9;
	hld(c);
	return 0;
}

##

      ## 可以把位于它两边的符号合成⼀个符号,它允许宏定义从分离的⽂本⽚段创建标识符。 ## 被称为记号粘合这样的连接必须产⽣⼀个合法的标识符。否则其结果就是未定义的。但是关于##的知识鄙人也是了解较少。因为平常生活中很少看见,也很少使用所以我就找了一个差不多的代码给大家展示:

        写⼀个函数求2个数的较⼤值的时候,不同的数据类型就得写不同的函数。日常情况下,我们是会写出不同类型的对比方式的,如:下面的那个便是使用了关于##的知识了,因为鄙人对这个的了解也是很少,所以大家看一下,有个了解吧。
int int_max(int x, int y)
{
 return x>y?x:y;
}
float float_max(float x, float y)
{
 return x>yx:y;
}

条件编译

        这里又提及到我上一篇博客写过的知识了。条件编译。那什么是条件编译嘞在编译⼀个程序的时候我们如果要将⼀条语句(⼀组语句)编译或者放弃是很⽅便的。因为我们有条件编译指令。那么我们还是来看看代码吧:

        是吧,大家可以看一下这个是看是否定义了宏,然后干什么的。当然还有其他的比如说:

#if 常量表达式
 //...
#endif
//常量表达式由预处理器求值。
如:
#define __DEBUG__ 1
#if __DEBUG__
 //..
#endif
2.多个分⽀的条件编译
#if 常量表达式
 //...
#elif 常量表达式
 //...
#else
 //...
#endif
3.判断是否被定义
#if defined(symbol)
#ifdef symbol
#if !defined(symbol)
#ifndef symbol
4.嵌套指令
#if defined(OS_UNIX)
#ifdef OPTION1
unix_version_option1();
#endif
#ifdef OPTION2
unix_version_option2();
#endif
#elif defined(OS_MSDOS)
#ifdef OPTION2
msdos_version_option2();
#endif
#endif

         这里就与我们平常使用的条件判断一样,只是这里是关于宏的。

命名约定

        好了上面的几乎就是这篇博客中的只要成分了,接下里的是一些相对于上面算补充的知识了。

       ⼀般来讲函数的宏的使⽤语法很相似。所以语⾔本⾝没法帮我们区分⼆者。
       那我们平时的⼀个习惯是:
把宏名全部⼤写
函数名不要全部⼤写

#undef

        这是移除一个宏定义,直接演示一下:因为开始我没有移除ha所以可以打印出一个,但是当我移除后,就没办法打印第二个了,这个比较简单大家应该都很容易就理解了。

#define ha
int main()
{
//#ifdef ha//如果定义了ha就打印
//	printf("hahah");
//#else//没定义的话就打印这个
//	printf("xixix");
//#endif
	#ifdef ha//如果定义了ha就打印
	printf("hahah");
#endif
#undef ha
#ifdef ha//如果定义了ha就打印
	printf("hahah");
#endif
	return 0;
}

命令行定义

        许多C 的编译器提供了⼀种能⼒,允许在命令⾏中定义符号。⽤于启动编译过程。例如:当我们根据同⼀个源⽂件要编译出⼀个程序的不同版本的时候,这个特性有点⽤处。(假定某个程序中声明了⼀个某个⻓度的数组,如果机器内存有限,我们需要⼀个很⼩的数组,但是另外⼀个机器内存⼤些,我们需要⼀个数组能够⼤些。)就是说我们可以先写代码,大小的事情,等我写完了之后。看要多少,我再改。但是很遗憾vs没法演示出来,鄙人只能搞个代码出来,希望大家可以在自己的电脑上上尝试一下,然后再评论区里面贴个照片,也便鄙人学习学习。

#include <stdio.h>
int main()
{
 int array [ARRAY_SIZE];
 int i = 0;
 for(i = 0; i< ARRAY_SIZE; i ++)
 {
 array[i] = i;
 }
 for(i = 0; i< ARRAY_SIZE; i ++)
 {
 printf("%d " ,array[i]);
 }
 printf("\n" );
 return 0;
}
//linux 环境演⽰
gcc -D ARRAY_SIZE=10 programe.c

头文件包含

      关于头文件的包含也算一个拓展知识了吧。我们知道包含头文件可以是

       我们知道<>是我们包含系统自带的,然后" "是包含我们自己写。但是我们也可以用" " 来写系统自带的文件。

       但是这样做查找的效率就低些,当然这样也不容易区分是库⽂件还是本地⽂件了。系统会在这个文件找一遍,然后去系统里面找,找到了就找到了,没找到的话就报错了。如果是正确的话是没什么问题的,这样也会加大系统运行效率呀是吧。虽然我们可以这样使用,但还是建议大家正常使用头文件的包含符号。

        好了,以上就是鄙人想与大家分享的预处理详解了。由于能力不足,还有很多地方是欠缺的,希望大家包含,并且可以在评论区里面指出。

相关推荐

最近更新

  1. TCP协议是安全的吗?

    2024-04-01 18:42:02       18 阅读
  2. 阿里云服务器执行yum,一直下载docker-ce-stable失败

    2024-04-01 18:42:02       19 阅读
  3. 【Python教程】压缩PDF文件大小

    2024-04-01 18:42:02       18 阅读
  4. 通过文章id递归查询所有评论(xml)

    2024-04-01 18:42:02       20 阅读

热门阅读

  1. 使用C++ opencv创建小视口

    2024-04-01 18:42:02       15 阅读
  2. redis 常用命令

    2024-04-01 18:42:02       15 阅读
  3. docker run 使用 -p 命令一直显示端口被占用

    2024-04-01 18:42:02       17 阅读
  4. git 更改仓库地址

    2024-04-01 18:42:02       19 阅读
  5. 2024.2.6力扣每日一题——魔塔游戏

    2024-04-01 18:42:02       17 阅读
  6. Python 树结构库treelib使用教程

    2024-04-01 18:42:02       15 阅读
  7. npm常用命令详解

    2024-04-01 18:42:02       15 阅读
  8. 蓝桥杯备考随手记: practise03

    2024-04-01 18:42:02       13 阅读
  9. git - - - 克隆仓库时输入指定账号和密码

    2024-04-01 18:42:02       15 阅读
  10. Profile Recovery

    2024-04-01 18:42:02       14 阅读