可变参数问题研究

什么是可变参数?

可变参数就是指参数个数不确定,举一例即可明白。

  • 实现一个函数计算整数和,形式如下
int sum(int a,...)   //被调用的函数,可变的部分用...来表示
{
    //....具体操作
}
sum(10,20);          //调用函数形式1
sum(10,20,30);       //调用函数形式2
sum(10,20,30,40);    //调用函数形式3

如代码所示,无法确定究竟传进来多少个参数,可能2个,3个,4个等等,但是却要求sum函数针对各种可能都能求得结果。

函数堆栈

C语言函数的参数是从右往左压入堆栈,有以下几个特点:

  • 栈底在高地址,栈顶在低地址,增长方向是从高字节->低字节
  • 入栈的次序是从右往左

这样说有点抽象,最好的方式就是观察内存,实践一下一目了然。

先写一个程序

#include "stdio.h"
void sum(int a,...)
{
    char *c=(char *)&a;
}                            //运行到此处
int main()
{
    sum(10,20,30,40);        //断点,F11进入函数
    return 0;
}

调试的内存数据如下:

根据上面的推论,分别为40,30,20,10四个数依次入栈,因此40处于最高字节,10处于最低字节,这验证了前面的结论。

手动计算结果

现在先用正常的思路来计算结果,四个整数既然已经入栈,并且第一个参数的地址可以得到(通过强制类型转换),那么顺着这条线往下挨个找到每个数是没有问题的。

代码:

#include "stdio.h"
void sum(int a,...)
{
    char *c=(char *)&a;            //第一个参数的地址,即处于栈顶位置(最低位置)的地址(也即本例中整数10的地址);
    printf("%d
",*((int *)c));
    c+=sizeof(int);                //手动计算,地址+4指向第二个参数,得到参数20
    printf("%d
",*((int *)c));
    c+=sizeof(int);                //手动计算,地址+4指向第三个参数,得到参数30
    printf("%d
",*((int *)c));
    c+=sizeof(int);                //手动计算,地址+4指向第四个参数,得到参数40
    printf("%d
",*((int *)c));
}
int main()
{
    sum(10,20,30,40);        //断点,F11进入函数
    return 0;
}

打印的结果分别是:10,20,30,40,说明手工获取每一个参数是可以的。

从上面可以看出,理解可变参数的问题在于知道参数是如何传入堆栈的,然后顺着把堆栈的每个数取出来即可,一句话概括就是:把堆栈里面的数据顺序取出来

衍生问题:字符和字符串是如何入栈的?

整型数据是直接数据入栈的,那么字符和字符串呢?根据程序验证可知

  • 字符依然是数据直接入栈,并且占用4个字节(对齐)
  • 字符串入栈的却是字符串地址

试验程序如下:

#include "stdio.h"
void sum(int a,...)
{
    char *c=(char *)&a;            //第一个参数的地址
}
int main()
{
    sum(10,20,'c',30,"test");    //断点,F11进入函数,传入五个参数,包括整数,字符和字符串
    return 0;
}

调试内存数据:

内存数据说明,结论是正确的。(对于其它类型的数据没有调试,有兴趣的自己试验)

回到计算整数和问题

前面已经总结了什么是可变参数,函数入栈的概念,以及整型,字符和字符串等入栈的方式,这些知识非常重要,一点一滴汇集然后来解决大问题。
回到问题,我们现在能做到:得到各个参数值。下面还剩下唯一的一个问题就是没法确定参数的个数

于是,我们采用一个边界限制一下,比如参数传进去一个END,如果得到的整数值等于这个END,说明传入的参数已经结束。

程序如下:

#include <stdio.h>

#define END -1

int sum (int first, ...)
{
    char * ap=(char *)&first;     //ap指向第一个数据的地址
    int result = first;           //和初始化等于第一个数据
    int temp = 0;

    for(;;)                       //循环相加一直到尾部
    {
        ap+=sizeof(int);          //指向第二个数据
        temp=*ap;                 //得到数据
        if(temp != END)           //判断边界,如果没有到尾
        {
            result+=temp;            
        }
        else
            break;
    }

    ap=(char *)NULL;              //指针置为空;
    return result;
}

int main ()
{
    int result = sum(1, 2, 3, 4, 5, END);
    printf ("%d", result);
    return 0;
}

 到了这一步,终于解决开头提出的问题,如何用一个函数sum计算不定长的参数和,虽然实现的有点简陋,但基本说明了不定长参数的解决样式。

三个宏

有了前面一系列通俗的解释,我们了解了不定长参数的解决方式,可以大致的总结一下:

  • 得到第一个参数的地址
  • 根据第一个参数的地址往下得到第二个参数的地址(并获取值),因为不管参数是什么,入栈的结果都是四个字节,整数、字符或者字符串的32位指针等等。
  • 依次类推,一直获取到参数的结尾

现在来看几个库中的宏

typedef char *  va_list;  //类型   
#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 )

这些宏的作用就是把前文中简陋的手工做法用宏来实现,效果相同。所以根据前面最基础的方式,一步一步的推导到现在就能理解这三个宏的用法,也就是知其所以然。

一、va_start(ap,v)

v:函数中的定位参数,可能是函数第一个参数,也可能是中间某一个。这个参数可能是整型、字符或者字符串(后面会逐步说明)
比如:
sum(int a,int b,int c,int d,...);
va_start(ap,c);  //执行之后,定位的开始参数为第三个参数c,那么ap就指向它的下一个参数d.
ap:等于定位参数的下一个参数的地址

因此整个宏的作用就是让ap指向v之后的参数。

宏实现中还有一个宏_INTSIZEOF(v),这个宏有点难,关于这个宏的详细内容可以参看这篇博客:
http://www.cnblogs.com/diyunpeng/archive/2010/01/09/1643160.html
其中牵涉到一些数学知识,*注: 凡是编程上稍难的东西都可能牵涉到数学。

二、va_arg(ap,t)

返回ap指向的参数值,难点同上。

三、va_end(ap)

将ap置空。

将手工方法改成宏的方法

上面说明了为什么会出现宏,其实是为了方便,比手工书写的严谨,但是原理是相同的。

现在将前面的代码改为宏的实现方式并做下对比:

#include <stdio.h>
#include "stdarg.h"

#define END -1

int sum (int first, ...)
{
    char *ap;
    va_start(ap,first);            //ap指向第二个参数;
    int result = first;            //和初始化等于第一个数据
    int temp = 0;

    for(;;)                        //循环相加一直到尾部
    {
        temp=va_arg(ap,int);      //得到当前参数值并使ap指向下一个参数
        if(temp != END)           //判断边界
        {
            result+=temp;
        }
        else
            break;
    }

    va_end(ap);
    return result;
}

int main ()
{
    int result = sum(1, 2, 3, 4, 5, END);
    printf ("%d", result);
    return 0;
}

printf的实现

前面的内容系统说明了可变参数的问题,实现了一个简易的求和函数,现在研究一下printf,看其形式:
printf(".%s,%d.. ",a,b,c,d);

事实上和前面的sum函数很相似,区别在于printf函数的第一个参数是字符串,后面接着的是各个具体参数。我们可以看出,最关键的部分在于前面第一个字符串参数,因为后面有多少个参数,每个参数具体又是什么类型的值都是由前面的这个字符串来指定。

比如:%d,说明后面打印的是整型,%s,说明后面打印的是字符串。

因此我们需要做的是把第一个字符串参数的每一个字符进行遍历,算法为:

一、假如字符不是%,就往后继续。
二、假如字符是%,则判断后面接着的这个字符是什么,比如是'c','d',或's'等。

看下代码:

#include "stdio.h"
#include "stdarg.h"
void print(char* fmt, ...)
{
    char* pfmt = NULL;
    va_list ap;
    va_start(ap, fmt);            //ap指向第一个参数(字符串之后的)
    pfmt = fmt;                    //pfmt指向字符串首地址,准备用它遍历

    while (*pfmt)
    {
        if (*pfmt == '%')        //遇到%号
        {
            switch (*(++pfmt))    //判断%号后面的符号
            {
            case 'c':
                printf("%c
", va_arg(ap, char));
                break;
            case 'd':
                printf("%d
", va_arg(ap, int));
                break;
            case 's':
                printf("%s
", va_arg(ap, char *));
                break;
            default:
                break;
            }
            pfmt++;
        }
        else
        {
            pfmt++;
        }
    }
    va_end(ap);
}
int main()
{
    print("test:%d,%s", 20, "字符串测试");
    return 0;
}

有两个需要说明的问题:

一、print模拟printf函数,关键在于对前面的字符串进行遍历,根据遍历结果进行后续处理。
二、后面又借用printf()函数打印只是为了说明问题,可以自行设计函数。

/*****待续*****/

原文地址:https://www.cnblogs.com/tinaluo/p/8108595.html