zoukankan      html  css  js  c++  java
  • C语言怎么实现可变参数

    可变参数

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

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

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

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

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

    实现

    代码

    code-A

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

    // file stack-demo.c
    
    #include <stdio.h>
    
    // int f(char *fmt, int a, char *str);
    int f(char *fmt, ...);
    int f2(char *fmt, void *next_arg);
    int main(int argc, char *argv)
    {
            char fmt[20] = "hello, world!";
            int a = 10;
            char str[10] = "hi";
            f(fmt, a, str);
            return 0;
    }
    
    // int f(char *fmt, int a, char *str)
    int f(char *fmt, ...)
    {
            char c = *fmt;
            void *next_arg = (void *)((char *)&fmt + 4);
            f2(fmt, next_arg);
            return 0;
    }
    
    
    int f2(char *fmt, void *next_arg)
    {
            printf(fmt);
            printf("a is %d
    ", *((int *)next_arg));
            printf("str is %s
    ", *((char **)(next_arg + 4)));
    
            return 0;
    }
    

    编译执行,结果如下:

    # 编译
    [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

    // file stack-demo.c
    
    #include <stdio.h>
    
    // int f(char *fmt, int a, char *str);
    int f(char *fmt, ...);
    int f2(char *fmt, void *next_arg);
    int main(int argc, char *argv)
    {
            char fmt[20] = "hello, world!";
            int a = 10;
            char str[10] = "hi";
      			char str2[10] = "hello";
            f(fmt, a, str, str2);
            return 0;
    }
    
    // int f(char *fmt, int a, char *str)
    int f(char *fmt, ...)
    {
            char c = *fmt;
            void *next_arg = (void *)((char *)&fmt + 4);
            f2(fmt, next_arg);
            return 0;
    }
    
    
    int f2(char *fmt, void *next_arg)
    {
            printf(fmt);
            printf("a is %d
    ", *((int *)next_arg));
            printf("str is %s
    ", *((char **)(next_arg + 4)));
      			printf("str2 is %s
    ", *((char **)(next_arg + 8)));
    
            return 0;
    }
    

    编译执行,结果如下:

    # 编译
    [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, ...);就是参数可变的函数。

    关键语句

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

    char c = *fmt;
    void *next_arg = (void *)((char *)&fmt + 4);
    printf("a is %d
    ", *((int *)next_arg));
    printf("str is %s
    ", *((char **)(next_arg + 4)));
    printf("str2 is %s
    ", *((char **)(next_arg + 8)));
    
    1. &fmt是第一个参数的内存地址。
    2. next_arg是第二个参数的内存地址。
    3. next_arg+4next_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点的代码。

    char str[20] = "hello";
    char *ptr = str;
    // 使用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

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

    求道之人,不问寒暑。
  • 相关阅读:
    #ifndef/#define/#endif使用详解
    快速排序
    一分钟看懂Docker的网络模式和跨主机通信
    Docker:网络模式详解
    Docker中使用Tomcat并部署war工程
    Docker学习笔记--Docker 启动nginx实例挂载目录权限不够(转)
    Centos 7 如何卸载docker
    Centos-7修改yum源为国内的yum源
    centOS 7镜像文件下载
    Python 垃圾回收机制(转)
  • 原文地址:https://www.cnblogs.com/chuganghong/p/15045539.html
Copyright © 2011-2022 走看看