一、 预定义符号
C语言设置了一些预定义符号,可以直接使用,预定义符号也是在预处理期间处理的。
FILE // 进行编译的源文件
LINE // 文件当前的行号
DATE // 文件被编译的日期
TIME // 文件被编译的时间
STDC // 如果编译器遵循 ANSI C ,其值为 1 ,否则未定义
举个例子:
printf("file:%s line:%d\n", FILE, LINE);
结果为:
file:D:\code\c-c-2\20260312\20260312\Test.c line:3
二、 #define 定义常量
基本语法:
#define name stuff
举个例子:
#define MAX 1000
#define reg register //为register这个关键字,创建一个简短的名字
#define do_forever for(;;) //用更形象的符号来替换一种实现
#define CASE break;case //在写case语句的时候自动把break写上。
// 如果定义的stuff过长,可以分成几行写,除了最后一行外,每行的后⾯都加一个反斜杠(续行符)
#define DEBUG_PRINT printf("file:%s\tline:%d\t \date:%s\ttime:%s\n" ,\FILE,LINE , \ DATE, TIME)
思考:在define定义标识符的时候,要不要在最后加上;?
比如:
#define MAX 1000;
#define MAX 1000
建议不要加上;,这样容易导致问题。
比如下面的场景:
if (condition)
max = MAX;
else
max = 0;
如果是加了分号的情况,等替换后,if和else之间就是2条语句,而没有大括号的时候,if后边只能有一条语句。这里会出现语法错误。
三、#define定义宏
#define 机制包括了一个规定,允许把参数替换到文本中,这种实现通常称为宏(macro)或定义宏 (definemacro)。 下⾯是宏的申明方式:
#define name( parament-list ) stuff
其中的 parament-list 是一个由逗号隔开的符号表,它们可能出现在stuff中。
注意:
参数列表的左括号必须与name紧邻,如果两者之间有任何空白存在,参数列表就会被解释为stuff的一部分。
举例:
#define SQUARE( x ) x * x
这个宏接收一个参数 x 如果在上述声明之后,你把 SQUARE(5); 置于程序中,预处理器就会用下⾯这个表达式替换上⾯的表达式: 5*5
警告: 这个宏存在一个问题: 观察下面的代码段:
int a = 5;
printf("%d\n" ,SQUARE( a + 1) );
乍一看,你可能觉得这段代码将打印36,事实上它将打印11,为什么呢? 替换文本时,参数x被替换成a+1,所以这条语句实际上变成了:
printf ("%d\n",a + 1 * a + 1 );
这样就较比清晰了,由替换产⽣的表达式并没有按照预想的次序进行求值。
在宏定义上加上两个括号,这个问题便轻松的解决了:
#define SQUARE(x) (x) * (x)
这样预处理之后就产生了预期的效果:
printf ("%d\n",(a + 1) * (a + 1) );
这里还有一个宏定义:
#define DOUBLE(x) (x) + (x)
定义中我们使用了括号,想避免之前的问题,但是这个宏可能会出现新的错误。
int a = 5;
printf("%d\n" ,10 * DOUBLE(a));
这将打印什么值呢?
看上去,好像打印100,但事实上打印的是55。我们发现替换之后:
printf ("%d\n",10 * (5) + (5));
乘法运算先于宏定义的加法,所以出现了 55 . 这个问题,的解决办法是在宏定义表达式两边加上一对括号就可以了。
#define DOUBLE( x) ( ( x ) + ( x ) )
提示:
所有用于对数值表达式进行求值的宏定义都应该用这种方式加上括号,避免在使用宏时由于参数中的操作符或邻近操作符之间不可预料的相互作用。
四、带有副作用的宏参数
当宏参数在宏的定义中出现超过一次的时候,如果参数带有副作用,那么你在使用这个宏的时候就可能出现危险,导致不可预测的后果。副作用就是表达式求值的时候出现的永久性效果。 例如:
x+1;// 不带副作用
x++;// 带有副作用
MAX宏可以证明具有副作用的参数所引起的问题。
#define MAX(a, b) ( (a) > (b) ? (a) : (b) )
...
x = 5;
y = 8;
z = MAX(x++, y++);
printf("x=%d y=%d z=%d\n", x, y, z);//输出的结果是什么?
这里我们得知道预处理器处理之后的结果是什么:
z = ( (x++) > (y++) ? (x++) : (y++));
所以输出的结果是:x=6y=10z=9
五、宏替换的规则
在程序中扩展#define定义符号和宏时,需要涉及几个步骤。
-
在调用宏时,首先对参数进行检查,看看是否包含任何由#define定义的符号。如果是,它们首先被替换。
-
替换文本随后被插入到程序中原来文本的位置。对于宏,参数名被他们的值所替换。
-
最后,再次对结果文件进行扫描,看看它是否包含任何由#define定义的符号。如果是,就重复上述处理过程。
注意:
-
宏参数和#define定义中可以出现其他#define定义的符号。但是对于宏,不能出现递归。
-
当预处理器搜索#define定义的符号的时候,字符串常量的内容并不被搜索。
六、宏函数的对比
宏通常被应用于执行简单的运算。 比如在两个数中找出较大的一个时,写成下面的宏,更有优势一些。
#define MAX(a, b) ((a)>(b)?(a):(b))
那为什么不用函数来完成这个任务?
原因有⼆:
-
用于调用函数和从函数返回的代码可能比实际执行这个小型计算工所需要的时间更多。所以宏比函数在程序的规模和速度方面更胜一筹。
-
更为重要的是函数的参数必须声明为特定的类型。所以函数只能在类型合适的表达式上使用。反之这个宏怎可以适用于整形、长整型、浮点型等可以用于 > 来比较的类型。宏的参数是类型无关的。
和函数相比宏的劣势:
-
每次使用宏的时候,一份宏定义的代码将插入到程序中。除非宏比较短,否则可能大幅度增加程序的长度。
-
宏是没法调试的。
-
宏由于类型无关,也就不够严谨。
-
宏可能会带来运算符优先级的问题,导致程容易出现错。
宏有时候可以做函数做不到的事情。比如:宏的参数可以出现类型,但是函数做不到。
#define MALLOC(num, type)\
(type )malloc(num sizeof(type))
...
//使⽤
MALLOC(10, int);//类型作为参数
//预处理器替换之后:
(int*)malloc(10 sizeof(int));
宏和函数的一个对比
|----------|------------------------------------------------------------------------|---------------------------------------------|
| 属性 | #define定义宏 | 函数 |
| 代码长度 | 每次使用时,宏代码都会被插入到程序中。除了非常小的宏之外,程序的长度会大幅度增长 | 函数代码只出现于一个地方;每次使用函数时,都调用那个地方的同一份代码 |
| 执行速度 | 更快 | 存在函数的调用和返回的额外开销,所以相对慢一些 |
| 操作符优先级 | 宏参数的求值是在所有周围表达式的上下文环境里,除非加上括号,否则邻近操作符的优先级可能会产生不可预料的后果,所以建议宏在书写的时候多写括号。 | 函数参数只在函数调用的时候求值一次的结果值传递给函数。表达式的求值结果容易预测。 |
| 带有副作用的参数 | 参数可能被替换到宏体中的多个位置,如果宏的参数被多次计算,带有副作用的参数求值可能会产生不可预料的结果。 | 函数参数只在传参的时候求值一次,结果容易控制。 |
| 参数类型 | 宏的参数与类型无关,只要对参数的操作是合法的,它就可以使用于任何参数类型。 | 函数的参数是与类型有关的,如果参数类型不同,就需要不同的函数,即使他们的任务是不同的。 |
| 调试 | 宏是不方便调试的 | 函数是可以逐语句调试的 |
| 递归 | 宏是不能递归的 | 函数是可以递归的 |
七、#和##
7.1 #运算符
#运算符将宏的一个参数转换为字符串字面量。它仅允许出现在带参数的宏的替换列表中。 #运算符所执行的操作可以理解为"字符串化"。 当我们有一个变量 int a = 10; 的时候,我们想打印出:the value of a is 10
就可以写:
#define PRINT(n) printf("the value of "#n " is %d", n);
当我们按照下面的方式调用的时候: PRINT(a);//当我们把a替换到宏的体内时,就出现了#a,而#a就是转换为"a",时一个字符串代码就会被预处理为:
printf("the value of ""a" " is %d", a);
#define PRINT(n) printf("the value of "#n " is %d", n);#include<stdio.h>
int main() {
int a = 10;
PRINT(a);
}
运行代码就能在屏幕上打印:
the value of a is 10
7.2 ##运算符
可以把位于它两边的符号合成一个符号,它允许宏定义从分离的文本片段创建标识符。## 被称为记号粘合。
这样的连接必须产生一个合法的标识符。否则其结果就是未定义的。 这⾥我们想想,写一个函数求2个数的较大值的时候,不同的数据类型就得写不同的函数。 比如:
int int_max(int x, int y)
{
return x > y ? x : y;
}
float float_max(float x, float y)
{
return x > y?x:y;
}
但是这样写起来太繁琐了,现在我们这样写代码试试:
//宏定义
#define GENERIC_MAX(type) \
type type##_max(type x, type y) { \
return (x > y ? x : y); \
}
使用宏,定义不同函数
GENERIC_MAX(int)//替换到宏体内后int##_max生成了新的符号int_max做函数名
GENERIC_MAX(float) //替换到宏体内后float##_max生成了新的符号float_max做函数名
int main()
{
//调用函数
int m = int_max(2, 3);
printf("%d\n", m);
float fm = float_max(3.5f, 4.5f);
printf("%f\n", fm);
return 0;
}
输出:
3
4.500000
在实际开发过程中##使用的很少,很难取出非常贴切的例子。