调用函数时的参数传递无非就是将参数放到固定位置,跳转到函数中再到固定位置取值,常用两个位置:
在《Arm v7-M Architecture Reference Manual》中有这么一句话:
The Armv7-M architecture uses a full-descending stack
意思是Armv7-M架构的栈是从高地址向低地址生长的。
变参函数是必须要有第一个参数的,且这个参数必须是字符串,因为我们需要根据字符串中的“特殊字符”确定其余参数的类型。最常见的变参函数:
int printf(const char *format, ...);
按照C标准,参数按从右到左的次序依次入栈。我们知道第一个参数的内存地址,根据格式化字符串还可以知道其他参数的类型及个数,从而可以推算出其他参数在栈中的位置,进而访问他们:
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;
}
把定位其他无名参数位置的代码可以写成宏,这样可以让代码看起来更简洁:
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;
}
c标准规定,对于变参函数,要对相应的实参做Integer Promotion,此外,相应的实参如果是float型的也要被提升为double类型,这条规则称为Default Argument Promotion。
参数中含有...
,属于变参函数,最常见的变参函数莫过于:
int printf(const char *format, ...);
参数提升的意思是,给printf传递char、short等类型时,在printf函数里面会得到一个int类型;给printf传递float类型时,在printf函数里面会得到一个double类型。
这也是为什么我们在上个例子中取float
要用va_arg(ap,double)
的原因。
我查了半天也没弄明白为什么C标准会定下这么奇怪的规定,没发现这个规定有啥用呀,反而写变参函数的时候一不小心就掉坑里了。
猜测可能是因为历史原因才有这条规定吧,我们自己写编译器完全没必要理会这条规定。
数组类型传参会自动转换为指针类型,这在语义检查时就完成了转换。
按 C 的语法要求,返回值不可以是数组类型。
结构体传参会跟int、float等类型一样,保存到栈中。
当结构体作为函数返回值时,情况比较特殊,例如:
typedef struct Data{
......
} dt;
dt = GetData();
如果结构体大小是{1,2,4,8},返回值同int、float一样,放入约定的寄存器中即可。
如果结构体大小不是{1,2,4,8},则C编译器会为函数添加结构体指针作为第1个参数,经 C 编译器处理后,真正执行的函数调用为:
dt = GetData(&dt);
为了简单,mcc不支持结构体作为返回值。