第十七讲:预处理详解

目录

1、预定义符号

2、#define定义常量

3、#define定义宏

4、带有副作用的宏参数

5、宏替换的规则

6、宏与函数的对比

7、#和##

7.1、#运算符

7.2、##运算符

8、命名约定

9、#undef

10、条件编译

11、头文件的包含

11.1头文件被包含的方式

11.1.1本地文件包含

11.1.2库文件包含

11.2嵌套文件包含

12、其他预处理指令

13、#define和typedef的对比


1、预定义符号

C语言设置了一些预处理符号,可以直接使用,预处理符号也是在预处理期间处理的。

1、__FILE__ //进⾏编译的源⽂件
2、__LINE__ //⽂件当前的⾏号
3、__DATE__ //⽂件被编译的⽇期
4、__TIME__ //⽂件被编译的时间
5、__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__);
	return 0;
}

运行结果为:

C:\Users\wang\Desktop\c语言\giteeC程序\c-language\Project1_1_25\Project1_1_25\test.c
197
Jan 28 2024
14:45:30

C:\Users\wang\Desktop\c语言\giteeC程序\c-language\Project1_1_25\x64\Debug\Project1_1_25.exe (进程 19224)已退出,代码为 0。
要在调试停止时自动关闭控制台,请启用“工具”->“选项”->“调试”->“调试停止时自动关闭控制台”。
按任意键关闭此窗口. . .

但是第五条在VS2022并不支持,下面在小熊猫C++上实验第五条:

代码如下:

#include<stdio.h>
int main()
{
	printf("%d\n",__STDC__);
	return 0;
}

结果为:

1

--------------------------------
Process exited after 0.005259 seconds with return value 0 (0 ms cpu time, 2964 KB mem used).

Press ANY key to exit...

2、#define定义常量

基本语法为:

#define name stuff

举个例子:

#define max 100   //定义max为100

#define reg register   //定义reg为register

#define doforever for(;;)  //定义doforever为for(;;)

#define CASE break;case  //在写case语句时自动会把break加上,本质上就是把CASE定义为break;case;

//如果定义的stuff过长,可以分几行来写,除最后一行外,每行的后面都要加上一个反斜杠(续行符)。
#define PRINT printf("%s\n \
                    %d\n \
                     %s\n \
                       %s\n",__FILE__,__LINE__, \
                            __DATE__,__TIME__);

但是要注意一个问题,在使用#define的时候,尽量不要在最后加上;这个符号,这样容易出问题。

比如:

#define MAX 100;
#include<stdio.h>
int main()
{
	int max = 0;
	if (max == 0)
		max = MAX;
	else
		max = 0;
	return 0;
}

这段代码在VS2022上运行会报错,报错为:

显示为:没有匹配if的非法else,原因在于预处理后,if语句下的一个语句就变成了两个语句,如下:

	max = 100;;

这段代码,其实和下面的代码是一个意思:

max = 100;
;

而又因为没有大括号时,if只能控制一个语句,所以会出现语法错误,导致else与无法与if配对。

3、#define定义宏

#define机制有一个规定,允许把参数替换到文本中,这种实现通常称为宏或定义宏。

宏的申明方式为:

1 #define name( parament-list ) stuff

其中的parament-list是一个由逗号隔开的符号表,他们可能出现在stuff中。

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

举例:

#define X(x) x*x
#include<stdio.h>
int main()
{
	int a = 3;
	int c = X(a);
	printf("%d\n", c);
	return 0;
}

运行结果为:

9

C:\Users\wang\Desktop\c语言\giteeC程序\c-language\Project1_1_25\x64\Debug\Project1_1_25.exe (进程 10632)已退出,代码为 0。
要在调试停止时自动关闭控制台,请启用“工具”->“选项”->“调试”->“调试停止时自动关闭控制台”。
按任意键关闭此窗口. . .

在上述的代码中,这个宏接受了一个参数,在预处理后,预处理器就会用3*3来替换上面的表达式中的X(a)。

但是这个宏存在一个问题,比如这样一个代码:

#define X(x) x*x
#include<stdio.h>
int main()
{
	int a = 3;
	int c = X(a+1);
	printf("%d\n", c);
	return 0;
}

我们观察代码后,认为结果应该为4*4=16,但实际上代码运行的结果为:

7

C:\Users\wang\Desktop\c语言\giteeC程序\c-language\Project1_1_25\x64\Debug\Project1_1_25.exe (进程 20400)已退出,代码为 0。
要在调试停止时自动关闭控制台,请启用“工具”->“选项”->“调试”->“调试停止时自动关闭控制台”。
按任意键关闭此窗口. . .

为什么会是7呢?

原因在于替换文本时,参数x被替换成a+1,所以这条语句就变成了:

int c = a + 1 * a + 1;

所以3+1*3+1=7。

如何解决这样的问题呢?

我们可以通过在宏定义上加上两个括号,这个问题就被解决了。如:

#define X(x) (x)*(x)

这样就程序运行结果就是16了,产生了我们的预期结果。

再比如我们还有这样的一个宏定义:

#define ADD(x) (x)+(x)

我们给每个x都加上了括号,想避免出现之前的问题,但是这个宏定义可能会出现新的错误。

比如这样一个程序:

#define ADD(x) (x)+(x)
#include<stdio.h>
int main()
{
	int a = 5;
	printf("%d\n", 10 * ADD(a));
	return 0;
}

这个程序看上去好像结果应该为100,但实际上运行结果为:

55

C:\Users\wang\Desktop\c语言\giteeC程序\c-language\Project1_1_25\x64\Debug\Project1_1_25.exe (进程 13300)已退出,代码为 0。
要在调试停止时自动关闭控制台,请启用“工具”->“选项”->“调试”->“调试停止时自动关闭控制台”。
按任意键关闭此窗口. . .

原因在于,替换后,变成了:

printf("%d\n", 10*(5)+(5));

所以,10*5+5=55。

这个问题解决的办法就是在宏定义表达式两边加上一对括号就可以了。比如:

#define ADD(x) ((x)+(x))

总结:对于数值表达式进行求值的宏定义最好要加上括号(一种是给每个表达式中的未知量加上括号,还有一种就是给表达式整体加上括号),这样就避免了在使用宏时由于参数中的操作符或临近的操作符之间不好预料的相互作用。

4、带有副作用的宏参数

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

例如:

x+1  //不带有副作用
x++  //带有副作用

我们可以写一个代码来验证一下:

#define MAX(a,b) ((a)>(b)?(a):(b))
#include<stdio.h>
int main()
{
	int x = 5;
	int y = 8;
	int z = MAX(x++, y++);
	printf("%d %d %d",x,y,z);
	return 0;
}

程序运行后结果为:

6 10 9
C:\Users\wang\Desktop\c语言\giteeC程序\c-language\Project1_1_25\x64\Debug\Project1_1_25.exe (进程 24388)已退出,代码为 0。
要在调试停止时自动关闭控制台,请启用“工具”->“选项”->“调试”->“调试停止时自动关闭控制台”。
按任意键关闭此窗口. . .

我们来分析一下,经过预处理后,结果是:

int z = ((x++) > (y++) ? (x++) : (y++));

首先,x和y比较大小,y大于x;然后x++和y++分别变成了6和9;然后z被赋值为9;最后执行y++,y变成了10。

由这个例子,我们今后在使用宏的时候,要多加注意这类带有副作用的宏参数可能会带来的一些问题。

5、宏替换的规则

在程序中扩展#define定义符号和宏时,有一下几个步骤。

1·在调用宏时,首先对参数进行检查,看看是否包含任何由#define定义的符号,如果是,则被替换。

2·接下来就是替换文本,随后再插入到程序中原来文本的位置。

最后,再对结果文件进行扫描,看看它是否包含任何由#define定义的符号,如果是,就重复上述处理过程。

注意:

1·宏参数和#define定义中可以出现其他#define定义的符号。但是对于宏,它不能出现递归。

2·当预处理器搜索#define时,字符串常量的内容并不被搜索。

6、宏与函数的对比

首先,宏通常用于一些比较简单的运算。

就比如我们上面写的一些代码中,用宏来解决一些比较简单的运算。

那为什么不用函数来完成呢?而是用宏来完成。

原因主要由两个:

1·调用函数和从函数返回代码可能比实际执行这个小型运算所需要的时间更多。所以宏比函数在程序的规模和速度方面更胜一筹。

2·更为重要的是,函数必须要指定形参的类型。所以函数只能在类型合适的表达式上使用。反之这个宏可以适用于任何类型。总之,宏的参数是和类型无关的。

比如:宏参数可以出现类型,但函数做不到。

#define MALLOC(num,type) (type*)malloc(num*sizeof(type))
#include<stdio.h>
#include<stdlib.h>
int main()
{
	int* p = MALLOC(10, int);
	free(p);
	p = NULL;
	return 0;
}

但宏并非只有好处,宏也有坏处:
1·宏是没法调试的。

2·宏由于和类型无关,也就不够严谨。

3·宏可能带来运算符优先级的问题,导致一些问题。

4·使用的宏除非比较短,否则可能大幅度增加代码的长度。

5·宏不像函数一样可以使用递归。

来个题目:

写一个宏,可以将一个整数的二进制位的奇数位和偶数位进行交换

参考代码:

#define SwapIntBit(n) (((n) & 0x55555555) << 1 | ((n) & 0xaaaaaaaa) >> 1)

解释:交换奇偶位,需要分别拿出奇数和偶数位。奇数位拿出,那就要&上01010101······,偶数位拿出那就要&上101010········,对应的十六进制分别为5555···和aaaa·····。一般我们认为是32位的整数,4位对应一位十六进制,就是8个5和8个a。通过&0x55555555的方式拿出奇数位和通过&0xaaaaaaaa拿出偶数位,奇数位左移一位就到了偶数位,偶数位右移一位就到了奇数位上,最后两个数字或起来,就完成了交换。

注:这个宏只能完成32位及以内整形的交换,想要完成六十四位的,那就将5和a翻倍即可。

再来一个题目:

offsetof宏的实现:

#define offsetof(StructType, MemberName) (size_t)&(((StructType *)0)->MemberName)

解释:先将0地址转换位一个结构体类型的指针,相当于某个结构体的首地址为0,此时每一个成员的偏移量就成了相对0地址的偏移量,这样就不需要减去首地址了来计算偏移量了。然后使用该指针访问其成员,并取出地址,取出该成员的地址后,强转成size_t类型。也就求出偏移量了。

7、#和##

7.1、#运算符

#运算符将宏的一个参数转换为字符串字面量。它仅允许出现在带参数的宏的替换列表中。

#运算符所执行的操作可以理解为字符串化;

简单来说就是如何把参数插入到字符串中呢?使用#就可以做到。

例如:

#define PRINT(n) printf("the value of "#n" is %d",n)
#include<stdio.h>
int main()
{
	int a = 10;
	PRINT(a);
	return 0;
}

当程序运行后,a替换到宏的体内时,就出现了#a,而#a就是转换成了"a",这时这个字符串的代码就会被预处理为:

printf("the value of ""a"" is %d",n);

运行结果为:

the value of a is 10
C:\Users\wang\Desktop\c语言\giteeC程序\c-language\Project1_1_25\x64\Debug\Project1_1_25.exe (进程 16140)已退出,代码为 0。
要在调试停止时自动关闭控制台,请启用“工具”->“选项”->“调试”->“调试停止时自动关闭控制台”。
按任意键关闭此窗口. . .

7.2、##运算符

##运算符可以把位于它两边的符号合成一个符号,它允许宏定义从分离的文本片段创建标识符。##被称为记号粘合。

但是这样的粘合必须要创建一个合法的标识符,否则其结果是未定义的。

例如:

#define CAT(x,y) x##y
#include<stdio.h>
int main()
{
	int classroom = 2024;
	printf("%d\n", CAT(class, room));
	return 0;
}

运行结果为:

2024

C:\Users\wang\Desktop\c语言\giteeC程序\c-language\Project1_1_25\x64\Debug\Project1_1_25.exe (进程 25660)已退出,代码为 0。
要在调试停止时自动关闭控制台,请启用“工具”->“选项”->“调试”->“调试停止时自动关闭控制台”。
按任意键关闭此窗口. . .

再比如:写一个函数求2个数的较大值的时候,不同的数据类型就得写不同的函数。

但是这样写太麻烦了,我们现在可以这样写:

#define MAX(type) type type##max(type x,type y)\
{\
   return (x>y?x:y);\
}
//使用宏定义不同的函数。
MAX(int);
MAX(float);
#include<stdio.h>
int main()
{
	int m = intmax(2, 3);
	printf("%d\n", m);
	float n = floatmax(2.5, 3.5);
	printf("%f\n", n);
	return 0;
}

运行结果为:

3
3.500000

C:\Users\wang\Desktop\c语言\giteeC程序\c-language\Project1_1_25\x64\Debug\Project1_1_25.exe (进程 13612)已退出,代码为 0。
要在调试停止时自动关闭控制台,请启用“工具”->“选项”->“调试”->“调试停止时自动关闭控制台”。
按任意键关闭此窗口. . .

8、命名约定

一般来说,函数和宏的使用语法很相似,所以语言本身没办法帮助我们区分二者。

但我们平时的一个习惯是:

1·把宏名全部大写。

2·函数名不要全部大写。

用这种方式来让我们简单的区分一下它们。

9、#undef

#undef这个指令是用于移除一个宏定义。

比如:

#undef NAME
//如果现存的一个名字需要被重新定义,那么它的旧的名字首先要被移除。

简单来说就是撤销一个已定义过的宏名。

例如:

#define MAX(x,y) ((x)>(y)?(x):(y))
#include<stdio.h>
int main()
{
#undef MAX  //从这之后就没有MAX这个宏定义了,也就没法使用MAX了。
	int d=MAX(3, 5);
	printf("%d", d);
	return 0;
}

对于这个我们前面提到过的程序,如果我们运行程序便会报错:

 无法解析的外部符号 MAX,函数 main 中引用了该符号

10、条件编译

在编译一个程序的时候,如果我们要将一条语句(一组语句)编译或者放弃编译是很方便的,因为有条件编译指令。

比如:调试性的代码,删除可惜,保留又碍事,所以我们可以选择性的编译。

#include<stdio.h>
#define __DEBUG__ 0
int main()
{
	int i = 0;
	int arr[10] = { 0 };
	for (i = 0; i <= 9; i++)
	{
		arr[i] = i;
#if __DEBUG__
		printf("%d ", arr[i]);//为了观察数组是否真的赋值成功;
#endif
	}
	return 0;
}

运行结果为:


C:\Users\wang\Desktop\c语言\giteeC程序\c-language\Project1_1_25\x64\Debug\Project1_1_25.exe (进程 7048)已退出,代码为 0 。
要在调试停止时自动关闭控制台,请启用“工具”->“选项”->“调试”->“调试停止时自动关闭控制台”。
按任意键关闭此窗口. . .

不参与编译的原因在于预处理后,这段代码就直接被删掉了。

如果将上面代码中#define后的零改为1,结果为:

0 1 2 3 4 5 6 7 8 9
C:\Users\wang\Desktop\c语言\giteeC程序\c-language\Project1_1_25\x64\Debug\Project1_1_25.exe (进程 11520)已退出,代码为 0。
要在调试停止时自动关闭控制台,请启用“工具”->“选项”->“调试”->“调试停止时自动关闭控制台”。
按任意键关闭此窗口. . .

常见的条件编译指令:

#if 常量表达式   //常量表达式为真则参与编译,如果为假则不参与编译
······
#endif

//多个分支的条件编译
#if 常量表达式
······
#elif 常量表达式
······
#else
······
#endif

//判断是否被定义
#if defined(symbol)//如果被定义过则执行,没被定义过则不执行。
······
#endif

#ifdef symbol //这种写法和上一个是一样的。
······
#endif

#if !defined(symbol)//如果被定义过则不执行,没被定义过则执行。
······
#endif

#ifndef symbol//这种写法与上一个写法是一个意思。
······
#endif

另外条件编译是可以嵌套使用的,这里不再过多介绍。

11、头文件的包含

11.1头文件被包含的方式

11.1.1本地文件包含
#include"filename"

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

11.1.2库文件包含
#include<filename.h>

查找策略:直接去标准路径下去查找,如果找不到就提示编译错误。

总的来说,使用库文件时两种文件的包含方式都可以使用,使用本地文件时只能使用本地头文件包含的方式。

但要注意,库文件用第一种方式包含,查找效率会低一些,另外就是不容易区分包含的是库文件还是本地文件。

11.2嵌套文件包含

我们已经知道使用#include可以让一个文件被编译。

它是如何做到的呢?

其实采用的是替换的方式:就是预处理器先删除掉这条指令,并用包含文件的内容替换。

一个头文件被包含十次,那它也就被编译十次,如果重复包含,那就对编译的压力较大。

例如:

//在源文件中

#include"test.h"
#include"test.h"
#include"test.h"
#include"test.h"
#include"test.h"

int main()
{
	return 0;
}
void test();  //在头文件中
struct stu
{
	int id;
	char name[20];
};

如果我们写了一个类似于这样的代码,在源文件中包含了五次头文件,那么头文件的内容将会被拷贝五份放在源文件中。

如果头文件比较大,那这样预处理后代码量就会大大增加。

如果工程比较大,有许多公共使用的头文件,被大家使用,又不做任何处理,那么后果不堪设想。

那么如何解决头文件被重复使用的问题?

答案就是我们之前提到的条件编译。

我们可以在每个头文件的开头写:

#ifndef __TEST_H__
#define __TEST_H__
//头文件的内容
#endif

或者我们也可以在头文件中加上:

#pragma once

注意:有些编译器会默认加上#pragma once,比如:VS2022。在VS2022上创建头文件,在文件的第一行就有#pragma once。

12、其他预处理指令

#error
#pragma
#line
···
//不做介绍,感兴趣可以去了解一下。

13、#define和typedef的对比

#define INT_PTR int*
typedef int* int_ptr;
int main()
{
	INT_PTR a, b;
	int_ptr c, d;
	return 0;
}

#define的本质是替换,因而替换后为int* a,b; 其中a为指针,b为int类型的变量。

而typedef不同,c和d都是指针。

  • 23
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值