C语言的预处理

发布时间:2023年12月26日

前言

本篇文章介绍C语言预处理

写在前面

  • 对于#标志开头定义的预处理指令,虽然不同C标准规则不一样,但是最推荐的方式是#标志从一行的最左边开始,#与指令之间不要添加空格
  • 预处理器指令可以出现在文件的任何地方
  • 预处理器的有效范围为从定义开始到文件末尾
  • 一条预处理器指令只能占用一行逻辑行
  • 预处理器指令结尾不需要分号

前期准备

我们在书写代码的过程中,使用了\字符进行了换行,预处理器会将多行拼接成一行,比如下面的代码:

printf("a=%lu\n",\
100);

经过预处理器以后会变为

printf("a=%lu\n",100);
 

注意多的那行用空行代替了

对于注释,预处理器会删除并用空行代替,看下面代码:

#include <stdio.h>
int main(int argv, char** argc)
{	
	// line comment
	printf("a=%lu\n",\
100);
	/*
	* lines comment
	*/
	return 0;
}

经过预处理以后:

省略了#include <stdio.h>添加的代码
int main(int argv, char** argc)
{

	printf("a=%lu\n",100);




	return 0;
}

可以看到,注释虽然被删除了,但是行位置都用空行替代保留了

对于添加到代码中的注释,比如下面这一行代码

return/*aaa*/0;

预处理后会变成

return 0;

行内注释被一个空格代替了

到这里,预处理前期准备完成,预处理器开始寻找#标志

#define

这是一个宏定义标志,宏定义分为三部分,看下面的宏定义

#define PI 3.141592654

#define:这一部分是预处理指令
PI:这一部分是宏名称,命名规则和C变量命名规则一致,宏名称内不允许有空格
3.141592654:这一部分是宏的替换体
一旦预处理器在程序中找到宏名称,就会使用替换体进行替换,但是也有例外,看下面例子:

#include <stdio.h>
#define A 100
int main(int argv, char** argc)
{	
	printf("A=%lu\n",A);
	return 0;
}

预编译后,结果如下(省略了不重要的地方):

printf("A=%lu\n",100);

可以发现,字符串内的A并没有被替换,所以预处理的替换规则:
对于双引号字符串内的宏不进行替换

#define宏不仅可以定义变量,还可以定义函数,如果宏的替换体是变量,这样的宏叫做类对象宏,如果宏的替换体是定义的函数,这样的宏叫做类函数宏
比如

#include <stdio.h>
#define SQUARE(X) X*X
int main(int argv, char** argc)
{
 int a = 10;
 printf("square=%d\n",SQUARE(a));
 return 0;
}

预处理后变成(省略了不重要的部分):

printf("square=%d\n",a*a);

这种情况没有问题
但是,如果我们换一种写法:

printf("square=%d\n",SQUARE(a+a));

这样,经过预处理后会变成

printf("square=%d\n",a+a*a+a);

这样显然是错误的,我们的本意是两个a相加以后再求面积,所以在定义预处理宏函数的时候,一定要对参数外面加一层括号,就像这样

#define SQUARE(X) (X)*(X)

这样,经过预处理后会变成

printf("square=%d\n",(a+a)*(a+a));

接下来,又换了一种方式:

printf("square=%d\n",100/SQUARE((a+a)));

我们期望的结果先求面积,然后100除以求得的面积
但实际上

printf("square=%d\n",100/(a+a)*(a+a));

会先进行除法,在进行乘法,这个时候我们必须对宏函数的整个定义外面也加上括号

#define SQUARE(X) ((X)*(X))

所以在定义预处理宏函数的时候,一定要在宏函数整个定义外加上括号

尽管我们使用足够多的圆括号来确保运算和结合的正确顺序,但是对于下面的情况:

printf("square=%d\n",SQUARE(++a));

展开以后:

printf("square=%d\n",((++a)*(++a)));

这样a递增了两次,与我们的本意不符,这种情况是没有什么好办法解决的,所以
在使用预处理宏函数的时候,不要使用++/--作为宏参数

宏实参字符串

在类函数宏中,如果我们想打印宏函数实参的名称,这时候直接把参数放到字符串中是打印不出来的,看下面的例子:

#define A 100
#define S1(x) printf("x value::%d\n",x)
S1(A);

打印结果为(这个实际上是打印的形参的名称):

x value::100

但是如果我们在宏参数前面添加#,就可以把实参参数名进行字符串序列化,比如我们这么写

#define A 100
#define S1(x) printf(#x" value::%d\n",x)
S1(A);

现在打印结果就变成了

A value::100

我们把A替换成变量也是可以的,比如

#define S1(x) printf(#x" value::%d\n",x)
int a = 100;
S1(a);

打印结果:

a value::100

宏定义中的黏合剂

记号

对于一个宏定义的替换体,我们可以把替换体看成一堆记号的序列,记号用空格分隔。比如对于下面的宏定义:

#define A 200 * 100
#define B 200*100

A的替换体有3个记号,200、*、100
B的替换体有1个记号,200*100

而##作为宏定义中的黏合剂的作用就是将两个记号合并到一起,比如对于宏定义:

#define M(X,Y) mXY

无论XY传递什么,返回的都是mXY,因为mXY作为一个独立标记是没法分别识别X和Y的。这个时候就可以使用##了

#define M(X,Y) m##X##Y

M(0,0)就会返回m00
M(1,0)就会返回m10

总结一下##的作用就是:
##能把他两边的标记作为独立标记处理,然后把两个标记合并成一个标识符

可变参数

类函数宏可以允许可变参数,就像printf一样,在函数宏的定义中,需要将最后一个参数标识为...,然后在替换体中,使用__VA_ARGS__替换…部分,比如下面例子:

#define ERROR(...) printf("ERROR:[%s][%s][%d]:\n\tmsg1:%s\n\tmsg2:%s\n",__DATE__,__TIME__,__LINE__,__VA_ARGS__)
ERROR("send message failed","param count error");

输出结果为:

ERROR:[Dec 26 2023][11:22:57][16]:
	msg1:send message failed
	msg2:param count error

注意:

  • __VA_ARGS__只能整体替换,不能获取单个值
  • 省略号只能代替最后的宏参数

使用宏还是变量或者函数

无论是类对象宏还是类函数宏,都有几个特点:

  • 预处理阶段就已经处理完成了,不会在运行时对内存大小和执行指令增加负担
  • 宏具有文件作用域,并且宏不用担心在不同文件重复定义,所以可以把宏定义放在头文件,任何其他代码都可以引用头文件继而使用当前宏
  • 也正是由于上面一条的自由性,如果两个头文件都定义了同一个宏,宏的使用跟头文件引用的顺序相关,这样容易导致不可控的问题
  • 宏定义可能产生奇怪的问题,比如++/–的不可预测性

总结:因为现在有了const变量和内联函数,所以宏的定义不是不可替代,对于项目使用的全局变量或者简单的函数,采用项目独有的宏名称并且对宏函数参数和函数添加括号,尤其是那些需要#ifdef #elif #endif进行不同宏定义选择的情况下,使用宏定义还是很方便的。

#include

#include是为了引入别的头文件,当预处理器发现#include指令时,会查看后面的文件名并把文件的内容包含到当前文件中,即替换源文件中的#include指令。这相当于把被包含文件的全部内容输入到源文件#include指令所在的位置

#include一般有两种使用方式:

  • 文件名放在尖括号中,比如:#include <stdio.h>
    使用尖括号告诉预处理器在标准系统目录中查找该文件
  • 文件名放在双引号中,比如:#include "alu.h"
    使用双引号告诉预处理器首先在当前目录中(或文件名中指定的其他目录)查找该文件,如果未找到再查找标准系统目录

包含头文件的作用:

  • 头文件中可能包含一些宏定义,比如EOF,getchar,putchar等都是通过宏定义的,引入头文件以后我们就能使用这个宏了
  • 头文件可能包含很多变量或者函数的声明,比如通过extern声明的变量或者函数,头文件对应的代码文件实现了这个函数或者定义了这些变量,我们引入头文件以后就可以使用这个变量和函数了。

注意:预处理后的头文件由于全部包含在代码中,所以代码文件会很大,但是经过编译以后,只会留下那些代码中使用的信息,别的信息都去掉了,所以最终的代码文件不会太大

#undef

#undef指令用于取消那些通过#define定义的指令,比如

#define PI 3.14
#undef PI

#ifdef #ifndef #endif #else

这四指令是条件编译经常使用的指令,直接通过一个例子来理解一下
注意:无论是使用#ifdef还是#ifndef都必须以#endif结尾

#ifdef PLAT_PC
#define A 100
#define B 200
#else
#define A 300
#define B 400
#endif

#ifndef PLAT_PC
#define A 300
#define B 400
#else
#define A 100
#define B 200
#endif

#if #elif #endif

使用#if的时候也必须以#endif结尾
#if后面跟的是一个整形常量表达式
通过#if和#elif的组合可以对多个分支进行判断

#if SYS == 1
#include "ibmpc.h"
#elif SYS == 2
#include "vax.h"
#elif SYS == 3
#include "mac.h"
#else
#include "general.h"
#endif

部分预定义宏

FILE

表示当前源代码文件名的字符串字面量

DATE

表示代码执行预处理时的日期,格式为"Mmm dd yyyy"

LINE

整形常量,表示当前宏所使用的代码所在的行数

STDC

设置为1时表明实现遵循C标准

STDC_HOSTED

本机环境设置为1,否则设置为0

STDC_VERSION

支持C99标准,设置为199901L,支持C11标准,设置为201112L

TIME

表示代码执行预处理时的时间,格式为"hh:mm:ss"

func

C99标准定义的预定义标识符。注意,这不是宏,我们可以看下面代码:

#include <stdio.h>
int main(int argv, char** argc)
{	
	printf("%s\n",__DATE__);
	printf("%s\n",__func__);
	return 0;
}

经过预编译后:

...
int main(int argv, char** argc)
{
	printf("%s\n","Dec 25 2023");
	printf("%s\n",__func__);
	return 0;
}

可以看到__func__并没有发生宏展开,实际上,因为__func__目的是打印当前函数的名称,所以具有函数作用域,只有在编译阶段才会获取到当前函数名,我们看一下上述代码的部分汇编代码如下:

.LFE0:
	.size	main, .-main
	.section	.rodata
	.type	__func__.1940, @object
	.size	__func__.1940, 5
__func__.1940:
	.string	"main"
	.ident	"GCC: (Ubuntu 4.8.4-2ubuntu1~14.04.4) 4.8.4"
	.section	.note.GNU-stack,"",@progbits

可以看到这个时候才知道当前函数的名称,作为只读字符串保存到最终的程序中。

#line

#line指令能够重置__LINE__和__FILE__的值,比如:

#define ERROR(...) printf("ERROR:\n\t[文件:%s]\n\t[时间:%s %s]\n\t[行号:%d]:\n\tmsg1:%s\n\tmsg2:%s\n",__FILE__,__DATE__,__TIME__,__LINE__,__VA_ARGS__)
int main(int argc, const char * argv[]) 
{
#line 999 "hahaha.c"
    ERROR("send message failed","param count error");
    return 0;
}

打印结果为:

ERROR:
	[文件:hahaha.c]
	[时间:Dec 26 2023 13:32:19]
	[行号:999]:
	msg1:send message failed
	msg2:param count error

#error

#error会让预处理器发出一条错误指令,并停止编译

文章来源:https://blog.csdn.net/b1049112625/article/details/135219874
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。