zoukankan      html  css  js  c++  java
  • 可变参数问题研究

    什么是可变参数?

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

    • 实现一个函数计算整数和,形式如下
    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()函数打印只是为了说明问题,可以自行设计函数。

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

  • 相关阅读:
    操作符详解(思维导图)
    数组(C语言、思维导图)
    函数(C语言、思维导图)
    分支语句与循环语句(知识点思维导图)
    单链表及其基本操作
    顺序表
    时间复杂度与空间复杂度
    javascript基础知识show
    Java中的四舍五入
    JavaScript中数组迭代方法(jquery)
  • 原文地址:https://www.cnblogs.com/tinaluo/p/8108595.html
Copyright © 2011-2022 走看看