【带你了解C语言预处理指令】
目录:
- 前言
- 一、#define
- (一)预定义符号
- (二)#define定义标识符
- (三)#define定义宏
- (四)#define替换规则
- (五)#和##
- 1.#
- 2.##
- (六)带副作用的宏参数
- (七)宏和函数的对比
- 1.宏的优点
- 2.宏的缺点
- 3.对比
- (八)命名约定
- (九)#undef
- (十)常见的条件编译指令
- 二、命令行定义
- 三、#include
- 1.本地文件包含
- 2.嵌套文件包含
- 3.头文件重复包含的处理
- 四、其他预处理指令
- 总结
前言
大家好,我们平时在写一个C语言程序时,大家第一行都写得是什么呢?
我想,大多数人都和我一样是:“#include<stdio.h>”。
那么这一行又是什么意思呢?
有很多小伙伴对他的认知都是“这是包含头文件 stdio.h,这样就可以使用scanf和printf了。”
是的,上面的理解的确是正确的,不过还不够全面,下面,
让我们一起开启今天的打怪升级之旅,一起领略不一样的程序世界!
一、#define
(一)预定义符号
_ _ FILE _ _ 显示文件名
_ _ LINE _ _ 显示行号
_ _ DATE _ _ 显示文件被编辑日期
_ _ TIME _ _ 显示文件被编辑时间
_ _ STDC _ _ 如果该编译器符合ANSIC,其值为1,否则未定义
上面这些为预定义符号,都是语言内置的可以直接使用。
示例:
int main()
{
printf("%s\n", __FILE__);
printf("%d\n", __LINE__);
printf("%s\n", __DATE__);
printf("%s\n", __TIME__);
//printf("%d\n", __STDC__);
return 0;
}
运行实例:
熊猫我是在VS上面实验的,说明VS并不完全满足标准c的语法。(例如:scanf_s)
(二)#define定义标识符
语法:#define name stuff
示栗:
#define EXT extern //为extern关键字定义一个更短的名字
#define MAX_NUM 100 //定义一个值为100的标识符
#define CASE break; case //在写case的时候自动把break写在后面
//如果定义的stuff过长,可以分成几行来写,此时除了最后一行外其他每行后面都需要加上反斜杠 '\'(续行符)。
#define PRINT printf("%d\n%s\n%s\n",__LINE__,\
__FILE__,\
__DATE__)
这里我们会有一个疑问:需不需要在#define后面加上分号?
我的建议是:不加,否则容易出错。
示例:
#define PRINT printf("%s\n", __TIME__);
int main()
{
int a=10;
if(a == 10)
PRINT;
else
printf("a=%d\n",a);
return 0;
}
运行实例:
(三)#define定义宏
在#define 机制中有一个规定,可以将参数替换到文本中,这里称为宏(macro)或者定义宏(define macro)。
宏的声明:
#define name(parament_list) stuff
注意:参数列表的左括号必须与name紧邻,
如果两者之间有任何空格,参数都将被解释为stuff的一部分。
示例:
#define MAX(x,y) (x)>(y)?(x):(y)
int main()
{
int a = 10;
int b = 20;
int c = MAX(a, b);
printf("max = %d\n", c);
return 0;
}
运行实例:
这里声明的宏会在预处理阶段进行替换,
我们看到的是 c = MAX(a,b);
实际上在预处理阶段会被替换为: c = (a)>(b)?(a):(b);
(四)#define替换规则
在程序中拓展#define定义符号和宏时,需要涉及几个步骤:
1.在调用宏时,首先对参数进行检查,看看是否包含任何由#define定义的字符,如果有,它们首先被替换。
2.替换文本随后被插入到程序中原来文本的位置。对于宏、参数名被他们的值所替换。
3.最后,再次对结果文件进行扫描,看看它是否被包含任何由#define定义的符号。如果是,就重复上述处理操作。
示例:
#define NUM 10
#define SUM(con) ((NUM)+(con))
#define MUL(x,y) ((x)*(y))
int main()
{
int a = 10;
int b = 20;
int c = MUL(SUM(a), b);
printf("c = %d\n", c);
return 0;
}
运行实例:
(五)#和##
1.#
如何把参数插入到字符串中?
在开始下面的内容之前我们先来看一下这个代码:
举个栗子:
int main()
{
printf("hello world\n");
printf("hello ""world\n");
return 0;
}
这个代码是正确的吗,如果正确那他打印出的结果是怎么样的?
运行实例:
通过实际操作我们发现,C语言可以实现字符串的自动连接,
当然前提是两个字符串之间只有空格类操作符,否则会当做两个字符串处理。
那么我们也就可以 定义下面的宏:
示例:
#define PRINT(formet, data) printf("the value is "formet"\n",data)
int main()
{
int a = 10;
int b = 20;
char str[20] = "hello world";
PRINT("%d", a);
PRINT("%d", b);
PRINT("%s", str);
return 0;
}
运行实例:
这里我们很顺利就打印了出来,不过,最后一个字符串用value表示好像不太贴切,
如果我们想要将用字符串“str”来替换“value”那又该怎么办?
示例:
#define PRINT(formet, data) printf("the "data" is "formet"\n",data)
int main()
{
int a = 10;
int b = 20;
char str[20] = "hello world";
PRINT("%d", a);
PRINT("%d", b);
PRINT("%s", str);
return 0;
}
运行实例:
这里直接将data插入显然是不行的,那么我们就要用到#这个特殊的字符,
它的作用就是将宏参数变成对应的字符串。
示例:
#define PRINT(formet, data) printf("the "#data" is "formet"\n",data)
int main()
{
int a = 10;
int b = 20;
char str[20] = "hello world";
PRINT("%d", a);
PRINT("%d", b);
PRINT("%s", str);
return 0;
}
运行实例:
看到这里我们不得不佩服C语言的设立者考虑的是多么的全面,体贴,这么一个小小的细节也可以给我们设计到。
2.##
上面讲了#是用来将宏参数转换为对应字符串的,那么接下来我们看一看另一个有趣的操作符##,
我们通过下面的例子来观察它的作用。
举个栗子:
#define ADD(value) \
a_##age += value;
int main()
{
int a_age = 19;
ADD(1);
printf("a_age = %d\n", a_age);
return 0;
}
运行实例:
我们可以看到 a_##age 拓展之后变成了 a_age,
正如我们所见,##将a_和age两个符号合成了一个符号,
他允许宏定义从分离的文本片段创建标识符。
需要注意的是这样连接产生的必须是一个合法的标识符,否则其结果未定义。
这个功能虽然看似比较鸡肘,但是,
熊猫我也确实没有找到合适的例子来和大家讲解,那么,我们现在就先知道##有这么个功能就足够了。
(六)带副作用的宏参数
当宏参数在宏的定义中不止出现一次,如果宏参数带有副作用,那么你在使用这个宏的时候就可能出现危险,导致不可预料的后果。(副作用就是表达式求值的时候出现永久性效果)
示例:
x+1;//不带副作用
x++;//带副作用
示例:
#define MAX(x,y) (x)>(y)?(x):(y)
int main()
{
int a = 1;
int b = 2;
int c = MAX(a++, b++);
printf("max = %d\n", c);
return 0;
}
运行实例:
这里宏参数的副作用就显现出来了,每个位置的参数都会进行替代,那么产生的副作用就会累计。
这里我们还需要注意的一点就是:写宏的时候不要省略括号!!
举个栗子:
#define MUL(x,y) x*y
int main()
{
int a = 3;
int b = 4;
int c = MUL(a, b);
int d = MUL(a + 1, b + 1);
printf("c = %d\n d = %d\n", c, d);
return 0;
}
运行实例:
正确的写法:
#define MUL(x,y) ((x)*(y))
int main()
{
int a = 3;
int b = 4;
int c = MUL(a, b);
int d = MUL(a + 1, b + 1);
printf("c = %d\n d = %d\n", c, d);
return 0;
}
运行实例:
注意:我们不仅要给x和y加上括号,最外侧的括号也同样不可省略。
举个栗子:
#define SUM(x,y) (x)+(y)
int main()
{
int a = 5;
int b = 5;
int c = 2 * SUM(a, b);
printf("c = %d\n", c);
return 0;
}
运行实例:
(七)宏和函数的对比
1.宏的优点
宏通常被用于执行小型运算,例如上面的计算最大值和两数求和等。
那么为什么不使用函数呢?
原因有二:
- 有时调用函数和函数返回值的过程执行的操作可能比实际执行这个小型计算更加耗时。
- 更为重要的一点是:函数需要定义参数类型,所以函数只能适用于类型合适的情况, 而宏参数是没有类型的,一个宏可以传入整形、浮点型、字符型等各种类型的数据。
2.宏的缺点
当然和函数比宏也有缺点:
- 每次使用宏的时候,一份宏定义的代码将被插入程序中。宏比较短则还好,如果代码很多,将会大大增加程序的长度。
- 宏无法进行调试。
- 由于宏没有类型限制所以也不够严谨。
- 宏可能会带来运算符优先级的问题,造成程序出错。
宏有时能做到函数做不到的事情,比如参数为类型名:
示例:
#define MALLOC(ty, num) (ty*)malloc(sizeof(ty)*(num))int* pa = MALLOC(int, 10);
3.对比
(八)命名约定
一般来说,函数和宏的使用方法很相似,所以语法本身无法帮我们区分两者,
我们平时的习惯是:
把宏名全部大写,
函数名不要全部大写
**补充:并非所以情况下都符合该命名约定,
eg:offsetof,这同样是一个宏,是用于求成员偏移量的。
**
(九)#undef
该指令用于移除一个宏定义
示例:
#define MAX 100
int main()
{
int a = MAX;
printf("a = %d\n", a);
//#undef MAX
// int b = MAX;
// printf("b = %d\n", b);
return 0;
}
运行实例:
(十)常见的条件编译指令
1.分支语句:
#if
#endif
示例:
#define MAX 100
int main()
{
int a = MAX;
printf("a = %d\n", a);
#undef MAX
#ifndef MAX
#define MAX 200
#endif
int b = MAX;
printf("b = %d\n", b);
return 0;
}
运行实例:
2.多分支语句:
#if
#elif
#else
#endif
示例:
#define MAX 100
#define MIN 0
int main()
{
int a = MAX;
printf("a = %d\n", a);
#ifdef MAX
#if defined(MIN)
int b = MIN;
printf("b = %d\n", b);
#else
printf("未定义MIN\n");
#endif//结束离它最近的#if指令
printf("定义的 MAX = %d\n",MAX);
#endif
return 0;
}
运行实例:
3.判断是否被定义
#ifdef
#if defined
#ifndef
#if !defined
示例:
同上一例
二、命令行定义
许多C 的编译器提供了一种能力,允许在命令行中定义符号。用于启动编译过程。
例如:当我们根据同一个源文件要编译出一个程序的不同版本的时候,这个特性有点用处。(假定某个
程序中声明了一个某个长度的数组,如果机器内存有限,我们需要一个很小的数组,但是另外一个机器
内存大些,我们需要一个数组能够大些。)
示例:
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
三、#include
#include是用于头文件的包含,使用#include指令可以使其他文件被编译。
它的替换方式为:
1.预处理器先删掉这行代码,并用包含文件的内容进行替换。
2.如果包含了10次#include<stdio.h>,stdio.h头文件中的内容就被编译10次。
1.本地文件包含
#include" "
查找策略:先从源文件所在目录下进行查找,如果该头文件未找到,编译器就会像查找库函数头文件一样进入标准位置查找头文件。
- LINUX环境的标准头文件位置: usr/include/
- windows环境的标准头文件位置: C:\Program Files (x86)\Microsoft Visual Studio 12.0\VC\include //这是VS2013的默认路径
2.嵌套文件包含
#include< >
查找策略:直接从库文件中进行查找,如果找不到就提示编译器报错。
补充:我们包含头文件的时候也可以使用双引号,
不过这样查找效率会变低,而且也不容易区分库文件和本地文件。
3.头文件重复包含的处理
1.在每个文件前后写上这些:
#ifndef STDIO_H
#define STDIO_H
//头文件的内容
#endif
示例:
#ifndef __STDIO_H__
#define __STDIO_H__
#include<stdio.h>
#endif
#ifndef __STDIO_H__
#define __STDIO_H__
#include<stdio.h>
#endif
或者在头文件中写上:
#pragma once
四、其他预处理指令
#error
#pragma
#pragma pack(2) //将系统默认对齐数改为2
#line
其他的内容大家感兴趣可以自行了解。
总结
以上就是我们预处理指令的全部内容,如果有什么疑问或者建议都可以在评论区留言,感谢大家的支持,欢迎来评论区一起探讨,大家的鼓励是继续更新的巨大动力。