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()函数打印只是为了说明问题,可以自行设计函数。

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

  • 相关阅读:
    ios 数据类型转换 UIImage转换为NSData NSData转换为NSString
    iOS UI 12 block传值
    iOS UI 11 单例
    iOS UI 08 uitableview 自定义cell
    iOS UI 07 uitableviewi3
    iOS UI 07 uitableviewi2
    iOS UI 07 uitableview
    iOS UI 05 传值
    iOS UI 04 轨道和动画
    iOS UI 03 事件和手势
  • 原文地址:https://www.cnblogs.com/tinaluo/p/8108595.html
Copyright © 2011-2022 走看看