• 可变参数函数原理


    可变参数列表

    我们想通过一个例子来引出我们这个话题.如果我们想要求两个数的最大值,这个函数是非常容易的.

    int GetMax(int x, int y)
    {
          if (x > y)
          {
             return x;
          }
          return y;
    }
    
    int main()
    {
         int a = 10;
         int b = 20;
         int max = GetMax(a, b);
         printf("max = %d\n", max);
         return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    但是突然有一天你想求10个、20个…数中的最大值,请问我们应该如何做,此时我们想很简单,我们是可以使用一个数组,可是有一天我们不想使用数组,就是想通过传入参数额形式来让我们完成,这就需要可变参数了.我们先来看一下,不用担心,后面都会和大家分析到.

    int GetMax(int num,...)
    {
    
    }
    
    • 1
    • 2
    • 3
    • 4

    先来说一下定义.在计算机程序设计,一个可变参数函数是指一个函数拥有不定引数,即是它接受一个可变数目的参数不同的编程语言对可变参数函数的支持有很大差异。 一般而言,在设计函数时会遇到许多数学和逻辑操作,是需要一些可变功能。例如,计算数字串的总和、字符串的联接或其他操作过程,都可以存在任意数量的参数

    我们是不是在之前使用过可变参数函数,是的,我们确实使用过,例如我们上文的格式化输入输出.

    image-20230930115316155

    int main()
    {
      int a = 10;
      int b = 20;
      printf("%d\n", a);                     //可以打印一个
      printf("%d %d\n", a,b);            //也可以打印两个
      return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    可变参数原理

    先来说一下可变参数的原理,我们知道当我们在函数调用进行传入参数的时候,请问我们参数是否会形成临时拷贝?一定会的,不仅仅会形成临时拷贝,而且是从右向左一个一个拷贝的,甚至VS系列中参数空间位置是紧邻的.

    int main()
    {
    	int max = FindMax(5, 1, 2, 3, 9, 7);
    	printf("max = %d\n", max);
    	system("pause");
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    他的栈帧地址我们可以这样的画.

    image-20230930120048618

    这里我们开始解释我们可变参数的原理,我们也是可以通过栈帧来找到了每一个参数,试想一下,如果我们拿到了一个参数的地址,按照某种特定的规则,我们就可以得到所有的参数.

    可变参数使用

    根据上面的说法,我们可以自己实现一个寻找参数的机制,不过C语言已经早就帮助我们实现好了,这里我们看一下.

    va_list 
    va_start
    va_arg
    va_end   
    
    • 1
    • 2
    • 3
    • 4

    首先这里面是四个宏,下面我们先说一下他们的用法,然后解释一下功能,最后基本的刨析原理.多的不说,我们先来看一下结果,后面和大家进行分析.

    #include 
    
    int FindMax(int num, ...)
    {
    	va_list arg;
    	va_start(arg, num);
    
    	int max = va_arg(arg, int);
    	for (int i = 0; i < num - 1; i++)
    	{
    		int x = va_arg(arg, int);
    		if (max < x)
    		{
    			max = x;
    		}
    	}
    	va_end(arg);
    	return max;
    }
    
    int main()
    {
    	int max = FindMax(5, 1, 2, 3, 9, 7);
    	printf("max = %d\n", max);
    	system("pause");
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27

    image-20230930120948651

    va_list : 这是一个类型, 被重命名过  typedef char* va_list;
    va_start: 这个暂停说下
    va_arg  : 这个也暂停说
    va_end  : 这个是只为空指针 
              #define va_end   __crt_va_end
              #define __crt_va_end(ap)        ((void)(ap = (va_list)0))
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    上面我们说了,既然我们是函数,那么函数的参数一定会被压入栈中,那么对于可变参数,由于我们的参数的个数是不确定的,所以我们必须手动传入的参数作为我们标识我们的参数有多少个,这就是我们第一个参数的值,所以我们绝对有能力知道所有的参数.

    va_list就是一个类型,本质就是char* 的指针,至于为何是char类型的指针,这是因为他加上1只会跳过一个字节.我们想要这个指针指向我们的实际的参数位置,这里就是我们的va_start作用了.看一下他的定义.

    #define va_start _crt_va_start
    #define _crt_va_start(ap,v) ( ap = (va_list)_ADDRESSOF(v) + _INTSIZEOF(v) )
    
    • 1
    • 2

    这里来解释一下_ADDRESSOF,这个就是给知道我们的世家的参数的位置提供的供能,具体等一下我们的谈.

    这个我们暂停一下,先说结论,他的作用就是计算4的的数,如果可以被4整除,可以的,但是如果不能,那么就计算最小的可以被数.例如7不可以被4整除,那么计算结果是8就可以了.

    #define _INTSIZEOF(n) ( (sizeof(n) + sizeof(int) - 1) & ~(sizeof(int) - 1) 
    
    • 1

    这里我们就可以解决下面的问题.

    #include 
    
    int FindMax(int num, ...)
    {
    	va_list arg;
    	va_start(arg, num);
    
    	int max = va_arg(arg, int);
    	for (int i = 0; i < num - 1; i++)
    	{
    		int x = va_arg(arg, int);
    		if (max < x)
    		{
    			max = x;
    		}
    	}
    	va_end(arg);
    	return max;
    }
    
    int main()
    {
    	int max = FindMax(5, 'a', 'b', 'c', 'd', 'e');
    	printf("max = %d\n", max);
    	system("pause");
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27

    image-20230930122955339

    我们知道,寻找我们参数的时候,我们需要知道两个最关键在的元素.

    • 参数地址
    • 参数的大小

    其中参数地址我们已经使用宏可以很容易的找到,我们也可以接受每一个参数的大小编译器也可以帮助我们做好,但是这里存在一个问题,我们的传入的是char,在计算的时候确实int,这里给我们很大的疑惑.其实我们这里只需要看一下我们的的汇编代码我们就可以明白了.在VS2013开发环境下看一下.

    image-20230930131709022

    看一下他的这条指令的解释,本质上可以理解为整型提升.

    MOV BL,80H
    MOVSX AX,BL
    运行完以上汇编语句之后,AX的值为FF80H。由于BL为80H=1000 0000,最高位也即符号位为1,在进行带符号扩展时,其扩展的高8位均为1,故赋值AX为1111 1111 1000 0000,即AX=FF80H。
    
    • 1
    • 2
    • 3

    通过查看汇编,我们看到,在可变参数场景下:

    1. 实际传入的参数如果是char,short,float,编译器在编译的时候,会自动进行提升(通过查看汇编,我们都能看到)
    2. 函数内部使用的时候,根据类型提取数据,更多的是通过int或者double来进行

    va_arg

    这里我们还要明白一件事情,我们找到了实际的参数的时候,指针需要一步步的遍历的我们的所有的参数,也就是这里我们需要知道两个内容

    • 拿到一个参数的完成的内容
    • 移动指针到下一个参数那里
    #define _crt_va_arg(ap,t) ( *(t *)((ap += _INTSIZEOF(t)) - _INTSIZEOF(t)) )
    
    • 1

    这里我们可以很容易的发现,在VS2013中,这里我们先移动的指针,让后让指针回指到原来的位置,这样就可以得到一整个的元素的内容,并且指针也被更新了.

    计算规则

    下面我们学下一下前面我们并没有仔细谈到的两个计算公式.

    _ADDRESSOF

    这个主要在va_start中使用.

    image-20230930130429542

    #define va_start _crt_va_start
    #define _crt_va_start(ap,v) ( ap = (va_list)_ADDRESSOF(v) + _INTSIZEOF(v) )
    
    • 1
    • 2

    他的作用可以理解找到我们可变参数第一个参数也就是num的地址,然后将ap指针移动到我们的实际参数的元素的位置.

    _INTSIZEOF

    这个宏才是比较的困难的,上面我们说了找4的倍数.

    #define _INTSIZEOF(n) ( (sizeof(n) + sizeof(int) - 1) & ~(sizeof(int) - 1) )
    
    • 1

    为了后面方便表述,我们假设sizeof(n)的值是n(char 1,short 2, int 4)我们在32位平台,vs2013下测试,sizeof(int)大小是4,其他情况我们不考虑_INTSIZEOF(n)的意思:计算一个最小数字x,满足 x>=n && x%4==0,其实就是一种4字节对齐的方式.

    • 比如n是:1,2,3,4 对n进行向 sizeof(int) 的最小整数倍取整的问题 就是 4
    • 比如n是:5,6,7,8 对n进行向 sizeof(int) 的最小整数倍取整的问题 就是 8

    那么他是如何办到的.既然是4的最小整数倍取整,那么本质是:x=4*m,m是具体几倍。对7来讲,m就是2,对齐的结果就是8而m具体是多少,取决于n是多少.如果n能整除4,那么m就是n/,如果n不能整除4,那么m就是n/4+1.上面是两种情况,如何合并成为一种写呢?
    ( n+sizeof(int)-1) )/sizeof(int) -> (n+4-1)/4

    如果n能整除4,那么m就是(n+4-1)/4->(n+3)/4, +3的值无意义,会因取整自动消除,等价于 n/4,如果n不能整除4,那么n=最大能整除4部分+r,1<=r<4 那么m就是 (n+4-1)/4->(能整除4部分+r+3)/4,其中4<=r+3<7 -> 能整除4部分/4 + (r+3)/4 -> n/4+1

    搞清楚了满足条件最小是几倍问题,那么,计算一个最小数字x,满足 x>=n && x%4==0,就变成了((n+sizeof(int)-1)/sizeof(int))[最小几倍] * sizeof(int)[单位大小] -> ((n+4-1)/4)*4这样就能求出来4字节对齐的数据了,其实上面的写法,在功能上,已经和源代码中的宏等价了.

    ((n+4-1)/4)* 4,设w=n+4-1, 那么表达式可以变化成为 (w/4)*4,而4就是22,w/4,不就相当于右移两位吗?,再次*4不就相当左移两位吗?先右移两位,在左移两位,最终结果就是,最后2个比特位被清空为0!也就是w & ~3 .所以就变成了(n+4-1) & ~(4-1)那么我们的结果就出来得了 (sizeof(n) + sizeof(int) - 1) & ~(sizeof(int) - 1) ).

  • 相关阅读:
    【数据库】将excel数据导入mysql数据库
    vxe-table 列表过滤踩坑_vxe-table筛选
    intel深度相机 D455及D4系列入门教程(逐行代码讲解)
    修改密码复杂度
    【技巧】各编辑器基础开发快捷键
    前端,CSS,背景颜色跟随轮播图片改变而改变(附源码)
    docker 服务自动重启
    vue富文本编辑器wangeditor输入空格回车的必填判断
    java mock单测
    【无标题】
  • 原文地址:https://blog.csdn.net/m0_61334618/article/details/133432083