可变参数列表详解

可变参数列表(Variadic Parameters)是指函数在定义时可以接受不定数量的参数。

参数传递机制

        在 C 语言中,函数参数通过栈(stack)来传递。当函数被调用时,参数的值会被依次压入栈中。对于可变参数函数,编译器使用函数栈内连续的空间来存储参数也就是栈帧。

栈帧

        每个函数调用会创建一个新的栈帧(stack frame),用于存储该函数的参数、局部变量以及返回地址。栈帧在函数调用时分配,在函数返回时释放。

内存布局

        当函数被调用时,所有参数(包括可变参数)都会依次从右向左依次压入栈中,而栈是向地址减小的方向增长的,所以当压入最后一个可变参数时就是该栈的栈顶。

通过获取第一个参数的地址,就可以通过正确地址偏移来找到其他参数的地址。

高地址
   +-------------------+  <- 栈底
   |   &"hello ptm"   |  <- 第五个参数 (const char*) 压入的是地址
   +-------------------+
   |     3.141590     |  <- 第四个参数 (double)
   +-------------------+
   |     3.140000     |  <- 第三个参数 (float, 提升为 double)
   +-------------------+
   |        2         |  <- 第二个参数 (int)
   +-------------------+
   |        4         |  <- 第一个参数 (int, 参数个数 n)
   +-------------------+  <- 栈顶
低地址
#include <stdio.h>

// 可变参数函数,第一个参数是参数的个数     主要是用来获取参数起始地址,后面是可变参数
void print(int n, ...) 
{
    // 将第一个参数的地址转换为char* 用来保存第一个参数的地址
    char* p = (char*)&n;

    // 获取第一个参数的地址的大小用于判断是32位还是64位系统
    int z = sizeof(&n);

    // 根据地址大小判断是32位还是64位系统
    if (z == 4) 
    {
        // 32位系统
        // 打印第一个参数的值
        printf("%d ", *p);

        // 打印第二个参数的值
        printf("%d ", *(p + 4));

        // 打印第三个参数的值(float提升为double)
        printf("%f ", *(double*)(p + 8));

        // 打印第四个参数的值(float 提升为 double)
        printf("%lf ", *(double*)(p + 16)); // 前面的 float 参数在传递时会提升为 double 类型,因此该参数占用 8 字节。
                                            // 为了访问下一个参数,需要在当前地址基础上增加 8 字节,即 16 字节,而不是只增加指针大小的 4 字节 (12 字节)。

        // 打印第五个参数的值(字符串)
        printf("%s ", *(char**)(p + 24));//(这里 p + 24 同上) 此处由于传入的是一个字符串常量值,实际是传入该字符的地址。
                                         // 因此,需要将 p + 24 强转为 char** 类型,以表示这是一个二级指针。
                                         // 解引用后可以得到指向该字符串的地址。
    }                                    
    else if (z == 8) 
    {
        // 64位系统
        // 打印第一个参数的值
        printf("%d ", *p);

        // 打印第二个参数的值
        printf("%d ", *(p + 8));

        // 打印第三个参数的值(float提升为double)
        printf("%f ", *(double*)(p + 16)); //因为64位系统本身就是8字节指针大小,所以都是当前地址基础上增加 8 字节

        // 打印第四个参数的值(float提升为double)
        printf("%lf ", *(double*)(p + 24));

        // 打印第五个参数的值(字符串)
        printf("%s ", *(char**)(p + 32));  //char** 同上 
    }
}

int main() 
{
    // 调用可变参数函数
    print(4, 2, 3.14f, 3.14159, "hello ptm");

    return 0;
}

float类型的参数在传递给函数时会被提升为double类型       

         在C语言中,float类型的参数在传递给函数时会被提升为double类型。这是因为C语言的函数调用约定规定,所有浮点数参数都必须以double类型传递。

这种提升是为了确保浮点数参数在函数调用过程中能够被正确地传递和处理。在早期的C语言版本中,浮点数的表示方式和处理方式并不统一,导致了浮点数参数在函数调用过程中可能会被错误地传递或处理。

为了解决这个问题,C语言标准委员会决定将所有浮点数参数提升为double类型,这样可以确保浮点数参数在函数调用过程中能够被正确地传递和处理。

具体来说,当你将一个float类型的变量传递给函数时,编译器会自动将其提升为double类型,这样函数就可以接收到一个double类型的参数。这种提升是隐式的,你不需要显式地进行类型转换。

C 标准库之stdarg.h

    <stdarg.h> 是 C 语言的标准库之一,定义了处理可变参数函数所需的宏和类型。这些宏提供了一种标准化的方式来访问不定数量的参数,确保代码的可移植性和一致性。

va_list用来存储可变参数的地址

 typedef char* va_list;

va_start 用于初始化 va_list 类型的变量,使其指向函数参数列表中的第一个可变参数。

void va_start(va_list ap, last);
  • ap: 一个 va_list 类型的变量,应该在调用 va_start 前声明。
  • last: 函数参数列表中最后一个确定参数的名称。va_start 使用它来确定可变参数的起始位置。
#define va_start __crt_va_start
//将 va_start 重定向到 __crt_va_start


#define __crt_va_start(ap, x) __crt_va_start_a(ap, x)
// __crt_va_start 则被定义为调用 __crt_va_start_a


#define __crt_va_start_a(ap, v) ((void)(ap = (va_list)_ADDRESSOF(v) + _INTSIZEOF(v))) 
// 定义 __crt_va_start_a 宏

#define _ADDRESSOF(v) (&(v))

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


(void)(ap = (char*)(&(v)) + ((sizeof(n) + sizeof(int) - 1) & ~(sizeof(int) - 1))

(char*)(&(v)):将参数 v 的地址转换为 char* 类型的指针。
sizeof(n):获取参数 n 的大小(以字节为单位)。
sizeof(int):获取整型数据类型的大小(以字节为单位)。
((sizeof(n) + sizeof(int) - 1) & ~(sizeof(int) - 1)):这个表达式是一个位运算操作,它将 sizeof(n) 加上一个整型数据类型大小减一的结果,然后与一个整型数据类型大小减一的取反结果进行按位与操作。这个操作的目的是将大小按照整型数据类型的对齐方式进行调整。
最终,整个表达式将计算出下一个参数在可变参数列表中的位置,并将该位置赋给 ap,同时使用 (void) 来抑制赋值表达式的返回值。用于获取可变参数列表中下一个参数的地址。

理论基础

当我们要将一个值 x 向上调整到下一个 4 的倍数时,可以使用以下逻辑:

  1. 4 的倍数: 一个数是 4 的倍数,当且仅当它的二进制表示的最低两位是 00

    • 例如:
      • 0 (二进制 00)
      • 4 (二进制 100)
      • 8 (二进制 1000)
  2. 加上 3(sizeof(int) - 1) 的原因:

    • 如果 x 不是 4 的倍数,添加 3 会确保下一个 4 的倍数能够被计算出来。
    • 例如,对于不同的 x 值:
      • x = 1: 1 + 3 = 4 (4 的倍数)
      • x = 2: 2 + 3 = 5 (5 & ~3 = 4)00000101 & 11111100 = 00000100 = 4
      • x = 3: 3 + 3 = 6 (6 & ~3 = 4)00000110 & 11111100 = 00000100 = 4
      • x = 4: 4 + 3 = 7 (7 & ~3 = 4)00000111 & 11111100 = 00000100 = 4
      • x = 5: 5 + 3 = 8 (8 & ~3 = 4)00001000 & 11111100 = 00001000 = 8
      • x = 6: 6 + 3 = 9 (9 & ~3 = 4)00001001 & 11111100 = 00001000 = 8
      • x = 7: 7 + 3 = 10 (10 & ~3 = 8)00001010 & 11111100 = 00001000 = 8

位运算解释

使用 & ~3 的目的是通过掩码来清除最低两位,使得结果是一个 4 的倍数。

  • 掩码 ~3 的二进制表示为 ...11111100,当与任何数进行按位与运算时,会将该数的最低两位清零。这可以确保结果是 4 的倍数。

总结

  • 使用 +3 是为了确保向上调整到下一个 4 的倍数。
  • 使用 & ~3 是为了清除最低两位,保证结果是 4 的倍数。
  • 计算当前大小: sizeof(n) 计算数据的大小。
  • 增加对齐值: sizeof(int) - 1 是为了确保即使当前大小不是 4 的倍数,也能向上调整。
  • 位运算:
    • ~(sizeof(int) - 1) 创建一个掩码,用于清除低位。
    • 将上述两者相与,得到符合 4 字节对齐的值。

假设 n 是一个 char 类型的参数,其 sizeof(n) 为 1,那么:

  • sizeof(n) + sizeof(int) - 1 = 1 + 4 - 1 = 4
  • ~(sizeof(int) - 1) = ~(4 - 1) = ~3 = ...11111100(在 32 位系统上)
  • 4 & ~3 = . . . 00000100 & ...11111100 = 4

最终结果是 4,这意味着下一个参数的地址将根据 4 字节对齐来计算。

va_arg()

    用于从可变参数列表中逐个获取参数的值。它接受两个参数,一个是 va_list 类型的参数列表,另一个是要获取的参数的类型,然后返回该参数并将 va_list 指针指向下一个参数。

#define va_arg   __crt_va_arg

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


  1. ap += _INTSIZEOF(t): 将指针 ap 向前移动 t 类型的大小。
  2. (ap += _INTSIZEOF(t)) - _INTSIZEOF(t): 将指针 ap 移动回到前一个参数的位置。
  3. *(t*): 将指针转换为 t* 类型,然后取该位置的值。

综合起来,这段代码的作用是从可变参数列表中获取下一个参数,并将其转换为类型 t,然后返回其值。

va_copy()

   用于将一个 va_list 对象的状态复制到另一个 va_list 对象中。这个宏的目的是为了在处理可变参数函数时能够保存和重新使用参数列表的状态。

#define va_copy(destination, source) ((destination) = (source))

它的作用是简单地将 source 的值赋给 destination

va_end()

    用于结束对可变参数列表的访问。当使用可变参数函数处理完参数后,应该使用 va_end 来清理资源并结束对参数列表的访问。

#define va_end   __crt_va_end

#define __crt_va_end(ap)        ((void)(ap = (va_list)0))
  • (va_list)0: 将整数 0 转换为 va_list 类型,这通常是将一个空指针值赋给 va_list 类型的指针。
  • ap = (va_list)0: 将上述转换后的空指针值赋给 ap,从而将 ap 设置为一个空指针。
#include <stdio.h>
#include <stdarg.h>

// 定义一个可变参数函数 print
void print(int n, ...) 
{
    va_list arg; // 声明一个 va_list 类型的变量,用于存储可变参数
    va_start(arg, n); // 初始化 va_list,指定最后一个固定参数为 n

    // 打印第一个参数,期待为 int 类型
    printf("%d ", va_arg(arg, int)); // 获取下一个参数并打印,类型为 int

    // 打印第二个参数,期待为 double 类型
    printf("%lf ", va_arg(arg, double)); // 获取下一个参数并打印,类型为 double
    // 注意:float 会提升为 double 类型了
    // printf("%f ", va_arg(arg, float)); 是不正确的

// 打印第三个参数,期待为 char* 类型
    printf("%s ", va_arg(arg, char*)); // 获取下一个参数并打印,类型为 char*
    // 字符串常量是一个指向字符数组的指针

    va_end(arg); // 清理 va_list,结束可变参数的处理
}

int main() 
{
    // 调用 print 函数,传入 3 个参数:一个 int,一个 float 和一个字符串
    print(3, 2, 3.14f, "hello"); // 输出: 2 3.140000 hello 

    return 0;
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值