当前位置:主页 > 软件编程 > C代码 >

C语言可变参数列表的用法与深度剖析

时间:2022-11-09 09:13:41 | 栏目:C代码 | 点击:

前言

可变参数列表,使用起来像是数组,学习过函数栈帧的话可以发现实际上他也就是在栈区定义的一块空间当中连续访问,不过他不支持直接在中间部分访问。

声明: 以下所有测试都是在x86,vs2013下完成的。

一、可变参数列表是什么?

在我们初始C语言的第一节课的时候我们就已经接触了可变参数列表,在printf的过程当中我们通常可以传递大量要打印的参数,但是我们却不知道他是如何做到的,今天就带大家剖析它。

二、怎么用可变参数列表

首先我们要引入windows.h的头文件

然后我们先要介绍以下几个宏。在这里我们先简述它的功能,在后面会有详细的讲解,这里是为了方便大家入门。

typedef char* va_list;  //类型的重定义

#define _ADDRESSOF(v) (&(v))//一个取地址的宏。

1._ADDRESSOF:取传入变量的地址。

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

2._INTSIZEOF:该宏功能是让n的类型往4的倍数上取整。

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

下面一段代码进行解释:

#pragma pack(1)//设置默认对其数为1
struct A
{
	char ch[11];
};

int main()
{
	printf("int : %d\n", _INTSIZEOF(int));
	printf("double: %d\n", _INTSIZEOF(double));
	printf("short: %d\n", _INTSIZEOF(short));
	printf("float: %d\n", _INTSIZEOF(float));
	printf("long long int: %d\n", _INTSIZEOF(long long int));
	printf("struct A:%d\n", _INTSIZEOF(struct A));
	return 0;
}

结果:

3.__crt_va_start_a:取变量v的地址强转为char*然后向指向v类型对其数后,即找到第一个可变参数列表当中的变量!

#define __crt_va_start_a(ap, v) \
((void)(ap = (va_list)_ADDRESSOF(v) + _INTSIZEOF(v)))

4.__crt_va_arg:将ap提前指向下一个要访问的位置,并且返回当前访问的内容。 注意+=后ap指向下一个要访问的地址,但是返回的内容是当前的。

#define __crt_va_arg(ap, t)   \
      (*(t*)((ap += _INTSIZEOF(t)) - _INTSIZEOF(t)))

5.__crt_va_end:将ap置成NULL。

#define __crt_va_end(ap)\
 ((void)(ap = (va_list)0))

紧接着我们看一下以下几个定义。

#define va_start __crt_va_start
#define va_arg   __crt_va_arg
#define va_end   __crt_va_end

测试:找一组不存放在数组当中的最大的一个数据返回。

int Find_Max(int num, ...)
{
	//定义一个char* 的变量arg
	va_list arg;
	//将arg指向第一个可变参数
	va_start(arg, num);
	//将max置成第一个可变参数,然后arg指向下一个可变参数
	int max = va_arg(arg, int);
	//循环num-1次,访问完剩下的可变参,找到最大的赋值给max
	for (int i = 1; i < num; ++i)
	{
		int r;
		if (max < (r = va_arg(arg, int)))
		{
			max = r;
		}
	}
	//将arg指针变量置成NULL,避免野指针
	va_end(arg);
	return max;
}
int main()
{
	int ret = Find_Max(5, 0x1, 0x2, 0x3, 0x4, 0x5);
	printf("ret :%d\n", ret);
	return 0;
}

结果:

三、对于宏的深度剖析

虽然在Linux下的进程地址空间是由高到低排列的,但是由于vs下的内存是从低字节到高字节的,我们的栈会和linux下画的不太一样,但是都是朝着低地址方向扩展的。这是方便大家理解。

Linux的进程地址空间示意图:

代码栈帧示意图:

隐式类型转换

举个栗子,当我们执行下面的代码,当我们以char类型传参,但函数体依旧以int的步长获取,此时会出错吗?

#include<stdio.h>
#include<windows.h>
int Find_Max(int num, ...)
{
	//定义一个char* 的变量arg
	va_list arg;
	//将arg指向第一个可变参数
	va_start(arg, num);
	//将max置成第一个可变参数,然后arg指向下一个可变参数
	int max = va_arg(arg, int);
	//循环num-1次,访问完剩下的可变参,找到最大的赋值给max
	for (int i = 1; i < num; ++i)
	{
		int r;
		if (max < (r = va_arg(arg, int)))
		{
			max = r;
		}
	}
	//将arg指针变量置成NULL,避免野指针
	va_end(arg);
	return max;
}

int main()
{
	char a = '1'; //ascii值: 49
	char b = '2'; //ascii值: 50
	char c = '3'; //ascii值: 51
	char d = '4'; //ascii值: 52
	char e = '5'; //ascii值: 53
	int ret = Find_Max(5, a, b, c, d, e);
	//int ret = Find_Max(5, 0x1, 0x2, 0x3, 0x4, 0x5);
	printf("ret :%d\n", ret);
	system("pause");
}

答案:

不会的,由于压栈的时候是通过寄存器传参的,32位下的寄存器就是4个字节。

压栈时的汇编:其中第一条不是mov,而是movsx,即汇编语言数据传送指令MOV的变体。带符号扩展,并传送。也就是整形提升。

同理:用float传参,用double字长走,也是没有问题的。

总结

所以我们习惯在函数体内部(Find_Max)用int/double为长度走,而传参的时候我们可以用char/short/float等等类型。

注意:

64位下的定义和32位下差异是很大的。

为什么按照4字节对齐:

先前讲到在短整型,在压栈的过程中会发生整形提升,那么从栈帧中要拿到对应的数据也要按照对应的方法提取。

_INTSIZEOF的数学理解:

_INTSIZEOF(n)的意思:计算一个最小数字x,满足 x>=n && x%4==0,n表示sizeof(n)的值。即该类型的大小要满足往n的整数倍对齐,且最小不能小于n。

以4字节对齐为栗子:

n%4 == 0,则 ret = n;

n %4 != 0 , 则 ret = (n+ 4 - 1)/4 *4;

(n+ 4 - 1)/4 -->假设 n为1到4,那么(n + 4 - 1)/4的结果都是1,再乘上对其数4就是以4对齐的最小对齐数了。就能将这4个数值范围最小对齐倍数控制在同一个值。

我们观察(n+ 4 -1)/4 *4,/4实际上就是将二进制序列往右移,*4就是把二进制序列往左移动,这一来一回实际上就是把最低两位置成0,那么我们还可以简化成:
(n+ 4 -1) & ~3 ,也就是源码当中的定义了!!

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

对两个函数的重新认知

对printf的理解:

在上述的例子中,宏是无法判断实际存在参数的数量,以及实际参数的类型的,那么在printf当中,必定有能够确定参数数量以及辨别参数类型的方法,实际上也就是%c,%d,%lf,其中%的数量除了%%外的%的数量实际上就能让我们得知参数的数量,而%c,%d,实际上也就说明了对应的类型。

对exec系列的理解:

进程控制,当时讲述了实际上只有一个系统调用execve,其他函数exec函数最终都是要调用execve函数,那么是如何实现从参数l到v这个过程的呢?

答案:

实际上访问到null为止,传参的数量用一个count一直计数就能拿到,而类型毫无疑问就是char*,我们可以用strlen去计算要走多长。(不过两个char数组通常会间隔多8个字节)

总结

您可能感兴趣的文章:

相关文章