C语言中的预处理详解
- 预定义符号
C语言设置了一些预定义符号,可以直接使用,预定义符号也是在预处理期间处理的。
例如:
__FILE__//进⾏编译的源⽂件
__LINE__//⽂件当前的⾏号
__DATE__//⽂件被编译的⽇期
__TIME__//⽂件被编译的时间
__STDC__//如果编译器遵循ANSI C,其值为1,否则未定义
代码演示:
#include<stdio.h>
int main()
{
printf("文件名称:%s\n行数:%d\n", __FILE__, __LINE__);
return 0;
}
输出结果:
文件名称:C:\Users\ABCD\Desktop\program\Project160\Project160\源.c
行数:5
- #define 定义常量
#define name stuff
//name表示替换的名字,stuff表示替换的符号
几个有关常量定义的例子:
- 例一:
#include<stdio.h>
#define CASE break;case
//使用了CASE替换掉了break;case
int main()
{
int m = 0;
printf("输入:");
scanf("%d", &m);
switch (m)
{
case 1:
printf("1\n");
case 2:
printf("2\n");
CASE 3:
printf("3\n");
CASE 4:
printf("4\n");
default:
printf("default\n");
}
return 0;
}
注:其中的CASE可以表示为:
//......
case 2:
printf("2\n");//CASE 3: 表示第四行与第五行代码
break;
case 3:
printf("3\n");
//......
输出结果:
输入:1
输出:1 2
输入:2
输出:3
- 例二:
#include<stdio.h>
#define FOR for (int i = 1; i <= 5; i++)
int main()
{
FOR{
printf("%d ",i);
}
return 0;
}
输出结果:1 2 3 4 5
例三:
//如果定义的stuff过长,可以分成几行写,除了最后一行外,每行的后面都加一个反斜杠(续行符)
#define PRINT for (char ch = 'A'; ch <= 'E'; ch++) {\
printf("%c ", ch);\
}
int main()
{
PRINT;
return 0;
}
输出结果:A B C D E
- #define 定义宏
声明方式:
#define name( parament-list ) stuff
其中的 parament-list 是一个由逗号隔开的符号表,它们可能出现在stuff中
注意:参数列表的左括号必须与name紧邻,如果两者之间有任何空白存在,参数列表就会被解释为stuff的一部分
例如:
#include<stdio.h>
#define SQUARE(x) x * x
int main()
{
printf("%d", SQUARE(10));
return 0;
}
但是,如果输入的为一个表达式,那么就可能造成问题
#include<stdio.h>
#define SQUARE(x) x * x
int main()
{
printf("%d", SQUARE(5 + 5));
return 0;
}
输出结果:35
计算思路:5 + 5 * 5 + 5 = 35
由于宏定义在编译过程中是直接被替换掉了,所以像这样的表达式计算时,应当加上括号,防止周围参数对宏的运算造成影响
- 带有副作用的宏参数
例如:
#include<stdio.h>
#define MAX(a, b) ((a) >= (b) ? (a) : (b))
int main()
{
int a = 10, b = 100;
printf("%d", MAX(a, b));
return 0;
}
输出结果:100
#include<stdio.h>
#define MAX(a, b) ( (a) >= (b) ? (a) : (b) )
int main()
{
int x = 5;
int y = 8;
int z = MAX(x++, y++);
printf("x=%d y=%d z=%d\n", x, y, z);
return 0;
}
输出结果:x=6 y=10 z=9
在编译过程中,z的表达式被替换为 x++ >= y++ ? x++ : y++,其中x >= y不成立,由于是后置++运算,x与y各自自增一,在这时将y的值赋值给z,所以z的值为9,之后y自增一,得到结果。
注意:当宏参数在宏的定义中出现超过一次的时候,如果参数带有副作用,那么你在使用这个宏的时候就可能出现危险,导致不可预测的后果,副作用就是表达式求值的时候出现的永久性效果。
- 宏在替换中的规则
在程序中使用 #define 定义符号和宏时,需要涉及以下几个步骤:
- 在调用宏时,首先对参数进行检查,看看是否包含任何由 #define 定义的符号。如果是,它们被替换
- 替换文本随后被插入到程序中原来文本的位置。对于宏,参数名被他们的值所替换
- 最后,再次对结果文件进行扫描,看看它是否包含任何由 #define 定义的符号。如果是,就重复上述处理过程。
注意:宏参数和 #define定义中可以出现其他 #define 定义的符号,但是对于宏,不能出现递归。
当预处理器搜索 #define 定义的符号的时候,字符串常量的内容并不被搜索。
- 宏与函数的对比
宏通常被应用于执行简单的运算,例如求两个数字的最大值,写成下面的宏,更有优势一些
#define MAX(a,b)
使用宏的优势如下:
- 用于调用函数的耗时和代码量比实际执行计算工作所需要的时间更多,所以宏比函数在程序的规模和速度方面更胜一筹。
- 函数的参数必须声明为特定的类型,所以函数只能在类型合适的表达式上使用。反之这个宏适用于整形、浮点型等多种类型,宏的参数与类型无关。
使用宏的劣势如下:
- 每次使用宏的时候,一份宏定义的代码将插入到程序中。除非宏比较短,否则可能大幅度增加程序的长度。
- 宏是没法调试的,且宏无法递归。
- 宏与类型无关,不够严谨。
- 宏可能会带来运算符优先级的问题,导致程容易出现错。
宏有时候可以做函数做不到的事情。例如:宏的参数可以出现类型,但是函数不可以。
#include<stdio.h>
#include<stdlib.h>
#define MALLOC(num,type) (type*)malloc(num * sizeof(type))
int main()
{
int* pm = MALLOC(10, int);
for (int i = 0; i < 10; i++) {
*(pm + i) = i + 1;
printf("%d ", *(pm + i));
}
free(pm);
pm = NULL;
return 0;
}
输出结果:1 2 3 4 5 6 7 8 9 10
- # 运算符与 ## 运算符
- # 运算符
# 运算符将宏的一个参数转换为字符串,它仅允许出现在带参数宏的替换列表中。# 运算符所执行的操作可以理解为字符串化。
例如:
#include<stdio.h>
int main()
{
int a = 10;
int b = 100;
int c = 1000;
printf("The value of a is %d\n", a);
printf("The value of b is %d\n", b);
printf("The value of c is %d\n", c);
return 0;
}
上述代码有很多重复的输出,我们就可以利用 # 运算符将上述代码改写为:
#include<stdio.h>
#define PRINT(value) printf("The value of "#value" is %d\n", value)
int main()
{
int a = 10;
int b = 100;
int c = 1000;
PRINT(a);
PRINT(b);
PRINT(c);
return 0;
}
输出结果:
The value of a is 10
The value of b is 100
The value of c is 1000
当我们按照上述的方式调用代码 PRINT( value ) 的时候,#value就自动转换为了 “value” 这个字符串。
- ## 运算符
## 运算符是将两个宏参数连接的,这样的连接必须产生一个合法的标识符,否则其结果就是未定义的。
例如我们想要写一个函数求两个数的较大值的时候,我们就需要根据不同的数据类型写出不同的函数
但是,如果我们通过宏来实现:
#include<stdio.h>
#define TYPE_MAX(type) \
type type##_max(type a,type b)\
{ \
return ((a)>=(b)?(a):(b));\
}
TYPE_MAX(double)//替换到宏体内后double##_max⽣成了新的符号double_max做函数名
int main()
{
double p = 3.14;
double e = 2.71;
double ret = double_max(p, e);
printf("%.2lf", ret);
return 0;
}
注意:在实际开发过程中##使用的很少,了解即可。
- #undef
这条指令用于移除一个宏定义
例如:
#include<stdio.h>
#define S(x) ((x) * (x))
int main()
{
int x = 10;
printf("%d", S(x));
#undef S(x)
printf("%d", S(x));
return 0;
}
编译失败,因为第8行的S( x ) 未定义。
- 条件编译
使用条件编译指令,可以在编译时选择将一条语句或是一组语句编译或者放弃。
例如:
#include<stdio.h>
#define DEBUG
int main()
{
int arr[10] = { 0 };
for (int i = 0; i < 10; i++) {
arr[i] = i;
}
#ifdef DEBUG
for (int i = 0; i < 10; i++) {
printf("%d ", arr[i]);//为了观察数组是否赋值成功
}
#endif
return 0;
}
当我们取消了DEBUG的宏定义,10~12行的代码将被隐藏,不会被编译。
常见的条件编译指令还有:
#if
#elif
#else
#ifndef
- #if、#elif与#else的使用
#include<stdio.h>
#define T1 0
#define T2 1
int main()
{
#if T1
printf("T1\n");
#elif T2
printf("T2\n");
#else
printf("else\n");
#endif
return 0;
}
输出结果:T2
因为T2结果为真
- 判断是否被定义
#include<stdio.h>
#define symbol
int main()
{
#if defined(symbol)
printf("Defined\n");
#else
printf("Undefined\n");
#endif
return 0;
}
输出结果:Defined
- 头文件的包含
- 头文件被包含的方式: 本地文件包含与库文件包含
#include "myfile"
查找策略:先在源文件所在目录下查找,如果该头文件未找到,编译器就像查找库函数头文件一样,在标准位置查找文件
#include<stdio.h>
查找头文件直接去标准路径下去查找
对于库文件也可以使用 “文件名” 的形式包含,但是这样做查找的效率就低些,而且也不容易区分是库文件还是本地文件
- 头文件的重复包含
头文件的重复包含会增大编译压力,我们为了避免头文件被重复包含,可以使用条件编译的方法。
我们可以在每个头文件中写:
#ifndef __TEST_H__
#define __TEST_H__
//头⽂件的内容......
#endif
第一次包含该头文件时,___TEST_H___还为被定义,会执行头文件内部代码,而第二次被包含时,该值已经被定义了,无法执行以下代码了。
或者可以在头文件首添加语句#pragma once,表明该文件只可以使用一次。
- 其他预处理指令
#error
#pragma
#line
#pragma pack()//改变结构体的默认对齐数
较为罕见,了解即可。