C语言变参函数解析

深入解析C语言变参函数实现原理
本文详细解析了C语言中实现变参函数的原理,包括函数声明、内部实现以及具体宏的用途,通过实例代码展示了如何利用这些宏进行参数的获取和管理。
everyday.smile();
文章作者:Leo chin
1 函数声明
   首先,要实现类似printf()的变参函数,函数的最后一个参数要用 ... 表示,如 
     int log(char * arg1, ...)
这样编译器才能知道这个函数是变参函数。这个参数与变参函数的内部实现完全没有关系,只是让编译器在编译调用此类函数的语句时不计较参数多少老老实实地把全部参数压栈而不报错,当然...之前至少要有一个普通的参数,这是由实现手段限制的。
2 函数实现
   C语言通过几个宏实现变参的寻址。下面是linux2.18内核源码里这几个宏的定义,相信符合C89,C99标准的C语言基本都是这样定义的。


   typedef char *va_list;


/*
   Storage alignment properties -- 堆栈按机器字对齐
*/
#define _AUPBND            (sizeof (acpi_native_uint) - 1)
#define _ADNBND            (sizeof (acpi_native_uint) - 1)


/*
   Variable argument list macro definitions -- 变参函数内部实现需要用到的宏
*/
#define _bnd(X, bnd)    (((sizeof (X)) + (bnd)) & (~(bnd)))
#define va_arg(ap, T) (*(T *)(((ap) += (_bnd (T, _AUPBND))) - (_bnd (T,_ADNBND))))
#define va_end(ap)      (void) 0
#define va_start(ap, A) (void) ((ap) = (((char *) &(A)) + (_bnd (A,_AUPBND))))


   下面以x86 32位机为例分析这几个宏的用途
   要理解这几个宏需要对C语言如何传递参数有一定了解。与PASCAL相反,与stdcall 相同,C语言传递参数时是用push指令从右到左将参数逐个压栈,因此C语言里通过栈指针来访问参数。虽然X86的push一次可以压2,4或8个字节入 栈,C语言在压参数入栈时仍然是机器字的size为最小单位的,也就是说参数的地址都是字对齐的,这就是_bnd(X,bnd)存在的原因。另外补充一点 常识,不管是汇编还是C,编译出的X86函数一般在进入函数体后立即执行 
   push ebp
   mov ebp, esp
   这两条指令。首先把ebp入栈,然后将当前栈指针赋给ebp,以后访问栈里的参数都使用ebp作为基指针。
  
   一一解释这几个宏的作用。
   _bnd(X,bnd) ,计算类型为X的参数在栈中占据的字节数,当然是字对齐后的字节数了。acpi_native_unit是一个机器字,32位机的定义是:typedef u32 acpi_native_uint;
   显然,_AUPBND ,_ADNBND 的值是 4-1 == 3 == 0x00000003 ,按位取反( ~(bnd))就是0xfffffffc 。 
因此,_bnd(X,bnd) 宏在32位机下就是 
   ( (sizeof(X) + 3)&0xfffffffc )
很明显,其作用是--
倘若sizeof(X)不是4的整数倍,去余加4。
   _bnd(sizeof(char),3) == 4 
   _bnd(sizeof(struct size7struct),3) == 8


   va_start(ap,A) ,初始化参数指针ap,将函数参数A右边第一个参数的地址赋给ap。 A必须是一个参数的指针,所以此种类型函数至少要有一个普通的参数啊。像下面的例子函数,就是将第二个参数的指针赋给ap。


   va_arg(ap,T) ,获得ap指向参数的值,并使ap指向下一个参数,T用来指明当前参数类型。
   注意((ap) += (_bnd (T, _AUPBND))) 是被一对括号括起来的,然后才减去(_bnd (T, _ADNBND),
而_AUPBND和_ADNBND是相等的。所以取得的值是ap当前指向的参数值,但是先给ap加了当前参数在字对齐后所占的字节数,使其指向了下一个参数。


va_end(ap), 作用是美观。
  
3 总结
先用一个 ... 参数声明函数是变参函数,接下来在函数内部以va_start(ap,A)宏初始化参数指针,然后就可以用va_arg(ap,类型)从左到右逐个获取参数值了


分析到此处算是一清二白了,下面给一个例子
int log(char * fmt,...)
{
va_list ap;
int d;
char c, *p, *s;


va_start(ap, fmt);
while (*fmt)
   switch(*fmt++) {
   case 's':        /* string */
   s = va_arg(ap, char *);
   printf("string %s/n", s);
   break;
   case 'd':        /* int */
   d = va_arg(ap, int);
   printf("int %d/n", d);
   break;
   case 'c':        /* char */
   c = va_arg(ap, char);
   printf("char %c/n", c);
   break;
}
va_end(ap);
}


### C语言函数的实现示例 #### 一、函数的核心概念 C语言中的函数依赖于标准库 `<stdarg.h>` 中的功能支持,主要包括以下几个关键组件: - **`va_list`**: 存储可数列表的相关信息。 - **`va_start`**: 初始化 `va_list` 类型的量,使其指向第一个数的位置[^1]。 - **`va_arg`**: 获取当前数并移动指针到下一个数位置,需指定数类型。 - **`va_end`**: 结束对可数的操作,完成必要的清理工作。 #### 二、具体实现案例分析 ##### 示例 1:简化版 `my_printf` 函数 以下是一个基于 `va_list` 的简化版 `printf` 函数实现,展示了如何解析格式化字符串 `%d` 和 `%s` 并输出对应的数值和字符串: ```c #include <stdio.h> #include <stdarg.h> // 自定义 printf 函数 int my_printf(const char* format, ...) { va_list argptr; va_start(argptr, format); int count = 0; // 记录已处理的数数量 const char* p = format; while (*p != '\0') { // 遍历整个格式化字符串 if (*p == '%') { // 发现占位符标志 p++; // 跳过 '%' switch (*p) { // 根据后续字符判断类型 case 'd': { // 整数类型 int value = va_arg(argptr, int); printf("%d", value); count++; break; } case 's': { // 字符串类型 char* str = va_arg(argptr, char*); printf("%s", str); count++; break; } default: // 不识别的占位符直接输出 putchar('%'); putchar(*p); break; } } else { // 普通字符直接输出 putchar(*p); } p++; // 移动到下一个字符 } va_end(argptr); // 清理资源 return count; // 返回已处理的数总数 } int main() { int a = 42; char* str = "World"; my_printf("Hello %s! The answer is %d.\n", str, a); return 0; } ``` 此代码片段实现了基本的格式化输出功能,能够正确解析 `%d` 和 `%s` 占位符,并将其替换为相应的整数值或字符串[^2]。 --- ##### 示例 2:通用求和函数一个常见的应用场景是对任意数量的整数进行求和。以下是其实现方法: ```c #include <stdio.h> #include <stdarg.h> // 可数求和函数 double sum(int count, ...) { va_list args; va_start(args, count); double total = 0.0; for (int i = 0; i < count; ++i) { total += va_arg(args, double); // 假设所有输入均为双精度浮点数 } va_end(args); return total; } int main() { double result = sum(5, 1.5, 2.3, 3.7, 4.1, 5.9); printf("Sum of numbers: %.2f\n", result); return 0; } ``` 在此示例中,`sum` 函数接收一个表示数个数的整数作为首个固定数,随后通过 `va_arg` 循环读取剩余的所有浮点数值并累加至总和中[^1]。 --- ##### 示例 3:向文件写入格式化数据 (`vfprintf`) `<cstdio>` 库提供了 `vfprintf` 函数,用于将格式化的数据从可数列表写入特定流。下面是对其工作机制的模拟实现: ```c #include <stdio.h> #include <stdarg.h> // 模拟 vfprintf 功能 int custom_vfprintf(FILE* stream, const char* format, va_list args) { int written = 0; const char* ptr = format; while (*ptr != '\0') { if (*ptr == '%') { ptr++; switch (*ptr) { case 'd': { int value = va_arg(args, int); fprintf(stream, "%d", value); written += snprintf(NULL, 0, "%d", value); break; } case 's': { char* str = va_arg(args, char*); fprintf(stream, "%s", str); written += strlen(str); break; } default: fputc('%', stream); fputc(*ptr, stream); written += 2; break; } } else { fputc(*ptr, stream); written++; } ptr++; } return written; } int main() { FILE* file = fopen("output.txt", "w"); if (!file) return -1; va_list args; va_start(args, "Example: %d and %s\n"); int number = 42; char* text = "variable arguments"; va_copy(args, number); va_copy(args, text); custom_vfprintf(file, "Example: %d and %s\n", args); va_end(args); fclose(file); return 0; } ``` 这段代码演示了如何创建一个类似于 `vfprintf` 的函数,它可以接受文件流对象以及格式化字符串和对应的实际数列表[^3]。 --- ###
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值