C 语言可变参数实现原理总结
1. 代码
#include <stdio.h>
typedef char * va_list;
#define _INTSIZEOF(n) ((sizeof(n) + sizeof(int) - 1) & ~(sizeof(int)-1))
#define va_start(ap, v) (ap = (va_list)&v + _INTSIZEOF(v))
#define va_arg(ap, t) ( *( t * ) ((ap += _INTSIZEOF(t)) - _INTSIZEOF(t)) )
#define va_end(ap) (ap = (va_list)0)
void mprintf(char *, ...);
int main()
{
int i;
char *s;
mprintf("test start...\n");
i = 1;
s = "hello world!";
mprintf("i=%d, s=%s\n", i, s);
s = "second test!";
mprintf("s=%s\n", s);
}
void mprintf(char *fmt, ...)
{
char *p, *sval;
int ival;
va_list ap;
va_start(ap, fmt);
for (p=fmt; *p; p++)
{
if (*p != '%')
{
putchar(*p);
continue;
}
switch (*++p)
{
case 'd':
ival = va_arg(ap, int);
printf("%d", ival);
break;
case 's':
for ( sval=va_arg(ap, char *); *sval; *sval++)
putchar(*sval);
break;
default:
putchar(*p);
break;
}
}
va_end(ap);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
2. 宏定义 _INTSIZEOF(n)
代码:
#define _INTSIZEOF(n) ((sizeof(n) + sizeof(int) - 1) & ~(sizeof(int)-1))
作用:
将变量 n 的长度向上对齐为 int 类型长度的整数倍.
即:
如果 sizeof(int) = 4; sizeof(n) = 2, 则 _INTSIZEOF(n) = 4;
如果 sizeof(int) = 4; sizeof(n) = 6, 则 _INTSIZEOF(n) = 8;
sizeof()函数:
sizeof()函数返回变量(如: n),或变量类型(如: int)在内存中所占的字节(byte)数.
运算原理:
设 sizeof(int) = INT, sizeof(n) = N. 则: INT, N 皆为正整数.
则存在正整数 q, 非正整数 r, 使得:
N = (INT * q) + r
其中:
* : 表示相乘
r : -INT < r <= 0 (最大非正剩余)
所谓 N 按 INT 向上对齐, 就是取 INT * q
为了便于求得 INT * q 的值, 需要将 r 变为 最小非负剩余, 等号两边加 (INT - 1), 则:
N + INT - 1 = (INT * q) + (r + INT - 1)
此时:
-1 < r + INT - 1 <= INT - 1
等价于:
0 <= r + INT - 1 < INT (即: r + INT - 1 为最小非负剩余)
那么, 根据运算法则, INT * q = [(N + INT - 1)/INT] * INT ([]为取整)
INT 的值在系统中为 2 的方幂, 设为 INT = 2^m, 则除 INT, 即为二进制右移 m 位. 乘为左移 m 位. 此处先除 INT, 再取整, 再乘 INT, 相当于将最后m 个二进制位清零.
故:
INT - 1 相当于二进制 最后 m 位全部为 1, 其他为 0
~(INT - 1) 相当于二进制最后 m 位全部为 0, 其他为 1
(N+INT-1) & ( ~(INT-1) ) 相当于将 (N+INT-1) 的最后 m 位全部清零. 其他不变.
从而:
(N + INT - 1) & ( (~(INT-1)) ) 即为所求的对齐后的值.
对齐
现代计算机中内存空间都是按照byte划分的,从理论上讲似乎对任何类型的变量的访问可以从任何地址开始,但实际情况是在访问特定变量的时候经常在特定的内存地址访问,这就需要各类型数据按照一定的规则在空间上排列,而不是顺序的一个接一个的排放,这就是对齐。
对齐的作用和原因:各个硬件平台对存储空间的处理上有很大的不同。一些平台对某些特定类型的数据只能从某些特定地址开始存取。其他平台可能没有这种情况, 但是最常见的是如果不按照适合其平台的要求对数据存放进行对齐,会在存取效率上带来损失。比如有些平台每次读都是从偶地址开始,如果一个int型(假设为 32位)如果存放在偶地址开始的地方,那么一个读周期就可以读出,而如果存放在奇地址开始的地方,就可能会需要2个读周期,并对两次读出的结果的高低 字节进行拼凑才能得到该int数据。显然在读取效率上下降很多。这也是空间和时间的博弈。
3. 宏定义 va_start(ap, v)
代码:
#define va_start(ap, v) (ap = (va_list)&v + _INTSIZEOF(v))
作用:
将 ap 赋值为指向函数的第一个参数之后的下一个参数(即第一个可变参数)的地址.
详解:
v 为函数的第一个参数。如本例子中的: char * fmt
(va_list)&v => 变量 v 的地址, 并将此地址的指针类型转为(va_list)类型.
_INTSIZEOF(v) => 变量 v 的对齐后的长度.
函数的可变参数以堆栈的方式存储. 故此处将 ap 赋值为指向函数的第一个参数之后的下一个参数(即第一个可变参数)的地址.
4. 宏定义 va_arg(ap, v)
代码:
#define va_arg(ap, t) ( *( t * ) ((ap += _INTSIZEOF(t)) - _INTSIZEOF(t)) )
作用:
返回 ap 所指向的可变参数的值 (类型为 t), 然后将 ap 指向下一个可变参数首地址.
详解:
(ap += _INTSIZEOF(t))
ap 所指的地址增加类型 t 的对齐后的长度. 即指向下一个可变参数.
((ap += _INTSIZEOF(t)) - _INTSIZEOF(t))
ap 虽改变, 但仍需返回原 ap 所指向的值. 故再减掉 _INTSIZEOF(t)
(t *)
将指针类型转换为 t
* (t *)
取出转换后的指针所指向的值.
5. 宏定义 va_end(ap)
代码:
#define va_end(ap) (ap = (va_list)0)
作用:
将指针 ap 置为 va_list 类型的空指针.