当前位置:   article > 正文

C语言——预处理详解_c语言预处理

c语言预处理

一,预定义符号

C语言中有一些预定义符号,这些符号是语言内置的,可以直接使用
例如:FILE LINE TIME DATE STDC(如果编译器支持ANSI C就返回1
,否则未定义)

int main()
{
	printf("%s\n", __FILE__);
	printf("%d\n", __LINE__);
	printf("%s\n", __TIME__);
	printf("%s\n", __DATE__);
	//printf("%d\n", __STDC__);
	return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

在这里插入图片描述

可以看到我的这个编译器是不支持ANSI C的

二,#define

1,#define定义标识符

语法:#define name stuff
#define定义的标识符,可以代表很多东西
例如:

#define M 10
#define Q "hello world"
#define PRINT printf("hello world")
int main()
{
	printf("%d\n", M);
	printf("%s\n", Q);
	PRINT;
	return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

在这里插入图片描述

上面那些都是正常的使用情况:
下面介绍两种特殊的情况:
1,定义一个死循环
2,如果定义的stuff过长,可以分几行写,除最后一行外,每行最后加上\作为分行符

#define DO_FOEVER for(;;)
#define DEBUG_PRINT printf("file:%s\tline:%d\t \
							 date:%s\ttime:%s\n" ,\
							__	FILE__,__LINE__ ,       \
							__DATE__,__TIME__ )  
  • 1
  • 2
  • 3
  • 4
  • 5

2,#define定义宏

#define 机制包括了一个规定,允许把参数替换到文本中,
这种实现通常称为宏(macro)或定义 宏(define macro)
语法:#define M(参数) stuff
无论在定义宏还是普通标识符的时候,最终都会在编译预处理阶段完成替换
并且在定义宏的时候,应注意带括号,防止运算符的优先级问题
下面举几个例子,自己体会一下:

#define DOUBLE(x) x*x
int main()
{
	printf("%d\n", DOUBLE(6));
	printf("%d\n", DOUBLE(6+1));
	printf("%d\n", 2*DOUBLE(6));

	return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

在这里插入图片描述

看到这个结果明显不对劲,这就是上述所说的应该给宏定义的文本中的各项加上括号,避免出错
下面看下修改过后:

#define DOUBLE(x) ((x)*(x))
  • 1

在这里插入图片描述

注意:无论在定义标识符还是在定义宏的时候,最好不要在结尾加上分号,
这样很容易在使用上出错,例如:

#define DOUBLE(x) ((x)*(x));
if(1)
  printf("%d\n", DOUBLE(6));
else
   ;
  • 1
  • 2
  • 3
  • 4
  • 5

实质上,编译预处理完后,DOUBLE(6)被替换成下面的样子

if(1)
  printf("%d\n", ((6) * (6)));;
else
   ;
  • 1
  • 2
  • 3
  • 4

上面这种情况是会报错的,一条if语句跟了两条语句(没有大括号前提下)。
所以,我们在定义标识符,或者宏的时候 末尾不要加分号,避免使用时的错误。

3,#

讲述这个之前,首先介绍一下C语言的一种机制:
字符串相邻在一起时,会合并成一个字符串
例如:

int main()
{
	printf("%s\n", "hello ""world");
	return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5

在这里插入图片描述

而,#的作用是,当宏参数前面加上#的时候,会将这个参数转化为字符串。
例如:#define(x) printf(“…”#x"…“);
最终会被替换成:printf(”…““x””…");
下面举一个业务场景做例子:

int main()
{
	int a = 10;
	printf("the value of a is %d\n", a);
	int b = 20;
	printf("the value of b is %d\n", b);
	float c = 30.0f;
	printf("the value of c is %f\n", c);
	return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

在这里插入图片描述

每一个数据后面都跟着一条输出语句,而且每条输出语句的内容极为相似,
我们能不能将它封装为一个函数呢?其实是不可以的,因为我们想让它以何种方式打印,
需要传给函数"%d" "%f"这样的参数,但是函数会误以为这些是字符串。
所以要借助宏来实现

#define PRINT(x,format) printf("the value of "#x" is "format"\n",x)
int main()
{
	int a = 10;
	PRINT(a, "%d");
	int b = 20;
	PRINT(b, "%d");
	float c = 30.0f;
	PRINT(c, "%f");
	return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

在这里插入图片描述

4,##

##的作用非常奇怪,是将##两边的字符合成一个字符
例如:

#define A(x,y) printf("%d\n",x##y)
int main()
{
	int ab = 10;
	A(a, b);
	return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

在这里插入图片描述

5,带副作用的宏参数

当宏的参数在宏定义的文本中出现了不止一次的时候,就可能会出现副作用。
例如:我们定义一个宏来输出两个数中的较大值

#define MAX(x,y) ((x)>(y)?(x):(y))
int main()
{
	int a = 2;
	int b = 3;
	//想输出++a 与++b中的较大值
	printf("%d\n", MAX(++a, ++b));
	return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

我们本想输出的是4,但是你看结果,这带副作用的参数

在这里插入图片描述

6,只能靠宏实现而函数实现不了的功能

(1)对malloc进行一次封装

比如,我们正常用malloc来开辟内存空间
int* a=(int*) malloc(sizeof(int)*10);
写起来较为麻烦,可以用宏来封装一层

#define MALLOC(type,num) (type*)malloc(sizeof(type)*num)
  • 1

这样开辟内存空间就可以这样写了,
int*a=MALLOC(int,10);
比上面的代码就简洁了许多

(2)offsetof的实现

我们在结构体那片博客中介绍过offsetof()的功能:是结构体变量的成员地址
相对于结构体变量的地址的偏移量
今天,我们就来模拟实现一下offsetof()

struct stu
{
	int a;
	char c;
	int b;
};
#define OFFSETOF(type,x) (int)&(((type*)0)->x)
int main()
{
	printf("%d\n", OFFSETOF(struct stu, a));
	printf("%d\n", OFFSETOF(struct stu, c));
	printf("%d\n", OFFSETOF(struct stu, b));
	return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

在这里插入图片描述

所以,可以看到函数与宏各有千秋,函数不能实现的宏可以实现,宏不能实现的但是函数可以实现,
所以下面我们来总结一下,函数与宏的区别

(3)特殊场景

#define PRINT(x,format) printf("the value of "#x" is "format"\n",x)
int main()
{
	int a = 10;
	PRINT(a, "%d");
	int b = 20;
	PRINT(b, "%d");
	float c = 30.0f;
	PRINT(c, "%f");
	return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

在这里插入图片描述

三,函数与宏的比较

1,代码长度

宏:每次使用时,在编译预处理阶段定义的宏的代码就会插入到程序中,如果定义的宏代码量很大
,这样的话就大大增加了代码量。
函数:函数的代码只出现在一个地方,每次调用函数的时候都只会调用这一份代码

2,执行速度

宏:当实现的功能比较简单是,宏的速度是很快的
函数:当实现的功能比较简单时,调用函数和释放函数栈帧的时间,比真正实现函数功能还长
举个简单例子:输出较大值

int main()
{
	int a = 2;
	int b = 3;
	max(a, b);
	MAX(a, b);
	return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

在这里插入图片描述
在这里插入图片描述

可以看到,如果实现简单的功能,宏要比函数运行速度快不少

3,操作符的优先级

宏:如果在定义宏的时候没有适当加上括号,可能会在周围环境的表达式中,与临近运算符出现
未曾预料的结果
函数:函数参数只在函数调用的时候计算一次,它的运算结果传递给函数。表达式的结果更容易预测。
例如:

#define DOUBLE(x) x+x
int main()
{
	int a = 3;
	printf("%d\n", 2 * DOUBLE(3));
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

我们预料的结果应该是12,但结果:

在这里插入图片描述

4,带有副作用的参数

宏:宏参数可能被替换到宏体中的多个位置,所以带有副作用的参数会造成不可预料的结果。

函数:函数只在传参的时候求值一次,结果更容易预料。

5,参数类型

宏:参数与类型无关,只要对参数的操作是合法的,他就可以运用到各种类型。

函数:函数参数是与类型有关的,参数的类型不同那么就需要不同的函数。
例如:比较大小用宏来实现,就可以比较多种数据类型的大小

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

6,调试。

宏:由于宏的实现是替换,所以宏不能够调试。

函数:函数是可以调试的。我们在调试的时候按F11进入到函数内部。

void print(int* arr, int len)
{
	int i = 0;
	for (i = 0; i < len; i++)
	{
		printf("%d ", arr[i]);
	}
}
int main()
{
	int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
	int sz = sizeof(arr) / sizeof(arr[0]);
	print(arr, sz);
	return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

在这里插入图片描述

7,递归

宏:宏是不能够递归的。

函数:函数可以实现递归。
例如我们写一个递归函数实现求前N项和

int sumN(int n)
{
	if (n == 1)
	{
		return 1;
	}
	else
	{
		return n + sumN(n - 1);
	}
}
int main()
{
	int n = 10;
	printf("%d\n", sumN(n));
	return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

综上,介绍了七点函数与宏的区别。

四,条件编译

选择性的编译,当满足某种条件时,才进行编译,比如说我们用来调试的代码,删了比较可惜,
就可以用条件编译的方式。

#include <stdio.h>
#define __DEBUG__
int main()
{
 int i = 0;
 int arr[10] = {0};
 for(i=0; i<10; i++)
 {
 arr[i] = i;
 #ifdef __DEBUG__
 printf("%d\n", arr[i]);//为了观察数组是否赋值成功。 
 #endif //__DEBUG__
 }
 return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

常见的条件编译指令:

1.
#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
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/不正经/article/detail/609478
推荐阅读
相关标签
  

闽ICP备14008679号