C|图文深入理解实现变参函数的4个宏和栈帧机制

回复 星标
更多

C|图文深入理解实现变参函数的4个宏和栈帧机制

C能够实现变参,在于其默认的函数调用约定__cdecl。关于函数的调用约定,请参照:

C/C++|图文深入理解函数调用的5种约定

实现变参的关键在于由第一个参数去推断其它参数的位置(指针偏移)。

根据函数调用约定,参数由右至左压入,函数的第一个参数的最后压入的,其地址相对比较固定,对于32位平台来说(下同),一般就是加8或加12(如果函数返回是复合类型):

513430

例如要调用下面函数:

printf("%dyear,%c2%c %s",020,'b','c',"b2b");

确定了第一个参数的位置后(第一个参数是一个字符串地址),由字符串中的字符“%”后面所跟的字母便可以确定其它变量的类型和长度以及相对偏移位置。

printf函数由四个宏来实现,这4个宏就要实现上述功能:

typedef int* va_list1; // 栈按字长对齐(32位的字长是4字节,int是一个字长)#define va_start1(ap, fmt) (ap = (int *)&(fmt) + 1) // fmt是第1个参数(存放字符串指针)                            // 转换成int*在算术运算时可以按sizeof(int)偏移,                            // +1 让ap得到第2个参数的压栈位置#define va_arg1(ap, T)     (*(T *)ap++) // ap类型转换及按转换的类型偏移1个位置#define va_end1(ap)        ((void)0)    // 指针置空

32位平台的字长就是int类型的长度,也是指针的长度。

首先将字符串的地址转换为int*指针,偏移一个指针长度(+1操作)得到字符串后面参数的地址。这就是va_start1宏的功能。

然后将获得的地址实现类型强转为T*,这个T类型是由字符“%”后面所跟的字母所确定的类型,并通过“++”实现指针移动。

看下面spritf()的简略版实现(细节见注释):

//va及自定义printf函数#include<stdio.h>  //put()//#include<stdarg.h>typedef int* va_list1; // 栈按字长对齐(32位的字长是4字节,int是一个字长)#define va_start1(ap, fmt) (ap = (int *)&(fmt) + 1) // fmt是第1个参数(存放字符串指针)                            // 转换成int*在算术运算时可以按sizeof(int)偏移,                            // +1 让ap得到第2个参数的压栈位置#define va_arg1(ap, T)     (*(T *)ap++) // ap类型转换及按转换的类型偏移1个位置#define va_end1(ap)        ((void)0)    // 指针置空char myitoa(unsigned long a, char * buf){    unsigned long num = a;    int i = 0;    int len = 0;        do     {        buf[i++] = num % 10 + '0';        num /= 10;            } while (num);    buf[i] = '\0';        len = i;    for (i = 0; i < len/2; i++) // 字符逆转    {        char tmp;        tmp = buf[i];        buf[i] = buf[len-i-1];        buf[len-i-1] = tmp;    }        return len;    }char itoa16(unsigned long a, char * buf){    char index[]="0123456789ABCDEF";    unsigned long num = a;    int i = 0;    int len = 0;    for(i = 0; i < 8; i++)    {        buf[7-i] = index[num % 16];                num /= 16;    }    buf[8] = '\0';    len = 8;    return len;    }int mysprintf(char *str, const char * format, va_list1 ap) // 输出变量str{    int i;    char c;        char *ptr = str;        char ch;    char * p;    unsigned long a;    char buf[20]={'\0'};    while ((c = *format++) != '\0')    {        switch (c)        {            ch = 0;            a  = 0;        case '%':            c = *format++;            switch (c)            {                             case 'c':                ch = va_arg1(ap, int); // ap强转为另一个参数的类型,并由其长度确定偏移量                *ptr++ = ch;                break;            case 's':                p = va_arg1(ap, char *);                for(; *p != '\0'; p++)                    *ptr++ = *p;                break;                                case 'x':                a = va_arg1(ap, int);                itoa16(a,buf);                                        p = buf;                for(; *p != '\0'; p++)                    *ptr++ = *p;                break;                    case 'd':                a = va_arg1(ap, int);                i = myitoa(a,buf);                p = buf;                for(; *p != '\0'; p++)                    *ptr++ = *p;                break;                                default:                break;            }                            break;                                default:                *ptr++ = c;                break;        }    }    *ptr = '\0';    return 0;    }void myprintf(const char *fmt,...){    va_list1 ap;       //与平台一致的指针类型    char string[100];    va_start1(ap, fmt);   // 初始化后pa指向fmt地址的后一个位置    mysprintf(string, fmt, ap); //     puts(string);    va_end1(ap);// 指针变量ap置空}int main(){    char str[66];    int arr[3] = {33,'b','c'};    //mysprintf(str,"%d,%c2%c",arr);     //printf("%s\n",str);//可通过调试,watch查看str到33,b2c    myprintf("%dyear,%c2%c %s",2020,'b','c',"b2b");    getchar();    return 0;}//2020year,b2c b2b

函数调用栈帧示意图:

513430

-End-

此帖已被锁定,无法回复
新窗口打开 关闭