1 函数与宏
看下面这两段代码有什么区别与联系。
上面这两段代码的功能都是将指针 p 所指的数组元素置为 0,一个是用宏实现的,一个用函数实现的。
宏与函数的区别:
- 宏是由预处理期直接替换展开的,编译器不知道宏的存在,函数是由编译器直接编译的实体,调用行为由编译器决定
- 多次使用宏会导致最终可执行程序的体积增大
- 函数是跳转执行的,内存中只有一份函数体存在
- 宏的效率比函数高,因为宏是直接展开,无调用开销
- 函数调用时会创建活动记录,有入栈出栈操作,效率不如宏
实例分析:
// 46-1.c
#include<stdio.h>
#define RESET(p, len) \
while(len > 0) \
((char*)p)[--len] = 0
void reset(void* p, int len)
{
while(len > 0)
((char*)p)[--len] = 0;
}
int main()
{
int array[] = {1, 2, 3, 4, 5};
int len = sizeof(array);
int i = 0;
RESET(array, len);
for (i = 0; i < 5; i++)
{
printf("array[%d] = %d\n", i, array[i]);
}
return 0;
}
上面的代码很简单,就是将数组 array 中每个字节的数据都置为0,然后打印数组元素。将第 17 行的 RESET(array, len); 换成 reset(array, len); 效果是一样的。
如果将改成 RESET(5, len); 重新编译运行:
$ gcc 46-1.c -o 46-1
$ ./46-1
段错误 (核心已转储)
编译器没有给出任何警告,地址为 5 的空间是不能访问的,所以运行结果为段错误,由于宏定义没有参数的类型检查,所以编译时编译器没有给出任何编译或者警告。
将第 17 行改成 reset(5, len); 重新编译:
$ gcc 46-1.c -o 46-1
46-1.c: In function ‘main’:
46-1.c:17:11: warning: passing argument 1 of ‘reset’ makes pointer from integer without a cast [-Wint-conversion]
reset(5, len);
^
46-1.c:7:6: note: expected ‘void *’ but argument is of type ‘int’
void reset(void* p, int len)
^~~~~
编译器给出了警告。
宏没有类型检查,函数有类型检查,所以函数比宏更加安全。
1.1 宏的副作用
- 宏的效率比函数高,但是其副作用巨大
- 宏时文本替换,参数无法进行类型检查
- 可以用函数完成的功能绝对不用宏
- 宏的定义中不能出现递归定义
实例分析:宏的副作用
// 46-2.c
#include<stdio.h>
#define _ADD_(a, b) a + b
#define _MUL_(a, b) a * b
#define _MIN_(a, b) ((a) < (b) ? (a) : (b))
int main()
{
int i = 1;
int j = 10;
printf("%d\n", _MUL_(_ADD_(1, 2), _ADD_(3, 4)));
printf("%d\n", _MIN_(i++, j));
return 0;
}
上面的代码从字面意思分析,第一条打印语句 _MUL_(_ADD_(1, 2), _ADD_(3, 4)) 想求解的是(1+2)*(3+4),结果应该是 21
第二条打印语句 _MIN_(i++, j) 想求解的是,i++ 和 j 的最小值,结果应该是 1。
下面编译运行:
$ gcc 46-2.c -o 46-2
$ ./46-2
11
2
可以看到结果和我们想象的不一样,下面单步编译一下:
$ gcc -E 46-2.c -o 46-2.i
打开文件 46-2.i
# 1 "46-2.c"
# 1 "<built-in>"
# 1 "<command-line>"
# 31 "<command-line>"
# 1 "/usr/include/stdc-predef.h" 1 3 4
# 32 "<command-line>" 2
# 1 "46-2.c"
int main()
{
int i = 1;
int j = 10;
printf("%d\n", 1 + 2 * 3 + 4);
printf("%d\n", ((i++) < (j) ? (i++) : (j)));
return 0;
}
可以看到宏定义直接被文本替换了, _MUL_(_ADD_(1, 2), _ADD_(3, 4)) 被替换为 1 + 2 * 3 + 4,结果为 11。
_MIN_(i++, j) 被替换为 (i++) < (j) ? (i++) : (j),结果为 2。
上面使用函数绝对不会出现这样的情况,这就是宏定义的副作用,使用时要注意文本直接展开之后可能会出现和最初定义的含义不一样的情况。
1.2 宏的妙用
使用宏可以封装函数,可以将参数的类型作为宏参数,这是函数办不到的
下面看一个实例分析:
// 46-3.c
#include<stdio.h>
#include<malloc.h>
#define MALLOC(type, x) (type*)malloc(sizeof(type) * x)
#define FREE(p) (free(p), p = NULL)
#define LOG_INT(i) printf("%s = %d\n", #i, i)
#define LOG_CHAR(c) printf("%s = #c\n", #c, c)
#define LOG_FLOAT(f) printf("%s = %f\n", #f, f)
#define LOG_POINTER(p) printf("%s = %p\n", #p, p)
#define LOG_STRING(s) printf("%s = %s\n", #s, s)
#define FOREACH(i, n) while(1) { int i = 0, l = n; for (i = 0; i < l; i++)
#define BEGIN {
#define END } break; }
int main()
{
int* pi = MALLOC(int, 5);
char* str = "Hello World!";
LOG_STRING(str);
LOG_POINTER(pi);
FOREACH(k, 5)
BEGIN
pi[k] = k + 1;
END
FOREACH(n, 5)
BEGIN
int value = pi[n];
LOG_INT(value);
END
FREE(pi);
LOG_POINTER(pi);
return 0;
}
- 第 4 行将参数类型作为宏的参数,我们可以使用这个宏申请不同参数类型的空间。第 5 行在释放空间时,同时将将指针置为 NULL,避免了野指针
- 函数第 7 行到第 11 行,将变量的名称打印出来,同时打印变量的值。
- 第13 行到 15 行提供了一种遍历的方式,看不懂没关系,后面将展开讲解
- 第 18 行将 int 作为参数,申请了 5 个 int 空间。
- 第 20,21 行将变量的名称及变量的值打印出来。
- 第 22 至 25 行,对申请的空间遍历,并赋值
- 第 27 至 31 行,遍历取出数组中的值并连通变量名打印出来。
编译运行结果如下:
$ gcc 46-3.c -o 46-3
$ ./46-3
str = Hello World!
pi = 0x55ec77ccd260
value = 1
value = 2
value = 3
value = 4
value = 5
pi = (nil)
对上面的代码是不是还是不太明白呢,下面我们将第 2,3 行注释(为了避免不必要的信息),单步编译:
$ gcc -E 46-3.c -o 46-3.i
打开文件 46-3.i,如下:
# 1 "46-3.c"
# 1 "<built-in>"
# 1 "<command-line>"
# 31 "<command-line>"
# 1 "/usr/include/stdc-predef.h" 1 3 4
# 32 "<command-line>" 2
# 1 "46-3.c"
# 16 "46-3.c"
int main()
{
int* pi = (int*)malloc(sizeof(int) * 5);
char* str = "Hello World!";
printf("%s = %s\n", "str", str);
printf("%s = %p\n", "pi", pi);
while(1) { int k = 0, l = 5; for (k = 0; k < l; k++)
{
pi[k] = k + 1;
} break; }
while(1) { int n = 0, l = 5; for (n = 0; n < l; n++)
{
int value = pi[n];
printf("%s = %d\n", "value", value);
} break; }
(free(pi), pi = NULL);
printf("%s = %p\n", "pi", pi);
return 0;
}
可以看到,语句 LOG_STRING(str); 被展开成 printf("%s = %s\n", “str”, str);
第 22 至 25 行展开后如下所示:
while(1)
{
int k = 0, l = 5;
for (k = 0; k < l; k++)
{
pi[k] = k + 1;
}
break;
}
第 27 至 31 行同理,展开之后和我们平时写的代码完全一样,只不过通过宏使得代码变得简洁了。
2 小结
1、宏和函数并不是竞争对手
2、宏能接受任何类型的参数(包括参数的类型),效率高,容易出错
3、函数的参数必须时固定类型,效率低,不易出错
4、宏可以实现函数不能实现的功能