经验首页 前端设计 程序设计 Java相关 移动开发 数据库/运维 软件/图像 大数据/云计算 其他经验
当前位置:技术经验 » 程序设计 » C 语言 » 查看文章
C语言如何实现可变参数详解
来源:jb51  时间:2021/7/26 14:07:25  对本文有异议

可变参数

可变参数是指函数的参数的数据类型和数量都是不固定的。

printf函数的参数就是可变的。这个函数的原型是:int printf(const char *format, ...)。

用一段代码演示printf的用法。

  1. // code-A
  2. #include <stdio.h>
  3. int main(int argc, char **argv)
  4. {
  5. printf("a is %d, str is %s, c is %c\n", 23, "Hello, World;", 'A');
  6. printf("T is %d\n", 78);
  7. return 0;
  8. }

在code-A中,第一条printf语句有4个参数,第二条printf语句有2个参数。显然,printf的参数是可变的。

实现

代码

code-A

先看两段代码,分别是code-A和code-B。

  1. // file stack-demo.c
  2.  
  3. #include <stdio.h>
  4.  
  5. // int f(char *fmt, int a, char *str);
  6. int f(char *fmt, ...);
  7. int f2(char *fmt, void *next_arg);
  8. int main(int argc, char *argv)
  9. {
  10. char fmt[20] = "hello, world!";
  11. int a = 10;
  12. char str[10] = "hi";
  13. f(fmt, a, str);
  14. return 0;
  15. }
  16.  
  17. // int f(char *fmt, int a, char *str)
  18. int f(char *fmt, ...)
  19. {
  20. char c = *fmt;
  21. void *next_arg = (void *)((char *)&fmt + 4);
  22. f2(fmt, next_arg);
  23. return 0;
  24. }
  25.  
  26.  
  27. int f2(char *fmt, void *next_arg)
  28. {
  29. printf(fmt);
  30. printf("a is %d\n", *((int *)next_arg));
  31. printf("str is %s\n", *((char **)(next_arg + 4)));
  32.  
  33. return 0;
  34. }
  35.  

编译执行,结果如下:

# 编译
[root@localhost c]# gcc -o stack-demo stack-demo.c -g -m32
# 反汇编并把汇编代码写入dis-stack.asm中
[root@localhost c]# objdump -d stack-demo>dis-stack.asm
[root@localhost c]# ./stack-demo
hello, world!a is 10
str is hi

code-B

  1. // file stack-demo.c
  2.  
  3. #include <stdio.h>
  4.  
  5. // int f(char *fmt, int a, char *str);
  6. int f(char *fmt, ...);
  7. int f2(char *fmt, void *next_arg);
  8. int main(int argc, char *argv)
  9. {
  10. char fmt[20] = "hello, world!";
  11. int a = 10;
  12. char str[10] = "hi";
  13. char str2[10] = "hello";
  14. f(fmt, a, str, str2);
  15. return 0;
  16. }
  17.  
  18. // int f(char *fmt, int a, char *str)
  19. int f(char *fmt, ...)
  20. {
  21. char c = *fmt;
  22. void *next_arg = (void *)((char *)&fmt + 4);
  23. f2(fmt, next_arg);
  24. return 0;
  25. }
  26.  
  27.  
  28. int f2(char *fmt, void *next_arg)
  29. {
  30. printf(fmt);
  31. printf("a is %d\n", *((int *)next_arg));
  32. printf("str is %s\n", *((char **)(next_arg + 4)));
  33. printf("str2 is %s\n", *((char **)(next_arg + 8)));
  34.  
  35. return 0;
  36. }
  37.  

编译执行,结果如下:

# 编译
[root@localhost c]# gcc -o stack-demo stack-demo.c -g -m32
# 反汇编并把汇编代码写入dis-stack.asm中
[root@localhost c]# objdump -d stack-demo>dis-stack.asm
[root@localhost c]# ./stack-demo
hello, world!a is 10
str is hi
str2 is hello

分析

在code-A中,调用f的语句是f(fmt, a, str);;在code-B中,调用f的语句是f(fmt, a, str, str2);。

很容易看出,int f(char *fmt, ...);就是参数可变的函数。

关键语句

实现可变参数的关键语句是:

  1. char c = *fmt;
  2. void *next_arg = (void *)((char *)&fmt + 4);
  3. printf("a is %d\n", *((int *)next_arg));
  4. printf("str is %s\n", *((char **)(next_arg + 4)));
  5. printf("str2 is %s\n", *((char **)(next_arg + 8)));
  • &fmt是第一个参数的内存地址。
  • next_arg是第二个参数的内存地址。
  • next_arg+4、next_arg+8分别是第三个、第四个参数的内存地址。

为什么

内存地址的计算方法

先看一段伪代码。这段伪代码是f函数的对应的汇编代码。假设f有三个参数。当然f也可以有四个参数或2个参数。我们用三个参数的情况来观察一下f。

f:

 ; 入栈ebp

 ; 把ebp设置为esp

 

 ; ebp + 0 存储的是 eip,由call f入栈

 ; ebp + 4 存储的是 旧ebp

 ; 第一个参数是 ebp + 8

 ; 第二个参数是 ebp + 12

 ; 第三个参数是 ebp + 16

 

 ; 函数f的逻辑

 

 ; 出栈ebp。ebp恢复成了刚进入函数之前的旧ebp

 ; ret

调用f的伪代码是:

; 入栈第三个参数

; 入栈第二个参数

; 入栈第一个参数

; 调用f,把eip入栈

在汇编代码中,第一个参数的内存地址很容易确定,第二个、第三个还有第N个参数的内存地址也非常容易确定。无法是在ebp的基础上增加特定长度而已。

可是,我们只能确定,必定存在第一个参数,不能确定是否存在的二个、第三个还有第N个参数。没有理由使用一个可能不存在的参数作为参照物、并且还要用它却计算其他参数的地址。

第一个参数必定存在,所以,我们用它作为确定其他参数的内存地址的参照物。

内存地址

在f函数的C代码中,&fmt是第一个参数占用的f的栈的元素的内存地址,换句话说,是一个局部变量的内存地址。

局部变量的内存地址不能作为函数的返回值,却能够在本函数执行结束前使用,包括在本函数调用的其他函数中使用。这就是在f2中仍然能够使用fmt计算出来的内存地址的原因。

难点

当参数是int类型时,获取参数的值使用*(int *)(next_arg)。

当参数是char str[20]时,获取参数的值使用*(char **)(next_arg + 4)。

为什么不直接使用next_arg、(next_arg + 4)呢?

分析*(int *)(next_arg)。

在32位操作系统中,任何内存地址的值看起来都是一个32位的正整数。可是这个正整数的值的类型并不是unsigned int,而是int *。

关于这点,我们可以在gdb中使用ptype确认一下。例如,有一小段代码int *a;*a = 5;,执行ptype a,结果会是int *。

next_arg只是一个正整数,损失了它的数据类型,我们需要把数据类型补充进来。我们能够把这个操作理解成”强制类型转换“。

至于*(int *)(next_arg)前面的*,很容易理解,获取一个指针指向的内存中的值。

用通用的方式分析*(char **)(next_arg+4)。

  1. 因为是第三个参数,因此next_arg+4。
  2. 因为第三个参数的数据类型是char str[20]。根据经验,char str[20]对应的指针是char *。
  3. 因为next_arg+4只是函数的栈的元素的内存地址,在目标元素中存储的是一个指针。也就是说,next_arg+4是一个双指针类型的指针。它最终又指向字符串,根据经验,next_arg+4的数据类型是char **。没必要太纠结这一点。自己写一个简单的指向字符串的双指针,使用gdb的ptype查看这种类型的数据类型就能验证这一点。
  4. 最前面的*,获取指针指向的数据。

给出一段验证第3点的代码。

  1. char str[20] = "hello";
  2. char *ptr = str;
  3. // 使用gdb的ptype 打印 ptype &ptr

打印结果如下:

Breakpoint 1, main (argc=1, argv=0xffffd3f4) at point.c:13
13  char str7[20] = "hello";
(gdb) s
14  char *ptr = str7;
(gdb) s
19  int b = 7;
(gdb) p &str
$1 = (char **) 0xffffd2fc

优化

在code-A和code-B中,我们人工根据参数的类型来获取参数,使用*(int *)(next_arg)或*(char **)(next_arg + 4)。

库函数printf显然不是人工识别参数的类型。

这个函数的第一个参数中包含%d、%x、%s等占位符。遍历第一个参数,识别出%d,就用*(int *)next_arg替换%d。识别出

%s,就用*(char **)next_arg。

实现了识别占位符并且根据占位符选择指针类型的功能,就能实现一个完成度很高的可变参数了。

总结

到此这篇关于C语言如何实现可变参数的文章就介绍到这了,更多相关C语言可变参数内容请搜索w3xue以前的文章或继续浏览下面的相关文章希望大家以后多多支持w3xue!

 友情链接:直通硅谷  点职佳  北美留学生论坛

本站QQ群:前端 618073944 | Java 606181507 | Python 626812652 | C/C++ 612253063 | 微信 634508462 | 苹果 692586424 | C#/.net 182808419 | PHP 305140648 | 运维 608723728

W3xue 的所有内容仅供测试,对任何法律问题及风险不承担任何责任。通过使用本站内容随之而来的风险与本站无关。
关于我们  |  意见建议  |  捐助我们  |  报错有奖  |  广告合作、友情链接(目前9元/月)请联系QQ:27243702 沸活量
皖ICP备17017327号-2 皖公网安备34020702000426号