文章详情

短信预约-IT技能 免费直播动态提醒

请输入下面的图形验证码

提交验证

短信预约提醒成功

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

2024-04-02 19:55

关注

前言

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

声明: 以下所有测试都是在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个字节)

总结

到此这篇关于C语言可变参数列表的文章就介绍到这了,更多相关C语言可变参数列表内容请搜索编程网以前的文章或继续浏览下面的相关文章希望大家以后多多支持编程网!

阅读原文内容投诉

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

软考中级精品资料免费领

  • 历年真题答案解析
  • 备考技巧名师总结
  • 高频考点精准押题
  • 2024年上半年信息系统项目管理师第二批次真题及答案解析(完整版)

    难度     801人已做
    查看
  • 【考后总结】2024年5月26日信息系统项目管理师第2批次考情分析

    难度     348人已做
    查看
  • 【考后总结】2024年5月25日信息系统项目管理师第1批次考情分析

    难度     311人已做
    查看
  • 2024年上半年软考高项第一、二批次真题考点汇总(完整版)

    难度     432人已做
    查看
  • 2024年上半年系统架构设计师考试综合知识真题

    难度     220人已做
    查看

相关文章

发现更多好内容

猜你喜欢

AI推送时光机
位置:首页-资讯-后端开发
咦!没有更多了?去看看其它编程学习网 内容吧
首页课程
资料下载
问答资讯