C预处理指令
预处理
gcc -E xxx.c 查看C代码的预处理结果,显示在终端
gcc -E xxx.c -o xxx.i 把预处理的结果保存到文件中,以.i结尾的文件也被称为预处理文件。
1、文件包含指令
#include 预处理指令的功能是导入一个头文件到当文件中,它用两中使用方法:
方法1:#include <file_name.h>
从系统指定的路径查找并导入头文件,一般用于导入标准库、系统、第三方库的头文件。
可通过设置操作系统的环境环境、编译器-I参数来指定头文件查找路径
方法2:#include “file_name.h”
从系统当前路径查找并导入头文件,如果没有再从系统指定的路径查找并导入头文件,一般用于导入自定义头文件。
2、宏替换指令
#define 宏名 会被替换的内容
预处理阶段,预处理器会把宏定义的内容原样替换到使用宏名的位置
//宏常量
#define ARR_LEN 20
int arr[ARR_LEN];
//宏表达式
#define STU_INFO "%s %c %hd %d"
typedef struct Student
{
int id;
char name[20];
char sex;
float score;
}Student;
printf(STU_INFO,stu.id,stu.name,stu.sex,stu.score);
注意:由于宏常量和宏表达式可能使用在表达式中,所以在定义时在末尾不要加分号。
编译器预定义的宏
__FILE__ 获取当前文件名
__func__ 获取当前函数名
__LINE__ 获取当前行号
__DATE__ 获取当前日期
__TIME__ 获取当前时间//编译时的时间
__WORDSIZE 获取当前编译器的位数
// 我们DEBUG时,适合用来显示警告、错误信息。
#include <stdio.h>
int main(int argc,const char* argv[])
{
printf("%s\n",__FILE__);
printf("%s\n",__func__);
printf("%d\n",__LINE__);
printf("%s\n",__DATE__);
printf("%s\n",__TIME__);
}
/*
test.c
main
8
Aug 14 2023
22:37:35
*/
标准库预定义的宏:
// limits.h 头文件中定义的所有整数类型最大值、最小值
#define SCHAR_MIN (-128)
#define SCHAR_MAX 127
#define UCHAR_MAX 255
#define SHRT_MIN (-32768)
#define SHRT_MAX 32767
#define USHRT_MAX 65535
#define INT_MIN (-INT_MAX - 1)
#define INT_MAX 2147483647
#define UINT_MAX 4294967295U
#define LLONG_MAX 9223372036854775807LL
#define LLONG_MIN (-LLONG_MAX - 1LL)
#define ULLONG_MAX 18446744073709551615ULL
// stdlib.h 头文件定义两个结标志
#define EXIT_SUCCESS (0)
#define EXIT_FAILURE (-1)
// stdbool.h 头文件定义了bool、true、false sizeof=>1,4,4 c++=>1,1,1
#define bool _Bool(c99)
#define true 1
#define false 0
// libio.h 头文件定义了NULL
#define NULL ((void*)0)
3、宏函数
宏函数不是真正的函数,而是带参数的宏替换,只是使用方法像函数而已。
在代码中使用宏函数,预处理时会经历两次替换,第一次把宏函数替换成它后面的一串代码、表达式,第二次把宏函数中的参数替换到表达式中。
#define ARR_SIZE(a) sizeof(a)/sizeof(a[0])
int arr[] = {1,2,3,4};
int len = ARR_SIZE(arr);
注意事项:
1、宏函数后面的代码不能直接换行,如果代码确定太长,可以使用续行符换行。
#define 宏名(a,b,c,...) { \
代码1; \
代码2; \
... \
}
2、为了防止宏函数出现二义性,对宏参数要尽量多加小括号。
假设如下定义:
#define SUM(a,b) a+b
//如果这样替换,那么替换后就变成了 3+4*12
int num = SUM(3,4)*12;
所以应该加上括号
#define SUM(a,b) (a+b)
再假设如下定义:
#define MUT(a,b) (a*b)
//像上面一样加上了括号 应该没问题了吧
//看看替换后 3+2*4 显然不是我们想要的结果
int num = MUT(3+2,4);
所以为了保险起见,我们给每个参数都套上括号
#define SUM(a,b) ((a)+(b))
#define MUT(a,b) ((a)*(b))
3、传递给宏函数的参数不能使用自变运算符,因为我们无法知道参数在宏代码中会被替换多少次。
#define MUT(a,b) ((a)*(a)*(b))
int n=1;
int num = MUT(n++,4));
printf("%d %d", num, n);
//(n++)*(n++)*(4)
//8 3 1*2*3 并不是2*2*4
优缺点
优点:
1、执行速度快,它不是真正的函数调用,而是代码替换,不会经历传参、跳转、返回值。
2、不会检查参数的类型,因此通用性强。
缺点:
1、由于它不是真正的函数调用,而是代码替换,每使用一次,就会替换出一份代码,会造成代码冗余、编译速度慢、可执行文件变大。
2、没有返回值,最多可以有个执行结果。
3、类型检查不严格,安全性低。
4、无法进行递归调用。
条件编译
条件语句(if、switch、for、while、do while)会根据条件选择执行哪些代码,条件编译就是预处理器根据条件选择哪些代码参与下一步的编译。
负责条件编译的预处理指令有:
#if #ifdef #ifndef #elif #else #endif
头文件卫士
#ifndef FILE_H // 判断FILE_H宏是否正在,不存在则条件为真
#define FILE_H // 定义FILE_H宏
// 头文件卫士能保证此处不重复出现
#endif//FILE_H // #ifndef的结尾
注释代码
#if 0|1
可注释大块代码,可以嵌套
#endif
版本、环境判断:
#if __WORDSIZE == 64
typedef long int int64_t;
#else
typedef long long int int64_t;
#endif
// 判断是否是Linux操作系统:
#if __linux__
#endif
// 判断是否是Windows操作系统:
#if __WIN32 | __WIN32__ | __WIN64__
#endif
// 判断gcc还是g++:
#if __cplusplus
printf("g++编译器\n");
#else
printf("gcc编译器\n");
#endif
DEBUG宏:
专门用于调试程序的宏函数,这种宏函数在程序测试、调试、试运行阶段执行,这类函数会根据DEBUG宏是否定义确定执行的流程。
可以配合之前提到的__LINE__等使用,方便我们调试。
宏函数的变长参数:
#define func(...) __VA_ARGS__
//这种用法必须配合,printf/fprintf/sprintf系列支持变长参数的函数使用。
总体来说就是将左边宏中…的内容替换到__VA_ARGS__所在位置
#define myprintf(...) printf( __VA_ARGS__)
myprintf("123testhehe");
//123testhehe
总结
预处理阶段的主要任务为:
1、宏替换:将宏名替换为对应宏定义
2、文件包含:将#include包含的文件插入到当前文件,这样在编译阶段才能正确处理所有指令
3、条件编译:决定后续哪些代码被编译