【浅谈】可变参数列表

【浅谈】可变参数列表

什么是可变参数列表?

  这里我在网上查了一些资料,发现并没有对可变参数列表做出的定义,那么我以我的理解给可变参数列表做出一个简单的定义(如果有不准确的地方,希望大家在留言区指出):
  可变参数列表:通过实现函数多个参数传递,使得函数的参数个数可以为一个到N个,这就是可变参数列表。
  

为什么需要可变参数列表?

  不知道大家有没有留意过在C语言学习过程中常用到的printf ( ) 这个函数,它的参数居然可以是一个到多个,而且参数类型可以不同,就如同下面的这几句代码,printf居然都通通搞的定,而平时我们写出的代码,却只能传递固定个参数。想必没有想到的同学被我这么提醒,也会对printf ( ) 的实现感到好奇吧?

    #include 

    int main()
    {

        printf("hello world\n");  //一个参数

        printf("hello ""world\n");  //一个参数

        printf("%s\n", "hello world");  //两个参数

        printf("%s %s\n", "hello", "world");  //三个参数

        return 0;
    }

  是不是很神奇?那么下面我将带领大家领略一下可变参数列表的神奇。
  

average的实现(求平均值函数)

  这里我希望先通过实现求平均值函数的这个过程让大家明白,可变参数列表究竟是怎么一回事。
  
  (注意:我是在VC 6.0编译器32位环境下调试运行的,因为VC 6.0作为早期编译器它的函数封装较为简单,在查看标准函数库函数时不会产生不必要的干扰)
  
  下面是average函数的代码,因为下面大家将要遇到很多以前没有见过的代码,我放出最终代码能让大家能够有一个对全局的概念,这样更容易理解我接下来的解析:

    #include 
    #include 

    int average(int number, ...)
    {
        va_list arg;
        int i = 0;
        int sum = 0;

        va_start(arg, number);
        for (i = 0; i < number; i++)
        {
            sum += va_arg(arg, int);
        }
        va_end(arg);

        return sum / number;
    }

    int main()
    {
        int ave = average(3, 2, 3, 7);
        printf("%d\n", ave);

        return 0;
    }

  想必大家看过这段代码后都一脸懵逼吧,average函数里面出现的va_list,_crt_va_start,_crt_va_arg,_crt_va_end都是些什么????
  (我觉得这些问号都无法解释你们满头的疑问)
  不过不要紧,接下来我会给大家逐步讲解这段代码,以及大家不认识的代码的作用。
  首先我们看函数定义部分:
  
 int average(int number, ...)
 

  上面的函数返回值类型(int),函数名(average),函数第一个参数(int number)相信大家都理解且写函数时经常写到。但是函数第二个参数位置确实一串 … ,这个可不读“省略号”!
  它其实是表示除了函数的第一个参数,后面的参数的个数和类型是未知。有了它,我们就可以编写接下来函数的实现内容了。
  
  va_list arg;
  

  我知道,下面函数中的这句代码大家还是看不懂(笑)。不要紧,我们可以在编译器中双击va_list然后右击它,选择转到定义(Go to definition)选项,我们可以看到这么一句话:
【浅谈】可变参数列表_第1张图片【浅谈】可变参数列表_第2张图片
  大家见到上图红框中的代码一定不陌生吧?
  没错,这是在重定义char* 这个类型,将他重定义成va_list,所以这里:
  
  va_list arg; //相当于 char* arg;
  

  有了这个思路,那么我相信大家对下面不认识的代码也就不那么畏惧了,只要查看对应代码的定义就可以很好的理解他们的作用。
  再看下面的代码,我们要求几个数的平均值,要想简便的写出代码,循环是不可少的一步,所以,这里定义了一个循环体条件变量 i ,以及中间变量 sum (用于求几个变量的和)。
  
  int i = 0;
  int sum = 0;
  

  紧接着下一句代码我们可能又不认识,没关系,继续转到定义:
  
  va_start(arg, number);
  

  这里写图片描述
  哎?看到这段代码,大家有没有发现,原来这个va_start居然是一个宏,为了便于理解这段代码的意思,我们首先将这段代码替换到我们的代码中去,这样便于我们理解:
  【浅谈】可变参数列表_第3张图片
  这里的 _INTSIZEOF我们还是不认识,我们接着转到它的定义:
  这里写图片描述
  哦?它也是个宏,这里的这个宏我们就不进行替换了,否则替换后不会便于我们理解这段代码。
  在这里,我为了让大家不那么痛苦的理解这个宏的作用,我不妨告诉大家,这个宏的作用是将传入的数字转化为4的整数倍输出,也就是说如果传入的是 1,2,3,4 它会输出 4 ,如果传入的是 5,6,7,8 它会输出 8 。是不是很有意思,大家肯定很想知道为什么需要这个宏呢?
  我们接着退出 _INTSIZEOF宏的定义,看替换后的代码,在main函数调用这个函数时,我们给average这个函数传递的第一个参数是 3 ,而这个number将 3 传入_INTSIZEOF宏,则_INTSIZEOF宏返回 4 ,而前面的 ( char * )&number将整形变量number的地址转化为char *指针,然后 + _INTSIZEOF宏返回值 4 。这个是什么意思呢?
  我们看下面的示意图,相信看过我栈帧的创建与销毁这篇文章的小伙伴应该会有(卧槽)的感叹吧?
  【浅谈】可变参数列表_第4张图片
  这个va_start宏的作用现在是不是有一点眉目了?哦~ 它的作用是将arg指针指向未知参数中的第一个参数去。这样我们就有了遍历其余未知参数的条件了!
那么有了未知参数,让我们开始循环将他们相加,然后除以参数个数输出吧:
循环体里我们又遇到了一个类似的代码va_arg,我们转到它的定义:
这里写图片描述
  可见va_arg也是一个宏,那么我们将它替换到原来的代码中:
  【浅谈】可变参数列表_第5张图片
  这段代码替换后可能有些难以理解,我们进行逐步分析,首先看红框中的代码,我们已经知道_INTSIZEOF宏的作用,将int类型传给他后,因为int类型定义的变量大小为 4 个字节,那么他将返回 4 个字节。我们再将_INTSIZEOF宏替换为 4 。这样是不是就容易看懂一点了?
  这里写图片描述
  (将arg这个指针所指向的内容加 4 ,然后将[arg - 4]的值转化成 int * 类型再用解引用操作符解引用,得到这片空间的值)如果大家对这段话很不能理解的话,可以看我下面的示意图,希望对你们的理解有帮助:
【浅谈】可变参数列表_第6张图片
  这段代码很优雅,它在使arg这个指针指向下一个未知参数的同时读取了原来位置上的参数。希望经过我的示意图以及这两段加粗的文字,大家能理解这段代码的意思。
  
  这个循环体使得va_arg这个宏每取到一个值,就加到中间变量sum中去。

  循环结束后,下面是我们最后看到的不熟悉的代码:
  
  va_end(arg);
  

  做法照旧,我们依然转到它的定义:
  这里写图片描述
  这里的va_end依然是一个宏,((va_list)0)相信大家知道这是在将整形 0 转化为字符型指针,然后赋值给arg。也就是使指针arg指向0x00000000地址处,这条语句相当于:
  这里写图片描述
  OK,讲到这里,我相信大家都能稍微理解可变参数列表了吧?
  试着运行一下这段代码吧,看看效果:
  【浅谈】可变参数列表_第7张图片
  

注意:如果大家在其他版本的编译器上查看va_list等代码的定义时,转到定义时改选项是灰色的。这里有两种可能:
  
  1.在你的编译器版本下va_start更名为_crt_va_start,需要使用这个宏并引出他们的头文件。
  2.有可能是编译器的问题,这里你需要关闭解决方案,删除掉这个工程下的sdf文件,然后再打开这个文件。
  3.如果这两种方法都无法解决你的问题的话,请重装电脑(笑),开个小玩笑,如果上面两种方法无法解决的话,你就上网请求帮助吧~

全文完,感谢浏览

你可能感兴趣的:(C语言)