参数可变的函数,形如printf(const char *, ...),原理与用法详解

原理非常简单,无非就是从栈中取出数据而已,为了实现这一目的,必须通过第1个参数指定后续参数的数目和类型,这样我们才能解析出栈中的数据。这也就是为什么参数可变的函数,都至少带着一个有名形参,例如printf(const char* fmt, ...),这个有名形参由两个作用:

(1)用来指出后续形参的数目和类型,例如printf函数就是通过%X的形式来指定的,有多少个%就有多少个后续参数,参数的类型由%后面的标识符来指定,例如%c,%d,%X。实际上fmt字符串就是一套标准化的传输协议而已,你完全可以自定义一套你自己的实参解析协议。

(2)用来提供本函数的栈的首地址,只有借助这个首地址,才能根据偏移取出后续参数。换句话说,如果你拿不到本函数运行时栈的地址,根本就无法取参。

注意:不是所有的实参都压栈,有些实参被压到了寄存器中,到底有“多少个实参入寄存器、多少的实参入栈?”,这个问题跟硬件平台有关,也跟编译器有关,也跟操作系统的位数等等诸多因素有关。为了保证程序取参的兼容性,建议不要手动取参,而要采用C库函数va_start等来取。https://www.jb51.net/article/93490.htm 《浅谈C语言函数调用参数压栈的相关问题》

下面是一个测试程序:

#include 
#include 
#include 

/*
功能:打印内存中的数据
形参:提示信息s,要打印的内存首地址p,要打印的字节数len
*/
void print_mem(const char *s, void *p, int len)
{
    printf("%s", s);
    for(int i = 0; i < len; i++)
    {
        printf("%02X ", ((uint8_t*)p)[i]);
        if(0 == (i+1)%4)
        {
            printf("| ");
        }
    }
    printf("\r\n");
}


/*
功能:用两种方法从栈中取出实参
形参: para_cnt为后置参数的数目,para1, para2, para3...为匿名参数
*/
void test1(int para_cnt, ...)
{
    int para1;
    char para2;
    double para3;
    float para4;

    //打印实参内存
    print_mem("", ¶_cnt, 4+4+4+8+30);

    //方法1:纯手动从栈中取出实参
    char* stackPtr = (char*)¶_cnt;
    printf("para_cnt = %d\r", *(int*)stackPtr);

    /* para1的地址 = para_cnt的地址+sizeof(para_cnt) */
    stackPtr += sizeof(int);
    para1 = *(int*)stackPtr;

    /* para2的地址 = para1的地址+sizeof(para1) */
    stackPtr += sizeof(int);
    para2 = *(char*)stackPtr;

    /* para3的地址 = para2的地址+sizeof(para2) */ //注意,每次压栈都是int的整数倍,所以sizeof(char)应替换为sizeof(int)
    stackPtr += sizeof(int);//sizeof(char);
    para3 = *(double*)stackPtr;

    /* para4的地址 = para3的地址+sizeof(para3) */
    stackPtr += 8;//32 sizeof(double)//这个偏移要改成32才能正确取出float(见打印的内存),原因未知。而且,库函数和我手动取出的数据同样是错的
    para4 = *(float*)stackPtr;

    printf("手动取出的实参  : %d, %c, %f, %f\r", para1, para2, para3, para4);

    //方法2:使用C库函数从栈取出实参
    va_list ptr;//等价于char * ptr;
    va_start(ptr, para_cnt);
    para1 = va_arg(ptr, int);
    para2 = va_arg(ptr, char);
    para3 = va_arg(ptr, double);
    para4 = va_arg(ptr, float);
    va_end(ptr);//本质上就是ptr = NULL,显然,这一句有和没有对程序没啥影响
    printf("库函数取出的实参: %d, %c, %f, %f\r", para1, para2, para3, para4);

}

void test2(int para_cnt, int para1, char para2, double para3, float para4)
{
    print_mem("", ¶_cnt, 4+4+4+8+4);
    printf("实参的栈地址:\r");
    printf("¶_cnt = %d\r", ¶_cnt);
    printf("¶1 = %d\r", ¶1);
    printf("¶2 = %d\r", ¶2);
    printf("¶3 = %d\r", ¶3);
    printf("¶4 = %d\r", ¶4);
    printf("每个实参所占的栈空间/B:\r");
    printf("para_cnt: %d\r", (char*)¶1 - (char*)¶_cnt);
    printf("para1: %d\r", (char*)¶2 - (char*)¶1);
    printf("para2: %d\r", (char*)¶3 - (char*)¶2);
    printf("para3: %d\r", (char*)¶4 - (char*)¶3);
}

void main(void)
{
    printf("start, sizeof(int) = %d\r\n", sizeof(int));

    int para_cnt =4;
    int para1 = 15;
    char para2 = 'E';
    double para3 = 3.141592;
    float para4 = 3.1415926f;

    print_mem("para3 mem= ", ¶3, 8);
    print_mem("para4 mem= ", ¶4, 4);

    test1(para_cnt, para1, para2, para3, para4);
    test2(para_cnt, para1, para2, para3, para4);
}

程序输出如下:

start, sizeof(int) = 4
para3 mem= 7A 00 8B FC | FA 21 09 40 | 
para4 mem= DA 0F 49 40 | 
04 00 00 00 | 0F 00 00 00 | 45 00 00 00 | 7A 00 8B FC | FA 21 09 40 | 00 00 00 40 | FB 21 09 40 | 7A 00 8B FC | FA 21 09 40 | 04 00 00 00 | 0F 00 00 00 | DA 0F 49 40 | 1C FE 
para_cnt = 4
手动取出的实参  : 15, E, 3.141592, 2.000000
库函数取出的实参: 15, E, 3.141592, 2.000000
04 00 00 00 | 0F 00 00 00 | 45 00 00 00 | 7A 00 8B FC | FA 21 09 40 | DA 0F 49 40 | 
实参的栈地址:
¶_cnt = 2686440
¶1 = 2686444
¶2 = 2686448
¶3 = 2686452
¶4 = 2686460
每个实参所占的栈空间/B:
para_cnt: 4
para1: 4
para2: 4
para3: 8

经过测试发现,不管是手动取数,还是使用库函数取数,都有bug,原因未知。但是原理就是这样了,而且我们发现,手动取数和库函数取数,出的错误都是一样的,这至少说明,我写的手动取数的方法,和库函数的实现原理是一样的

你可能感兴趣的:(C/C++)