Skip to content

Latest commit

 

History

History
129 lines (97 loc) · 5.03 KB

Variadic function.md

File metadata and controls

129 lines (97 loc) · 5.03 KB

要深入理解 C 语言中的可变参数机制,关键是了解 <stdarg.h> 头文件中定义的宏(如 va_listva_startva_argva_end)以及这些宏背后的原理。虽然它们在不同的系统上可能有不同的实现,但它们的工作原理是基于栈的内存布局和指针运算。

可变参数的基本原理

在C语言中,当一个函数被调用时,参数通常会依次压入栈中(堆栈的增长方向因体系结构而异)。对于可变参数函数(如 `printf`),编译器无法在编译时确定函数被调用时会传递多少个参数,因此需要一种机制在运行时遍历这些参数。

栈帧的布局(假设栈从高地址向低地址增长):向下增长

 +-----------------------+   <- 高地址
|        参数 1          |
+-----------------------+
|        参数 2          |
+-----------------------+
|        参数 3          |
+-----------------------+
|   返回地址 (ret addr)  |   <- 栈顶
+-----------------------+   <- 低地址

在这种布局下,第一个参数(假设是 int a)位于栈的最低位置,最后一个参数位于最高位置。 第一个参数为低地址

可变参数的宏定义

va_list

typedef char* va_list;

在某些实现中,va_list 实际上只是一个指向栈中某个位置的指针,也可能是更复杂的结构,用于记录当前参数的位置和相关信息。

va_start

#define va_start(ap, last) \
    (ap = (va_list)&last + sizeof(last))
  • va_start 的目的是初始化 va_list 变量 ap。它将 ap 设置为指向最后一个已知参数(last)之后的位置,即第一个可变参数的起始位置。
  • &last 返回 last 在栈中的地址,加上 sizeof(last) 就是下一个参数的地址。

va_arg

#define va_arg(ap, type) \
    (*(type *)((ap += sizeof(type)) - sizeof(type)))
  • va_arg 用于从 va_list 中获取当前参数,并将 ap 移动到下一个参数的位置。
  • (ap += sizeof(type)) 先将 ap 移动到下一个参数的位置。
  • sizeof(type) 再将指针向回移动一个 type 大小的步长,指向当前参数。
  • *(type *) 将指针解引用,返回该类型的参数值。

示例

int arg = va_arg(ap, int);

  • 这段代码会返回当前 ap 指向的 int 值,并将 ap 移动到下一个参数的位置。

va_end

#define va_end(ap) \ (ap = (va_list)0)

  • va_end 用于清理 va_list 变量。通常只是将 ap 置为 NULL(或 0),在某些复杂的实现中可能会执行其他清理工作。

可变参数的内存操作

  • va_start:通过 va_startap 被设置为指向第一个可变参数的位置,ap 的初始值基于最后一个已知参数 last 的地址。
  • va_arg:每次调用 va_argap 都会向上移动,指向下一个参数。ap 实际上就是一个指针,它在栈中遍历每个参数的位置。
  • va_end:清理 va_list,通常只是将 ap 置为 NULL

不同体系结构上的实现差异

在不同的 CPU 体系结构上,函数参数可能会通过不同的方式传递,比如通过寄存器或栈。在这些情况下,<stdarg.h> 中的宏实现可能会有所不同,以适应具体的调用约定(calling conventions)。

  • 栈传递:在大多数体系结构中,参数是通过栈传递的,因此上述的宏定义可以直接使用。
  • 寄存器传递:在某些体系结构中,前几个参数可能会通过寄存器传递。这种情况下,va_list 可能不仅仅是一个指针,还需要保存寄存器的状态。va_start 可能会将 va_list 初始化为指向寄存器中的参数,然后在寄存器用完时切换到栈参数。

可变参数的使用示例

考虑一个可变参数函数 printf,它的实现会通过 va_list 和相关宏遍历所有传入的参数,并根据格式字符串进行处理。

#include <stdio.h>
#include <stdarg.h>

void my_printf(const char *fmt, ...) {
    va_list args;
    va_start(args, fmt);

    const char *p;
    for (p = fmt; *p != '\0'; p++) {
        if (*p == '%') {
            p++;
            switch (*p) {
                case 'd': {
                    int i = va_arg(args, int);
                    printf("%d", i);
                    break;
                }
                case 'c': {
                    char c = va_arg(args, int); // char 提升为 int
                    putchar(c);
                    break;
                }
                case 's': {
                    char *s = va_arg(args, char*);
                    printf("%s", s);
                    break;
                }
                default:
                    putchar(*p);
                    break;
            }
        } else {
            putchar(*p);
        }
    }

    va_end(args);
}

int main() {
    my_printf("Integer: %d, Char: %c, String: %s\n", 42, 'A', "Hello");
    return 0;
}