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)
。
- 因为是第三个参数,因此
next_arg+4
。 - 因为第三个参数的数据类型是
char str[20]
。根据经验,char str[20]
对应的指针是char *
。 - 因为
next_arg+4
只是函数的栈的元素的内存地址,在目标元素中存储的是一个指针。也就是说,next_arg+4
是一个双指针类型的指针。它最终又指向字符串,根据经验,next_arg+4
的数据类型是char **
。没必要太纠结这一点。自己写一个简单的指向字符串的双指针,使用gdb的ptype
查看这种类型的数据类型就能验证这一点。 - 最前面的
*
,获取指针指向的数据。
给出一段验证第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语言中的可变参数-printf的实现原理
C语言中的可变参数-printf的实现原理 在C/C++中,对函数参数的扫描是从后向前的.C/C++的函数参数是通过压入堆栈的方式来给函数传参数的(堆栈是一种先进后出的数据结构),最先压入的参数最后出 ...
- C语言中函数可变参数解析
大多数时候,函数中形式参数的数目通常是确定的,在调用时要依次给出与形式参数对应的所有实际参数.但在某些情况下希望函数的参数个数可以根据需要确定.典型的例子有 大家熟悉的函数printf().scanf ...
- C语言中的可变参数函数的浅析(以Arm 程序中的printf()函数实现为例) .
我们在C语言编程中会遇到一些参数个数可变的函数,一般人对它的实现不理解.例如Printf(): Printf()函数是C语言中非常常用的一个典型的变参数函数,它 的原型为: int printf( c ...
- C语言中的可变参数函数
C语言编程中有时会遇到一些参数个数可变的函数,例如printf()函数,其函数原型为: int printf( const char* format, ...); 它除了有一个参数format固定以外 ...
- c语言中的# ## 可变参数宏 ...和_ _VA_ARGS_ _
1.#假如希望在字符串中包含宏参数,ANSI C允许这样作,在类函数宏的替换部分,#符号用作一个预处理运算符,它可以把语言符号转化程字符串.例如,如果x是一个宏参量,那么#x可以把参数名转化成相应的字 ...
- 【C/C++开发】C语言实现函数可变参数
函数原型: int printf(const char *format[,argument]...) 返 回 值: 成功则返回实际输出的字符数,失败返回-1. 函数说明: ...
- C语言笔记 12_可变参数&内存管理&命令行参数
可变参数 有时,您可能会碰到这样的情况,您希望函数带有可变数量的参数,而不是预定义数量的参数.C 语言为这种情况提供了一个解决方案,它允许您定义一个函数,能根据具体的需求接受可变数量的参数.下面的实例 ...
- C语言学习020:可变参数函数
顾名思义,可变参数函数就是参数数量可变的函数,即函数的参数数量是不确定的,比如方法getnumbertotal()我们即可以传递一个参数,也可以传递5个.6个参数 #include <stdio ...
- C语言之函数可变参数
先上一段代码: #include<cstdarg> #include<iostream> #include<string> using namespace std; ...
- 各种语言中的可变参数(java、python、c++、javascript)
索引: java python c++ js 1.Java public class Animal { // 接受可变参数的方法 void eat(String... Objects) { for ( ...
随机推荐
- GEMM与AutoKernel算子优化
GEMM与AutoKernel算子优化 随着AI技术的快速发展,深度学习在各个领域得到了广泛应用.深度学习模型能否成功在终端落地应用,满足产品需求,一个关键的指标就是神经网络模型的推理性能.一大波算法 ...
- TensorRT PoolingLayer
TensorRT PoolingLayer IPoolingLayer在通道中实现池.支持的池类型有maximum.average和maximum average混合. Layer Descripti ...
- 「题解」300iq Contest 2 H. Honorable Mention
本文将同步发布于: 洛谷博客: csdn: 博客园: 简书. 题目 题目链接:gym102331H. 题意概述 给定一个长度为 \(n\) 的序列 \(a\),有 \(q\) 次询问,每次询问给定三个 ...
- 一、DNS服务器的搭建
一.介绍 DNS服务:域名解析 将域名解析ip地址 DNS服务器的功能– 正向解析:根据注册的域名查找其对应的IP地址– 反向解析:根据IP地址查找对应的注册域名,不常用 所有完整的域名都要以点结 ...
- 使用NDepend衡量代码的SOLID程度
SOLID是面向对象的软件开发中的5条准则,也是开发人员可以提升自己代码质量的准则.那么如何衡量自己的代码是否符合SOLID准则呢?NDepend这款工具也许可以帮得上忙.本文将介绍一些NDepend ...
- 【NX二次开发】开发环境搭建
1.Visual Studio 版本按照下表选择. UG版本 VS版本 NX1847-NX1872版 Visual Studio 2017 Build 19.10.25017 NX12版 Visual ...
- spring boot 加载web容器tomcat流程源码分析
spring boot 加载web容器tomcat流程源码分析 我本地的springboot版本是2.5.1,后面的分析都是基于这个版本 <parent> <groupId>o ...
- Redis与DB的数据一致性解决方案(史上最全)
文章很长,而且持续更新,建议收藏起来,慢慢读! 高并发 发烧友社群:疯狂创客圈(总入口) 奉上以下珍贵的学习资源: 疯狂创客圈 经典图书 : 极致经典 + 社群大片好评 < Java 高并发 三 ...
- NOIP模拟测试17「入阵曲·将军令·星空」
入阵曲 题解 应用了一种美妙移项思想, 我们先考虑在一维上的做法 维护前缀和$(sum[r]-sum[l-1])\%k==0$可以转化为 $sum[r]\% k==sum[l-1]\%k$开个桶维护一 ...
- 浏览器中js怎么将图片下载而不是直接打开
网上找了好多方法都是不能用的,经过试验在Chrome中都是直接打开. 经过自己的摸索,找到了一套能用的解决方案 var database = "data:image/jpg;base64,& ...