可变参数列表

文章介绍了C语言中处理可变参数的机制,包括va_list、va_start、va_arg和va_end宏的使用,以及它们在函数栈帧中的工作原理。讨论了参数的整型提升现象,并解释了4字节对齐的重要性,确保正确读取不同类型的参数。此外,文章还探讨了可变参数列表在处理不确定数量参数时的作用和限制。
摘要由CSDN通过智能技术生成

"多少人都,生来纯洁完美,心底从不染漆黑。"


        我们想要实现一个函数,这个函数的功能是返回一个整形的最大值。

        emm,似乎有那点味道。但这应用场景似乎很受限制,因为这个函数比较的有效区间,只能装下两个数…… 也许你会说,那我就增添几个参数不就得了?

         似乎问题,没有得到本质的解决。因为当你参数变多时,函数内的代码逻辑也得跟着更改。况且,如果遇到一个,你不知道会有多少个参数传入的场景,你又该如何选择参数个数的多少呢?

        也许你会说可以将数据存储在一个数组中,将数组作为参数进行传递。但,你同样不知道应该创建多大的数组来适应这种情况。

        难道我们对此就束手无策了吗?当然不是,C语言中提供了对这种不确定参数传入的解决方法

        “可变参数列表”。

——前言


一、 可变参数列表 

(1) 什么是可变参数

        在计算机程序设计,一个可变参数函数是指一个函数 "拥有不定引数" ,即是它接受一个可变数目的参数。简单来说,就是函数的参数个数可变,参数类型不定的函数。

        不同的编程语言对可变参数函数的支持有很大差异。

                                                                                                                            取自这里

        而可变参数的应用场景,前言的例子,也已经很恰当的演示了。

(2) 可变参数的使用

参数列表宏定义" 四板斧"

va_list arg: 其实就是定义一个char*类型,方便后续按照字节进行指针移动

va_start(arg,n(第一个参数)): 让指针指向(...)里的参数部分

va_arg(arg,类型): 从(...)取数据,并根据类型 朝后指向下一个参数 

va_end(arg): arg使用完毕,收尾工作。本质就是讲arg指向NULL

注意事项:

① 参数列表中至少有一个命名参数。
② 可变参数必须从头到尾逐个访问。如果你在访问了几个可变参数之后想半途终止,这是可以的,但是,如果你想一开始就访问参数列表中间的参数,那是不行的。

③ 参数列表中至少有一个命名参数。如果连一个命名参数都没有,就无法使用 va_start 。

④ 这些宏是无法直接判断实际存在参数的数量。
⑤ 如果在 va_arg 中指定了错误的类型,那么其后果是不可预测的。

        

        介绍了基本的可变参数的使用,我们来试试编写看。

         这样,我们就能够很轻松地使用这个可变参数列表了,完成了我们对“不定参数”下,求最大值的需求。


        仅仅三言两语讲解它的使用就截止了嘛?当然不是,因为从使用上来说,它确实很简单。可是要问你它底层是怎么实现的,指针是怎么移动的,如果传入的类型不是int呢?是字符型呢?是短整型呢?可变参数内部又是作何处理的?

二、可变参数原理

(1) 基本原理

        要弄清楚可变参数如何传参,如何让指针指向不同的值,来完成对参数的读取,我们一定得看看这 宏定义 " 四板斧"的底层是什么。

① va_list:

        

        va_list没有什么可以值得说的,因为它底层就是一个类型重定义的 char*指针。至于为什么是char*,因为在之后它好按照每字节进行偏移。

② va_start:

 

③ va_arg:

④ va_end:

         这个同va_list 一样没什么讲头,就是让创建的arg指针,指向空,防止野指针。

(2) 图示四板斧

        我知道,恐怕我就算将这四个宏定义函数讲得怎样绘声绘色,你也难以理解,更何况我讲得很烂。因此,以下根据图示看看这四个宏定义做了什么工作。

        可变参数是函数调用,而这里和函数栈帧的开辟以及参数的创建强相关,为此,在进行图示之前,你可以先看看下面的几个问题,看你是否真的掌握了函数栈帧的细节。

         根据上图,我们可以得出三个结论:

① 函数参数在函数调用前,就会在被压栈。

② 函数参数压栈的顺序是从右到左。

③ 每个参数的位置都是相对的,只要找到了第一个参数的位置,根据偏移量,就可以通过地址偏移的方式,获得其他参数值。

 

调用va_list 与 va_start;         

 

迭代va_arg:

置空va_end:
  

(3) 整形提升

        我们上面的实验是针对传入的是整型而言,现在我们来看看如果将传入的参数是" char"类型时,该函数还能否实现我们预期的功能?

         很显然,这些字符里最大的就是 'e',所对应的ASCII码为101,完成了我们预期的功能。可是,我们明明传入的是char类型诶,为什么按照 "int"类型的大小偏移指针,没有出错呢?

         唯一能给我们解释的,就是mov 与 movsx 。

        汇编语言数据传送指令MOV的变体。带符号扩展,并传送。简单来说,使用movsx汇编命令,让原本是char类型的变量,整型提升成了int!不管是char还是短整型都会被整型提升! 

        因此,如果我们看到以下代码,其实是不正确的。

 __crt_va_arg(arg, char);
 __crt_va_arg(arg, short);

        因此,为什么要按照4字节向上取整对齐? 本质上是为了 便于指针+偏移量的方式,按照字节读取每个地址处的参数值。

        

(4) 如何理解4字节对齐?

        这里的前提是,32位平台下,sizeof(int)大小是4,其他情况我们不考虑。

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

        单独这么看,似乎很难理解,为什么这个宏函数的功能能够实现4字节向上取整的功能。

第一步:        

第二步:

第三步:

         这就是大佬们的智慧,真的遥望而不可及。


总结:

        当然,对于可变参数列表的使用肯定是比原理更加得现实的,比较对于最后讲的4字节向上对齐怎么来的,是需要一定的数学基础和推理的,难度在这里摆着。

本篇到此结束,感谢你的阅读

祝你好运,向阳而生~

 

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值