一、预处理详解
1.预定义符号
符号 | 打印出 |
__FILE__
|
进行编译的源文件
|
__LINE__
|
文件当前的行号
|
__DATE__
|
文件被编译的日期
|
__TIME__
|
文件被编译的时间
|
__STDC__
|
如果编译器遵循ANSI C,其值为1,否则未定义
|
使用:
int main()
{
printf("%s\n", __FILE__);// '-'是两个
printf("%d\n", __LINE__);//%d
printf("%s\n", __DATE__);
printf("%s\n", __TIME__);
return 0;
}
作用:写日志
int main()
{
FILE* pf = fopen("date.txt", "w");
if (pf == NULL)
{
printf("%s\n", strerror(errno));
return 0;
}
fprintf(pf, "%s %d %s %s", __FILE__, __LINE__, __DATE__, __TIME__);
fclose(pf);
pf = NULL;
return 0;
}
需要注意的是:
1.
这些预定义符号都是语言内置的
2. 对于'__STDC__' , VS2022是不遵循ANSIC
2.#define
2.1 #define定义标识符
#define MM 100
#define lo long long
//如果定义过长,可以分成几行写,除了最后一行外,每行的后面都加一个反斜杠(续行符)
#define DATE_printf printf("%s %d %s %s\n",\
__FILE__,\
__LINE__,\
__DATE__,\
__TIME__);
int main()
{
printf("%d\n", MM);//MM即等于100
lo a = 10;//只是一种替换,写起来更简便,效果没什么不同,在预处理阶段替换
DATE_printf;
return 0;
}
************define在定义标识符时,最后不能加上;
#define MM 100;
int main()
{
int a = 0;
int b = 0;
if (a < 10)
a = MM;//这里替换后相当于 a = 100;; , 会出现语法上的错误
else
a = -10;
return 0;
}
2.2 #define定义宏
宏的申明方式:
#define name( parament-list ) stuff
注意点:
1.参数列表的左括号必须与name
紧邻。
2.如果两者之间有任何空白存在,参数列表就会被解释为
stuff的一部分
#define MAX(x,y) ((x)>(y)?(x):(y))
//在宏定义和宏定义表达式上,加括号,替换产生的表达式才好按照预想的次序进行求值。
int main()
{
int a = 10;
int b = 5;
int m = MAX(a, b);
return 0;
}
所以用于对数值表达式进行求值的宏定义都应该用这种方式加上括号,避免在使用宏时由于参数中
的操作符或邻近操作符之间不可预料的相互作用。
2.3#define 替换规则
1. 在调用宏时,首先对参数进行检查,看看是否包含任何由#define定义的符号。如果是,它们首先
被替换。
2. 替换文本随后被插入到程序中原来文本的位置。对于宏,参数名被他们的值所替换。
3. 最后,再次对结果文件进行扫描,看看它是否包含任何由#define定义的符号。如果是,就重复上
述处理过程。
注意:
1. 宏参数和#define 定义中可以出现其他#define定义的符号。但是对于宏,不能出现递归。
2. 当预处理器搜索#define定义的符号的时候,字符串常量的内容并不被搜索,如下:
#define MM 100
int main()
{
printf("MM \n");//字符串里的MM并不会被扫描
return 0;
}
2.4 #和##
字符串有自动连接的特点
int main()
{
printf("hello""world");//打印结果为helloworld
return 0;
}
# 的作用:把一个宏参数变成对应的字符串
#define PRINT(m,n) printf(" "#n" is "m"\n",n);
int main()
{
int i = 100;
PRINT("%d", i+3);
//处理成printf("i+3 is %d \n",i+3);
return 0;
}
最终输出结果:i+3 is 103
## 的作用:
可以把位于它两边的符号合成一个符号
它允许宏定义从分离的文本片段创建标识符
#define CAT(c,num) c##num
int main()
{
int class674 = 10000;
printf("%d\n", CAT(class, 674));//合并class674
//打印结果10000
return 0;
}
注意:这样的连接必须产生一个合法的标识符。否则其结果就是未定义的
2.5 带副作用的宏参数
当宏参数在宏的定义中出现超过一次的时候,如果参数带有副作用,那么你在使用这个宏的时候就可能出现危险,导致不可预测的后果。副作用就是表达式求值的时候出现的永久性效果。
#define MAX(x,y) ((x)>(y)?(x):(y))
int main()
{
int a = 10;
int b = 5;
int max = MAX(a++, b++);
//((a++)>(b++)?(a++):(b++)) ,先判断, a,b分别++一次 ,a大于b,则a又加加一次
//a++两次 , b++一次
printf("a=%d b=%d\n", a, b);
return 0;
}
输出结果: a=12,b=6
2.6 宏和函数的对比
宏通常被应用于执行简单的运算
原因:
1.用于调用函数和从函数返回的代码可能比实际执行这个小型计算工作所需要的时间更多。
所以宏比函数在程序的规模和速度方面更胜一筹。
2. 更为重要的是函数的参数必须声明为特定的类型。
所以函数只能在类型合适的表达式上使用。反之这个宏怎可以适用于整形、长整型、浮点型等可以
用于>来比较的类型。
宏是类型无关的。
宏的缺点:
1.每次使用宏的时候,一份宏定义的代码将插入到程序中。除非宏比较短,否则可能大幅度增加程序的长度。
2.宏是没法调试的。
3.宏由于类型无关,也就不够严谨。
4.宏可能会带来运算符优先级的问题,导致程容易出现错。
*******宏有时候可以做函数做不到的事情,比如:宏的参数可以出现类型,但是函数做不到。
#define MALLOC(num,type) (type*)malloc(num*sizeof(type))
int main()
{
int* p1 = (int*)malloc(10 * sizeof(int));
if (p1 = NULL)
{
return 0;
}
int* p2 = MALLOC(10, int);
//预处理器替换后: (int*)malloc(10*sizeof(int));
free(p2);
p2 = NULL;
free(p1);
p1 = NULL;
return 0;
}
属
性
|
#define
定义宏
|
函数
|
代
码
长
度
|
每次使用时,宏代码都会被插入到程序中。除了非常
小的宏之外,程序的长度会大幅度增长
|
函数代码只出现于一个地方;每
次使用这个函数时,都调用那个
地方的同一份代码
|
执
行
速
度
|
更快
|
存在函数的调用和返回的额外开销,
所以相对慢一些
|
操
作
符
优
先
级
|
宏参数的求值是在所有周围表达式的上下文环境里,除非加上括号,否则邻近操作符的优先级可能会产生不可预料的后果,所以建议宏在书写的时候多些括号。
|
函数参数只在函数调用的时候求值一次,它的结果值传递给函数。表达式的求值结果更容易预测。
|
带
有
副
作
用
的
参
数
|
参数可能被替换到宏体中的多个位置,所以带有副作用的参数求值可能会产生不可预料的结果。
|
函数参数只在传参的时候求值一次,结果更容易控制。
|
参
数
类
型
|
宏的参数与类型无关,只要对参数的操作是合法的,它就可以使用于任何参数类型
|
函数的参数是与类型有关的,如
果参数的类型不同,就需要不同
的函数,即使他们执行的任务是
不同的。
|
调
试
|
宏是不方便调试的
|
函数是可以逐语句调试的
|
递
归
|
宏是不能递归的
|
函数是可以递归的
|
2.7 函数是可以递归的
命名约定俗成的习惯:
1.把宏名全部大写
2.函数名不要全部大写
3.#undef
这条指令用于移除一个宏定义。
#define MAX(x,y) ((x)>(y)?(x):(y))
int main()
{
#undef MAX//这里移除了MAX
int a = 10;
int b = 5;
int m = MAX(a, b);//MAX在这里已经用不了
return 0;
}
4.命令行定义
许多C 的编译器提供了一种能力,允许在命令行中定义符号。用于启动编译过程。
例如:当我们根据同一个源文件要编译出不同的一个程序的不同版本的时候,这个特性有点用处。(假定某个程序中声明了一个某个长度的数组,如果机器内存有限,我们需要一个很小的数组,但是另外一个机器内存大写,我们需要一个数组能够大写。)
int main()
{
int MAX;
int arr[MAX] = { 0 };
//根据需求,在命令行中定义MAX的大小,例如需要的数组大则定义大些,需要小些就定义小些
int i = 0;
for (i = 0; i < MAX; i++)
{
arr[i] = i;
}
for (i = 0; i < MAX; i++)
{
printf("%d ", arr[i]);
}
printf("\n");
return 0;
}
5.条件编译
#define M 1
int main()
{
int n = 10;
#if M//与if else的用法差不多 , M判断为真即运行
printf("%d\n", n);
#endif //要有#endif结尾
return 0;
}
常见的条件编译指令:
1. if else
#define M 1
int main()
{
int n = 10;
#if M
printf("%d\n", n);
#elif
;
#endif //要有#endif结尾
return 0;
}
2. 多个分支的条件编译
#define M 150
int main()
{
#if M==100
printf("%d\n", M);
#elif M<100
printf("%d\n", M);
#else
printf("%d\n", M);
#endif //要有#endif结束
return 0;
}
3.判断是否被定义
int main()
{
#if defined(M)//如果定义了就会是真,打印hehe
printf("hehe\n");
#endif
#if !defined(M)//这里的M未定义了,反逻辑为真,打印haha
printf("haha\n");
#endif //用#endif结尾
return 0;
}
6.文件包括
1.头文件被包含的方式
本地文件包括:
#include "filename"
库文件包括:
#include <filename.h>
对于库文件也可以使用 “” 的形式包含,但是这样做查找的效率就低些,而且这样也不容易区分是库文件还是本地文件了。
2.嵌套文件包含
#include <stdio.h>
#include <stdio.h>
#include <stdio.h>
int main()
{
return 0;
}
这样最终程序中就会出现三份stdio.h的内容。这样就造成了文件内容的重复。
为了解决这个问题,每个头文件都应该写:
#ifndef _TESH_H//判断是否定义_TEST_H,如果未定义则为真,进入
#define _TEST_H
#endif
或者
#pragma once