C语言的预处理
前言
本篇文章介绍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会让预处理器发出一条错误指令,并停止编译
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。 如若内容造成侵权/违法违规/事实不符,请联系我的编程经验分享网邮箱:veading@qq.com进行投诉反馈,一经查实,立即删除!