编译一个C语言程序的第一步骤就是预处理阶段,这一阶段就是宏发挥作用的阶段。C预处理器在源代码编译之前对其进行一些文本性质的操作,主要任务包括删除注释、插入被#include进来的文件内容、定义和替换由#define 定义的符号以及确定代码部分内容是否根据条件编译(#if )来进行编译。”文本性质”的操作,就是指一段文本替换成另外一段文本,而不考虑其中任何的语义内容。宏仅仅是在C预处理阶段的一种文本替换工具,编译完之后对二进制代码不可见
C语言在对源程序进行编译之前,会先对一些特殊的预处理指令作解释(比如之前使用的#include文件包含指令),产生一个新的源程序(这个过程称为编译预处理),之后再进行通常的编译,为了区分预处理指令和一般的C语句,所有预处理指令都以符号“#”开头,并且结尾不用分号,预处理指令可以出现在程序的任何位置,它的作用范围是从它出现的位置到文件尾。习惯上我们尽可能将预处理指令写在源程序开头,这种情况下,它的作用范围就是整个源程序文件,C语言提供了多种预处理功能,如宏定义、文件包含、条件编译等。合理地使用预处理功能编写的程序便于阅读、修改、移植和调试,也有利于模块化程序设计。
注意点:
被定义为“宏”的标识符称为“宏名”。在编译预处理时,对程序中所有出现的“宏名”,都用宏定义中的字符串去代换,这称为“宏代换”或“宏展开”。宏定义是由源程序中的宏定义命令完成的。宏代换是由预处理程序自动完成的。在C语言中,“宏”分为有参数和无参数两种。##不带参数的宏定义:#define 标识符 字符串 其中的“#”表示这是一条预处理命令。凡是以“#”开头的均为预处理命令。“define”为宏定义命令。“标识符”为所定义的宏名。“字符串”可以是常数、表达式、格式串等。
#include
// 源程序中所有的宏名PI在编译预处理的时候都会被3.14所代替
#define PI 3.14
// 根据圆的半径计radius算周长
float girth(float radius) {
return 2 * PI *radius;
}
int main ()
{
float g = girth(2);
printf("周长为:%f", g);
return 0;
}
C语言允许宏带有参数。在宏定义中的参数称为形式参数,在宏调用中的参数称为实际参数。对带参数的宏,在调用中,不仅要宏展开,而且要用实参去代换形参#define 宏名(形参表) 字符串
// 第1行中定义了一个带有2个参数的宏average,
#define average(a, b) (a+b)/2
int main ()
{
// 第4行其实会被替换成:int a = (10 + 4)/2;,
int a = average(10, 4);
// 输出结果为:7是不是感觉这个宏有点像函数呢?
printf("平均值:%d", a);
return 0;
}
宏名和参数列表之间不能有空格,否则空格后面的所有字符串都作为替换的字符串.
我们知道函数虽然可以传递参数,但是却不能把类型作为参数传递,有时我们为了实现函数的复用性,就要使用STL模板,但是我们这个时候还有另外一种选择,就是写成宏函数
一个开辟内存的函数
#define Malloc(type,size) (type*)malloc(sizeof(type)*size)
这个时候,我们只有把类型,容量作为参数传递进行,就可以开辟各种类型的内存了
int *p=Malloc(int,100); //开辟int类型的内存
char *q=Malloc(char,100); //开辟字符类型的内存
传递数组
由数组作为函数参数传递时,会失去其数组特性,也就是无法使用sizeof()函数计算出数组的大小,比如我们写一个排序函数,排序时我们不仅需要知道数组的首地址,还需要知道数组的大小,但是仅仅把数组名作为参数传递时,无法得知其数组大小,这时我们的函数就需要传递第二个参数,也就是数组的大小,于是函数就要写成Sort(int *a,int size).但宏函数就可以解决这个问题
#define InsertSort(list)\
{\
int s=sizeof(list)/4;\
int i,j;\
for(i=2;i<=s;i++)\
{\
list[0]=list[i];\
for(j=i-1;list[j]>list[0];j--)\
list[j+1]=list[j];\
list[j+1]=list[0];\
}\
}
int main()
{
int num[]={0,2,5,7,3,1,8,0,8,22,57,56,74,18,99,34,31,55,41,12,9,4};
InsertSort(num);
for(int i=1;i<sizeof(num)/4;i++)
printf("%d ",num[i]);
return 0;
}
我们定义宏语句或者宏函数时不可能总是一条语句呀,那要是有很多条语句时怎么办?都写在一行吗?这样显然代码就不美观,可读性不好,所以有多条语句时,我们就在每行末尾(除了最后一行)加上"",代表换行的意思
#include"stdio.h"
#define Print printf("这是第1条语句\n");\
printf("这是第2条语句\n");\
printf("这是第3条语句\n")
#define Show(str1,str2,str3)\
{\
printf("%s\n",str1);\
printf("%s\n",str2);\
printf("%s\n",str3);\
}
"#"是“字符串化”的意思,将出现在宏定义中的#是把跟在后面的参数转换成一个字符串
#define Print(str)\
{\
printf(#str"的值是%d",str);\
}
“##”是一种分隔连接方式,它的作用是进行强制连接。, 简单来说就是类似字符串拼接的方式, 但是这种方式是可以执行的
#define TEXT_A "Hello, world!"
#define msg(x) puts( TEXT_ ## x )
msg(A);
无论标识符 A 是否定义为一个宏名称,预处理器会先将形参 x 替换成实参 A,然后进行记号粘贴。当这两个步骤做完后,结果如下:
puts( TEXT_A );
现在,因为 TEXT_A 是一个宏名称,后续的宏替换会生成下面的语句
puts( "Hello, world!" );
案例:
#define trace(x, format) printf(#x " = %" #format "\n", x)
#define trace2(i) trace(x##i, d)
int main(int argc, char* argv[])
{
int i = 1;
char *s = "Hello";
float y = 2.0;
trace(i, d); // 相当于 printf("i = %d\n", i)
trace(y, f); // 相当于 printf("y = %f\n", y)
trace(s, s); // 相当于 printf("s = %s\n", s)
int x1 = 1, x2 = 2, x3 = 3;
trace2(1); // 相当于 trace(x1, d)
trace2(2); // 相当于 trace(x2, d)
trace2(3); // 相当于 trace(x3, d)
return 0;
}
#define PRINT(fmt, ...) printf(# fmt, ##__VA_ARGS__)
#define SHOW_LIST(...) printf(# __VA_ARGS__)
int main(int argc, char* argv[])
{
SHOW_LIST(HELLO, 250, 3.14); //输出:HELLO, 250, 3.14
PRINT(Hello); //输出:Hello
return 0;
}

预处理指令使用注意事项
1)预处理功能是C语言特有的功能,它是在对源程序正式编译前由预处理程序完成的,程序员在程序中用预处理命令来调用这些功能。
2)宏定义可以带有参数,宏调用时是以实参代换形参,而不是“值传递”。
3)为了避免宏代换时发生错误,宏定义中的字符串应加括号,字符串中出现的形式参数两边也应加括号。
4)文件包含是预处理的一个重要功能,它可以用来把多个源文件连接成一个源文件进行编译,结果将生成一个目标文件。
5)条件编译允许只编译程序中满足条件的程序段,使生成的目标程序较短,从而减少了内存的开销并提高了程序的效率
6)使用预处理功能便于程序的修改、阅读、移植和调试,也便于实现模块化程序设计。

我们常用到的比较大小,宏定义:
#define MAX( a, b) ( (a) > (b) (a) : (b) )
自定义函数实现就是:
int max( int a, int b)
{
return (a > b a : b)
}
区别:
最直观的来讲,自定义的函数已经指定了类型,只能比较两个整数的大小,却不能再去比较浮点数的大小,或者两个ASCII码的大小;而宏定义是不会限定类型的,只要比较的类型一致即可。
自定义接口在程序运行时,会产生临时的堆空间,有临时的空间消耗,如果是递归的话,需要的临时栈空间可能更多;宏定义是在程序运行时,会将宏定义这段代码插入到程序中执行,会有额外的代码段。
自定义接口在调用时,实际的开销要比代码段大,规模更大; 而宏比自定义函数在程序的规模和速度方面,比自定义函数更胜一筹。
