C++ 可变长度参数是如何工作的
在本文中,我们将讨论可变长度参数的工作原理。
- 可变参数函数
- 调用约定
- C/C++程序的内存布局
- 负下标
可变参数函数: 可变参数函数是一种可以接收任意数量参数的模板。在某些情况下,函数根据需求处理变量数量的参数,例如:
- 给定数字的总和。
- 给定数字的最小值等。
可变数量的参数由三个点(…)表示。
程序1:
输出
调用约定: 调用约定指函数的调用方式、参数传递方式以及堆栈如何清除。C/C++有各种调用约定,但我们只关心__cdecl和__stdcall。
两者非常相似,但也有一些区别:
- __cdecl是C/C++的默认调用约定。
- __stdcall是Windows API函数的默认调用约定。
现在,两种调用约定都是从右往左传递参数。C/C++开发人员选择从右往左传递参数,而不是从左往右。
内存布局: 内存布局如下:
唯一需要注意的是堆和堆栈段朝相反的方向增长。
- 堆朝较高的地址增长。
- 堆栈朝较低的地址增长。这意味着在堆栈上越高的位置实际上是较低的地址。当我们将某些内容推到堆栈上时,它会获得堆栈中最低的即时地址。
让我们通过函数来理解:
在main()中,我们有一个函数func(arg1, arg2, arg3)
当func被调用时,main()被称为”caller”,func()被称为”callee”
让我们看一下它的堆栈
caller本地变量 ->栈底地址高
—-(其他东西)
arg3 (最右边的)
arg2
arg1
—-
callee本地变量 ->在堆栈上较高的位置,地址较低^在此处创建新的堆栈,而不是在顶部创建
在上面的部分中,您可以清楚地看到第一个参数获得了最低的地址。这就是为什么开发人员选择从右到左而不是从左到右的原因,因为从左到右的调用约定会给第一个参数最高的地址,这可能会引起问题。
第一个参数获得最低的地址,所有参数在堆栈中具有连续的地址。
负下标: [] 是下标运算符。以下是下标运算符的一些重要注意事项:
- 如果下标运算符作用于一个指针上,那么它的行为是不同的。即,ptr[x]意味着*(ptr + x),即在ptr的x * sizeof(data_type_of_pointer)之后的值。
- 同样,ptr[-x]意味着*(ptr – x),即在ptr的x * sizeof(data_type_of_pointer)之前的值。
下面是一个示例,说明如何使用<stdarg.h>:
输出
解释:
内置实现有一些限制,让我们看看这些限制,并看看如何克服它们。其中之一是需要强制传递第一个参数,让我们看看为什么需要它以及如何避免它。
<stdarg.h>使用va_list、va_start、va_arg和va_end。问题可以分为2部分:
- 他们做了什么?
- 我们如何复制它?
va_list: 这是 char*** 的一个 **typedef ,但这是非常不同的,因为当它作为C字符串类型使用时,我们会得到不可预测的结果。这是因为它不是常见的 typedef 。它是内置定义的。
va_start: 这是一个宏,它的作用是使用在第一个参数(即arg1)之前的地址初始化ap,这实际上是va_list类型(即char*)。这就是为什么需要强制传递第一个参数的原因。它可以是任何数据类型的任何值,但为了简单起见,通常传递参数的数量。它用于识别连续参数的堆栈上的地址。
va_arg: 这个宏非常复杂。它做两件事。
- 返回所需的参数
- 推进到下一个参数
// arg.h
define va_get(ap, type) ((type*)ap)
// 将由ap(这里是arg2)持有的地址强制转换为type*
define va_advance(ap, type) ap = ap + sizeof(type)
让我们看看这些三个点是什么。实际上,它是一个 椭圆运算符 ( . . . ) 并且是C++定义的。这个运算符用于传递变量数量的参数。这就是 stdarg.h 的工作方式,如果无法使用可变函数中的第一个参数。我们可以像这样利用它
define va_start(ap, arg1) (ap = (char*)(&arg1))
但是,这没有任何意义,因为我们不知道有多少个参数被传递,那么我们怎么能利用它们呢?