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\n", 23, "Hello, World;", 'A');
   printf("T is %d\n", 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\n", *((int *)next_arg));
        printf("str is %s\n", *((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\n", *((int *)next_arg));
        printf("str is %s\n", *((char **)(next_arg + 4)));
     printf("str2 is %s\n", *((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\n", *((int *)next_arg));
printf("str is %s\n", *((char **)(next_arg + 4)));
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点的代码。

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。

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

总结

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

(0)

相关推荐

  • C语言中编写可变参数函数

    通过stdarg.h头文件为函数提供了定义可变参数列表的能力.声明一个可变参数的函数类似: void f1(int n,...); 其中n表示参数列表个数,而用省略号来表示未知参数列表.stdarg.h中提供了一个va_list类型,用于存放参数.一个大概的使用过程类似: void f1(int n,...) { va_list ap; va_start(ap,n); //初始化参数列表 double first=va_arg(ap,double); //取第一个参数 int second=va

  • c语言基于stdarg.h的可变参数函数的用法

    C语言编程中有时会遇到一些参数个数可变的函数,本文详细讲解了可变参数函数的实现原理,分享给大家 在开始学习C语言的函数的时候,我们就知道函数的参数个数应该是在函数声明的时候就指定的,这一点我们没有任何疑问.但是不知道大家有没有注意到我们的printf()函数,他的函数参数理论上并不是确定的,而是随着匹配字符串中的格式控制符的个数控制的.其实当时也没有注意到这一点,到是最近,偶然间看到了 <嗨翻C语言> 这本书,这里就详细讲解了这种可变参数函数的实现原理,今天考试间隙就顺带学习了一下,其实就是一

  • C语言可变参数函数详解示例

    先看代码 复制代码 代码如下: printf("hello,world!");其参数个数为1个.printf("a=%d,b=%s,c=%c",a,b,c);其参数个数为4个. 如何编写可变参数函数呢?我们首先来看看printf函数原型是如何定义的.在linux下,输入man 3 printf,可以看到prinf函数原型如下: 复制代码 代码如下: SYNOPSIS#include <stdio.h>int printf(const char *form

  • 用c语言根据可变参数合成字符串的实现代码

    写代码时, 经常需要根据参数值得到一特定的字符串. 每次都调用vsprintf, malloc很烦. 以下是一个实现了此功能的接口. 复制代码 代码如下: #include <stdio.h>#include <stdlib.h>#include <string.h>#include <stdarg.h> char *xm_vsprintf_ex(int len, char *fmt, ... ); int main(int argc, char *argv

  • c语言可变参数实现示例

    这段代码展示了如何不使用<stdarg.h>中的va_list.va_start.va_end宏来实现自定义可变参数以及如何改变默认的%d.%f.%s等格式字符. 复制代码 代码如下: #include <stdio.h>#include <stdlib.h> // itoa() and ltoa()#include <string.h> // strcat() and strlen() // echo("$i, $s, $l, $c",

  • C语言中可变参数的使用方法示例

    前言 在C语言程序编写中我们使用最多的函数一定包括printf以及很多类似的变形体.这个函数包含在C库函数中,定义为 int printf( const char* format, ...); 除了一个格式化字符串之外还可以输入多个可变参量,如: printf("%d",i); printf("%s",s); printf("the number is %d ,string is:%s", i, s); 格式化字符串的判断本章暂且不论,下面分析一

  • 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,

  • java 可变参数详解及实例

    java 可变参数详解 可变参数(Varargs)使程序员可以声明一个接受可变数目参数的方法. 可变参数也是JDK5.0中出现的新特性. 可变参数本质上就是一个数组,对于某个声明了可变参数的方法来说,我们既可以传递离散的值,也可以传递数组对象. 但如果将方法中的参数定义为数组,那么只能传递数组对象而不能传递离散的值. 注意,可变参数必须是方法声明中的最后一个参数.一个方法不可能具有两个或两个以上的可变参数. 附上例子程序: public class TestVarargs { private s

  • Java中支持可变参数详解

    意思就是:参数的个数可以根据需要写,你可以写1个.2个.3个....他们都被保存到一个参数的数组中. 但是这些参有一些约束:他们必须是同类型的,比如都是String字符串类型. 同时,可变参数的函数中的参数的写法也有约束:比如,可变参数的数组必须写在参数的最后,否则程序不知道你的参数到底有多少个. 例子:输出可变参数中的参数值 public class VariableArgument { public static void main(String[] args) { printArgumen

  • Go语言中函数可变参数(Variadic Parameter)详解

    目录 基本语法 示例一:函数中获取可变参数 示例二:将切片传给可变参数 示例三:多参数 基本语法 在Python中,在函数参数不确定数量的情况下,可以使用如下方式动态在函数内获取参数,args实质上是一个list,而kwargs是一个dict def myFun(*args, **kwargs): 在Go语言中,也有类似的实现方式,只不过Go中只能实现类似*args的数组方式,而无法实现**kwargs的方式.实现这种方式,其实也是利用数组的三个点表达方式,我们这里来回忆一下. 关于三个点(…)

  • C语言sizeof与字符串处理与动态内存分配及main函数参数详解

    目录 常用的字符串处理函数(标准库里面的) sizeof 求字节运算符 动态内存分配函数 main的参数问题 常用的字符串处理函数(标准库里面的) strlen : 用来求一个字符串的长度 #include <string.h>                     size_t strlen(const char *s);                @s : 指向要计算的那个字符串的首地址                     “const char *” : 表示在函数调用过程中,

  • Python进阶-函数默认参数(详解)

    一.默认参数 python为了简化函数的调用,提供了默认参数机制: def pow(x, n = 2): r = 1 while n > 0: r *= x n -= 1 return r 这样在调用pow函数时,就可以省略最后一个参数不写: print(pow(5)) # output: 25 在定义有默认参数的函数时,需要注意以下: 必选参数必须在前面,默认参数在后: 设置何种参数为默认参数?一般来说,将参数值变化小的设置为默认参数. python标准库实践 python内建函数: prin

  • go语言编程实现递归函数示例详解

    目录 前言 函数中的 return 递归的问题 总结 前言 本篇文章主要是记录一下在 GScript 中实现递归调用时所遇到的坑,类似的问题在中文互联网上我几乎没有找到相关的内容,所以还是很有必要记录一下. 在开始之前还是简单介绍下本次更新的 GScript v0.0.9 所包含的内容: 支持可变参数 优化 append 函数语义 优化编译错误信息 最后一个就是支持递归调用 先看第一个可变参数: //formats according to a format specifier and writ

  • js匿名函数作为函数参数详解

    由衷的感叹,js真是烦. 学到现在,渐渐理解了什么是:语言都是通用的,没有好不好,只有擅长不擅长. 继承,多态,甚至指针,c能实现,c++,java有,javascript(和java是雷锋和雷峰塔的区别,名字上不知道坑了多少人)也能变通实现. 温故知新,今天又回味了一遍,匿名函数作为函数参数. 代码很短,五脏俱全. <!DOCTYPE html> <html lang="en"> <head> </head> <body>

  • C语言动态内存分配的详解

    C语言动态内存分配的详解 1.为什么使用动态内存分配 数组在使用的时候可能造成内存浪费,使用动态内存分配可以解决这个问题. 2. malloc和free C函数库提供了两个函数,malloc和free,分别用于执行动态内存分配和释放. (1)void *malloc(size_t size); malloc的参数就是需要分配的内存字节数.malloc分配一块连续的内存.如果操作系统无法向malloc提供更多的内存,malloc就返回一个NULL指针. (2)void free(void *poi

  • 易语言操作数据库“替换打开”命令详解

    打开指定的数据库文件.成功返回真,并自动关闭当前数据库后将当前数据库设置为此数据库,失败返回假. 语法: 逻辑型 替换打开 (数据库文件名,[在程序中使用的别名],[是否只读],[共享方式],[保留参数1],[数据库密码],[索引文件表],- ) 参数名 描 述 数据库文件名 必需的:文本型. 在程序中使用的别名 可选的:文本型.别名为在后面的程序中引用本数据库时可使用的另一个名称.欲引用一个已经被打开的数据库可以使用该数据库本身的名称(数据库名称为数据库文件名的无路径和后缀部分.譬如 c:\m

随机推荐