用过 printf 的同学都知道,C 语言支持可变参数作为函数的入参,但可变参数是如何实现的呢?简单来说,C 语言通过栈传递参数(其实也有可能通过寄存器,加快访问速度),那么通过约定的格式化字符串,依次从栈中取出各个参数即可,取出参数的正确性和数目仅仅依赖于格式化字符串的内容,属于一种“君子协定”。实际如何取,取了多少,完全由程序员决定,且对造成的结果负责 :)。

下面是一段示例代码,模拟了一种简化的 var_arg 宏的实现(实际会更复杂,不同的平台也不相同)。这里用一个结构体模拟了栈中的数据,也即函数的若干个“参数”,通过 var_arg 宏依次取出这些参数,并打印出来。

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
#include <stdio.h>

typedef struct __attribute__((packed)) {
char CH;
short INT;
long LONG;
float FLT;
double DBL;
char *STR;
} ARGS_STRUCT;


/* https://blog.nelhage.com/2010/10/amd64-and-va_arg/ */
#define FAKE_VAR_ARG(list, type) ((type *)(list = (char *)list + sizeof(type)))[-1]

int main(int argc, char *argv[])
{
ARGS_STRUCT fake_args;
char *p = NULL;

char c = 0;
short s = 0;
long l = 0;
float f = 0.0f;
double d = 0;
char *str = NULL;

fake_args.CH = 'A';
fake_args.INT = 6666;
fake_args.LONG = 12345678;
fake_args.FLT = 3.14f;
fake_args.DBL = 0.61828;
fake_args.STR = "\"HELLO ARGS!\"\r\n";

printf("sizeof(char) %d sizeof(short) %d sizeof(long) %d sizeof(float) %d sizeof(double) %d sizeof(char *) %d\r\n", \
sizeof(char), sizeof(short), sizeof(long), sizeof(float), sizeof(double), sizeof(char *));

printf("sizeof(fake_args): %d fake_args.CH %c fake_args.INT %d fake_args.LONG %ld fake_args.FLT %.2f fake_args.DBL %.6f fake_args.STR %s\r\n", \
sizeof(fake_args), fake_args.CH, fake_args.INT, fake_args.LONG, fake_args.FLT, fake_args.DBL, fake_args.STR);


p = (char *)&fake_args;
c = FAKE_VAR_ARG(p, char);
s = FAKE_VAR_ARG(p, short);
l = FAKE_VAR_ARG(p, long);
f = FAKE_VAR_ARG(p, float);
d = FAKE_VAR_ARG(p, double);
str = FAKE_VAR_ARG(p, char *);

printf("c-%c s-%d l-%ld f-%.2f d-%.6f str-%s\r\n", c, s, l, f, d, str);

return 0;
}

输出结果:

1
2
3
4
sizeof(char) 1 sizeof(short) 2 sizeof(long) 8 sizeof(float) 4 sizeof(double) 8 sizeof(char *) 8
sizeof(fake_args): 31 fake_args.CH A fake_args.INT 6666 fake_args.LONG 12345678 fake_args.FLT 3.14 fake_args.DBL 0.618280 fake_args.STR "HELLO ARGS!"

c-A s-6666 l-12345678 f-3.14 d-0.618280 str-"HELLO ARGS!"

参考资料

  1. https://stackoverflow.com/questions/12371450/how-are-variable-arguments-implemented-in-gcc
  2. https://stackoverflow.com/questions/5272703/how-do-vararg-functions-find-out-the-number-of-arguments-in-machine-code
  3. https://blog.nelhage.com/2010/10/amd64-and-va_arg/
  4. http://redstack.net/blog/x86-calling-conventions.html