系统程序员成长计划-像机器一样思考(一)
转载时请注明出处和作者联系方式
文章出处:http://www.limodev.cn/blog
作者联系方式:李先静 <xianjimli at hotmail dot com>
变参函数的实现原理
C语言要求函数调用者按照函数原型进行调用,如果调用参数与函数原型不一致,编译器就会发出警告。而变参函数的参数是不确定的,它允许同一个函数有 多种不同的参数组合,编译器不会对可变部分的参数做类型检查,因而在使用的时候拥有较大的灵活性(当然也容易出错)。本节我们将一起研究一下变参函数的实 现原理,先看一个例子程序:
o 使用变参函数,需要libc库支持,头文件stdarg.h里提供一些必要的宏定义。
#include <stdarg.h>
#include <stdio.h>
o 实现变参函数。
int accumlate(int nr, ...)
{
int i = 0;
int result = 0;
va_list arg = NULL;
va_start(arg, nr);
for(i = 0; i < nr; i++)
{
result += va_arg(arg, int);
}
va_end(arg);
return result;
}
变参函数可变参数用…来表示。这里accumlate把多个整数累加起来,参数的个数由 accumlate的第一个参数决定,然后返回计算结果。
o 调用变参函数。
int main(int argc, char* argv[])
{
printf("%d/n", accumlate(1, 100));
printf("%d/n", accumlate(2, 100, 200));
printf("%d/n", accumlate(3, 100, 200, 300));
return 0;
}
这里以三种方式调用了accumlate。
在上面的例子中,va_list/ va_start/ va_arg/ va_end几个宏搞定了所有的实现细节,它们是怎么实现的呢,我们先看看参数在内存里的布局:
用gdb调试上述程序,在函数accumlate里设置断点,然后显示nr相邻区域的数据:
Breakpoint 1, accumlate (nr=3) at varg.c:13
13 int i = 0;
(gdb) x /4w &nr
0xbf904440: 0×00000003 0×00000064 0×000000c8 0×0000012c
C语言函数调用时,参数按值传递,并从最后一个参数开始压栈。先压入最后一个参数,再压入倒数第二参数,最后压入第一个参数。由于栈是向下增长的,也就是先压入的参数放在高地址,后压入的参数放在低地址。所以这里:
地址 内容 说明
===============================================
0xbf904440 3 第一个参数
0xbf904444 100 第二个参数
0xbf904448 200 第三个参数
0xbf90444c 300 第四个参数
(这里的具体地址与运行环境有关,因操作系统而异)
这样一来,只要我们知道可变参数的前一个参数,就可以依次取出后面的参数了。
在前面的例子中,这两行代码让arg指向了第一个变参数:
va_list arg = NULL;
va_start(arg, nr);
va_list是一个指针,由于参数的类型是不定的,它可以指向任意类型的指针,我们这样定义它:
#define va_list void*
为了让arg指向第一个可变参数,可以通过nr的地址加上nr的数据类型大小就行了,我们这样定义 va_start:
#define va_start(arg, start) arg = (va_list)(((char*)&(start)) + sizeof(start))
在前面的例子中,这行代码让arg指向了下一个变参数:
result += va_arg(arg, int)
为了让arg指向下一个可变参数,可以通过当前可变参数的地址加上当前可变参数的数据类型大小就行了,我们这样定义 va_arg:
#define va_arg(arg, type) *(type*)arg; arg = (char*)arg + sizeof(type);
可变参数的实现原理简单吧,所以不用标准C的支持,我们也可以实现变参函数:
#include <stdio.h>
#define va_list void*
#define va_end(arg)
#define va_arg(arg, type) *(type*)arg; arg = (char*)arg + sizeof(type);
#define va_start(arg, start) arg = (va_list)(((char*)&(start)) + sizeof(start))
int accumlate(int nr, ...)
{
int i = 0;
int result = 0;
va_list arg = NULL;
va_start(arg, nr);
for(i = 0; i < nr; i++)
{
result += va_arg(arg, int);
}
va_end(arg);
return result;
}
int main(int argc, char* argv[])
{
printf("%d/n", accumlate(1, 100));
printf("%d/n", accumlate(2, 100, 200));
printf("%d/n", accumlate(3, 100, 200, 300));
return 0;
}
对于变参函数还需要说明几点:
1.编译器优化。
如果加了编译优化标志,参数可能是通过寄存器传递的,那参数在内存里的布局与前面所展示的就不一样了。这个不用担心,编译器会处理的,它会保存变参函数所有的参数到内存里。我们可以看下ARM平台上汇编代码:
00000000 <accumlate>:
0: e92d000f stmdb sp!, {r0, r1, r2, r3}
4: e59dc000 ldr ip, [sp]
8: e35c0000 cmp ip, #0 ; 0x0
c: d3a00000 movle r0, #0 ; 0x0
10: da000008 ble 38 <accumlate+0x38>
14: e3a00000 mov r0, #0 ; 0x0
18: e28d1008 add r1, sp, #8 ; 0x8
1c: e1a02000 mov r2, r0
20: e5113004 ldr r3, [r1, #-4]
24: e2822001 add r2, r2, #1 ; 0x1
28: e15c0002 cmp ip, r2
2c: e0800003 add r0, r0, r3
30: e2811004 add r1, r1, #4 ; 0x4
34: 1afffff9 bne 20 <accumlate+0x20>
38: e28dd010 add sp, sp, #16 ; 0x10
3c: e12fff1e bx lr
r0, r1, r2和r3 四个寄存器通常用来传递函数的前面四个参数,编译器在这里对此做了特殊处理,用stmdb sp!, {r0, r1, r2, r3}这条指令把r0到r3的四个寄存器的值保存到内存里了。
2.关于参数结束标识的问题。
变参函数的参数个数是变化的,怎么知道实际参数的个数呢?通常的做法有三种:
o 指定参数的个数。比如这里 accumlate的第一个参数指明了参数的个数。
o 用固定值(如-1或NULL)表示最后一个参数。
o 用格式化字符串。比如printf使用了格式化字符串。
3.变参函数至少要提供一个参数。
这很明显,没有可变参数前面的一个参数,我们是无法取得第一个可变参数的地址的。