刀刀网
您的当前位置:首页C语言可变参数函数实现原理

C语言可变参数函数实现原理

来源:刀刀网

一、可变参数函数实现原理

C函数调用的栈结构:

可变参数函数的实现与函数调用的栈结构密切相关,正常情况下C的函数参数入栈规则为__stdcall, 它是从右到左的,即函数中的最右边的参数最先入栈。

例如,对于函数:

void fun(int a, int b, int c)
  {
        int d;
        ...
  }

其栈结构为

    0x1ffc-->d

    0x2000-->a

    0x2004-->b

    0x2008-->c

对于在32位系统的多数编译器,每个栈单元的大小都是sizeof(int), 而函数的每个参数都至少要占一个栈单元大小,如函数 void fun1(char a, int b, double c, short d) 对一个32的系统其栈的结构就是

    0x1ffc-->a  (4字节)(为了字对齐)

    0x2000-->b  (4字节)

    0x2004-->c  (8字节)

    0x200c-->d  (4字节)

因此,函数的所有参数是存储在线性连续的栈空间中的,基于这种存储结构,这样就可以从可变参数函数中必须有的第一个普通参数来寻址后续的所有可变参数的类型及其值。

先看看固定参数列表函数:

void fixed_args_func(int a, double b, char *c)
{
        printf("a = 0x%p\n", &a);
        printf("b = 0x%p\n", &b);
        printf("c = 0x%p\n", &c);
}
void var_args_func(const char * fmt, ...) 
{
    ... ... 
}

我们先用上面的那个fixed_args_func函数确定一下入栈顺序。

int main() 
{
    fixed_args_func(17, 5.40, "hello world");
    return 0;
}
a = 0x0022FF50
b = 0x0022FF54
c = 0x0022FF5C

我们基本可以得出这样一个结论:

 c.addr = b.addr + x_sizeof(b);  /*注意:  x_sizeof !=sizeof */
 b.addr = a.addr + x_sizeof(a);

有了以上的"等式",我们似乎可以推导出 void var_args_func(const char * fmt, ... ) 函数中,可变参数的位置了。起码第一个可变参数的位置应该是:first_vararg.addr = fmt.addr + x_sizeof(fmt);  根据这一结论我们试着实现一个支持可变参数的函数:

期待输出结果:

4
5
hello world



改正后的代码如下:

var_args_func只是为了演示,并未根据fmt消息中的格式字符串来判断变参的个数和类型,而是直接在实现中写死了。

为了满足代码的可移植性,C标准库在stdarg.h中提供了诸多便利以供实现变长长度参数时使用。这里也列出一个简单的例子,看看利用标准库是如何支持变长参数的:

下面我们来探讨如何写一个简单的可变参数的C 函数.

使用可变参数应该有以下步骤: 
1)首先在函数里定义一个va_list型的变量,这里是arg_ptr,这个变量是指向参数的指针. 
2)然后用va_start宏初始化变量arg_ptr,这个宏的第二个参数是第一个可变参数的前一个参数,是一个固定的参数. 
3)然后用va_arg返回可变的参数,并赋值给整数j. va_arg的第二个参数是你要返回的参数的类型,这里是int型. 
4)最后用va_end宏结束可变参数的获取.然后你就可以在函数里使用第二个参数了.如果函数有多个可变参数的,依次调用va_arg获取各个参数.

在《C程序设计语言》中,Ritchie提供了一个简易版printf函数:

二、stdarg.h头文件源代码分析 

谈到C语言中可变参数函数的实现,有一个头文件不得不谈,那就是stdarg.h

接下来从minix源码中的stdarg.h头文件入手进行分析:

 1 #ifndef _STDARG_H
 2 #define _STDARG_H
 3 
 4 
 5 #ifdef __GNUC__
 6 /* The GNU C-compiler uses its own, but similar varargs mechanism. */
 7 
 8 typedef char *va_list;
 9 
10 /* Amount of space required in an argument list for an arg of type TYPE.
11  * TYPE may alternatively be an expression whose type is used.
12  */
13 
14 #define __va_rounded_size(TYPE)  \
15   (((sizeof (TYPE) + sizeof (int) - 1) / sizeof (int)) * sizeof (int))
16 
17 #if __GNUC__ < 2
18 
19 #ifndef __sparc__
20 #define va_start(AP, LASTARG)                                           \
21  (AP = ((char *) &(LASTARG) + __va_rounded_size (LASTARG)))
22 #else
23 #define va_start(AP, LASTARG)                                           \
24  (__builtin_saveregs (),                                                \
25   AP = ((char *) &(LASTARG) + __va_rounded_size (LASTARG)))
26 #endif
27 
28 void va_end (va_list);          /* Defined in gnulib */
29 #define va_end(AP)
30 
31 #define va_arg(AP, TYPE)                                                \
32  (AP += __va_rounded_size (TYPE),                                       \
33   *((TYPE *) (AP - __va_rounded_size (TYPE))))
34 
35 #else    /* __GNUC__ >= 2 */
36 
37 #ifndef __sparc__
38 #define va_start(AP, LASTARG)                         \
39  (AP = ((char *) __builtin_next_arg ()))
40 #else
41 #define va_start(AP, LASTARG)                    \
42   (__builtin_saveregs (), AP = ((char *) __builtin_next_arg ()))
43 #endif
44 
45 void va_end (va_list);        /* Defined in libgcc.a */
46 #define va_end(AP)
47 
48 #define va_arg(AP, TYPE)                        \
49  (AP = ((char *) (AP)) += __va_rounded_size (TYPE),            \
50   *((TYPE *) ((char *) (AP) - __va_rounded_size (TYPE))))
51 
52 #endif    /* __GNUC__ >= 2 */
53 
54 #else    /* not __GNUC__ */
55 
56 
57 typedef char *va_list;
58 
59 #define __vasz(x)        ((sizeof(x)+sizeof(int)-1) & ~(sizeof(int) -1))
60 
61 #define va_start(ap, parmN)    ((ap) = (va_list)&parmN + __vasz(parmN))
62 #define va_arg(ap, type)      \
63   (*((type *)((va_list)((ap) = (void *)((va_list)(ap) + __vasz(type))) \
                             - __vasz(type))))
65 #define va_end(ap)
66 
67 
68 #endif /* __GNUC__ */
69 
70 #endif /* _STDARG_H */
View Code

从代码中可以看到,里面编译器的版本以及相关的大量宏定义

第5行: #ifdef __GNUC__
作用是条件编译,__GNUC__为GCC中定义的宏。GCC的版本,为一个整型值。如果你需要知道自己的程序是否被GCC编译,可以简单的测试一下__GNUC__,假如你代码需要运行在GCC某个特定的版本下,那么你就要小心了,因为GCC的主要版本在增加,如果你想定义宏的方式直接实现控制,你可以写如下的代码(参见):

/* 测试 GCC > 3.2.0 ? */
#if __GNUC__ > 3 || \
    (__GNUC__ == 3 && (__GNUC_MINOR__ > 2 || \
                       (__GNUC_MINOR__ == 2 && \
                        __GNUC_PATCHLEVEL__ > 0))
View Code

你还可以使用下面一个类似的方法:

#define GCC_VERSION (__GNUC__ * 10000 \
                     + __GNUC_MINOR__ * 100 \
                     + __GNUC_PATCHLEVEL__)
...
/*测试 GCC > 3.2.0 ?*/
#if GCC_VERSION > 30200
View Code

第8行: 使用typedef进行了一个声明:typedef char *va_list;

第14行:定义了用于编译器的内存对齐宏,参见:(),

#define __va_rounded_size(TYPE)  \
     (((sizeof (TYPE) + sizeof (int) - 1) / sizeof (int)) * sizeof (int))

第17行:#if __GNUC__ < 2,进行GCC的版本判断,看当前版本是否大于2

第19行:#ifndef __sparc__ 可扩充处理器架构宏(以后再深入研究)

#define va_start(AP, LASTARG)                                           \
   (AP = ((char *) &(LASTARG) + __va_rounded_size (LASTARG)))

第31行:

#define va_arg(AP, TYPE)                                                \
   (AP += __va_rounded_size (TYPE),                                       \
    *((TYPE *) (AP - __va_rounded_size (TYPE))))

va_arg宏使得ap指向下一个参数,已经处理了内存对齐,其中参数的类型为TYPE

第48行:

void va_end (va_list);          /* Defined in gnulib */

定义在gnulib中,va_end 与va_start成对使用.在有些代码中定义为:

#define va_end(ap)      ( ap = (va_list)0 )

三、C语言scanf函数的实现

上文从理论上详细介绍了C语言中可变参数函数的实现,接下来从minix内核源码中的scanf函数入手,学习C语言经典可变参数函数的实现过程

在scanf.c文件中,可以看到scanf函数,代码如下:

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

int scanf(const char *format, ...)
{
    va_list ap;
    int retval;

    va_start(ap, format);

    retval = _doscan(stdin, format, ap);

    va_end(ap);

    return retval;
}
View Code

 对于va_list、va_start、va_end等在stdarg.h头文件中定义的宏,都已经在上文中介绍过。

在上述代码中我们可以看到有一个_doscan函数,而这一函数在头文件loc_incl.h中定义,函数声明如下:

int _doscan(FILE * stream, const char *format, va_list ap);

_doscan函数的实现源代码如下:

  1 int
  2 _doscan(register FILE *stream, const char *format, va_list ap)
  3 {
  4     int        done = 0;    /* number of items done */
  5     int        nrchars = 0;    /* number of characters read */
  6     int        conv = 0;    /* # of conversions */
  7     int        base;        /* conversion base */
  8     unsigned long    val;        /* an integer value */
  9     register char    *str;        /* temporary pointer */
 10     char        *tmp_string;    /* ditto */
 11     unsigned    width = 0;    /* width of field */
 12     int        flags;        /* some flags */
 13     int        reverse;    /* reverse the checking in [...] */
 14     int        kind;
 15     register int    ic = EOF;    /* the input character */
 16 #ifndef    NOFLOAT
 17     long double    ld_val;
 18 #endif
 19 
 20     if (!*format) return 0;
 21 
 22     while (1) {
 23         if (isspace(*format)) {
 24             while (isspace(*format))
 25                 format++;    /* skip whitespace */
 26             ic = getc(stream);
 27             nrchars++;
 28             while (isspace (ic)) {
 29                 ic = getc(stream);
 30                 nrchars++;
 31             }
 32             if (ic != EOF) ungetc(ic,stream);
 33             nrchars--;
 34         }
 35         if (!*format) break;    /* end of format */
 36 
 37         if (*format != '%') {
 38             ic = getc(stream);
 39             nrchars++;
 40             if (ic != *format++) break;    /* error */
 41             continue;
 42         }
 43         format++;
 44         if (*format == '%') {
 45             ic = getc(stream);
 46             nrchars++;
 47             if (ic == '%') {
 48                 format++;
 49                 continue;
 50             }
 51             else break;
 52         }
 53         flags = 0;
 54         if (*format == '*') {
 55             format++;
 56             flags |= FL_NOASSIGN;
 57         }
 58         if (isdigit (*format)) {
 59             flags |= FL_WIDTHSPEC;
 60             for (width = 0; isdigit (*format);)
 61                 width = width * 10 + *format++ - '0';
 62         }
 63 
          switch (*format) {
 65         case 'h': flags |= FL_SHORT; format++; break;
 66         case 'l': flags |= FL_LONG; format++; break;
 67         case 'L': flags |= FL_LONGDOUBLE; format++; break;
 68         }
 69         kind = *format;
 70         if ((kind != 'c') && (kind != '[') && (kind != 'n')) {
 71             do {
 72                 ic = getc(stream);
 73                 nrchars++;
 74             } while (isspace(ic));
 75             if (ic == EOF) break;        /* outer while */
 76         } else if (kind != 'n') {        /* %c or %[ */
 77             ic = getc(stream);
 78             if (ic == EOF) break;        /* outer while */
 79             nrchars++;
 80         }
 81         switch (kind) {
 82         default:
 83             /* not recognized, like %q */
 84             return conv || (ic != EOF) ? done : EOF;
 85             break;
 86         case 'n':
 87             if (!(flags & FL_NOASSIGN)) {    /* silly, though */
 88                 if (flags & FL_SHORT)
                      *va_arg(ap, short *) = (short) nrchars;
 90                 else if (flags & FL_LONG)
 91                     *va_arg(ap, long *) = (long) nrchars;
 92                 else
 93                     *va_arg(ap, int *) = (int) nrchars;
 94             }
 95             break;
 96         case 'p':        /* pointer */
 97             set_pointer(flags);
 98             /* fallthrough */
 99         case 'b':        /* binary */
100         case 'd':        /* decimal */
101         case 'i':        /* general integer */
102         case 'o':        /* octal */
103         case 'u':        /* unsigned */
104         case 'x':        /* hexadecimal */
105         case 'X':        /* ditto */
106             if (!(flags & FL_WIDTHSPEC) || width > NUMLEN)
107                 width = NUMLEN;
108             if (!width) return done;
109 
110             str = o_collect(ic, stream, kind, width, &base);
111             if (str < inp_buf
112                 || (str == inp_buf
113                     && (*str == '-'
114                     || *str == '+'))) return done;
115 
116             /*
117              * Although the length of the number is str-inp_buf+1
118              * we don't add the 1 since we counted it already
119              */
120             nrchars += str - inp_buf;
121 
122             if (!(flags & FL_NOASSIGN)) {
123                 if (kind == 'd' || kind == 'i')
124                     val = strtol(inp_buf, &tmp_string, base);
125                 else
126                     val = strtoul(inp_buf, &tmp_string, base);
127                 if (flags & FL_LONG)
128                     *va_arg(ap, unsigned long *) = (unsigned long) val;
129                 else if (flags & FL_SHORT)
130                     *va_arg(ap, unsigned short *) = (unsigned short) val;
131                 else
132                     *va_arg(ap, unsigned *) = (unsigned) val;
133             }
134             break;
135         case 'c':
136             if (!(flags & FL_WIDTHSPEC))
137                 width = 1;
138             if (!(flags & FL_NOASSIGN))
139                 str = va_arg(ap, char *);
140             if (!width) return done;
141 
142             while (width && ic != EOF) {
143                 if (!(flags & FL_NOASSIGN))
144                     *str++ = (char) ic;
145                 if (--width) {
146                     ic = getc(stream);
147                     nrchars++;
148                 }
149             }
150 
151             if (width) {
152                 if (ic != EOF) ungetc(ic,stream);
153                 nrchars--;
154             }
155             break;
156         case 's':
157             if (!(flags & FL_WIDTHSPEC))
158                 width = 0xffff;
159             if (!(flags & FL_NOASSIGN))
160                 str = va_arg(ap, char *);
161             if (!width) return done;
162 
163             while (width && ic != EOF && !isspace(ic)) {
1                 if (!(flags & FL_NOASSIGN))
165                     *str++ = (char) ic;
166                 if (--width) {
167                     ic = getc(stream);
168                     nrchars++;
169                 }
170             }
171             /* terminate the string */
172             if (!(flags & FL_NOASSIGN))
173                 *str = '\0';    
174             if (width) {
175                 if (ic != EOF) ungetc(ic,stream);
176                 nrchars--;
177             }
178             break;
179         case '[':
180             if (!(flags & FL_WIDTHSPEC))
181                 width = 0xffff;
182             if (!width) return done;
183 
184             if ( *++format == '^' ) {
185                 reverse = 1;
186                 format++;
187             } else
188                 reverse = 0;
1 
190             for (str = Xtable; str < &Xtable[NR_CHARS]
191                             ; str++)
192                 *str = 0;
193 
194             if (*format == ']') Xtable[*format++] = 1;
195 
196             while (*format && *format != ']') {
197                 Xtable[*format++] = 1;
198                 if (*format == '-') {
199                     format++;
200                     if (*format
201                         && *format != ']'
202                         && *(format) >= *(format -2)) {
203                         int c;
204 
205                         for( c = *(format -2) + 1
206                             ; c <= *format ; c++)
207                             Xtable[c] = 1;
208                         format++;
209                     }
210                     else Xtable['-'] = 1;
211                 }
212             }
213             if (!*format) return done;
214             
215             if (!(Xtable[ic] ^ reverse)) {
216             /* MAT 8/9/96 no match must return character */
217                 ungetc(ic, stream);
218                 return done;
219             }
220 
221             if (!(flags & FL_NOASSIGN))
222                 str = va_arg(ap, char *);
223 
224             do {
225                 if (!(flags & FL_NOASSIGN))
226                     *str++ = (char) ic;
227                 if (--width) {
228                     ic = getc(stream);
229                     nrchars++;
230                 }
231             } while (width && ic != EOF && (Xtable[ic] ^ reverse));
232 
233             if (width) {
234                 if (ic != EOF) ungetc(ic, stream);
235                 nrchars--;
236             }
237             if (!(flags & FL_NOASSIGN)) {    /* terminate string */
238                 *str = '\0';    
239             }
240             break;
241 #ifndef    NOFLOAT
242         case 'e':
243         case 'E':
244         case 'f':
245         case 'g':
246         case 'G':
247             if (!(flags & FL_WIDTHSPEC) || width > NUMLEN)
248                 width = NUMLEN;
249 
250             if (!width) return done;
251             str = f_collect(ic, stream, width);
252 
253             if (str < inp_buf
254                 || (str == inp_buf
255                 && (*str == '-'
256                     || *str == '+'))) return done;
257 
258             /*
259              * Although the length of the number is str-inp_buf+1
260              * we don't add the 1 since we counted it already
261              */
262             nrchars += str - inp_buf;
263 
2             if (!(flags & FL_NOASSIGN)) {
265                 ld_val = strtod(inp_buf, &tmp_string);
266                 if (flags & FL_LONGDOUBLE)
267                     *va_arg(ap, long double *) = (long double) ld_val;
268                 else
269                     if (flags & FL_LONG)
270                     *va_arg(ap, double *) = (double) ld_val;
271                 else
272                     *va_arg(ap, float *) = (float) ld_val;
273             }
274             break;
275 #endif
276         }        /* end switch */
277         conv++;
278         if (!(flags & FL_NOASSIGN) && kind != 'n') done++;
279         format++;
280     }
281     return conv || (ic != EOF) ? done : EOF;
282 }
View Code

 在上面的源代码中,值得注意的是第26行的getc宏,定义代码如下:

#define    getc(p)        (--(p)->_count >= 0 ? (int) (*(p)->_ptr++) : \
                __fillbuf(p))

getc的调用形式:ch=getc(fp); 功能是从文件指针指向的文件读入一个字符,并把它作为函数值返回给int型变量ch。

第4行~第17行,定义一些后面需要用到的变量

第23行~第34行,跳过format格式串中的空格,并且跳过输入流中的空格

第37行~第42行,输入流stream与format格式串中的空白符(空白符可以是空格(space)、制表符(tab)和新行符(newline))保持一致

第44行~第52行,在format中的字符为'%'的前提下,stream中的字符也为'%',则继续

第54行~第57行,format当前字符为'*',表示读指定类型的数据但不保存

第58行~第62行,指定说明最大域宽。 在百分号(%)与格式码之间的整数用于从对应域读入的最大字符数于宽度

第行~第282行,switch语句,用于格式修饰符,这些修饰符包括: h、l、L、c、p、b、d、i、o、u……,还有基于扫描集的'['修饰符


对scanf函数的源码分析,需要在scanf函数的语法格式详细的理解基础上进行,由于scanf函数实现十分复杂,需要仔细的品味,这里只是比较初步的分析,具体还有待后期不断的完善

 

因篇幅问题不能全部显示,请点此查看更多更全内容