可变参数函数原理

可变参数列表

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

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;
}

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

int GetMax(int num,...)
{

}

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

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

image-20230930115316155

int main()
{
  int a = 10;
  int b = 20;
  printf("%d\n", a);                     //可以打印一个
  printf("%d %d\n", a,b);            //也可以打印两个
  return 0;
}

可变参数原理

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

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

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

image-20230930120048618

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

可变参数使用

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

va_list 
va_start
va_arg
va_end   

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

#include <stdarg.h>

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;
}

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))

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

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) )

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

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

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

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

#include <stdarg.h>

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;
}

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

va_arg

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

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

这里我们可以很容易的发现,在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) )

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

_INTSIZEOF

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

#define _INTSIZEOF(n) ( (sizeof(n) + sizeof(int) - 1) & ~(sizeof(int) - 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) ).

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

玄鸟轩墨

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值