我的编程空间,编程开发者的网络收藏夹
学习永远不晚

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

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

北京

  • 北京
  • 上海
  • 天津
  • 重庆
  • 河北
  • 山东
  • 辽宁
  • 黑龙江
  • 吉林
  • 甘肃
  • 青海
  • 河南
  • 江苏
  • 湖北
  • 湖南
  • 江西
  • 浙江
  • 广东
  • 云南
  • 福建
  • 海南
  • 山西
  • 四川
  • 陕西
  • 贵州
  • 安徽
  • 广西
  • 内蒙
  • 西藏
  • 新疆
  • 宁夏
  • 兵团
手机号立即预约

请填写图片验证码后获取短信验证码

看不清楚,换张图片

免费获取短信验证码

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个字节)

总结

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

免责声明:

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

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

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

下载Word文档到电脑,方便收藏和打印~

下载Word文档

猜你喜欢

C语言可变参数使用与内存管理的方法是什么

这篇“C语言可变参数使用与内存管理的方法是什么”文章的知识点大部分人都不太理解,所以小编给大家总结了以下内容,内容详细,步骤清晰,具有一定的借鉴价值,希望大家阅读完这篇文章能有所收获,下面我们一起来看看这篇“C语言可变参数使用与内存管理的方
2023-07-04

编程热搜

  • Python 学习之路 - Python
    一、安装Python34Windows在Python官网(https://www.python.org/downloads/)下载安装包并安装。Python的默认安装路径是:C:\Python34配置环境变量:【右键计算机】--》【属性】-
    Python 学习之路 - Python
  • chatgpt的中文全称是什么
    chatgpt的中文全称是生成型预训练变换模型。ChatGPT是什么ChatGPT是美国人工智能研究实验室OpenAI开发的一种全新聊天机器人模型,它能够通过学习和理解人类的语言来进行对话,还能根据聊天的上下文进行互动,并协助人类完成一系列
    chatgpt的中文全称是什么
  • C/C++中extern函数使用详解
  • C/C++可变参数的使用
    可变参数的使用方法远远不止以下几种,不过在C,C++中使用可变参数时要小心,在使用printf()等函数时传入的参数个数一定不能比前面的格式化字符串中的’%’符号个数少,否则会产生访问越界,运气不好的话还会导致程序崩溃
    C/C++可变参数的使用
  • css样式文件该放在哪里
  • php中数组下标必须是连续的吗
  • Python 3 教程
    Python 3 教程 Python 的 3.0 版本,常被称为 Python 3000,或简称 Py3k。相对于 Python 的早期版本,这是一个较大的升级。为了不带入过多的累赘,Python 3.0 在设计的时候没有考虑向下兼容。 Python
    Python 3 教程
  • Python pip包管理
    一、前言    在Python中, 安装第三方模块是通过 setuptools 这个工具完成的。 Python有两个封装了 setuptools的包管理工具: easy_install  和  pip , 目前官方推荐使用 pip。    
    Python pip包管理
  • ubuntu如何重新编译内核
  • 改善Java代码之慎用java动态编译

目录