深入理解可变参数列表

news2024/11/25 16:00:18

目录

1.前言

2.基本使用方法

        1.引入

        2.相关宏介绍

3.原理剖析

         1.传参

         2.va_list

         3.va_start()

         4.va_arg()

         5.va_end() 

 4.注意事项

 5.总结


1.前言

       在C语言中,对于一般的函数而言,参数列表都是固定的,而且各个参数之间用逗号进行分开。而除了这些函数外,还有些函数的参数列表是不固定的,例如我们常用的printf()函数,会根据我们传入的参数个数来调整最终打印的个数。本期我们会从宏观到微观从如何使用可变参数列表到可变参数列表实现原理来理解可变参数列表。

2.基本使用方法

        1.引入

        假设,我们需要实现一个函数FindMax()来比较num个数的最大值,但我们不清楚num的具体值。此时我们就可以通过可变参数列表的形式设计这个函数,如下:

#include<stdio.h>
#include<stdarg.h>

int FindMax(int num, ...)
{
	va_list arg;
	va_start(arg, num);
	int max = va_arg(arg, int);
	for (int i = 1; i < num; i++)
	{
		int cur = va_arg(arg, int);
		if (max < cur)
		{
			max = cur;
		}
	}
	va_end(arg);
	return max;
}
int main()
{
	int max = FindMax(5, 2, 4, 7, 4, 3);
	printf("%d", max);
	return 0;
}

 首先,我们需要包含stdarg.h头文件,然后我们通过va_list,va_start(),va_arg(),va_end()

这四个宏来实现对可变参数部分的访问,进而实现求出最大值的功能。可变参数部分用...来表示。

需要注意的是,函数中必须要有一个参数,编译器需要通过这个参数的地址来确定可变参数部分的地址(后面讲解)。在本题中,第一个参数用于传入可变参数部分的数量,因为函数内部是无法确定有多少个可变参数需要被访问。

        2.相关宏介绍

        四个宏的功能如下表所示:

符号及使用说明
va_list arg

定义可以访问可变参数部分的变量,实际上是个

char*类型

va_start(arg,num)使arg指向可变参数部分(通过压栈的特点)
va_arg(arg,int)

通过指针的方式得到参数,int表示每次arg向后读取

4个字节

va_end()arg使用完毕,将arg置为0

 所以,我们使用可变参数列表的基本步骤分为以下几步:

1.定义一个va_list类型变量

2.使用va_start()使变量指向可变参数部分

3.通过va_arg()访问可变参数

4.访问完毕后使用va_end使va_list变量置0


注意,使用va_arg()访问可变参数时只能按照顺序向后访问,可以中途停止,但是不能返回或者跳跃访问(后面分析)

  

3.原理剖析

        上面我们了解了可变参数列表的基本使用方法,但是在这之中还存在着一些注意事项,下面我们将通过栈帧和底层代码的实现来分析可变参数列表的实现原理(使用上面求最大值的例子)。提示:下面会使用到往期函数栈帧的内容,传送门:C语言之函数栈帧(动图详解)。

        1.传参

        我们知道,函数调用前会将参数按照从右往左的顺序进行压栈,形成临时变量,可变参数列表也不例外,会将我们传入的参数压入栈中:

        对应栈空间如下: 

 


          2.va_list

        我们查看va_list宏的定义如下:

 va_list实际上是一个char*类型的指针,其指向大小为一个字节的空间。因此上面的

va_list arg实际上是char* arg。


         3.va_start()

        查看va_start()宏的定义如下(底层定义时通过多个宏一起实现,便于封装):

//最终展开相当于

#define va_start(ap,v) ((void)(ap = (char*)(&(v)) + _INTSIZEOF(v)))

 对于_INTSIZEOF(n)宏,作用是将n所占字节大小按照4字节进行对齐,即向上取整。如n占2个字节,这个宏的值就为4,n占6个字节,这个宏的值就为8(原因见下面注意事项)。

 所以,va_start(arg,num)的作用就是取出num的地址并强转为char*,然后向下偏移4个字节并赋给arg,通过栈空间我们可以发现通过以上操作arg就指向了可变参数部分。 

这便说明了为什么可变参数列表为什么至少需要一个参数是已知的,因为需要确定可变参数部分的地址。


         4.va_arg()

        查看va_arg()宏的定义如下:

//最终展开相当于

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

 由于压栈形成的临时变量的地址空间是连续的,所以va_arg(arg,int)的作用就是将ap向下偏移4个字节并改变arg的值,然后再回到起初的位置 (arg没有改变)向下读取4个字节的值作为va_arg(arg,int)宏的值。简单来说,va_arg(arg,int)起到了两个作用:1.将arg指向下一个参数  2.取出原位置的参数

        动图如下:

 通过以上原理,我们就可以发现va_arg()宏的访问顺序是顺序且单向的,无法进行返回或跳跃访问。


         5.va_end() 

        查看va_end()宏的定义如下:

//最终展开相当于

#define va_end(ap) ((void)(ap = (char*)0))

因此,va_end(arg)实际上就是将arg的值置为0,结束可变参数列表的访问


4.注意事项

        目前,我们还剩最后一个问题,为什么va_start和va_arg宏在定义时偏移的字节都需向上取整,而不是直接偏移sizeof(n)个字节?我们通过以下例子来说明:

#include<stdio.h>
#include<stdarg.h>

int FindMax(int num, ...)
{
	va_list arg;
	va_start(arg, num);
	int max = va_arg(arg, char);
	for (int i = 1; i < num; i++)
	{
		int cur = va_arg(arg, char);
		if (max < cur)
		{
			max = cur;
		}
	}
	va_end(arg);
	return max;
}
int main()
{
	char a = 'a';
	char b = 'b';
	char c = 'c';
	char d = 'd';
	char e = 'e';
	int max = FindMax(5, a, b, c, d,e);
	printf("%c", max);
	return 0;
}

        我们将传入的参数改为字符型变量,其余地方不变,va_arg的参数依旧是int,我们发现最终运行的结果依旧是我们想要看到的结果:

         到这里我们可能就疑惑了,char类型的变量占一个字节,而我们va_start(arg,num)或va_arg(arg,int)进行偏移的时候一次是偏移四个字节,而参数间地址差为一个字节,显然不能正确依次访问我们传入的5个可变参数部分。我们查看汇编代码如下:

        通过以上汇编,我们发现字符型实参在形成临时变量时使用的是movsx+push命令,不同于整形变量的mov+push命令。

movsx的作用为:

将数据进行符号扩展,再进行传送,即将char类型数据扩展为int类型数据后再进行传送。

类似的还有movzx,其作用为:
将数据进行零扩展,再进行传送,二者的区别就在于扩展时是补符号位还是零。


一般来说,如果传入的参数是短整形,一般要进行int类型提升,汇编指令为movsx(movzx),

如果传入的是float,则会提升为double类型

        由此,我们知道了当函数传参形成临时变量时,会先进行扩展提升放入寄存器中,然后再将扩展后的数据压入栈中。即我们最终形成的临时变量实际上不是占一个字节,而是四个字节。因此,实际上va_arg()的参数为int才是正确的,如果为char则最后解引用结果就是向后访问一个字节。(不过由于我们的机器采用小端存储,数据低位放在低地址处,因此两种写法结果一样)

//对于上面的例子
va_arg(arg,int);   //正确的
va_arg(arg,char);  //严格来说错误

        回到之前的问题,为什么偏移的字节需要向上取整?我们可以假设我们传入的类型是char类型,由于传参时扩展提升的存在,可变参数部分之间地址的差距实际上是扩展提升后的字节,即4个字节,而此时如果我们直接使用sizeof,求得的结果为1个字节,最后arg指针只会向下偏移1个字节,以致无法正确指向下一个参数。因此利用_INTSIZEOF(n)宏使偏移量进行向上4字节取整

 5.总结

1.我们可以通过va_list,va_start(),va_arg(),va_end()四个宏来实现可变参数列表

2.使用可变参数列表时定义函数至少要有一个已知变量

3.可变参数列表的访问是顺序且单向的

4.对于短整形和float类型,传参形成临时变量时数据会发生扩展提升


 以上,就是本期的全部内容。

制作不易,能否点个赞再走呢qwq

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/168051.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

面试,演讲为什么一说话就紧张? 的底层解读

信息存在损耗在说话中,说话者与接受者之间传递信息存在损耗。而且人表达情绪不管是说话&#xff0c;还有肢体语言&#xff0c;这使得增加了信息接受者的信息量。下面引入两个词语来说明。给予与流露的含义给予&#xff1a;别人评价你做的菜好吃。流露&#xff1a;别人眉头一皱。…

探讨下如何更好的使用缓存 —— Redis缓存的特殊用法以及与本地缓存一起构建多级缓存的实现

大家好&#xff0c;又见面了。 本文是笔者作为掘金技术社区签约作者的身份输出的缓存专栏系列内容&#xff0c;将会通过系列专题&#xff0c;讲清楚缓存的方方面面。如果感兴趣&#xff0c;欢迎关注以获取后续更新。 通过前面的文章&#xff0c;我们一起剖析了Guava Cache、Caf…

如何在线录屏?怎么录制网课

当我们需要学习特定知识时&#xff0c;可以在网络上寻找教学视频进行学习。一般情况下&#xff0c;视频会一直保留在网上&#xff0c;可以随时随地观看。那您知道我们平常观看的网络课程是如何录制的吗&#xff1f;如何进行在线录屏&#xff1f;今天小编教您如何在线录制视频网…

C语言——指针进阶(含例题及详细代码分析)

文章目录导语&#xff1a;思维导图&#xff1a;1.字符指针2.指针数组3.数组指针3.1 数组指针的定义3.2 &数组名和数组名3.3 数组指针的使用4.数组参数、指针参数4.1 一维数组传参4.2 二维数组传参4.3 一级指针传参4.4 二级指针传参5.函数指针6.函数指针数组7.指向函数指针数…

Django自定义认证系统原理及源码分析解读

疑问 Django在如何自定义用户登录认证系统的时候&#xff0c;大家都会里面立马说 自定义一个 或者多个backend&#xff0c;比如通过账号密码、邮箱密码&#xff0c;邮箱验证码、手机号短信验证码等等。 然后设置 在settings中配置一个 AUTHENTICATION_BACKENDS就行。 但是为什…

【尚硅谷】Java数据结构与算法笔记11 - 树结构的实际应用

文章目录一、堆排序1.1 堆排序基本介绍1.2 堆排序的基本思想1.3 堆排序步骤图解1.4 堆排序思路总结1.5 堆排序代码实现二、赫夫曼树2.1 基本介绍2.2 重要概念1.3 赫夫曼树构建思路图解1.4 赫夫曼树代码实现三、赫夫曼编码3.1 基本介绍3.2 原理剖析3.3 实践&#xff1a;数据压缩…

java运算符2023010

运算符&#xff1a; Java语言中的运算符可分为如下几种。 ➢ 算术运算符 ➢ 赋值运算符&#xff0c;/—/各种和等号组合的都是赋值运算符&#xff0c;赋值表达式是有值的&#xff0c;赋值表达式的值就是右边 被赋的值。例如String str2str表达式的值就是str。因此&#xff0c;赋…

Java技能树-操作符(一)-练习篇

算术运算符 执行完下面的代码&#xff0c;变量b的值是&#xff1a; java int a 1; int b a; 答案是&#xff1a;D 在后,先赋值再运算 自动递增和递减 下面代码执行后的结果是&#xff1a; int a 0; a a; int b 0; b b; System.out.println("a " a); S…

Numpy的轴及numpy数组转置换轴

Numpy的轴 import numpy as np 数组np.array([[[1,2],[4,5],[7,8]],[[8,9],[11,12],[14,15]],[[10,11],[13,14],[16,17]],[[19,20],[22,23],[25,26]]]) print(数组.shape) # 返回 (4, 3, 2)最内层一对 [ ] 可以代表一个1维数组 加粗的一对 [ ] 里面有3个一维数组&#xff0c;也…

Layout布局(element ui)

Layout布局嘚吧嘚gutter示例发现el-row行内容居中默认局左上角水平居中垂直居中水平垂直居中嘚吧嘚 其实layout布局的使用在element官网上都有相关描述&#xff0c;也有相关示例&#xff0c;很容易快速上手。但是在实际使用的过程还是发现一些问题&#xff0c;于是做了一些学习…

递归(基础)

目录 一、递归的定义 1、什么时候会用到递归的方法 1. 定义是递归的 2. 数据结构是递归的 3. 问题的解法是递归的 2、应用递归的原则 3、递归调用顺序问题 1. 首先递归的过程可以总结为以下几点&#xff1a; 2. 递归工作栈​​​​​​​ 二、 递归和非递归的转化 …

Allegro如何快速把Class高亮成不同的颜色操作指导

Allegro如何快速把Class高亮成不同的颜色操作指导 在做PCB设计的时候,高亮Class组是一个非常频繁的操作,Allegro支持快速的将Class高亮成不同的颜色,并且还可以形成一个列表,如下图 具体操作如下 选择File选择Change Editor

select ( ) for update 锁行还是锁表?

select &#xff08; &#xff09; for update 锁行还是锁表&#xff1f; 一、验证 创建SQL表 //id为主键 //name 为唯一索引 CREATE TABLE user (id INT ( 11 ) NOT NULL AUTO_INCREMENT,name VARCHAR ( 255 ) DEFAULT NULL,age INT ( 11 ) DEFAULT NULL,code VARCHAR ( …

SpringCloud微服务项目实战 - 6.延迟任务

我没有失约&#xff0c;我与春风共至&#xff0c;我与夏蝉共鸣&#xff0c;我与秋叶共舞&#xff0c;我与凛冬共至&#xff0c;唯独你背道而行&#xff01; 系列文章目录 项目搭建App登录及网关App文章自媒体平台&#xff08;博主后台&#xff09;内容审核(自动)延迟任务 - 精…

JVM快速入门学习笔记(一)

参考&#xff1a; https://blog.csdn.net/m0_38075425/article/details/81627349 www.kuangstudy.com JVM 常问面试题 请你谈谈你对JVM的理解&#xff1f; java—>class---->Java8虚拟机和之前的变化更新&#xff1f;什么是OOM 内存溢出什么是栈溢出StackOverFlowErr…

Matplotlab绘制散点图小节

前言现有一堆数据&#xff0c;是散点坐标形式&#xff0c;现在需要将它们绘制成散点图&#xff0c;并解决了关于Matplotlib绘图不能显示汉字的问题。读取数据数据格式如下图。第一行为一个数字&#xff0c;表示当前文件共有多少行数据。 第二行开始为真正的数据&#xff0c;各数…

如何冻结Excel中的行

在Excel中有一个冻结行的功能。在冻结行的帮助下,我们可以固定我们选择的窗格或行,以超出特定的限制工作表。 可以从“视图”菜单选项卡的“窗口”部分的“冻结窗格”下拉列表中访问“冻结行”。首先,要冻结列,请选择要冻结的列或将光标放在该列的任何位置,然后从列表中选…

vue 使用hook 对 chekbox 做简单的逻辑抽离,一个核心多套模板

现在的组件库都会包含些相同的基础组件&#xff0c;功能大差不差&#xff0c;只是不同UI规范下的具体实现。这些基础组件基本能满足大部分的开发需求。 但世上无银弹&#xff0c;有时我们需要对组件做细微的调整可能是功能上的&#xff0c;可能是UI上的&#xff0c;例如 tab切换…

JavaWeb基础(三) Request和Response详解

JavaWeb基础(三) Request和Response详解 1&#xff0c;Request和Response的概述 Request是请求对象&#xff0c;Response是响应对象。 此时&#xff0c;我们就需要思考一个问题request和response这两个参数的作用是什么? request: 获取请求数据 浏览器会发送HTTP请求到后台…

跨站脚本攻击漏洞(XSS)-基础篇

数据来源 跨站脚本攻击 1、什么是跨站脚本攻击? 跨站脚本( Cross-site Scripting)攻击&#xff0c;攻击者通过网站注入点注入客户端可执行解析的 payload&#xff08;脚本代码&#xff09;&#xff0c;当用户访问网页时&#xff0c;恶意 payload自动加载并执行&#xff0c;…