【C语言】预处理详解
大家好,我是苏貝,本篇博客带大家再次优化上一篇的通讯录,实现将录入的数据在程序退出后存储到文件中,在下一次程序开始时打开文件获取数据,如果你觉得我写的还不错的话,可以给我一个赞👍吗,感谢❤️
目录
- 1.预定义符号
- 2.#define
- 2.1 #define定义标识符常量
- 2.2 #define 定义宏
- 2.3 #和##
- 2.4 带副作用的宏参数
- 2.5 宏和函数对比
- 2.6 命名约定
- 3. #undef
- 4. 命令行定义
- 5. 条件编译
- 6.文件包含
- 6.1 头文件被包含的方式:
- 6.2 嵌套文件包含
1.预定义符号
_ _ FILE_ _ //进行编译的源文件的文件名
_ _ LINE _ _ //文件当前的行号
_ _ DATE _ _ //文件被编译的日期
_ _ TIME _ _ //文件被编译的时间
_ _ STDC _ _ //如果编译器遵循ANSI C,其值为1,否则未定义
int main()
{
printf("%s\n", __FILE__);
printf("%d\n", __LINE__);//从下图中可以看出,在第9行
printf("%s\n", __DATE__);
printf("%s\n", __TIME__);
printf("%s\n", __FUNCTION__);//该语句所在的函数名
//printf("%s\n", __STDC__);//当前使用的VS2019不遵循ANSI C
return 0;
}
2.#define
#define是一个预处理指令,有2个作用
1.#define定义标识符常量
2.#define定义宏
2.1 #define定义标识符常量
语法格式:
#define name//常量名 stuff//内容
以后再遇见name,直接将name替换成stuff,不会去计算staff
#define MAX 100
#define STR "abcdef"
#define INT int
#define do_forever for(;;)//死循环
int main()
{
int a = MAX;//int a=100;
INT b = 10;//int b=10;
printf("a=%d b=%d\n", a, b);
printf("%s\n", STR);//printf("%s\n", "abcdef");
return 0;
}
那么,下面的M是替换成3+2还是5呢?
替换成3+2
#define M 3+2
int main()
{
int a = M * 3;//3+2*3=9
printf("%d\n", a);
return 0;
}
提问:
在define定义标识符的时候,要不要在最后加上 ; ?
不用,why?因为上面有说过,是将name替换成stuff,如果stuff里面有;,那么替换时;也会被替换过去。例如下面的代码,这显然是有问题的
#define M 100;
int main()
{
printf("%d", M);//printf("%d", 100;);
return 0;
}
2.2 #define 定义宏
#define 机制包括了一个规定,允许把参数替换到文本中,这种实现通常称为宏(macro)或定义宏(define macro)。
下面是宏的申明方式:
#define name( parament-list ) stuff
其中的 parament-list 是一个由逗号隔开的符号表,它们可能出现在stuff中。
注意:
参数列表的左括号必须与name紧邻。
如果两者之间有任何空白存在,参数列表就会被解释为stuff的一部分
下面就是一个简单的#define 定义的宏
#define ADD(x,y) x+y
int main()
{
int a = 10;
int b = 20;
int c = ADD(a, b);//int c=a+b;
printf("%d\n", c);
return 0;
}
但是上面定义的宏容易出问题,如下:
#define ADD(x,y) x+y
int main()
{
int a = 10;
int b = 20;
int c = 4*ADD(a, b);
printf("%d\n", c);
return 0;
}
我们本想先算出ADD(a, b);=30后,再让30*4=120。但是因为是直接替换,所以该语句替换后为int c=4 * a+b=60。那么该如何修改呢?在宏里加括号
#define ADD(x,y) ((x)+(y))
对x和y分别加括号,是防止要计算的是x*y,而x=1+2,y=3,这时如果不加括号,替换的结果是1+2 * 3=7而非3 *3=9。对整体加括号,是想要先算出整体的大小
注意:
宏参数和#define 定义中可以出现其他#define定义的符号。但是对于宏,不能出现递归。
2.3 #和##
首先我们看看这样的代码:它们都能打印出helloworld吗?
int main()
{
printf("helloworld\n");
printf("hello""world\n");
return 0;
}
是的,我们发现字符串是有自动连接的特点的
- #的作用
我们再来学习一下#的作用,#可以把一个宏参数变成对应的字符串
#define P(n) printf(#n)
int main()
{
P(a);//printf("a");
return 0;
}
我们再来看看下面的代码
int main()
{
int a = 10;
printf("the value of a is %d\n", a);
float b = 20;
printf("the value of b is %f\n", b);
double c = 2;
printf("the value of c is %f\n", c);
return 0;
}
不难发现,3个printf语句非常相似,这不免让我们想到,是否可以用一个函数来实现这3个printf语句。但是可惜的是,因为这3个变量的类型不同,打印的形式也不同,所以不能用函数。那是否可以用宏呢?因为在上面的学习中,我们发现,宏不拘泥于某种特定类型
#define PRINT(n,format) printf("the value of " #n " is "format"\n", n)
int main()
{
int a = 10;
PRINT(a, "%d");//printf("the value of " "a" " is ""%d""\n", a);
float b = 20;
PRINT(b, "%f");//printf("the value of " "b" " is ""%f""\n", b);
double c = 2;
PRINT(c, "%f");//printf("the value of " "c" " is ""%f""\n", c);
return 0;
}
- ##的作用
##可以把位于它两边的符号合成一个符号。它允许宏定义从分离的文本片段创建标识符。
#define CAT(m,n) m##n
int main()
{
int value10 = 100;
printf("%d", CAT(value, 10));//printf("%d",value10);
return 0;
}
宏CAT的作用是将value和10连接起来成为一个符号value10
2.4 带副作用的宏参数
x+1;//不带副作用
x++;//带有副作用
当宏参数在宏的定义中出现超过一次的时候,如果参数带有副作用,那么你在使用这个宏的时候就可能出现危险,导致不可预测的后果。副作用就是表达式求值的时候出现的永久性效果
下面代码的结果是什么?
#define MAX(x,y) ((x)>(y)?(x):(y))
int main()
{
int a = 3;
int b = 5;
int c = MAX(a, b);//int c= ((a)>(b)?(a):(b))
printf("%d\n", c);
return 0;
}
这道题很简单,MAX是要找出a,b中较大的一个=b=5
那下面代码的结果呢?
#define MAX(x,y) ((x)>(y)?(x):(y))
int main()
{
int a = 3;
int b = 5;
int c = MAX(a++, b++);
//int c = (a++) > (b++) ? (a++) : (b++);
printf("%d\n", c);
printf("%d\n", a);
printf("%d\n", b);
return 0;
}
对MAX替换,先进行a++和b++的大小判断,因为都是后置++,所以先使用再自增,因为3<5,所以执行b++。执行b++之前,ab都自增,所以a=4,b=6。执行b++,后置++,先使用再自增,所以将未自增前的b赋值给c=6,b再自增=7
2.5 宏和函数对比
①
宏通常被应用于执行简单的运算。比如上面代码中的MAX,在两个数中找出较大的一个。其实我们也可以用函数来找出较大值,那为什么不用函数来完成这个任务?
原因有二:
1.用于调用函数和从函数返回的代码可能比实际执行这个小型计算工作所需要的时间更多。所以宏比函数在程序的规模和速度方面更胜一筹。
2.更为重要的是函数的参数必须声明为特定的类型,所以函数只能在类型合适的表达式上使用。而宏怎可以适用于整形、长整型、浮点型等可以用(> < ==)来比较的类型,宏是类型无关的。
②
宏的缺点:
1.每次使用宏的时候,一份宏定义的代码将插入到程序中。除非宏比较短,否则可能大幅度增加程序的长度。
2. 宏是不方便调试的。
3. 宏由于类型无关,也就不够严谨。
4. 宏可能会带来运算符优先级的问题,导致程容易出现错
③
宏有时候可以做函数做不到的事情。比如:宏的参数可以出现类型,但是函数做不到
申请一块空间,用来存放4个整型
#define MALLOC(type,num) (type*)malloc(num*sizeof(type))
int main()
{
int* p = (int*)malloc(4 * sizeof(int));
int* a = MALLOC(int, 4);
return 0;
}
④
宏和函数的一个对比
2.6 命名约定
一般来讲函数的宏的使用语法很相似。所以语言本身没法帮我们区分二者。那我们平时的一个习惯是:
把宏名全部大写
函数名不要全部大写
3. #undef
这条指令用于移除一个宏定义。
#undef NAME
//如果现存的一个名字需要被重新定义,那么它的旧名字首先要被移除。
#define M 100
int main()
{
printf("%d\n", M);//100
#undef M
printf("%d\n", M);//报错
#define M 10
printf("%d\n", M);//10
return 0;
}
4. 命令行定义
许多C 的编译器提供了一种能力,允许在命令行中定义符号。用于启动编译过程。
例如:当我们根据同一个源文件要编译出不同的一个程序的不同版本的时候,这个特性有点用处。(假定某个程序中声明了一个某个长度的数组,如果机器内存有限,我们需要一个很小的数组,但是另外一个机器内存大写,我们需要一个数组能够大写。)
#include <stdio.h>
int main()
{
int array[ARRAY_SIZE];
int i = 0;
for (i = 0; i < ARRAY_SIZE; i++)
{
array[i] = i;
}
for (i = 0; i < ARRAY_SIZE; i++)
{
printf("%d ", array[i]);
}
printf("\n");
return 0;
}
编译指令:
linux 环境演示
gcc -D ARRAY_SIZE=10 programe.c
5. 条件编译
在编译一个程序的时候我们如果要将一条语句(一组语句)编译或者放弃是很方便的。因为我们有条件编译指令。
常见的条件编译指令:
1.
#if 常量表达式
…
#endif
//常量表达式由预处理器求值。
如果if后面的常量表达式为真,则执行…的语句;若表达式为假,则不执行…的语句
#define M 2
int main()
{
#if M==2
printf("haha");
#endif
#if M==3
printf("hehe");
#endif
return 0;
}
M== 2为真,执行printf(“haha”);语句。M==3为假,不执行printf(“hehe”);语句。
- 多个分支的条件编译
#if 常量表达式
//…
#elif 常量表达式
//…
#else
//…
#endif
#define M 2
int main()
{
#if M==2
printf("haha");
#elif M==3
printf("hehe");
#else
printf("heihei");
#endif
return 0;
}
结果:打印haha
- 判断是否被定义
#if defined(symbol)
#ifdef symbol
#if !defined(symbol)
#ifndef symbol
int main()
{
#if defined(M)//判断是否定义了M,定义了就执行
printf("haha\n");
#endif
#ifdef M//判断是否定义了M,定义了就执行
printf("hehe\n");
#endif
#if !defined(M)//判断是否没有定义M,没有定义就执行
printf("heihei\n");
#endif
#ifndef M//判断是否没有定义M,没有定义就执行
printf("xixi\n");
#endif
return 0;
}
注意:
上面4条判断是否被定义的语句只是看是否被定义,对定义的值的真假不做判断
#define M 0
int main()
{
#if defined(M)
printf("haha\n");
#endif
return 0;
}
- 嵌套指令
6.文件包含
我们已经知道, #include 指令可以使另外一个文件被编译。就像它实际出现于 #include 指令的地方一样。
这种替换的方式很简单:
预处理器先删除这条指令,并用包含文件的内容替换。
这样一个源文件被包含10次,那就实际被编译10次。
6.1 头文件被包含的方式:
#include “filename”
查找策略:先在源文件所在目录下查找,如果该头文件未找到,编译器就像查找库函数头文件一样在标准位置查找头文件。如果找不到就提示编译错误。
#include <filename.h>
查找头文件直接去标准路径下去查找,如果找不到就提示编译错误.
这样是不是可以说,对于库文件也可以使用 “” 的形式包含?答案是肯定的,可以。
但是这样做查找的效率就低些,当然这样也不容易区分是库文件还是本地文件了
6.2 嵌套文件包含
如果出现这样的场景:
comm.h和comm.c是公共模块。
test1.h和test1.c使用了公共模块。
test2.h和test2.c使用了公共模块。
test.h和test.c使用了test1模块和test2模块。
这样最终程序中就会出现两份comm.h的内容。这样就造成了文件内容的重复
如何解决这个问题?
答案:条件编译。
每个头文件的开头写:
#ifndef _ _ TEST_H_ _
#define _ _ TEST_H_ _
//头文件的内容
#endif //_ _ TEST_H_ _
其中,_ _ TEST_H_ _是根据头文件的名字写的
或者:
#pragma once
用这两种方法,就可以避免头文件的重复引入。
好了,那么本篇博客就到此结束了,如果你觉得本篇博客对你有些帮助,可以给个大大的赞👍吗,感谢看到这里,我们下篇博客见❤️