C语言预处理详解(上)(30)

时间:2024-10-13 21:50:26

文章目录

  • 前言
  • 一、预定义符号
  • 二、#define定义标识符
  • 三、#define定义宏
  • 四、#define的替换规则
  • 五、带有副作用的宏
  • 六、宏和函数的对比
  • 七、#undef的作用
  • 八、# 和
    • #的作用
    • ##的作用
  • 总结


前言

  C语言的入门学习差不多要到尾声了,感觉如何呢~
  前文说编译的第一步就是预编译(即预处理),那具体这个阶段会做哪些工作呢?

正文开始!


一、预定义符号

  在C语言中,有一些有意思的预定义符号,这些预定义符号都是语言内置的,即以及定义好的,我们可以直接使用。预定义符号主要有以下几个:

// 实际项目可能会有用
__FILE__        //进行编译的源文件
__LINE__        //文件当前的行号
__DATE__        //文件被编译的日期
__TIME__        //文件被编译的时间
__FUNCTION__    //进行编译的函数
__STDC__        //如果编译器遵循ANSI C,其值为1,否则未定义

这些预定义符号是已经用#define定义好的,在代码运行后的预处理阶段会被替换为相应的内容

其使用就是直接打印即可,只要注意占位符是 %s 还是 %d 即可:

#include <stdio.h>
int main()
{
	printf("%s\n", __FILE__);
	printf("%d\n", __LINE__);
	printf("%s\n", __DATE__);
	printf("%s\n", __TIME__);
	printf("%s\n", __FUNCTION__);
	//printf("%d\n", __STDC__); // VS2022不支持
	
	return 0;
}

运行结果如下:
在这里插入图片描述

二、#define定义标识符

用#define定义标识符的格式如下:

#define MAX 100
#define reg register //懒人觉得register太长了

这些被#define定义的标识符都将在预处理阶段被编译器替换成对应的内容

我产生了一些联想,事实上每个初学者经常会犯以下错误:
main->mian;,->,;(->( ;true->ture

于是我们将错就错,有了以下解决方案

#define mian main
#define ,
#define (
#define )
#define ture true
#define ;

开玩笑的,我们还是要从源头上解决,多敲多练,尽可能避免这种一般性的低级错误

三、#define定义宏

#define机制包括了一个规定,允许把参数替换到文本中,这种实现通常称为宏(macro)或定义宏(define macro)
其声明方式为:#define name( parament-list ) stuff,其中的 parament-list 是一个由逗号隔开的符号表,它们可能出现在stuff中。
注意:参数列表的左括号必须与name紧邻。如果两者之间有任何空白存在,参数列表就会被解释为stuff的一部分

#include <stdio.h>
#define SQUARE(x) x*x //求x的平方

int main()
{
	int ret = SQUARE(5);
	//相当于int ret = 5*5,在预处理阶段就被展开了;
	printf("%d\n", ret); //结果为25
	
	return 0;
}

意思是这么个意思,但是我们要注意这个声明是不太正确的,我们不能在宏的使用里吝啬括号,不然可能会发生以下错误:

printf(“%d\n” ,SQUARE( 5 + 1) );
展开为:printf(“%d\n” ,5 + 1 * 5 + 1);
结果是打印11,与我们想要的25相悖
正确声明是:#define SQUARE(x) ((x)*(x))

也就是说用于对数值表达式进行求值的宏定义都应该用这种方式加上括号,避免在使用宏时由于参数中的操作符或邻近操作符之间不可预料的相互作用

四、#define的替换规则

  以下面代码为例,现在我们来讲解下#define的替换规则

#include <stdio.h>
#define MAX 100
#define SQUARE(x) ((x)*(x)*MAX)

int main()
{
	int ret = SQUARE(5);
	printf("%d\n", ret);
	return 0;
}
  1. 在调用宏时,首先对参数进行检查,看看是否包含任何由#define定义的符号。如果是,它们首先被替换

#define定义的宏中含有#define定义的符号MAX,则调用该宏时,首先将MAX替换

#include <stdio.h>
#define SQUARE(x) ((x)*(x)*100)
int main()
{
	int ret = SQUARE(5);
	printf("%d\n", ret);
	return 0;
}
  1. 替换文本随后被插入到程序中原来文本的位置。对于宏,参数名被他们的值替换

此时代码等价于:

#include <stdio.h>
int main()
{
	int ret = ((5)*(5)*100);
	printf("%d\n", ret);
	return 0;
}
  1. 最后,再次对结果文件进行扫描,看看它是否包含任何由#define定义的符号。如果是,就重复上述处理过程

上例不再包含任何由#define定义的符号

我们也要注意以下几点:

  1. 宏参数和#define 定义中可以出现其他#define定义的变量。但是对于宏,不能出现递归
#define FAC(x) (x)*FAC(x-1) //error
  1. 当预处理器搜索#define定义的符号的时候,字符串常量的内容并不被搜索
#include <stdio.h>
#define MAX 100
int main()
{
	// 以下代码字符串中的MAX不会被替换为100,而字符串外的MAX会被替换
	printf("MAX = %d\n", MAX); // 结果为MAX = 100
	return 0;
}

五、带有副作用的宏

  代码执行后,除了达到我们想要的结果之外,还导致了其他问题的发生,我们就说该条语句带有副作用

例如,我们现在想比较a和b的大小,并将其较大值赋值给c,之后再将a和b同时加1:

#include <stdio.h>
#define MAX(x,y) ((x)>(y)?(x):(y))
int main()
{
	int a = 10;
	int b = 20;
	int c = MAX(a++, b++);
	printf("%d\n", c);
	return 0;
}

这段代码看似没有问题,但是结果却是不正确的,因为该宏经过替换后,等价于以下代码:

#include <stdio.h>
int main()
{
	int a = 10;
	int b = 20;
	int c = ((a++)>(b++)?(a++):(b++));
	printf("%d\n", c);
	
	return 0;
}

所以,当我们使用宏的时候,应该避免传入带有副作用的宏参数

六、宏和函数的对比

  可能通过前面的学习,你也发现了一个问题,就是宏和函数的感觉特别像,那两者可以等同吗?
  事实上是不行的,两者有以下区别:

  1. 用于调用函数和从函数返回的代码可能比实际执行这个小型计算工作所需要的时间更多。所以宏比函数在程序的规模和速度方面更胜一筹(因为函数要开栈帧,宏不用,这个我们下篇会介绍
  2. 更为重要的是函数的参数必须声明为特定的类型。所以函数只能在类型合适的表达式上使用。但是宏可以适用于整形、长整型、浮点型等可以用于来比较的类型。宏是类型无关的
  3. 宏有时候可以做到函数做不到的事情。例如,宏的参数可以出现类型,但是函数却不可以
#include <stdio.h>
#include <stdlib.h>
#define MALLOC(num,type) (type*)malloc(num*sizeof(type))

int main()
{
	int* p2 = MALLOC(10, int);
	if (p2 == NULL)
	{
		printf("p2开辟失败\n");
		return 1;
	}
	
	free(p2);
	p2 = NULL;
	
	return 0;
}

可是,宏也有自己的劣势,在于:

  1. 每次使用宏的时候,一份宏定义的代码将插入到程序中。除非宏比较短,否则可能大幅度增加程序的长度
  2. 宏是没法调试的(我认为这点非常不友好!)
  3. 宏由于类型无关,也就不够严谨
  4. 宏可能会带来运算符优先级的问题,导致程容易出现错

倘若用一张图来表明宏和函数的区别,那么可以用下图:
在这里插入图片描述

七、#undef的作用

  #undef可以移除一个#define定义的标识符或宏

#include <stdio.h>
#define MAX 100
int main()
{
	printf("%d\n", MAX);//正常使用
#undef MAX // 此时MAX失效
	printf("%d\n", MAX); //报错,MAX未定义
}

八、# 和

这个我真感觉没什么用,但是还是讲一下吧:

#的作用

把一个宏参数变成对应的字符串

在介绍#的作用的之前,我先向大家说明一下:字符串是有自动连接的特点的:

	char arr[] = "hello ""world!";
	//等价于char arr[] = "hello world!";
	printf("helll ""world!\n");
	//等价于printf("helll world!\n");

认识到这点后,我们来看代码:

#include <stdio.h>
int main()
{
	int age = 10;
	printf("The value of age is %d\n", age);
	double pi = 3.14;
	printf("The value of pi is %f\n", pi);
	int* p = &age;
	printf("The value of p is %p\n", p);
	
	return 0;
}

我们发现,printf要打印的内容大部分是一样的,那么,为了避免代码冗余,我们可不可以将其封装成一个函数或是宏呢?
答案是不行,不信大家可以自行尝试一下,这时候就可以考虑用这个#了:

#include <stdio.h>
#define print(data,format) printf("The value of "#data" is "format"\n",data)
int main()
{
	int age = 10;
	print(age, "%d");
	double pi = 3.14;
	print(pi, "%f");
	int* p = &age;
	print(p, "%p");
	
	return 0;
}

这时我们只需将要打印的变量的变量名和打印格式传入即可。该代码经过预处理后等价于以下代码

#include <stdio.h>
int main()
{
	int age = 10;
	printf("The value of ""age"" is ""%d""\n", age);
	double pi = 3.14;
	printf("The value of ""pi"" is ""%f""\n", pi);
	int* p = &age;
	printf("The value of ""p"" is ""%p""\n", p);
	return 0;
}

又因为字符串有自动连接的特点,所以可以打印出期望的结果

##的作用

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

#include <stdio.h>
#define CAT(x,y) x##y
int main()
{
	int workhard = 100;
	printf("%d\n", CAT(work, hard));//打印100
	return 0;
}

总结

  其实预处理的过程还是蛮复杂的,关于宏和函数的对比那块大家要自行好好掌握