C语言入门(三十一):预处理详解(1)

目录

[1. 预定义符号](#1. 预定义符号)

[2. #define 定义常量](#define 定义常量)

3. #define定义宏

[4. 带有副作⽤的宏参数](#4. 带有副作⽤的宏参数)

[5. 宏替换的规则](#5. 宏替换的规则)

[6. 宏函数的对⽐](#6. 宏函数的对⽐)

7. #和##

7.1 #运算符

7.2 ##运算符


1. 预定义符号

C语⾔设置了⼀些预定义符号,可以直接使⽤,预定义符号也是在预处理期间处理的。

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

举个例⼦:

cpp 复制代码
int main()
{
	printf("%s\n", __FILE__);
	printf("%s\n", __TIME__);
	printf("%d\n", __LINE__);
	printf("%s\n", __DATE__);
	printf("%s\n", __STDC__); //如果编译器支持则返回值是1,否则会报错误,在VS2022是不支持的

	return 0;
}

输出结果:

2. #define 定义常量

基本语法:

cpp 复制代码
#define name  stuff

举个例⼦:

cpp 复制代码
#define max 1000
#define str "hello,world"

int main()
{
	
	int a = max;
	printf("%d\n", a);
	printf("%d\n", max);

	printf("%s\n", str);

	char* b = str;
	printf("%s\n",b);

	return 0;
}

输出结果:

定义过长的名字的话,可以在末尾加 \ 这个符号叫续行符,相当于回车,下一行算成一行

不用 \ 的情况:

cpp 复制代码
#define debug_print printf("file: %s \t line: %d \t data: %s \t time: %s\n",__FILE__,__LINE__,__DATE__,__TIME__)

int main()
{
	debug_print;
	return 0;
}

输出结果:

上面的这种情况非常的不合适,长起来看着也不舒服,而且容易出问题

使用 \ 的情况:

注意:\ 后面不能加任何东西,包括空格

cpp 复制代码
#define debug_print printf("file: %s \tline: %d \t   \
                            data: %s \t              \
                            time: %s\n",__FILE__,__LINE__,__DATE__,__TIME__)

上面的代码是利用续行符 \ 之后的样子,会简洁许多,看着也舒服,这里的结果是一样的,就不打印出来了

思考:在define定义标识符的时候,要不要在最后加上 ;

⽐如:

cpp 复制代码
#define MAX 1000 ;
#define MAX 1000

建议不要加上 **;**这样容易导致问题。

如下:

cpp 复制代码
if(condition)
max = MAX;
else
max = 0;

如果是加了分号的情况,等替换后,if和else之间就是2条语句,⽽没有⼤括号的时候,if后边只能有⼀ 条语句。这⾥会出现语法错误。

3. #define定义宏

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

下⾯是宏的申明⽅式:

cpp 复制代码
#define name( parament-list ) stuff

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

问题:利用定义宏来完成一个数的平方

cpp 复制代码
问题:利用定义宏来完成一个数的平方
#define square(x) x*x
int main()
{
	int a = 5;
	int ret = square(a); //使用宏定义后,这个代码等价于 int ret = x*x
	printf("%d\n", ret);

	return 0;
}

输出结果:

注意:

使用宏的时候,里面是不会进行计算的,而是直接替换

cpp 复制代码
使用宏的时候,里面是不会进行计算的,而是直接替换
#define square(x) x*x
int main()
{
	int a = 5;
	int ret = square(a+1); //使用宏定义后,这个代码等价于 int ret = a+1*a+1 = 11,不是36
	printf("%d\n", ret);

	return 0;
}

输出结果:

解析:

使用宏定义后,这个代码等价于 int ret = a+1*a+1 = 11,不是36

cpp 复制代码
	int a = 5;

    int ret = square(a+1);

如果非要算出36,则我们要利用运算符的优先级来进行,加个括号就行了

cpp 复制代码
#define square(x) (x)*(x)
int main()
{
	int a = 5;
	int ret = square(a + 1); //此时相当于 (a+1)*(a+1) = 6*6 =36
	printf("%d\n", ret);

	return 0;
}

输出结果:

解析:

此时int ret相当于**(a+1)*(a+1) = 6*6 =36**

cpp 复制代码
int a = 6;

int ret = square(a + 1); 

定义中我们使⽤了括号,想避免之前的问题,但是这个宏可能会出现新的错误

cpp 复制代码
int a = 5;
printf("%d\n" ,10 * DOUBLE(a));

这将打印什么值呢?看上去,好像打印100,但事实上打印的是55.

乘法运算先于宏定义的加法,所以出现了 55

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

cpp 复制代码
#define DOUBLE(x)   ( ( x ) + ( x ) )

提⽰:

所以⽤于对数值表达式进⾏求值的宏定义都应该⽤这种⽅式加上括号,避免在使⽤宏时由于参数中的 操作符或邻近操作符之间不可预料的相互作⽤。

4. 带有副作⽤的宏参数

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

例如:

cpp 复制代码
x+1;//不带副作⽤
 
x++;//带有副作⽤

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

问题:写一个宏,来求最大值

正常的参数(就是没有副作用)

cpp 复制代码
问题:写一个宏,来求最大值
正常的参数
#define max(x,y) ( (x)>(y) ? (x):(y) )
int main()
{
	int a = 3;
	int b = 5;
	int m = max(a, b);
 
	printf("%d\n", m);
  printf("%d\n", a);
  printf("%d\n", b);
 
	return 0;
}

输出结果:

带有符号的参数(有副作用)

cpp 复制代码
#define max(x,y) ( (x)>(y) ? (x):(y) )
int main()
{
	int a = 3;
	int b = 5;
	int m = max(a++, b++);

	printf("%d\n", m); //6
	printf("%d\n", a); //4
	printf("%d\n", b); //7

	return 0;
}

输出结果:

这就是使用了带有副作用的宏参数,它会改变我们的a和b的值

5. 宏替换的规则

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

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

注意:

  • 宏参数和#define定义中可以出现其他#define定义的符号。但是对于宏,不能出现递归。
  • 当预处理器搜索#define定义的符号的时候,字符串常量的内容并不被搜索。

代码如下:

cpp 复制代码
#define max(x,y) ( (x)>(y) ? (x):(y) )
#define g 5

int main()
{
	int a = 3;

	int m = max(a,g); //这里 g 先等于5 然后在传参到max宏定义里面去
	printf("%d\n", m);
   
	int c = max(a, max(2,3)); //先计算里面的max ,把值传回来,然后在计算外面的max值,在传回来,则是最终的结果
	printf("%d\n", c);
	printf("g = %d\n", c); //这里的g是不会被替换的 ,所以输出值是 g=3 不是 5 3

	return 0;

}

输出结果:

解析:
这里 g 先等于5 然后再传参到max宏定义里面去,再进行比较大小,结果是5最大,于是打印5

cpp 复制代码
#define g 5

int m = max(a,g); 
printf("%d\n", m);

先计算里面的max ,把值传回来,然后再计算外面的max值,在传回来,则是最终的结果

cpp 复制代码
int c = max(a, max(2,3)); 
printf("%d\n", c);

这里的g是不会被替换的 ,所以输出值是 g=3 不是 5 是 3

cpp 复制代码
printf("g = %d\n", c); 

6. 宏函数的对⽐

宏通常被应⽤于执⾏简单的运算

⽐如在两个数中找出较⼤的⼀个时,写成下⾯的宏,更有优势⼀些

cpp 复制代码
#define MAX(a, b) ((a)>(b)?(a):(b))

那为什么不⽤函数来完成这个任务?

原因有⼆:

  1. ⽤于调⽤函数和从函数返回的代码可能⽐实际执⾏这个⼩型计算⼯作所需要的时间更多。所以宏⽐函数在程序的规模和速度⽅⾯更胜⼀筹。
  2. 更为重要的是函数的参数必须声明为特定的类型。所以函数只能在类型合适的表达式上使⽤。反之这个宏可以适⽤于整形、⻓整型、浮点型等,可以⽤于> 来⽐较的类型。宏的参数是类型⽆关的

和函数相⽐宏的劣势:

  1. 每次使⽤宏的时候,⼀份宏定义的代码将插⼊到程序中。除⾮宏⽐较短,否则可能⼤幅度增加程序的⻓度。
  2. 宏是没法调试的。(不方便调试)
  3. 宏由于和类型⽆关,也就不够严谨。
  4. 宏可能会带来运算符优先级的问题,导致程容易出现错。
cpp 复制代码
#define MAX(a,b) ((a)>(b)?(a):(b))

int max(int x, int y)
{
	int r = x > y ? x : y;
	return r;
}

int main()
{
	int m1 = MAX(3,-6); //利用宏
	int m2 = max(3,-6); //利用函数

	printf("m1 = %d\n", m1); //利用宏计算,并且打印
	printf("m2 = %d\n", m2); //调用函数计算,并且打印

	return 0;
}

输出结果:

宏和函数的⼀个对⽐

宏的参数可以有类型(宏的优点)

如果是函数的话,它的参数是不能有类型的,即传个int float 的参数,但是宏传过去的参数是可以这样的,因为宏本身没有类型的定义,这也是宏的一个优点

cpp 复制代码
#define Malloc(n,type) (type*)malloc(n*sizeof(int))

int main()
{
	//int* p = (int*)malloc(10 * sizeof(int));  //申请40个字节的空间

	int* ptr = Malloc(10,int); 
	//宏定义之后等价于 int* p = (int*)malloc(10 * sizeof(int));

	return 0;
}

7. #和##

7.1 #运算符

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

#运算符所执⾏的操作可以理解为"字符串化"。

在使用#运算符之前,先的理解这个代码:

cpp 复制代码
int main()
{
	printf("helloworld\n");
	printf("helloworld""\n"); 
	printf("hello""world\n"); //可以分开加双引号,是一样的结果
	printf("hello""world""\n");

	return 0;

}

输出结果:

我们可以看到,上面的输出结果是一模一样的,说明字符串可以分开加双引号,最终是一样的结果

我们理解了这个,那么下面的#运算符就更好的理解了

#运算符的运用

cpp 复制代码
#运算符的运用
#define Print(n,format)  printf("the value of "  #n  " is "  format  "\n" , n)

int main()
{
	int a = 1;
	Print(a, "%d"); //利用宏来进行打印
	//printf("the value of a is %d\n", a); //正常的打印
	//printf("thr value of" " a " "is " "%d" "\n" , a); //利用宏定义之后,替换出来的式子

	int b = 20;
	Print(b, "%d");
	//printf("the value of b is %d\n", b);

	float f = 5.6f;
	Print(f, "%.1f"); //保留1位小数
	//printf("the value of f is %f\n", f);

	return 0;
}

输出结果:

解析:

没有使用宏定义的时候,正常的打印:

cpp 复制代码
printf("the value of a is %d\n", a); //正常的打印

使用宏定义(预处理)之后,替换的代码变成了这个样子

cpp 复制代码
printf("thr value of" " a " "is " "%d" "\n" , a); //利用宏定义之后,替换出来的式子

这里的打印方式是不是和我们刚刚理解的那个helloworld是个意思

7.2 ##运算符

**##**可以把位于它两边的符号合成⼀个符号,它允许宏定义从分离的⽂本⽚段创建标识符。

**##**被称为记号粘合

这样的连接必须产⽣⼀个合法的标识符。否则其结果就是未定义的。

这⾥我们想想,写**⼀个函数求2个数的较⼤值的时候** ,不同的数据类型就得写不同的函数。

cpp 复制代码
#define GENERIC_MAX(type)      \
type type##_max(type x, type y)\
{                              \
     return (x > y ? x : y);   \
}

GENERIC_MAX(int)   //替换到宏体内后int##_max⽣成了新的符号int_max做函数名
GENERIC_MAX(float) //替换到宏体内后float##_max⽣成了新的符号float_max做函数名

int main()
{
    //调⽤函数

    int m = int_max(2, 3);
    printf("%d\n", m);

    float fm = float_max(3.5f, 4.5f);
    printf("%f\n", fm);

    return 0;
}

输出结果:

解析:

cpp 复制代码
#define GENERIC_MAX(type)       \
type type##_max(type x, type y) \
{                               \
    return (x > y ? x : y);     \
}

知识点:带参数的宏定义

  • #define 是宏定义指令,用于创建文本替换规则

  • GENERIC_MAX(type) 是一个带参数的宏,type 是参数

  • 宏的名称是 GENERIC_MAX,它接受一个参数 type

知识点:多行宏定义

  • 反斜杠**\ 是续行符**,表示宏定义延续到下一行

  • 在VS中,反斜杠后面不能有任何空格(包括Tab),否则会报错

  • 这个宏实际上定义了4行代码,但因为续行符的存在,预处理器把它看作一个整体

知识点:令牌粘贴操作符(##)

  • ## 是预处理器的令牌粘贴操作符

  • type##_max 会把 type_max 连接成一个新的标识符

  • 例如:当 typeint 时,int##_max 变成 int_max

  • typefloat 时,float##_max 变成 float_max

知识点:宏展开过程

  • 预处理器在编译前展开宏

  • 对于 GENERIC_MAX(int),展开过程如下:

    1. int 替换所有 type

    2. int##_max 变成 int_max

    3. 最终展开为:

cpp 复制代码
int int_max(int x, int y)
{
    return (x > y ? x : y);
}

在实际开发过程中##使⽤的很少,很难取出⾮常贴切的例⼦

后面的内容我们放到下一节内容里面去!!!!

相关推荐
BD_Marathon3 小时前
关于JS和TS选择的问题
开发语言·javascript·ecmascript
YJlio3 小时前
Python 一键拆分 PDF:按“目录/章节”建文件夹 + 每页单独导出(支持书签识别&正文识别)
开发语言·python·pdf
IT方大同3 小时前
C语言进制转化
c语言·开发语言
SELSL3 小时前
标准IO总结
linux·c语言·标准io·stdio·标准io与文件io的区别
野生风长3 小时前
从零开始的C语言:文件操作与数据存储(上)(文件的分类,文件的打开和关闭)
c语言·开发语言
小柯博客3 小时前
从零开始打造 OpenSTLinux 6.6 Yocto 系统 - STM32MP2(基于STM32CubeMX)(九)
c语言·stm32·单片机·嵌入式硬件·物联网·嵌入式·yocto
我是哈哈hh3 小时前
【Python数据分析】数据可视化(全)
开发语言·python·信息可视化·数据挖掘·数据分析
良木生香3 小时前
【诗句结构-初阶】详解栈和队列(2)---队列
c语言·数据结构·算法·蓝桥杯
拾贰_C3 小时前
【python| pytorch】卸载py库,手动法
开发语言·pytorch·python