可变参数列表的使用与原理

news2024/12/28 8:10:00

序言

        我们自己编写的函数通常参数是固定的,这样使得某些功能不能得到我们想要的结果,比如我们想求出2个数的最大值的代码不能用于求处3个数的最大值,因此,C语言定义了可变参数列表来编写参数个数不确定的函数。具有可变参数列表的函数就是:函数定义时,参数列表中的的参数不完全定义;调用该函数时,可以根据实际情况传入多个参数,且可以成功完成其函数功能。而该函数的参数列表就是可变参数列表

可变参数列表的使用

// demo 1:求两个数据中的最大值
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#include <windows.h>
int FindMax(int x, int y)
{
    if (x > y){
        return x;
    }
    return y;
}
int main()
{
    int x = 0;
    int y = 0;
    printf("Please Eneter Two Data# ");
    scanf("%d %d", &x, &y);
    int max = FindMax(x, y);
    printf("max = %d\n", max);
    system("pause");
    return 0;
}
//demo 2:求任意多个数据中的最大值(至少一个),要求不能使用数组
//因为目前参数个数不确定,那么函数编写的时候,参数个数也无法确定,换句话说,函数也就没法编写
//不过,C提供了满足该场景的解决方案:可变参数列表
#include <stdio.h>
#include <windows.h>
//num:表示传入参数的个数
int FindMax(int num, ...)
{
    va_list arg; //定义可以访问可变参数部分的变量,其实是一个char*类型
    va_start(arg, num); //使arg指向可变参数部分
    int max = va_arg(arg, int); //根据类型,获取可变参数列表中的第一个数据
    for (int i = 0; i < num - 1; i++){//获取并比较其他的
        int curr = va_arg(arg, int);
        if (max < curr){
        max = curr;
        }
    }
    va_end(arg); //arg使用完毕,收尾工作。本质就是讲arg指向NULL
    return max;
}
int main()
{
int max = FindMax(5,0x11,0x22,0x33,0x44,0x55);
printf("max = %d\n", max);
system("pause");
return 0;
}

demo1很简单,demo2是什么鬼?什么是va_list,什么是va_start,什么是va_arg,什么是va_end。接下来,我将带领你们一步一步理解这段代码。

我先告诉你们这段代码的作用是什么而将这段代码的底层实现的放在后面讲解,先简单的有一个直观的感受,就比如我们想要吃饭就只需要会用筷子即可,不需要知道筷子的构成。当然,讲清楚作用后会来讲述它们的底层原理,现在让我们从最简单如何使用可变参数列表开始讲解~

在正式讲解之前,我们需要回顾下相关函数栈帧里面的知识点,不清楚的可以看这篇文章

😼syseptember的个人博客:图解函数栈帧😼http://t.csdn.cn/8zpGH

🚩知识点1:函数的调用需要为函数形参开辟空间,并且为形参开辟空间是在调用函数前进行的。

🚩知识点2:形参开辟空间的顺序是从右向左的,即将最右边的形参最先压栈。

🚩知识点3:为形参所开辟的空间是连续的。

🫵现在我们正式开始学习可变参数列表~🫵

-------------------------------------------------------------------------------------------------------------------------

        当函数定义中参数列表中至少出现了一个具体的形参,未出现的形参用...代替,那么这个函数的参数列表就是可变参数列表

        结合栈帧的相关知识,我们来理解一下demo2的代码。demo2中FindMax的定义中出现了一个具体的形参num和可变参数列表

        主函数调用FindMax函数时,传递的实参是5,0x11,0x22,0x33,0x44,0x55。函数的功能是找出这5个数的最大值,在调用该函数前,会为形参分配空间,并且形参从右往左依次分配

为形参分配好空间后,就需要为FindMax函数分配栈帧了 

 

FindMax中定义了一个变量arg,该变量类型是va_list,本质上是char*类型,

在FindMax函数中掉用va_start(arg, num)使得arg指向位于低地址处的可变参数

ps: 我们现在只需要知道va_start可以完成这件事即可,后面会将va_start是如何工作来完成这件事的

        然后将max的初始值通过max = va_arg(arg, int)赋值为0x11,此时max就是可变参列表的第一个参数,进入for循环,curr= va_arg(arg, int)可以理解为将下一个可变参数赋给curr,在进行常规的比较寻找max操作,这样就保证了所有的可变参数都和max进行了比较从而可以找出可变参列表中最大的参数。最后va_end(arg)的作用就是将arg置空防止出现野指针。

我们现在复习一下上述长相和函数一样的代码作用

🚩代码1:va_list arg:定义的了一个char* 名为arg的变量

🚩代码2:va_start(arg, num):另arg变量指向可变参数列表中的第一个参数

🚩代码3:curr =  va_arg(arg, int):将当前arg指向int变量赋值给curr并且arg指向下一个int变量

🚩代码4:va_end(arg):将arg置为空指针,防止出现野指针

再次提醒:现在我们只需要知道它们可以完成这件事,现在你不需要思考如何完成的

提问:代码3中va_arg(arg,int)是取出arg指向的int变量,并且另arg指向下一个int变量,那如果可变参数全部是char类型,上面代码是否还可以找出最大值呢?

        答案是可以的。原因在于当可变参数类型为短整型参数时,会隐式提升为int型,当可变参数类型为float时,会被自动提升为double类型

 不信?我们再来看一下汇编指令

movsx可不是一般的mov指令哟,movsx会将数据先进行整型提升,在将数据赋给寄存器

         这说明了将char类型作为可变参数部分,首先会将char类型参数进行整形提升再压栈,这样就和比较int类型数据大小的过程是一模一样的了。无需对FindMax函数进行更改,如果此时多此一举将curr = va_arg(arg, int)改成curr = va_arg(arg, char)会出现什么样的结果呢?原本是读取arg所指向的4个字节(int值)变成读取arg所指向的1个字节(char值),而又因为char类型的参数已经被提升为int类型,所以只会读取arg所指向的一个字节因此不要想当然地认为可变参数是char类型所以函数体也要对char类型进行操作注意:short类型也同理

float类型作为可变参数会被自动转换为double类型数据

 

总结:

通过查看汇编,我们看到,在可变参数场景下:
1. 实际传入的可变参数如果是 char short ,float,实际传递的非可变参数是char,short,编译器在编译的时候,会自动进行提升(通过查看汇编,我们都能看到)
2. 函数内部使用的时候,根据类型提取数据,更多的是通过 int 或者 double 来进行
注意事项
  • ❗可变参数必须从头到尾逐个访问。如果你在访问了几个可变参数之后想半途终止,这是可以的,但是,如果你想一开始就访问参数列表中间的参数,那是不行的。
  • ❗参数列表中至少有一个命名参数。如果连一个命名参数都没有,就无法使用 va_start 。
  • ❗这些宏是无法直接判断实际存在参数的数量。
  • ❗这些宏无法判断每个参数的是类型。
  • ❗如果在 va_arg 中指定了错误的类型,那么其后果是不可预测的。

可变参数的原理

在可变参数的使用部分,我们知道了通过va_list,va_start,va_arg,va_end可以访问所有的可变参数,但是我们不知道这些宏具体是怎么实现的,现在我将逐个介绍每一个宏的实现和原理

-------------------------------------------------------------------------------------------------------------------------

🐯va_list,va_end

va_list:char*的一个别名

 va_end(arg):是一个宏,将arg置空

重点是va_arg(arg,int)和va_start(arg,num)

 🦁va_arg

va_arg(arg, int):取出arg指向的int数值,并将arg指向下一个int数据

先记住_INTSIZEOF(n) 的意思:计算一个最小数字 x ,满足 x>=sizeof(n) && x%4==0 ,其实就是求出一种 4 字节对齐的方式是什么:比如sizeof(n)为1,2,4时,_INTSIZEOF(n)就是4,sizeof(n)为5,6,8时,_INTSIZEOF(n)就是8.
思考:为什么要有4字节对齐? 因为可变参为短整型时会提升为整形,所以ap的偏移量应该是4字节的整数倍。
再来看va_arg(arg,int)的含义:(*(t*)((ap+=_INTSIZEOF(t))-_INTSIZEOF(t)))等价于(*(int*)(arg+=4)-4),arg首先指向下一个int数据,其次通过减去arg的偏移量得到该数据的起始地址再强制转换为int类型的地址并且解引用取出该int数据注意:当我们实参传短整型时,如果va_arg(arg,char)那么会解引用只能得到一个字节的数据,但是arg仍然会指向后4个字节的数据,所以在函数体内t通常是int或者double。
va_arg的功能:1. 把当前元素提取出来。2. 使arg指向下一个待访问元素.

🐯va_start

va_start(arg,num):另arg指向可变参数列表中的第一个参数

        va_start(arg,num)相当于arg = (char*)&num+_INTSIZOF(num),前面说过,当sizeof(num)的值为1,2,4时,_INTSIZEOF(num)为4;当sizeof(num)的值为5,6,7,8时,_INTSIZEOF(num)的值为8.所以va_start(arg, num)相当于让arg指向后4个或8个字节的数据,这个设计是非常巧妙的因为当非可变参数num为短整型时,会先提升为int在压栈,即使传递的第一个实参num是char类型,arg也会指向可变参数列表的第一个参数,因为在传递参数时num作为形参会提升为4字节,并且va_start(arg,num)使arg=(char*)&arg+4。

🦝_INTSIZEOF(n)

现在我们明白了va_start可以让arg指向可变参数列表的第一个参数。我们现在剩下最后一个问题,_INTSIZEOF(n)为什么可以得到最小x,并且满足x>=sizeof(n)&&x%4==0?

为了方便理解,我们将sizeof(n)直接记作n(char-1 short-2 int-4)

 第一步理解:4的倍数

        既然是4的最小整数倍,那么本质是:x=4*m,m是具体几倍。对于sizeof(n)等于7来讲,m就是2,x就是8,所以m是多少取决于sizeof(n)是多少

  •  如果n能整除4,则m就是sizeof(n)/4-->n/4
  • 如果n不能整除4,则m就是sizeof(n)/4+1-->n/4+1

这两种写法合并成一种写法就是sizeof(n)+sizeof(int)-1/sizeof(int)--->n+3/4

  • 如果n%4==0&&n/4==k那么n+3%4==3&&n+3/4==k
  • 如果n%4==k(1<=k<=3)&&n/4==m那么4<=n+3<=4&&n+3/4==m+1

第二步理解:最小4字节对齐数

        搞清楚了满足条件是最小几倍的问题,那么计算一个x使得x>=n&&x%4==0就变成了

((n+sizeof(n)-1)/sizeof(n))[ 最小倍数 ]*sizeof(int)[ 单位大小 ]--->(n+3)/4*4

第三步理解:理解源代码中的宏

        上面代码在功能上已经和源代码等价了,现在将上述代码简化记w=n+3

w/4*4相当于先讲w右移2位再将x左移两位,最后的结果相当于讲w最右边2个比特位清零!

所以不需要先/再*,直接另w & ~3也可以将最右边2个bit位清零,所以就有源代码((sizeof(n)+sizeof(int)-1)/sizeof(int)) & ~(sizeof(int) - 1))

原理图 

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

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

相关文章

Golang每日一练(leetDay0054)

目录 157. 用 Read4 读取 N 个字符 Read-n-characters-given-read4 &#x1f31f;&#x1f31f; 158. 用 Read4 读取 N 个字符 II Read-n-characters-given-read4-ii-call-multiple-times II &#x1f31f;&#x1f31f; &#x1f31f; 每日一练刷题专栏 &#x1f31f; Gol…

拿捏SQL:以“统计连续登录天数超过3天的用户“为例拿捏同类型SQL需求

文章目录 [TOC](文章目录) 一、介绍案例&#xff1a;以"统计连续登录天数超过3天的用户"为需求。数据准备方案1&#xff1a;常规思路针对对数据user_id分组&#xff0c;根据用户的活动日期排序用登录日期与rn求date_sub&#xff0c;得到的差值日期如果是相等的&#…

Python非线性回归预测模型实验完整版

非线性回归预测模型 实验目的 通过非线性回归预测模型&#xff0c;掌握预测模型的建立和应用方法&#xff0c;了解非线性回归模型的基本原理 实验内容 非线性回归预测模型 实验步骤和过程 (1)第一步&#xff1a;学习非线性回归预测模型相关知识。 非线性回归预测模型是指…

Spring框架中的单例Beans是线程安全的么?

在Spring框架中&#xff0c;单例Beans默认是线程安全的。 当你在Spring框架中声明一个单例Bean并配置为默认的单例作用域时&#xff0c;Spring会确保对该Bean的并发访问是线程安全的。以下是一个简单的代码演示&#xff1a; 假设我们有一个名为 SingletonBean 的单例 Bean 类…

Mysql目录结构

一、目录结构 <1> 主要目录结构 find / -name mysql<2> 数据库文件目录 目录&#xff1a;/var/lib/mysql/ 配置方式&#xff1a;show variables like ‘datadir’; <3> 相关命令目录 目录&#xff1a;/usr/bin&#xff08;mysqla…

VMware安装CentOS7遇到的问题记录

文章目录 1、执行ifconfig后&#xff0c;ip地址不显示解决方法&#xff1a; 2、CentOS ip地址老是变动&#xff0c;配置固定ip解决方法 1、执行ifconfig后&#xff0c;ip地址不显示 问题背景&#xff1a;VMware安装centos7后&#xff0c;启动虚拟机&#xff0c;在终端中执行if…

MySQL示例数据库(MySQL Sample Databases) 之 sakila数据库

文章目录 MySQL示例数据库(MySQL Sample Databases) 之 sakila数据库官方示例数据介绍sakila数据库sakila数据库安装sakila/sakila-schema.sql的脚本内容sakila的结构参考 MySQL示例数据库(MySQL Sample Databases) 之 sakila数据库 官方示例数据介绍 MySQL 官方提供了多个示…

Mysql存储json格式数据需要掌握的

目录 一、前言二、什么是 JSON三、Mysql当中json函数四、JSON值部分更新4.1.使用 Partial Updates 的条件4.2.如何在 binlog 中开启 Partial Updates4.3.关于 Partial Updates 的性能测试 五、如何对 JSON 字段创建索引六、mybatis取json类型的数据七、总结 一、前言 最近做的一…

[实训] 实验1-SPI数据传输基础实验(上)

目 录​​​​​​​ 一、实验目的 二、实验仪器及器件 三、实验内容及原理 四、实验步骤​​​​​​​ 五、实验测试数据表格记录 六、实验数据分析及处理 七、实验结论与感悟 一、实验目的 使用FPGA/ARM实现SPI数据传输实验&#xff1b;实现数据传输程序的编写、下载…

【项目原理】多点触摸屏驱动原理

一、屏幕介绍 ATK-7016 这款屏幕其实是由 TFT LCD触摸屏组合起来的。底下是 LCD 面板&#xff0c;上面是触摸面板&#xff0c;将两个封装到一起就成了带有触摸屏的 LCD 屏幕。电容触摸屏也是需要一个驱动 IC的&#xff0c;驱动 IC 一般会提供一个 I2C 接口给主控制器&#xff…

[实训] 实验1-SPI数据传输基础实验(下)

目录 五、实验测试数据表格记录 六、实验数据分析及处理 七、实验结论与感悟 五、实验测试数据表格记录 实验现象数码管显示见第四节图4.4&#xff0c;示波器测量结果见下列图片。 图5.1 RST、MOSI/MISO波形测量结果 图5.2 SCLK、MOSI/MISO波形测量结果 仅调整示波器波…

C生万物 | 剖析函数指针经典应用 —— 回调函数

不懂函数指针的老铁可以先看看这篇文章【指针函数与函数指针】&#xff0c;上车&#xff0c;准备出发&#x1f697; 文章目录 一、回调函数的概念二、为什么要使用回调函数&#xff1f;三、回调函数使用场景场景一&#xff1a;模拟计算器的加减乘除场景二&#xff1a;模拟qsort…

MySQL集群方案总结与方案原理

前言 在给自己做着玩的一个项目准备数据库集群&#xff0c;顺带自己大致系统复习并记录一下。 单节点mysql存在的常见问题 当数据量和并发量上去后&#xff0c;单节点数据库无法满足大并发时性能上的要求。单节点的MySQL无法满足高可用&#xff0c;数据库宕机或者意外中断等故障…

数据结构学习分享之双向链表详解

数据结构第四课 1.前言2. 结构分析3. 双链表的实现3.1 初始化结构3.2 初始化函数3.3 尾插函数3.4 尾删函数3.5 头插函数3.6 头删函数3.7 销毁链表3.8 其他函数 4. 缓存利用率5. 总结 1.前言 &#x1f493;博主CSDN:杭电码农-NEO&#x1f493;&#x1f389;&#x1f389;&#x…

Python(一) 基础二(语句、文件读写)

1.语句 1.1.if…elif…else 类似于java的if…else if…else语句 1.1.1.判断条件 比较运算符: 、>、<、<、>、!、is、is not、in、not in 1.1.2.和is的区别 list_1 [aaa, bbb] list_2 [aaa, bbb] print(list_1 list_2) #结果:True print(list_1 is list_2)…

async await

async await async await 都是修饰符&#xff0c;修饰函数的。 async/await一定是成对出现的。比如用async也没有什么太大意义。只要函数体中出现了await&#xff0c;则当前函数必须用async来修饰。 用async修饰的函数&#xff0c;相当于用promise包裹起来。其实相当于把同步修…

基于jeecgboot的OA日程安排开发(一)

日程安排也是OA里的一项重要功能&#xff0c;所以基于jeecgboot开发这个日程安排。 日程安排主要涉及以下几个方面&#xff1a; 1、数据库方面&#xff0c;主要是分日历与日程 日历可以分个人日历与工作日历&#xff0c;一般情况下&#xff0c;个人日历只给自己查看&#xff0…

2023-05-02 动态规划简介

动态规划简介 1 动态规划的基本概念 阶段、状态、决策、策略、状态转移方程 1) 阶段和阶段变量 将问题的全过程恰当地分成若干个相互联系的阶段闫氏DP分析法&#xff1a;对应f[i][j]的ij遍历时形成的所有f[i][j]阶段的划分一般根据时间和空间的自然特征去划分阶段的划分便于把…

Nginx原理解析

master和worker 当linux启动的时候&#xff0c;会有两个和nginx相关的进程&#xff0c;一个是master,一个是worker。 master如何工作 当客户端发送请求到nginx之后&#xff0c;master会接收到这个请求&#xff0c;然后通知所有的worker进程&#xff0c;此时&#xff0c;work…

【STL十六】函数对象:包装器(std::function)——绑定器(std::bind)——函数适配器

【STL十六】函数对象&#xff1a;包装器(std::function&#xff09;——绑定器&#xff08;std::bind&#xff09;——函数适配器 一、包装器(std::function&#xff09;1、简介2、头文件3、构造函数4、demo5、异常 二、绑定器&#xff08;std::bind&#xff09;1、简介2、头文…