C 语言的变参函数

#include <stdio.h>

float a = 1.0f;
double b = 2.0;
int c = 3;
char d = 'q';

int main(int argc, char * argv[]){
	printf("a = %f,b = %f,c = %d,d = %c\n",a,b,c,d);
	return 0;
}

在定义printf的时候,是不知道我们要传多少个参数的。例如我们的参数a、b、c、d,这些参数在函数 printf 的声明中并没有与之对应的形参。

按 C 标准的要求,C 编译器会对这些无名参数进行实参提升的操作,即把小于 int 型的 char 和 short 提升为 int 类型,把 float 类型提升为 double 类型。

从编译器生成的抽象语法树中可以看出实参 a 从float类型被转换成 double 类型,实参 d 从char类型被转换成 int 类型:

function main
{
  (call printf
        str0,
        (cast double
              a),
        b,
        c,
        (cast int
              d))

  (ret 0)
}

编译器产生的中间代码更直观地反映了实参提升的过程:

function main
	t0 :&str0;
	t1 : (double)(float)a;
	t2 : (int)(char)d;
	printf(t0, t1, b, c, t2);
	return 0;
	ret

再从汇编的角度看一下参数如何传递的:

.data
.str0:	.string	"a = %f,b = %f,c = %d,d = %c\012"

.align 4
.globl	a
a:	.long	1065353216

.align 8
.globl	b
b:	.long	0
.long	1073741824

.globl	c
c:	.long	3

.globl	d
d:	.byte	113

.text
.globl	main
main:
	pushl %ebp
	pushl %ebx
	pushl %esi
	pushl %edi
	movl %esp, %ebp
	subl $16, %esp
.BB0:
	leal .str0, %eax
	flds a           // 把 float 类型的 a 转换为 double 类型
	fstpl -12(%ebp)  // 转换后的结果存到临时变量-12(%ebp)中
	movsbl d, %ecx   // 把参数 d 由 char 到 int 的转换
	pushl %ecx       // 参数d入栈
	pushl c          // 参数c入栈
	subl $8, %esp    // 从全局静态数据区加载双精度浮点数 b 并入栈
	fldl b           //
	fstpl (%esp)     //
	subl $8, %esp    // 从临时变量-12(%ebp)中 加载双精度浮点数,并入栈
	fldl -12(%ebp)   //
	fstpl (%esp)     //
	pushl %eax       // 把格式化字符串的首地址入栈
	call printf      // 调用printf
	addl $28, %esp
	movl $0, %eax
	movl %ebp, %esp
	popl %edi
	popl %esi
	popl %ebx
	popl %ebp
	ret

由于所有的参数都是存放在栈中,printf只需按照格式化字符串的说明,从栈中取出相应的参数:

// 实际上只有 10 这一个参数,但 printf 看到有两个%d, 
// 于是仍试图从栈中取两个参数,打印出形如 10,1074172310 的垃圾值 
printf(“%d, %d “,10);

// 实际上有 10,20,30 这 3 个参数,但 printf 只看到一个%d, 
// 于是只打印出参数 10 
printf(“%d”, 10,20,30);

按照 C 调用约定,参数按从右到左的次序依次入栈:
image

我们通过表达式&format可以知道 format 在栈中的内存地址。根据格式化字符串还可以知道其他参数的类型及个数,从而可以推算出其他参数在栈中的位置,进而访问他们:

int printf(const char *format, ...);

float a = 1.0f;
double b = 2.0;
int c = 3;
char d = 'q';

// 不同的平台栈的内存布局不同,以下代码仅适用于i386
void OurPrintfV1(const char *format, ...){
	unsigned int addr = (unsigned int) &format;
	addr += sizeof(char *);
	printf("%f ", *((double *)addr));
	addr += sizeof(double);
	printf("%f ", *((double *)addr));
	addr += sizeof(double);
	printf("%d ", *((int *)addr));
	addr += sizeof(int);
	printf("%c ", (char)(*((int *)addr)));
}

int main(){
	OurPrintfV1("a=%f,b=%f,c=%d,d=%c \n",a,b,c,d);
	return 0;
}

由 format 的地址定位其他无名参数位置的代码可以写成宏定义,这样可以让代码看起来更简洁:

int printf(const char *format, ...);

float a = 1.0f;
double b = 2.0;
int c = 3;
char d = 'q';

// 不同的平台栈的内存布局不同,以下代码仅适用于i386
typedef char * va_list;
#define ALIGN_INT(n) ((sizeof(n) + sizeof(int) - 1) & ~(sizeof(int) - 1))
#define va_start(list, start) (list = (va_list)&start + ALIGN_INT(start))
#define va_arg(list, t) (*(t *)((list += ALIGN_INT(t)) - ALIGN_INT(t)))
#define va_end(list) (list = (va_list)0)

void OurPrintfV1(const char *format, ...){
	va_list ap;
	va_start(ap,format);
	printf("%f ",va_arg(ap,double));
	printf("%f ",va_arg(ap,double));
	printf("%d ",va_arg(ap,int));
	printf("%c \n",(char)va_arg(ap,int));
	va_end(ap);
}

int main(){
	OurPrintfV1("a=%f,b=%f,c=%d,d=%c \n",a,b,c,d);
	return 0;
}

我们有意定义这些宏,是因为这些宏已经在标准C头文件stdarg.h中被实现。头文件stdarg.h中的宏有对各种平台都有不同的处理,所以下面的代码可以在所有平台上运行:

#include <stdarg.h>

int printf(const char *format, ...);

float a = 1.0f;
double b = 2.0;
int c = 3;
char d = 'q';


void OurPrintfV1(const char *format, ...){
	va_list ap;
	va_start(ap,format);
	printf("%f ",va_arg(ap,double));
	printf("%f ",va_arg(ap,double));
	printf("%d ",va_arg(ap,int));
	printf("%c \n",(char)va_arg(ap,int));
	va_end(ap);
}

int main(){
	OurPrintfV1("a=%f,b=%f,c=%d,d=%c \n",a,b,c,d);
	return 0;
}
posted @ 2021-04-18 15:10:14
评论加载中...

发表评论