预处理详解

💓 博客主页:C-SDN花园GGbond
⏩ 文章专栏:玩转c语言

前言

前言
 上一次分享了与程序有关的编译链接大致过程,在执行环境中又细分出了 预处理(预编译)、 编译、 汇编、 链接几个过程,今天就让我们来深入了解一下 预处理过程都干了些什么,话不多说,让我们开启今天的学习吧!

📖预定义符号

1.FILE //进⾏编译的源⽂件
2.LINE //⽂件当前的⾏号
3.DATE //⽂件被编译的⽇期
4.TIME //⽂件被编译的时间
5.STDC //如果编译器遵循ANSI C,其值为1,否则未定义

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

上面这段代码是在VS2019这个环境下运行的,__STDC__显示未定义说明VS2019不支持ANSI C标准

在这里插入图片描述

这些预处理符号在预处理阶段就会被具体的值替换,如下图所示:
在这里插入图片描述

📖#define 定义常量

基本语法:

1 #define name stuff

举个例⼦:

#define MAX 1000
#define reg register
//为 register这个关键字,创建⼀个简短的名字
#define do_forever for(;;)
//⽤更形象的符号来替换⼀种实现
#define CASE break;case
//在写case语句的时候⾃动把 break写上。
// 如果定义的 stuff过⻓,可以分成⼏⾏写,除了最后⼀⾏外,每⾏的后⾯都加⼀个反斜杠(续⾏符)。
#define DEBUG_PRINT printf("file:%s\tline:%d\t \
date:%s\ttime:%s\n" ,\
__FILE__,__LINE__ ,       \
__DATE__,__TIME__ )

#define定义标识符的时候,建议不要在最后加**;**
 比如:

#define MAX 1000
#define MAX 1000;

#define MAX 1000
#define MAX 1000;

建议不要加上;,这样容易导致问题,比如下面的场景:

#define MAX 100;

int main()
{
    int min = MAX;
    return 0;
}

1.情景一:(加上;没有影响)
**
在这里插入图片描述
上面的代码在预处理阶段,用100;去替换程序中的MAX,这就导致在text.i文件中100的后面有两个分号,其中一个时我们在源代码(text.c)中写的,另一个是在对MAX替换后得到的。这两个分号并不会对程序造成什么影响,第二个分号会被当成一条空语句去执行。

2.情景二:(加上;导致异常)

#define MAX 100;

int main()
{
    int i = 1;
    int n = 0;
    if(i > 0)
        n = MAX;
    else
        n = 0;
    return 0;
}


在这里插入图片描述
可以看出,此时程序报错了。为什么呢?因为根据语法规定:if语句后面如果没有大括号的话只能有一条语句,但是从预编译的得到的text.i文件中可以看出if语句后面跟了两条语句,分别是赋值语句n = 100;和空语句;,这显然不符合语法规定,报错也是理所当然。当然针对上面的错误也有以下几种修改手段:

1.在#define的时候,后面不加;,这是一本万利的方法
2.在写源代码的时候MAX后面不写;,这种方法虽然可行,但是不符合我们的常使用习惯,一条语句结束没有;给我们的第一感觉就是代码写的有问题
3.在写源代码的时候对if else子句加上大括号,当然这种方法也仅仅是针对当前的情况有效,如果是其他情况还是需要另寻它路。
 *****通过分析我们得出结论:在用#define定义标识符的时候不要加;

📖 #define定义宏

#define 机制包括了⼀个规定,允许把参数替换到⽂本中,这种实现通常称为宏或定义宏

1.下⾯是宏的申明⽅式:

#define name( parament-list ) stuff

  1. name是宏的名字
    2.parament-list是一个用逗号隔开的符号表,它们可能会出现在stuff中(类似于参数,没有类型)
    3.stuff会用parament-list来实现一定的功能

要注意到的是:参数列表必须的左括号必须与name紧邻,如果两者之间有任何空白存在,参数列表就会被解释为stuff的一部分。

实例:

#define SQUARE(x) x*x

int main()
{
	printf("%d\n", SQUARE(2));
	printf("%f\n", SQUARE(5.0));
	return 0;
}

原理如图:
在这里插入图片描述
可以看出在预处理阶段对源程序中的SQUARE(2)和SQUARE(5.0)进行了替换。以SQUARE(2)为例在预处理阶段宏的参数x就是2,然后用xx就变成了22再用2*2去替换SQUARE(2)最终就得到了text.i文件中的结果。

存在缺陷:

#define SQUARE(x) x*x

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

按照一般思维去看上面的代码,首先5+1=6我们会以为是把6传给x,然后用6*6来替换SQUARE(5 + 1)最终得到36,可结果并不像我们想的那样,正确结果是11,为什么会这样呢?去看看预处理后得到的文件我们就会恍然大悟

在这里插入图片描述
可以看出真实的替换结果并不是我们想的用66去替换SQUARE(5 + 1),而是用5+15+1去替换的。这说明在预处理的时候并没有执行5+1,而是直接把5+1传了过去,最终得到的结果就是11。
总结: 宏的参数是不加运算直接进行替换的
 如何得到我们想要的36呢?有以下两种方法供大家参考:

对宏调用进行修改:SQUARE((5 + 1)),给5+1再加一层括号,此时在替换的时候,x就是**(5+1)SQUARE((5 + 1))就会被替换成(5+1)(5+1)最终得到的结果就是36。
对定义的宏进行修改:
#define SQUARE(x) (x)
(x),此时在替换的时候,x是5+1**,SQUARE(5+1)会被替换成(5+1)*(5+1)最终得到的结果也是36。
 对比上面的两种方案,第一种方案带两个括号看起来比较别扭,所以更推荐第二种方案,也就是在
定义宏的时候给参数带上括号

在这里插入图片描述
存在的陷阱二:

//我们希望计算一个数的二倍再乘10
#define DOUBLE(x) (x)+(x)//这里定义了一个宏来计算一个数的二倍

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

根据需求描述,我们希望得到的应该是60,但实际却得到的是33,本质原因就是宏只会进行替换,先进行参数的替换,再进行宏体的替换。让我们来看看上面的代码经过预处理会得到什么

在这里插入图片描述

**不难看出,为了得到我们希望的结果,应该对宏替换后得到的内容加上括号,也就是在定义宏的时候对处理结果加上括号:#define DOUBLE(x) ((x)+(x))

**
在这里插入图片描述
通过上面介绍的两种陷阱,我们可以得出一个结论:在宏定义的时候,千万不要吝啬括号!!,先给每个参数带上括号,再给stuff整体带上括号。
 对于#define定义宏的注意事项总结如下:

1.参数列表必须的左括号必须与宏的名字name紧邻
3.宏的参数都是不加计算直接替换的
4.不要吝啬括号

📖4. 带有副作⽤的宏参数

当宏参数在宏的定义中出现超过⼀次的时候,如果参数带有副作⽤,那么你在使⽤这个宏的时候就可能出现危险,导致不可预测的后果。副作⽤就是表达式求值的时候出现的永久性效果。

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

MAX宏可以证明具有副作⽤的参数所引起的问题。

证明具有副作⽤的参数所引起的问题。
1.#define MAX(a, b) ( (a) > (b) ? (a) : (b) )
2…
3.x = 5;
4.y = 8;
5.z = MAX(x++, y++);
6.printf(“x=%d y=%d z=%d\n”, x, y, z);//输出的结果是什么?

这⾥我们得知道预处理器处理之后的结果是什么:

1 z = ( (x++) > (y++) ? (x++) : (y++));
所以输出的结果是:x=6 y=10 z=9

📖宏替换的规则

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

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

📖宏函数的对⽐

宏通常被应⽤于执⾏简单的运算。 ⽐如在两个数中找出较⼤的⼀个时,写成下⾯的宏,更有优势⼀些。 1 #define MAX(a, b) ((a)>(b)?(a):(b)) 那为什么不⽤函数来完成这个任务? 原因有⼆:

  1. ⽤于调⽤函数和从函数返回的代码可能⽐实际执⾏这个⼩型计算⼯作所需要的时间更多。所以宏⽐ 函数在程序的规模和速度⽅⾯更胜⼀筹。
  2. 更为重要的是函数的参数必须声明为特定的类型。所以函数只能在类型合适的表达式上使⽤。反之 这个宏怎可以适⽤于整形、⻓整型、浮点型等可以⽤于 > 来⽐较的类型。宏的参数是类型⽆关 的。 和函数相⽐宏的劣势:
  3. 每次使⽤宏的时候,⼀份宏定义的代码将插⼊到程序中。除⾮宏⽐较短,否则可能⼤幅度增加程序 的⻓度。
  4. 宏是没法调试的。
  5. 宏由于类型⽆关,也就不够严谨。
  6. 宏可能会带来运算符优先级的问题,导致程容易出现错。

在这里插入图片描述

📖#和##

先来看下面的代码

int main()
{
char* p = "hello "“world\n”;
printf(“hello ““world\n”);
printf(”%s”, p);
return 0;
}

在这里插入图片描述
通过结果可以看出:对于两个字符串"hello "和"world\n"编译器会自动对它们进行合并得到一个字符串hello world\n。字符串是有自动连接的特点我们是否可以写出下面这种代码

//这里我们写了一个宏用来打印信息
//我们希望传a的时候它可以打印出:
//a的值是:0
//传b的时候它可以打印出:
//b的值是:10
#define PRINT(x) printf(“x的值是:%d\n”, x)
int main()
{
int a = 0;
PRINT(a);
int b = 10;
PRINT(b);
return 0;
}

在这里插入图片描述
结果不尽如人意,printf(“x的值是:%d\n”, x)中的第一个x并没有被替换掉,那如何实现我们的需求呢?此时就需要用到#,它可以把宏参数转换成字符串,不信我们试试,对上面的代码稍作修改:

#define PRINT(x) printf(#x"的值是:%d\n", x)
int main()
{
int a = 0;
PRINT(a);
int b = 10;
PRINT(b);
return 0;
}

在这里插入图片描述
此时我们的需求就得以实现,以PRINT(a);为例分析一下过程:首先用a去替换宏参数x,再把x带入到后面的宏体中,此时a是一个整型,再通过#把整型a转换成一个字符串"a",再利用两个字符串可以自动合并的特性,最终就实现了我们的需求。再来看看预处理后得到的结果:

在这里插入图片描述
此时我们定义的宏PRINT只能打印整型变量,因为宏体里的格式化打印已经被固定为%d了,我们可以对当前的宏稍作修改,使它也可以打印浮点型数据,即把格式也当作宏参数传递:

#define PRINT(format, x) printf(#x"的值是:“format”\n", x)
int main()
{
int a = 0;
PRINT(“%d”,a);
int b = 10;
PRINT(“%d”,b);
float c = 2.5f;
PRINT(“%f”,c);
return 0;
}

在这里插入图片描述
总结: #可以把宏参数转换成对应的字符串来进行显示,了解了#的作用,接下来了解##;它可以把位于它两边的符号合并成一个符号,它允许宏定义从分离的文本片段创建标识符。如下面的代码:
在这里插入图片描述

📖命名约定

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

📖#undef

这条指令用于移除一个宏定义,例如:

#define MAX 100
int main()
{
printf(“%d\n”, MAX);
#undef MAX
printf(“%d\n”, MAX);//这里的MAX就成未定义了
return 0;
}

📖命令行定义

许多C的编译器提供了一种能力,允许在命令行中定义符号。用于启动编译过程。例如:当我们根据同一个源文件要编译出一个程序的不同版本的时候,这个特性很有用处。(假定某个程序中声明了一个某个长度的数组,如果机器内存有限,我们需要一个很小的数组,但是另外一个机器内存大一些,我们需要一个大一点的数组)

#include <stdio.h>
int main()
{
int arr[SIZE];//这里并没有给SIZE任何初值
for(int i = 0; i < SIZE; i++)
{
arr[i] = i;
}
for(int i = 0; i < SIZE; i++)
{
printf("%d ",arr[i]);
}
return 0;
}

通过下面这条指令对这段代码进行编译gcc text.c -D SIZE=10 -o text.exe。
在这里插入图片描述
预处理之后的.i文件:
在这里插入图片描述

📖条件编译

借用条件编译指令,我们在编译一个程序的时候可以对一条语句或者一组语句选择性的进行编译。举个例子

#define PRINT s//只要定义了就行,可以是任何数字或者字母,甚至没有也可以

int main() {
#ifdef PRINT printf(“hehe\n”);//只要PRINT定义了,这段代码就可以被编译,没定义就不能被编译
#endif return 0; }

#ifdef和endif必须是成对出现的。只要#ifdef后面的标识符被定义了,那么#ifdef和endif之间的代码就可以被编译,反知则不能被编译。

在这里插入图片描述
源文件中没有对PRINT的定义,所以经过预编译,#ifdef和endif之间的代码块就会被删掉。

#if defined(symbol)//判断symbol是否被定义 //…
#endif

#ifdef symbol//和上面的语句等价,判断symbol是否被定义 //…
#endif

#if !defined(symbol)//如果定义了symbol则不编译下面的代码块 //…
#endif

#ifbdef symbol//如果定义了symbol则不编译代码块 //…
#endif

📖文件包含

本地文件包含

#include “filename”

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

库文件包含

include <filename.h>

查找策略: 查找头文件直接去标准路径下查找,如果找不到就提示编译错误。这意味着:对库文件的包含也可以用双引号,但是这样查找的效率就低一些,当然这样也不容易区分是库文件还是本地文件了。

**注意:**一个头文件被包含几次,就会被编译几次

然而,大家在写代码的时候应该不会出现上面这种极端情况,在一个源文件里边显式的把一个头文件包含多次。我们可能遇到的是:a包含了b,c也包含了b,d包含了a和c,那这样一来d就间接的把b给包含了两次。如何解决这种问题呢?有以下两种方案
条件编译解决上述问题

#ifndef THE_TEXT//如果_THE_TEXT_没有被定义就编译下面的代码,否则就不编译 //第一次包含的时候,由于_THE_TEXT_没有定义,所以会编译下面的代码
//当第二次包含的时候,由于第一次包含已经定义了_THE_TEXT_,第二次就不再编译
#define THE_TEXT
struct S
{
int a;
char c;
};
#endif

在这里插入图片描述
可见此时虽然包含了三次,但是预处理的时候只拷贝了一份,这将大大的降低代码冗余

评论 8
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值