c中变参函数的理解和编写(hello world引发的思考)
前面从编译链接的角度从新理解了hello world代码,这里还有个printf这个函数很神奇。参数可变。下载glibc库
/stdio-common$ cat printf.c可以看到printf的源码
/* Copyright (C) 1991-2013 Free Software Foundation, Inc. This file is part of the GNU C Library. The GNU C Library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2.1 of the License, or (at your option) any later version. The GNU C Library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with the GNU C Library; if not, see <http://www.gnu.org/licenses/>. */ #include <libioP.h> #include <stdarg.h> #include <stdio.h> <span style="color:#FF0000;">#undef printf /* Write formatted output to stdout from the format string FORMAT. */ /* VARARGS1 */ int __printf (const char *format, ...) { va_list arg; int done; va_start (arg, format); done = vfprintf (stdout, format, arg); va_end (arg); return done; }</span> #undef _IO_printf ldbl_strong_alias (__printf, printf); /* This is for libg++. */ ldbl_strong_alias (__printf, _IO_printf);
这里面ldbl_strong_alias是替换语句
这里调用fprintf,看看fprintf.c函数中相关的代码
# define vfprintf _IO_vfprintf_internal
vfrintf代码涉及很多,另外开专题看看。
这里看看va_list va_start等东西的实现
根据前面学习的编译链接的知识,函数之间传递,是通过栈来完成,对于未知参数个数的,在编译时,是需要编译器提前解释的。如同#define宏定义一样,这样先确定参数个数才能编译成汇编语言。书《c标准库》将这是宏。
在stdarg.h中看到
#define va_start(v,l) __builtin_va_start(v,l) #define va_end(v) __builtin_va_end(v) #define va_arg(v,l) __builtin_va_arg(v,l)
typedef __gnuc_va_list va_list;
typedef __builtin_va_list __gnuc_va_list;
在glibc中没有找到这个__builtin_va_list,
在c标准库找到在stdarg.h中有定义
typedef char* va_list;
---------------------------------------------先看看例子-----------------------------
add.c
cat add.c #include<stdarg.h> int add(int n,...) { <span style="color:#FF0000;">va_list ap;</span> <span style="color:#FF0000;">va_start(ap,n);</span> int temp=0; int i; for(i=0;i<n;i++) temp+=<span style="color:#FF0000;">va_arg(ap,int)</span>; <span style="color:#FF0000;">va_end(ap);</span> return temp; }main.c
cat main.c #include<stdio.h> extern int add(int,...); int main() { int s= add(3,1,2,3); printf("va_add is %d\n",s); }
va_list是一个宏,由va_start和va_end界定。
typedefchar* va_list;
void va_start ( va_list ap, prev_param );
type va_arg ( va_list ap, type );
void va_end ( va_list ap );
其中,va_list是一个字符指针,可以理解为指向当前参数的一个指针,取参必须通过这个指针进行。
<Step 1> 在调用参数表之前,应该定义一个 va_list类型的变量,以供后用(假设这个 va_list 类型变量被定义为ap);
<Step 2> 然后对 ap进行初始化,让它指向可变参数表里面的第一个参数。这是通过 va_start 来实现的,其第一个参数是 ap本身,第二个参数是在变参表前面紧挨着的一个变量;
<Step 3> 然后是获取参数,调用 va_arg。它的第一个参数是ap,第二个参数是要获取的参数的指定类型,并返回这个指定类型的值,同时把 ap 的位置指向变参表的下一个变量位置;
<Step 4> 获取所有的参数之后,我们有必要将这个 ap指针关掉,以免发生危险,方法是调用 va_end。它是将输入的参数 ap 置为NULL,应该养成获取完参数表之后关闭指针的习惯。
gcc -E add.c -o add.i
cat add.i # 1 "add.c" # 1 "<built-in>" # 1 "<命令行>" # 1 "add.c" # 1 "/usr/lib/gcc/i686-linux-gnu/4.6/include/stdarg.h" 1 3 4 # 40 "/usr/lib/gcc/i686-linux-gnu/4.6/include/stdarg.h" 3 4 <span style="color:#FF0000;">typedef __builtin_va_list __gnuc_va_list;</span> # 102 "/usr/lib/gcc/i686-linux-gnu/4.6/include/stdarg.h" 3 4 <span style="color:#FF0000;">typedef __gnuc_va_list va_list;</span> # 2 "add.c" 2 int add(int n,...) { <span style="color:#FF0000;">va_list</span> ap; <span style="color:#3333FF;">_<span style="color:#330099;">_builtin_va_start</span></span>(ap,n); int temp=0; int i; for(i=0;i<n;i++) temp+=<span style="color:#000099;">__builtin_va_arg(</span>ap,int); <span style="color:#000066;">__builtin_va_end</span>(ap); return temp; }
va_宏都已替换
gcc -g -S add.c -o add.s
gcc -c add.s -o add.o
objdump -S add.o
add.o: file format elf32-i386 Disassembly of section .text: 00000000 <add>: #include<stdarg.h> int add(int n,...) { 0: 55 push %ebp 1: 89 e5 mov %esp,%ebp 3: 83 ec 10 sub $0x10,%esp va_list ap; <span style="color:#CC0000;">va_start(ap,n); 6: 8d 55 0c lea 0xc(%ebp),%edx 9: 8d 45 f4 lea -0xc(%ebp),%eax c: 89 10 mov %edx,(%eax)</span> int temp=0; e: c7 45 f8 00 00 00 00 movl $0x0,-0x8(%ebp) int i; for(i=0;i<n;i++) 15: c7 45 fc 00 00 00 00 movl $0x0,-0x4(%ebp) 1c: eb 12 jmp 30 <add+0x30> <span style="color:#FF0000;">temp+=va_arg(ap,int); 1e: 8b 45 f4 mov -0xc(%ebp),%eax 21: 8d 50 04 lea 0x4(%eax),%edx 24: 89 55 f4 mov %edx,-0xc(%ebp) 27: 8b 00 mov (%eax),%eax 29: 01 45 f8 add %eax,-0x8(%ebp) {</span> va_list ap; va_start(ap,n); int temp=0; int i; for(i=0;i<n;i++) 2c: 83 45 fc 01 addl $0x1,-0x4(%ebp) 30: 8b 45 fc mov -0x4(%ebp),%eax 33: 3b 45 08 cmp 0x8(%ebp),%eax 36: 7c e6 jl 1e <add+0x1e> temp+=va_arg(ap,int); va_end(ap); return temp; 38: 8b 45 f8 mov -0x8(%ebp),%eax } 3b: c9 leave 3c: c3 ret
--------------------------------------------------------------------------------------------------------
汇编目前看不出编写思想,换个方向试试
对几个宏的展开
#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 ) // 将指针置为无效
#define _INTSIZEOF(n) ((sizeof(n)+sizeof(int)-1)&~(sizeof(int) - 1) )
为了测试_INTSIZEOF(n),编写test.c
cat test.c #include<stdio.h> #include<stdlib.h> #define _intsizeof(n) ((sizeof(n)+sizeof(int)-1)&~(sizeof(int)-1)) int main() { printf("test _intsizeof %d\n",_intsizeof(int)); printf("test _intsizeof %d\n",_intsizeof(double)); printf("test _intsizeof %d\n",_intsizeof(char)); return 0; }
./a.out test _intsizeof 4 test _intsizeof 8 test _intsizeof 4
因为这里涉及到内存对其。
测试va_start va_arg va_end,编写c测试代码
#include<stdio.h> #include<stdlib.h> typedef char * va_list; #define _intsizeof(n) ((sizeof(n)+sizeof(int)-1)&~(sizeof(int)-1)) #define va_arg_o(ap,t) ( *(t *)((ap += _intsizeof(t)) - _intsizeof(t)) ) <span style="color:#FF0000;">#define va_arg(ap,t,r) do{ap += _intsizeof(t); printf("ap3 %p\n",ap); \ printf("ap4 %p\n",ap-_intsizeof(t)); \ r=*(t*)(ap-_intsizeof(t)); }while(0);</span> <span style="color:#CC0000;">/*此处将增加一个参数,方便测试地址。_intsizeof(n)是计算当前数据类型的占内存大小*/</span> <span style="color:#FF0000;">/*在调用va_arg时,ap已经指向新的变参地址,这个宏有两个作用,将ap指向下一个参数地址,同时又返本变参的数*/</span> #define va_start(ap,v) ( ap = (va_list)&v + _intsizeof(v) ) <span style="color:#FF0000;">/*va_start()是获取最后一个固定参数的地址,然后加上所占内存大小,地址对齐后,正好定位到第一个变参地址*/</span> #define va_end(ap) ( ap = (va_list)0 ) int foo(int a,...) { va_list ap; printf("parmN %p\n",&a); va_start(ap,a); printf("ap1 %p\n",ap); printf("ap1 %c\n",*(char*)ap); char b; <span style="color:#330099;">va_arg(ap,char,b);</span> printf("ap2 %p\n",ap); int c; <span style="color:#330099;">va_arg(ap,int,c);</span> printf("ap6 %p\n",ap); printf("b %c\n",b); printf("c %d\n",c); va_end(ap); } int foo_o(int a,...) { va_list ap; printf("parmN %p\n",&a); va_start(ap,a); printf("ap1 %p\n",ap); printf("ap1 %c\n",*(char*)ap); char b; b=va_arg_o(ap,char); printf("ap2 %p\n",ap); int c; c=va_arg_o(ap,int); printf("ap6 %p\n",ap); printf("b %c\n",b); printf("c %d\n",c); va_end(ap); } int main() { printf("test _intsizeof %d\n",_intsizeof(int)); printf("test _intsizeof %d\n",_intsizeof(double)); printf("test _intsizeof %d\n",_intsizeof(char)); foo(1,'b',3); printf("---------------------\n"); foo_o(1,'b',3); return 0; }
运行
./a.out test _intsizeof 4 test _intsizeof 8 test _intsizeof 4 parmN 0xbf878180 ap1 0xbf878184 ap1 b ap3 0xbf878188 ap4 0xbf878184 ap2 0xbf878188 ap3 0xbf87818c ap4 0xbf878188 ap6 0xbf87818c b b c 3 --------------------- parmN 0xbf878180 ap1 0xbf878184 <span style="color:#000066;">-->av_start之后</span> ap1 b ap2 0xbf878188 <span style="color:#000066;"><span style="background-color: rgb(255, 255, 255);">-->av_arg之后</span></span> ap6 0xbf87818c <span style="color:#000066;">-->av_arg之后</span> b b c 3
----------------------------参考资料----------------
http://www.tuicool.com/articles/uaqmQbm
http://www.cnblogs.com/diyunpeng/archive/2010/01/09/1643201.html(十分推荐)
更多相关文章
- linux 下命令行传参数问题
- 自己实现的C++智能指针的功能代码和测试用例
- linux 输入参数利用getopt、struct option、getopt_long、getopt
- 使用Bash编写Linux Shell脚本-9. 参数和子壳
- linux-参数-argparse模块-(未完待续)
- 在PreparedStatement中重用参数?
- LINUX下用SHELL脚本执行带输入输出参数的ORACLE存储过程并得到结
- C#中操作Oracle时的SQL语句参数的用法
- 彻底理解初始化参数SERVICE_NAMES和客户端TNS中的SERVICE_NAME