从printf谈可变参数函数的实现

printf可变参数原理说明 使ap不再 指向堆栈,而是跟NULL一样.有些直接定义为((void*)0),这样编译器不 会为va_end产生代码,例如gcc在linux的x86平台就是这样定义的. 在这里大家要注意一个问题:由于参数的地址用于va_start宏,所 以参数不能声明为寄存器变量或作为函数或数组类型. 关于va_start, va_arg, va_end的描述就是这些了,我们要注意的 是不同的操作系统和硬件平台的定义有些不同,但原理却是相似的.4、取得地址后,再结合参数的类型,程序员就可以正确的处理参数了。 阅读详情
 

摘要:一直以来都觉得printf似乎是c语言库中功能最强大的函数之一,不仅因为它能格式化输出,更在于它的参数个数没有限制,要几个就给几个,来者不拒。printf这种对参数个数和参数类型的强大适应性,让人产生了对它进行探索的浓厚兴趣。

关键字:printf, 可变参数

1. 使用情形

int a =10;
double b = 20.0;
char *str = "Hello world";
printf("begin print/n");
printf("a=%d, b=%.3f, str=%s/n", a, b, str);
...
  从printf的使用情况来看,我们不难发现一个规律,就是无论其可变的参数有多少个,printf的第一个参数总是一个字符串。而正是这第一个参数,使得它可以确认后面还有有多少个参数尾随。而尾随的每个参数占用的栈空间大小又是通过第一个格式字符串确定的。然而printf到底是怎样取第一个参数后面的参数值的呢,请看如下代码

2. printf 函数的实现

//acenv.h
typedef char *va_list;

#define  _AUPBND        (sizeof (acpi_native_int) - 1)
#define  _ADNBND        (sizeof (acpi_native_int) - 1)
                        
#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))))

//start.c
static char sprint_buf[1024];
int printf(char *fmt, ...)
{
	va_list args;
	int n;
	va_start(args, fmt);
	n = vsprintf(sprint_buf, fmt, args);
	va_end(args);
	write(stdout, sprint_buf, n);
	return n;
}

//unistd.h
static inline long write(int fd, const char *buf, off_t count)
{
	return sys_write(fd, buf, count);
}
3. 分析

  从上面的代码来看,printf似乎并不复杂,它通过一个宏va_start把所有的可变参数放到了由args指向的一块内存中,然后再调用vsprintf. 真正的参数个数以及格式的确定是在vsprintf搞定的了。由于vsprintf的代码比较复杂,也不是我们这里要讨论的重点,所以下面就不再列出了。我们这里要讨论的重点是va_start(ap, A)宏的实现,它对定位从参数A后面的参数有重大的制导意义。现在把 #define va_start(ap, A) (void) ((ap) = (((char *) &(A)) + (_bnd (A,_AUPBND)))) 的含义解释一下如下:

    va_start(ap, A)
    {
         char *ap =  ((char *)(&A)) + sizeof(A)并int类型大小地址对齐
    }
  在printf的va_start(args, fmt)中,fmt的类型为char *, 因此对于一个32为系统 sizeof(char *) = 4, 如果int大小也是32,则va_start(args, fmt);相当于 char *args = (char *)(&fmt) + 4; 此时args的值正好为fmt后第一个参数的地址。对于如下的可变参数函数
    void fun(double d,...)
    {
        va_list args;
    	int n;
    	va_start(args, d);
    }
则 va_start(args, d);相当于
    char *args = (char *)&d + sizeof(double);
  此时args正好指向d后面的第一个参数。

  可变参数函数的实现与函数调用的栈结构有关,正常情况下c/c++的函数参数入栈规则为__stdcall, 它是从右到左的,即函数中的最右边的参数最先入栈。对于函数

    void fun(int a, int b, int c)
    {
        int d;
        ...
    }
其栈结构为
    0x1ffc-->d
    0x2000-->a
    0x2004-->b
    0x2008-->c
  对于任何编译器,每个栈单元的大小都是sizeof(int), 而函数的每个参数都至少要占一个栈单元大小,如函数 void fun1(char a, int b, double c, short d) 对一个32的系统其栈的结构就是
    0x1ffc-->a  (4字节)
    0x2000-->b  (4字节)
    0x2004-->c  (8字节)
    0x200c-->d  (4字节)
  对于函数void fun1(char a, int b, double c, short d)

  如果知道了参数a的地址,则要取后续参数的值则可以通过a的地址计算a后面参数的地址,然后取对应的值,而后面参数的个数可以直接由变量a指定,当然也可以像printf一样根据第一个参数中的%模式个数来决定后续参数的个数和类型。如果参数的个数由第一个参数a直接决定,则后续参数的类型如果没有变化并且是已知的,则我们可以这样来取后续参数, 假定后续参数的类型都是double;

void fun1(int num, ...)
{
    double *p = (double *)((&num)+1);
    double Param1 = *p;
    double Param2 = *(p+1);
    ...
    double Paramn  *(p+num);
}
  如果后续参数的类型是变化而且是未知的,则必须通过一个参数中设定模式来匹配后续参数的个数和类型,就像printf一样,当然我们可以定义自己的模式,如可以用i表示int参数,d表示double参数,为了简单,我们用一个字符表示一个参数,并由该字符的名称决定参数的类型而字符的出现的顺序也表示后续参数的顺序。 我们可以这样定义字符和参数类型的映射表,
i---int
s---signed short
l---long
c---char 
"ild"模式用于表示后续有三个参数,按顺序分别为int, long, double类型的三个参数那么这样我们可以定义自己版本的printf 如下
void printf(char *fmt, ...)
{
    char s[80] = "";
    int paramCount = strlen(fmt);
    write(stdout, "paramCount = " , strlen(paramCount = ));
    itoa(paramCount,s,10);
    write(stdout, s, strlen(s));
    char *p = (char *)(&fmt) + sizeof(char *);
    int *pi = (int *)p;
    for (int i=0; i<paramCount; i++)
    {
        char line[80] = "";
        strcpy(line, "param");
        itoa(i+1, s, 10);
        strcat(line, s);
        strcat(line, "=");
        switch(fmt[i])
        {
            case 'i':
            case 's':
                itoa((*pi),s,10);
                strcat(line, s);
                pi++;
                break;
            case 'c':
                {
                    int len = strlen(line);
                    line[len] = (char)(*pi);
                    line[len+1] = '/0';
                }
                break;
            case 'l':
                ltoa((*(long *)pi),s,10);
                strcat(line, s);
                pi++;
                break;
            default:
                break;
        }
    }
}
也可以这样定义我们的Max函数,它返回多个输入整型参数的最大值
int Max(int n, ...)
{
    int *p = &n + 1;
    int ret = *p;
    for (int i=0; i<n; i++)
    {
        if (ret < *(p + i))
            ret = *(p + i);
    }
    return ret;
}
可以这样调用, 后续参数的个数由第一个参数指定
int m = Max(3, 45, 12, 56);
int m = Max(1, 3);
int m = Max(2, 23, 45);

int first = 34, second = 45, third=5;
int m = Max(5, first, second, third, 100, 4);
结论

  对于可变参数函数的调用有一点需要注意,实际的可变参数的个数必须比前面模式指定的个数要多,或者不小于, 也即后续参数多一点不要紧,但不能少, 如果少了则会访问到函数参数以外的堆栈区域,这可能会把程序搞崩掉。前面模式的类型和后面实际参数的类型不匹配也有可能造成把程序搞崩溃,只要模式指定的数据长度大于后续参数长度,则这种情况就会发生。如:

printf("%.3f, %.3f, %.6e", 1, 2, 3, 4);
  参数1,2,3,4的默认类型为整型,而模式指定的需要为double型,其数据长度比int大,这种情况就有可能访问函数参数堆栈以外的区域,从而造成危险。但是printf("%d, %d, %d", 1.0, 20., 3.0);这种情况虽然结果可能不正确,但是确不会造成灾难性后果。因为实际指定的参数长度比要求的参数长度长,堆栈不会越界。

从零开始搭建GD32F103CBT6开发环境:VSCode配置与实战技巧 本文详细介绍了如何从零开始为国产MCU GD32F103CBT6搭建高效的VSCode开发环境。内容涵盖工具链安装、VSCode核心插件配置、工程目录结构规划、关键配置文件编写以及Makefile实战,旨在帮助开发者摆脱传统IDE束缚,利用现代化工具链提升嵌入式开发效率。 阅读详情

相关推荐

ESP32驱动GC9A01圆形屏实战:Arduino平台LVGL移植与图像显示优化

本文详细介绍了在Arduino平台上使用ESP32驱动GC9A01圆形屏的实战指南,包括硬件连接、LVGL移植与图像显示优化。通过SPI配置、LVGL库安装和双缓冲机制等技巧,帮助开发者快速实现高效显示效果,适用于智能家居控制面板等应用场景。

weixin_29182481的博客 734

c语言可变参数函数(printf函数为例)

可变参数函数,顾名思义,就是形参数量可以改变的函数,其中以printf函数最为典型,看一下它的使用格式: printf("asd%d%f\n", z,n); 在使用printf进行输出时,除第一个字符串参数外,传进去几个参数,字符串中就必须有几个百分号,这些百分号其实就是告诉printf我传进来了几个参数以及其类型,如%d代表整型,%f代表浮点型。如若不然,printf输出就会出现错误结果。 按照这样的思想其实我们也可以写一个类似的函数: int a=1; int b=1; int c=1; i

xiaolong_lili的博客 3161

基于TL431与MOSFET的高效过压保护电路设计详解

本文详细解析了基于TL431与MOSFET的高效过压保护电路设计。通过TL431精准监测电压,驱动MOSFET作为主开关,实现快速可靠的电源保护。文章涵盖电路原理、参数计算、性能优化及实战调试要点,为电子设备提供了一套经典且成本低廉的过压保护解决方案。

weixin_29215391的博客 771

从printf()看可变参数函数

1. 使用情形 int a =10;double b = 20.0;char *str = "Hello world";printf("begin print/n");printf("a=%d, b=%.3f, str=%s/n", a, b, str);...  从printf的使用情况来看,我们不难发现一个规律,就是无论其可变的参数有多少个,printf的第一个参数总是

603

海波龙(Oracle Hyperion)单点登陆问题记录

Oracle海波龙套件已经多年未更新,按Oracle官方文档实施单点登陆启用之后,SmartView工具通过专用链接方式无法登陆系统。 在抓包逆向分析之后 定位到HspSmartViewServlet类在执行以下代码时获得的ssotoken无效导致,在某大厂工作的时候曾通过在filter中截获请求的方式植入Attribute,保留账号密码并在单点程序中处理实现登陆。 但在完美世界的环境中...

cainiaoEzu的专栏 1970

西瓜书第二章——模型评估与选择(十折交叉验证/查准查全F1)

第二章——模型评估与选择

xsddys的博客 953

C/C++杂谈-printf的可变参数机制

C++11引入了可变参数模板机制,对模板参数进行了高度泛化,但是对于可变参数其实C语言学习中早已遇到过,那就是printf可以进行多参数的输出,这是怎么实现的呢?

EQUINOX1的博客 722

思科—OSPF多区域配置实验

实验目的:配置OSPF多区域,使得不同网段的PC互相ping通。 网络拓扑如下: R0的配置 Router>en Router#conf t Router(config)#int g0/1 Router(config-if)#ip add 192.168.1.1 255.255.255.0 Router(config-if)#no shut Router(config-if)#exit Router(config)#int g0/0 Router(config-if)#ip add 192.168.

m0_46237205的博客 1万+

C语言可变参数函数(printf/scanf)

C 语言允许定义参数数量可变的函数,这称为可变参数函数(variadic function)。这种函数需要固定数量的强制参数(mandatory argument),后面是数量可变的可选参数(optional argument)。 这种函数必须至少有一个强制参数。可选参数的类型可以变化。可选参数的数量由强制参数的值决定,或由用来定义可选参数列表的特殊值决定。 平时我们写C语言函数时,一般是固定参数的,但是像打印输出格式化内容时,其参数个数就不确定了,类似如下: printf("This is a s.

DayDayUp 5119

第4篇:使用 NModbus 库快速搭建 Modbus TCP 通信(从零开始用 C# 开发 Modbus 上位机)

本文介绍了如何使用 NModbus 库快速搭建 Modbus TCP 上位机通信程序。通过安装 NModbus4 NuGet 包,开发者可以轻松实现 Modbus TCP 连接,无需手动处理底层协议细节。文章详细演示了核心功能实现: 创建 TCP 客户端连接设备 读取保持寄存器(功能码0x03) 写入单个寄存器(功能码0x06) 关闭连接释放资源 提供完整C#示例代码,包含初始化连接、数据读写等关键操作,帮助开发者快速构建Modbus TCP通信应用。相比手动实现,NModbus库大幅简化了开发流程,特别适

这里只有干货:从 Modbus 通信到 MES 对接,从 YOLO 训练到工控机部署,带你搞定工业软件开发全流程。 1044

基于STM32L4XX的环境光传感器(VCNL4040M3OE)应用程序设计

• 集成模块:红外发射器(IRED)、环境光传感器(ALS)、接近传感器(PS)及信号调理IC。#define VCNL4040_I2C_ADDR (0x60 << 1) // 7位地址左移一位。// 组合成16位值。// I2C总线句柄。// 设置参数:ALS集成时间160ms,中断禁止,ALS功能开启。// 设置参数:PS工作频率80Hz,开红外二极管,PS功能开启。// 初始化I2C外设。• 输出类型:I2C总线(ALS/PS)// 2. 配置环境光传感器 (ALS) 并启动。

myron6688的博客 143

printf谈可变参数函数的实现

一直以来都觉得printf似乎是c语言库中功能最强大的函数之一,不仅因为它能格式化输出,更在于它的参数个数没有限制,要几个就给几个,来者不拒。printf这种对参数个数和参数类型的强大适应性,让人产生了对它进行探索的浓厚兴趣。关键字:printf, 可变参数1. 使用情形int a =10;double b = 20.0;char *str = "Hel

WINCOL的专栏 663

使用NXP SBC插件开发FS26软件

FS26作为功能安全可达ASIL D的SBC,在配合NXP MCU的使用中非常广泛,本文介绍基于NXP官网的SBC代码包快速进行FS26软件的开发FS26使用过程中还有不少坑的地方,后面有空可以再介绍相关的功能。

赞的博客 647
上一篇: hook api的一个简单的例子
下一篇: 英中术语对照
ShenRui
博客等级 码龄20年 63粉丝 36原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

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

抵扣说明:

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

余额充值