C++ 可变长度参数是如何工作的

C++ 可变长度参数是如何工作的

在本文中,我们将讨论可变长度参数的工作原理。

  • 可变参数函数
  • 调用约定
  • C/C++程序的内存布局
  • 负下标

可变参数函数: 可变参数函数是一种可以接收任意数量参数的模板。在某些情况下,函数根据需求处理变量数量的参数,例如:

  • 给定数字的总和。
  • 给定数字的最小值等。

可变数量的参数由三个点(…)表示。

程序1:

// C program to demonstrate the use of
// variable number of arguments
#include <stdarg.h>
#include <stdio.h>
  
// Function to find the minimum of integer
// numbers passed, ftrst argument is count
// of numbers
int min(int arg_count, ...)
{
    int i;
    int min, a;
  
    // va_list is a type that holds the
    // information about variable arguments
    va_list ap;
  
    // va_start must be called before
    // accessing variable argument list
    va_start(ap, arg_count);
  
    // Now arguments can be accessed one
    // by one using va_arg macro.
  
    // Initialize min as the first
    // argument in list
    min = va_arg(ap, int);
  
    // Traverse the rest of arguments
    // to find out minimum
    for (i = 2; i <= arg_count; i++)
        if ((a = va_arg(ap, int)) < min)
            min = a;
  
    // va_end should be executed before
    // the function returns whenever
    // va_start has been previously
    // used in that function
    va_end(ap);
  
    return min;
}
  
// Driver Code
int main()
{
    int count = 5;
    printf("Minimum value is %d",
           min(count, 12, 67, 6, 7, 100));
  
    return 0;
}  

输出

Minimum value is 6

调用约定: 调用约定指函数的调用方式、参数传递方式以及堆栈如何清除。C/C++有各种调用约定,但我们只关心__cdecl和__stdcall。

两者非常相似,但也有一些区别:

  • __cdecl是C/C++的默认调用约定。
  • __stdcall是Windows API函数的默认调用约定。

现在,两种调用约定都是从右往左传递参数。C/C++开发人员选择从右往左传递参数,而不是从左往右。

内存布局: 内存布局如下:

C++ 可变长度参数是如何工作的

唯一需要注意的是堆和堆栈段朝相反的方向增长。

  1. 堆朝较高的地址增长。
  2. 堆栈朝较低的地址增长。这意味着在堆栈上越高的位置实际上是较低的地址。当我们将某些内容推到堆栈上时,它会获得堆栈中最低的即时地址。

让我们通过函数来理解:

在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>:

// C++ program to implement stdarg.h
#include <iostream>
#include <stdarg.h>
using namespace std;
  
// Function to find the sum of numbers
int sum(int num, ...)
{
    int res = 0;
    va_list ap;
    va_start(ap, num);
    for (int i = 0; i < num; i++) {
        res += va_arg(ap, int);
    }
    // Return the resultant sum
    return res;
}
  
// Driver Code
int main()
{
    // First argument is the number
    // of arguments
    cout << sum(4, 6, 89, 34, 26);
    return 0;
}  

输出

155

解释:
内置实现有一些限制,让我们看看这些限制,并看看如何克服它们。其中之一是需要强制传递第一个参数,让我们看看为什么需要它以及如何避免它。

<stdarg.h>使用va_list、va_start、va_arg和va_end。问题可以分为2部分:

  • 他们做了什么?
  • 我们如何复制它?

va_list: 这是 char*** 的一个 **typedef ,但这是非常不同的,因为当它作为C字符串类型使用时,我们会得到不可预测的结果。这是因为它不是常见的 typedef 。它是内置定义的。

// arg.h
typedef char* va_list;

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))

但是,这没有任何意义,因为我们不知道有多少个参数被传递,那么我们怎么能利用它们呢?

Python教程

Java教程

Web教程

数据库教程

图形图像教程

大数据教程

开发工具教程

计算机教程