在上一篇博客,我们了解了一个程序是是怎么运行起来的,一个程序的运行过程,是由编译环境和运行环境组成的,而编译环境分为编译和链接两个阶段,编译的第一步就是预处理,这个过程其实还有很多我们需要了解的知识,本篇博客将会对此进行讲解。
其实在C语言中给我们提供了很多预定义的符号,这些符号代表着某些值,这里举出几个例子
__FILE__//进行编译的源文件
__LINE__//文件当前的行号
__DATE__//文件被编译的日期
__TIME__//文件被编译的时间
这些符号是语言内置的,我们直接可以使用的。
int main()
{
int i = 0;
printf("file:%s line:%d date:%s time:%s i=%d\n", __FILE__, __LINE__, __DATE__, __TIME__, i);
}
可以看到我们的源文件绝对路径,行号,以及日期时间。
这只是预定义符号的一部分,感兴趣可以百度完整的。
在这里提供一个思路,就是我们可以利用这些预处理符号和文件操作弄一个日志,把重要的状态信息保存下来,方便查看:
#define _CRT_SECURE_NO_WARNINGS
#include
int main()
{
int i = 0;
FILE* pf = fopen("sts.txt", "a");
if (pf == NULL)
{
return 1;
}
for (i = 0; i < 10; ++i)
{
fprintf(pf,"file:%s line:%d date:%s time:%s i=%d\n", __FILE__, __LINE__, __DATE__, __TIME__, i);
}
fclose(pf);
pf = NULL;
return 0;
}
#define是预处理中很重要的一个指令,而且我们平时也很常用,常用的功能主要有两个,在这里详细的介绍一下:
1.#define定义标识符
这也是我们在初学阶段经常使用的功能,语法是这样的:
#define name stuff
#define 后一个空格 再加上标识符的名字 后再加一个表达式,系统就会在预处理阶段把标识符替换成表达式,要注意的是,由于替换的时候只替换了表达式,所以容易出现各种问题,下面我们先看看标识符的使用:
#include
#define N 10
int main()
{
int a = N;
return 0;
}
之前我们说啊,预处理之后,这#define定义的标识符都会被替换,现在我们有办法查看预处理之后的文件了:
只需把预处理器的预处理到文件改成是,我们就可以看到test.i文件了
能看到啊,经过预处理之后直接多了一万行代码,那都是stdio提供的,我们不用管,主要看从 int main开始的这几行,之前的#define已经消失了,a=N,N也被替换成10了。当然,标识符的表达式可以不是一个值,也可以是一个算式,一个字符串,反正你放进去什么,最后你的标识符都会原封不动的被替换成表达式。
2.#define定义宏
#define定义宏,这个有点类似函数啊,先看看他的语法:
#define name(parament-list) stuff
在这里解释一下啊,parament-list 是一个由逗号分隔开的符号表,这个符号表可以应用在stuff这个表达中,例如
#define sqare(a) ((a)*(a))
这时候a就类似于一个函数,能求平方的函数:
大家可能有点疑惑啊,就是我为什么要加这么多括号,有啥用呢?
在这里提醒大家,在宏定义中千万不要吝括号,因为#define是直接把你给的表达式打印出来,那在遇到乘法时就有可能出现问题,举个例子:
如果我们现在传的是N,进去就是b+1,原封不动的放到sqare中去,
((b+1)*(b+1)) = 121
,这是正确的对吧,那如果我们去掉括号呢?
那就是b+1*b+1 = 21
所以啊,无论是宏定义,或者是定义标识符,都不要吝啬括号,不然会出现我们预料不到的问题。
1.在调用宏的是,先检查参数,看看参数有没有标识符,有标识符先替换。
2.替换文本(也就是stuff中的表达式)在参数检查之后被插入到宏或者标识符被使用的位置,原封不动的替换。
3.再次检查,看看有没有遗漏的宏和标识符,如果有,重复流程。
这三条规则都挺好理解的,这里就不多说了,主要说一些需要注意的地方:
1)宏不能出现递归调用,虽然宏中可以出现标识符,但是宏中不能有宏。
2)当预处理器搜索#define定义的符号时,字符串常量并不被搜索。
在这里解释一下第二个,举个例子大家就明白了。
看这样一段代码,N是我们用#define定义的标识符,我们在printf中也打印N,如果他能被检测那就会出现 11 11 11 11 11 21,不能就是 N N N N N 21
在讲这个之前,我们需要先补充一些知识储备:字符串是有自动连接的特点的,这是什么意思呢?
printf("hello world!");
printf("hello"" world!");
这两行代码,打印的结果都是一样的:
这就证明了,字符串是有自动连接的特点的,无论我们分成多少段给他,他都会整合成一个字符串。
那我们现在就可以玩一些比较有意思的了,在这里向大家提出一个问题:
我想封装一个函数,传入a就打印 a = a的值,传入b就打印b = b的值,看起来似乎很简单:
#define _CRT_SECURE_NO_WARNINGS
#include
void print(int a)
{
printf("a = %d\n", a);
}
int main()
{
int b=10;
int a = 22;
print(a);
print(b);
return 0;
}
但是这个函数有一个问题,无论我们传入的参数是谁,他都显示的是 a = 多少,那有没有办法让显示的跟着传入的参数改变呢?当然是有办法的,这里我们要介绍#的用法
#define _CRT_SECURE_NO_WARNINGS
#include
#define print(format,value) printf(#value " is " format"\n",value)
int main()
{
int b=10;
int a = 22;
print("%d",a);
print("%d",b);
return 0;
}
看一下这串代码,我们用宏定义的方式写了一个输出,format是格式,表示你传入的是什么类型的数据,value是你传入数据的值,#value,这个放在宏定义里面,把一个宏定义的参数,变成其对应的字符串,而不是该参数对应的值,所以他能实现一件事,就是传入a就打印a,传入b就打印b。
那把这个printf中的语句翻译一下,就是 “参数名”" is "参数类型 “\n” ,注意这个参数类型我没有给双引号,因为给了双引号他就是一个字符串了,不能被宏识别,所以我们要自己输入对应的格式。
还有一个##符号,他的作用是把位于他两边的符号构成一个符号,例如
sum##num,num是我们传入的可变的参数,这样num是1传入的就是sum1
2就是sum2,这里再举个例子:
sum会和num连接在一起形成一个符号,传入1自然就是sum1。
先说说副作用是啥意思,就是你在调用这个参数的时候,参数发生了不可逆的改变,例如a++,++a这样的,这时候可能引起一些出乎意料的结果,举个例子:
#define MAX(a,b) ((a)>(b)?(a):(b))
int main()
{
int a = 1;
int b = 2;
int c = MAX(a++, b++);
printf("%d %d %d\n", a, b, c);
return 0;
}
就像这样的代码,我们原本是为了求a,b中的最大值,然后给c,a,b,c的值应该是1 2 2,可是我们传入的不是a和b是a++和b++,这是带有副作用的参数,如果预处理完,表达式就应该是这样的:((a++)>(b++)?(a++):(b++))
,首先判断的时候,a,b ++就已经执行了依次,然后由于a 既然提到了函数,那我们就说一说函数与宏的区别:
先拿个例子说,就像刚才我们写的求最大值的宏函数,为什么用宏不用函数呢?
原因有二:
第一点,调用函数,返回值,其中还要占用空间,这比直接使用都复杂,所以第一点就是宏更快,更少的占用空间。
第二点,我们可以发现宏只是负责打印,和我们传入什么没有关系,所以所以宏并不限定类型,这点是十分重要的。
在这里总结了一些宏和函数的区别,大家有兴趣可以看看。
#define是一个很方便且拥有很多功能的指令,希望大家能够熟练运用,并且区分宏和函数的区别,什么时候用宏,什么时候用函数,要有感觉。