C语言如何定义“可变参数”函数?计算机是如何处理可变参数函数的?

如果一定要说哪段C语言代码最“著名”,我想非“hello world”莫属了。大多数初学者人生中编写的第一段C语言代码就是这段“里程碑”式的代码:

#include <stdio.h>

int main()
{
    printf("hello world\n");
    return 0;
}

也正因为这段著名的程序,printf() 函数成为大多数C语言初学者接触到的第一个标准库函数。

C语言中的可变参数函数

随着学习的推进,初学者逐步学会调用别的C语言函数,以及定义自己的函数,观察力敏锐的会注意到 printf() 函数似乎与其他函数不太一样——printf()函数没有固定数目的参数,它似乎可以接收任意多的参数。

而其他C语言函数则不同,它们大都有固定数量的参数(0个,3个等),调用这些函数必须传递对应数目的参数。

有些持有“特殊论”的初学者认为像 printf() 这样的“可变参数”函数是“特殊的”,是系统定义的,我们程序员只能定义固定参数的函数,其实不是的,C语言是有手段定义自己“可变参数”函数的。

printf() 究竟是不是只能由系统定义的“特殊”函数呢?

怎样定义自己的可变参数函数?

事实上,标准库 <stdarg.h>就是方便C语言程序员定义自己的“可变参数”函数的。如果读者和我一样使用的是 Linux 系统,则可以方便的通过 man 命令查询到相关库函数:
file
头文件<stdarg.h>声明了 va_list 类型用于描述可变参数,并且定义了上述 4 个方法解析。这里不打算介绍过多枯燥的理论知识,我们直接看实例,请看相关C语言代码:

#include <stdio.h>
#include <stdarg.h>

void foo(char *fmt, ...)
{
    va_list ap;
    int d;
    char c, *s;

    va_start(ap, fmt);
    while (*fmt)
        switch (*fmt++) {
        case 's':              /* string */
            s = va_arg(ap, char *);
            printf("string %s\n", s);
            break;
        case 'd':              /* int */
            d = va_arg(ap, int);
            printf("int %d\n", d);
            break;
        case 'c':              /* char */
            /* need a cast here since va_arg only
               takes fully promoted types */
            c = (char) va_arg(ap, int);
            printf("char %c\n", c);
            break;
        }
    va_end(ap);
}

int main()
{
    foo("%s, %d, %c\n", "hello", 12, 'm');

    return 0;
}

file
上述代码定义了可变参数函数 foo(),它可以接收类似于 printf() 的函数,并且将 fmt 中的 s 解析为字符串,d 解析为整数,c 解析为字符,因此编译并执行这段C语言代码,可得到如下输出:

# gcc t.c
# ./a.out 
string hello
int 12
char m

通过这段实例,可以看出使用C语言定义可变参数函数并不复杂,在处理可变参数时,只需先调用 va_start() 将参数序列加载到 va_list 结构的变量中,然后调用 va_arg() 依次解析。解析完毕后,再调用 va_end() 结束解析。

va_start -> va_arg -> va_end。

唯一需要注意的是使用 va_arg() 解析参数时,需要指定类型。但是这个过程也很简单,可变参数函数的实现者可以指定一套规则,用于约束函数调用者传递参数,这样就知道接下来需要解析的参数是何种类型。例如上面的C语言代码就约定了 fmt 中的 s 表示接下来的要解析的参数是字符串,d 表示整数等。

计算机是如何处理可变参数函数的?

C语言定义可变参数函数的过程并不复杂,借助于<stdarg.h>,我们能够轻易的定义接收任意多参数的函数,不过到这里,有读者发现问题了:我们人类可以按照规则写出可变参数函数,但是计算机是如何理解这一套规则的呢?或者换句话说,计算机是如何处理“可变参数”的?

以Linux为例,看过我之前文章的读者应该明白,每个C语言程序进程都有属于自己的栈,进程中的每个函数则有属于自己的栈帧,当有函数调用时,例如:

foo("%d%d%d", 3,2,1);

C语言编译器会产生类似于下面这样的汇编代码:

push 1
push 2
push 3
push "%d%d%d"
call foo

也即将 foo() 函数的参数先压入栈中,然后再调用 foo() 函数。鉴于栈这种数据结构“先进后出”的特点,一般函数参数的入栈顺序是从右至左的。

              []   // 空栈
-------------------------------
push 1:  [1]  
-------------------------------
push 2:  [1]
              [2]
-------------------------------
push 3:  [1]
              [2]
              [3]  // 参数 1 2 3 被压入栈中
-------------------------------
push "%d%d%d":[1]
              [2]
              [3]
              ["%d%d%d"]
-------------------------------
call foo   ...  // foo 函数开始使用参数

file
按照这样的参数入栈顺序,foo() 函数使用参数很方便,依次从栈中将参数取出就可以了。至于如何解析栈中的参数,则可以根据可变参数实现者指定的规则,例如在格式化字符串 fmt 中遇到 s 就解析为字符串等。

如果可变参数 foo() 接收到其他数目的参数,对于最终程序来说,也仅仅只需要修改压栈的参数数目,其他并无太多不同。

小结

本文主要讨论了C语言中可变参数函数的定义方法,以及计算机如何处理可变参数函数的过程,其实并不复杂。C语言不像C++那样支持函数重载,但是借助于可变参数函数和宏,我们可以像定义“伪类”那样,定义自己的“伪函数重载”,这是一种编程技巧,以后有机会再讨论了。

https://stackoverflow.com/questions/23104628/technically-how-do-variadic-functions-work-how-does-printf-work?answertab=active#tab-top

阅读更多:   C语言
添加新评论

icon_redface.gificon_idea.gificon_cool.gif2016kuk.gificon_mrgreen.gif2016shuai.gif2016tp.gif2016db.gif2016ch.gificon_razz.gif2016zj.gificon_sad.gificon_cry.gif2016zhh.gificon_question.gif2016jk.gif2016bs.gificon_lol.gif2016qiao.gificon_surprised.gif2016fendou.gif2016ll.gif