- 在ANSI C的标准中,任何一种实现都存在两个不同的环境:翻译环境和执行环境;
而预处理就是在翻译环境进行的,预处理也称为预编译
接下来就来详细讲讲预处理环节的细节 (为了方便观看代码,下面的对代码的解释和板书会采用全角输出的模式)
目录
一、预定义符号
c语言会内置预定义符号,这种标识符在c语言中没有特定的含义,可以用printf函数进行格式化输出。可作为用户标识符使用,让我们了解到当前文件更多的编译信息,但是使用不当也会使程序报错。
他们的功能如下:
二、#define
#define也是预处理指令的一种 ,c语言中可以用#define来定义标识符号和宏;
1、定义标识符号:
可以直接将对应的用户关键字替换为#define所定义的内容,遵循替换原则
(这个相比都不陌生)在写链表或者其他的程序的时候我们经常使用#define来定义字母,来让程序后期的维护更加的安全快捷。
首先我们可以想到的就是 用 #define去定义一个符号,例如:
#define Max 100 //用#define定义整形
#define exm "abcdefg" //用#define定义字符串
我们将其打印,结果如下:
(原则上,只要#define定义的内容符合代码语法规则,就可以定义,例如整形,字符串,甚至是一段代码)
2、定义宏
#define允许把参数替换到文本当中,这种实现通常称为宏:
下面是宏申明的格式
#difne name ( paramen_list ) stuff 其中parament_lsit 是用逗号隔开的参数列表
(需要注意的是,参数列表左边的括号必须和name紧邻,如果留有空白的话就变成了前面所讲的情况)
举个例子:例如我想求一下 一个int a 和 int b 两个整形的较大值
#include<stdio.h>
#define Max(x,y) (x>y?x:y)
int main()
{
int a=1;
int b=2;
int m=Max(a,b);
printf("%d\n",m);
return 0;
}
最后运行结果为 : 2.
上面提到define的替换性,可见,我们所定义的整形或者字符串在define看来是没有数据类型的;所以在定义宏的时候,不会直接将某些表达式的结果传入宏当中,而是将这个表达式整体传过去进行计算。如果是这种情况的话,就难免会产生表达式计算优先级的问题;
举个简单的例子:
#include<stdio.h>
#define CALC(x) x*x
int main()
{
int m = CALC( 6);
printf("%d\n", m);
return 0;
}
我们写一个这样的代码,我们传入6,那么用宏来计算之后的值必然是36;
---->但是如果,我们将b改成 3+3 ,结果又会是什么呢?结果如下:
虽然3+3的值也为6,但是传入的时候由于是整体代入,所以最终CALC(3+3)就被替换成了
: int m = 3 + 3 *3 + 3。这种优先级的问题也就体现出来了。
但是居然遇到这种问题,那必然就有解决的方法:
针对于优先级的问题,我们可以想到用括号来解决,例如将上述的 #define后面的内容改为
#define CALC(x) (x)*(x)
这样一来问题就解决了,但是如果我们经常使用#define定义宏之后,其实还会发现参数列表右括号右边的表达式,最好在最外层也带上括号:
#define CALC(x) ((x)*(x))
目前这个平方的宏暂时看不出来什么猫腻,因为这些代码一般都是内层替换的时候出现的问题;我们换个代码来看看:
#include<stdio.h>
#define JIA(x) (x)+(x)
int main()
{
printf("%d\n", 2 * JIA(2));
return 0;
}
按理来说JIA(2)的结果应该为4,但是打印后的结果确为》》6 ,不难分析,是#define将整体替换为了
printf ("%d\n", 2 * 2 + 2);
这就体现了在宏的外层加上括号的重要性(不管是外层还是内层!):
#include<stdio.h>
#define JIA(x) ((x)+(x))
int main()
{
printf("%d\n", 2 * JIA(2));
return 0;
}
3、#define替换规则
接下来详细了解一下#define的替换规则
1、在调用宏时,首先要对参数进行检查,查看是否包含#define定义的字符,如果是,它们首先被替换
2、替换文本随后会被插入到程序中原来的文本位置,对于宏,参数名被他们的值所替换。
3、最后再次对文件结果进行扫描,查看是否还包含#define定义的标识符,如果存在,就重复上述操作。
注意:
1、宏参数和#define定义中可以出现其他#define定义的字符,但是对于宏,不能出现递归
2、当预处理器搜索#define定义的符号的时候,字符串常量里的内容不被搜索,例如:
#define MAX 100
char arr[ 10 ] = "abcdefgxxxMAX"; //其中的MAX不被#define搜索和替换
4、预处理符号#与##
1、#
简单直入,在二、3的替换规则末尾,也就是上面两行内容中提到,#define不能搜索字符串常量里面的内容,那么问题来了,如果某一天需要去替换字符串里面的内容的时候该怎么办?
这就要说说#和##的作用了————将参数插入到字符串当中。
我们先来看看这样的一串代码:
#include<stdio.h>
int main()
{
printf("hello world\n");
printf("hello"" world");
return 0;
}
它的运行结果都为:hello world
我们可以看出来:两个紧邻的字符串最后合成了一个字符串。
其实在敲代码的时候,不免遇到这样的场景:有一些整形变量如下
int a = 1; int b = 3 ; int c=4.......
我们是否能在一个for循环内使用一个printf函数将如下内容打印下来:
the value of a is 1;
the value of b is 3;
the value of c is 4;
...........以此类推
如果不用for循环,用一个一个用printf函数打印的时候,过程不免冗杂:
int a =1; float b=2.3f; int c =3;
printf("the value of a is %d",a);
printf("the value of a is %f",b);
printf("the value of a is %d",c);
对于一个for循环,我们发现,一个函数时解决不了问题的,所以宏的重要性就体现出来了:
#define print(x,y) printf("the value of "#x" is "format"\n",x);
对上述内容进行一个解释:由于#define不能搜索字符串里面的内容,所以我们的printf里面的内容用多个字符串("the value of" #x " is "" format" "\n")替代,但是由前面的内容可以看到,如果x为我们上述的a,b,c等变量的时候,他就会把x替换为相应的值,而不是直接替换成用户变量(如果传入a,那么#x就会变成"a"),这个时候就要用到#了。
在这里#的作用就是直接将x对应的字符传过去,而不是值的替换,例如我们传入print(a,"%d"):
结果为: printf("the value of " "a" " is " "%d" "\n",x);
即: printf("the value of a is %d\n",x);
我们依次传入 print(变量名,数据类型);就能将其打印出来,这样就方便了很多。
2、##
##可以将位于它两边的符号合成一个符号,它允许宏定义从分离的文本片段创建标识符:
有个变量的名为 int jinitaimei =6;
#define cat(x,y) x##y
printf("%d\n",cat( ji , nitaimei ));
输出结果为6;
(标识符必须合法,否则会显示标识符未定义)
5、#define定义宏和函数对比
1、#define和函数的简单对比
从上面的例子不免看出来,越是复杂运算,宏所带来副作用越大,所以宏在非必要时一般都用来执行简单的计算,宏被用来实现量大但是计算过程简单的代码时非常适合不过的;
2、表格对比
属性 | #define | 函数 |
---|---|---|
代码长度 |
每次使用时,宏代码都会被插入到程序中。除了非常小的宏之外,程序的长度会大幅度增长
|
函数代码只出现于一个地方;每次使用这个函数时,都调用那个 地方的同一份代码
|
执行速度 | 更快 |
存在函数的调用和返回的额外开销,所以相对慢一些
|
操作符优先级 | 宏参数的求值是在所有周围表达式的上下文环境里,除非加上括号,否则邻近操作符的优先级可能会产生 不可预料的后果,所以建议宏在书写的时候多些括号。 | 函数参数只在函数调用的时候求值一次,它的结果值传递给函数。表达式的求值结果更容易预测 |
副作用参数 |
参数可能被替换到宏体中的多个位置,所以带有副作用的参数求值可能会产生不可预料的结果
|
函数参数只在传参的时候求值一次,结果更容易控制。
|
参数类型 |
宏的参数与类型无关,只要对参数的操作是合法的,它就可以使用于任何参数类型
|
函数的参数是与类型有关的,如果参数的类型不同,就需要不同的函数,即使他们执行的任务是
相同的
|
调试 | 不方便调试 | 可以逐语句调试 |
递归 | 不能递归 | 可以递归 |
三、#undef
用于移除#define定义的宏;我们来看这样的一个例子:
我们在上面引用define定义MAX 100 ,然后打印MAX,发现MAX可用,但是在接下来的一行使用了#undef MAX后,后面再去打印就显示语法错误----未定义标识符,可见在这个地方定义的MAX已经被移除了。
四、命令行定义
#include <stdio.h>
int main()
{
int arr [size];
int i = 0;
for(i = 0; i< size i ++)
arr[i] = i;
for(i = 0; i< size; i ++)
printf("%d " ,arr[i]);
printf("\n" );
return 0;
}
在编译的时候根据不同的需求,来命令size的大小,以确保程序在不同运行环境下的稳定性。
五、条件编译(选择性编译)
在编译的时候我们可以进行选择性编译,使用一下条件编译指令
1、单分支
#if 常量表达式
// .....
#endif
// .....
如:#define __DEBUG__ 1#if __DEBUG__//..#endif
2、多分支
#if 常量表达式//...#elif 常量表达式//...#else//...#endif
3.判断是否被定义#if defined(symbol)#ifdef symbol#if !defined(symbol)#ifndef symbol
4.嵌套指令#if defined(OS_UNIX)#ifdef OPTION1unix_version_option1 ();#endif#ifdef OPTION2unix_version_option2 ();#endif#elif defined(OS_MSDOS)#ifdef OPTION2msdos_version_option2 ();#endif#endif
单分支语句举例 运行结果为 0 1 2 3 4 5 6 7 8 9
#include <stdio.h>
#define __DEBUG__
int main()
{
int i = 0;
int arr[10] = {0};
for(i=0; i<10; i++)
{
arr[i] = i;
#ifdef __DEBUG__ //判断条件,如果表达式结果为1则参与编译
printf("%d\n", arr[i]);
#endif
}
return 0;
}
看上面这个例子,如果__DEBUG__被定义,则编译执行printf语句,否则不编译
多分支举例 运行结果为 3;
#include<stdio.h>
#include<stdlib.h>
#define MAX 100
int main()
{
#if 1>2
printf("1");
#elif 2>3
printf("2");
#elif 4>3
printf("3");
#endif
system("pause");
return 0;
}
判断是否被定义: 结果为MAX has been defined
#include<stdio.h>
#include<stdlib.h>
#define MAX 100
int main()
{
#if defined(MAX)
printf("MAX has been defined\n");
#endif
#if defined(min) //仅仅只看你是否定义,不会显示未定义标识符
printf("???");
#endif
system("pause");
return 0;
}
(也可以将#if defined (MAX)改为
#ifdef MAX 这两者是等价的)
此外,#ifndef MAX则等价于 #if !defined(MAX)
加了一个n和!,意思就变为了:如果MAX没定义就真,执行语句就参与编译
这里的嵌套使用和我们c语言中的if else if语句用法差不多,这里不再赘述。
六、头文件包含
我们知道,工程里面的源文件通过编译器转化为目标文件后,通过链接器和链接库 链接形成可执行程序。
- 预处理器先删除这条指令,并用包含文件的内容替换。
- 这样一个源文件被包含10次,那就实际被编译10次。
1、本地文件包含
#include"filename"
2、库文件包含
#include<filename.h>
七、嵌套文件包含
一个复杂的项目往往需要很多个公共文件模块来辅助完成,这样子就避免不了重复包含而造成问价内容重复的问题。
如何解决: 用我们前面所用到的条件编译。
即每个头文件开头和结尾分别加上
#ifndef __TEST_H__
#define __TEST_H__
//文件内容
#endif
或者
#pragma once //这种方法是最简洁的