博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
C语言可变参数函数实现原理解析 - 重写printf
阅读量:4223 次
发布时间:2019-05-26

本文共 4617 字,大约阅读时间需要 15 分钟。

可变参数基于函数调用及参数传递的方式实现

前题
本文部分内容参考此文:https://blog.csdn.net/yexiangCSDN/article/details/83900366

在C语言中,函数调用有4个主流的调用惯例,cdecl、stdcall、fastcall、pacall,它们之间主要的区别在于参数传递时的压栈顺序以及参数栈清理方。

如下表:

调用惯例

参数出栈

参数压栈顺序

函数编译修饰规则

cdecl

函数调用方

从右到左压栈

下划线+函数名(_fun)

stdcall

函数自身

从右到左压栈

下划线+函数名—+@+参数字节数(_fun@bytes)

fastcall

函数自身

从右到左,但头两个DWORD(4byte)类型或者更少字节的参数被放入寄存器

@+函数名+@+参数的字节数(@fun@bytes)

pacall

函数自身

从左至右

较为复杂,此处不表

上述四种调用惯例有个了解即可,想要深入了解那是另一个课题了。在Windows系统下的IDE大部分都遵从cdecl和stdcall两种调用惯例。而C语言可变参数的实现正式基于cdecl调用惯例实现的,因为其参数出栈清理工作是由被调用方处理的。在可变参数函数中,函数本身并不知道参数个数,继而也无法主动去清理参数栈,只能由被调用方去清理。

实现原理

如1所述,C函数调用惯例cdecl中,参数压栈顺序是从右到左,本着先入后出的栈规则(只能拿到栈顶指针),在栈顶端的应该是一个确定的参数,既最左边的参数必须是可确定的,或者说可变长参数左边紧邻的一个参数必须是确定的,它通常用于确定可变长参数的个数、类型等。

这里需要注意的是,可变参数函数中并没有限制确定参数的个数,只是要确保可选参数在最右边,以及确定参数中描述了可选参数的信息。

通常把这两个必要参数叫做强制参数(mandatory)、可选参数(optional argument),在形式上,可选参数通常用省略号(…)来表示,我们最常用的printf函数就是最典型的可变参数函数(variadic function)如下:

printf(const char *format, …);//其中format用于确定可选参数个数及类型

 

在标准库中,实现可变参数函数是由四个宏完成的,va_list、va_start、va_arg、va_end、va_copy

#define va_list char*   #define va_lisst void*

va_list用于声明参数指针(argument pointer),参数指针既在函数内部移动指向函数各个参数,因为可选参数类型未知,所以通常被宏包装成char*或void*类型,用于在函数中指向各个参数地址。嵌入式系统中使用char*很显然是合适的。在标准库中它被声明在头文件stdarg.h

#define va_start(ap, arg) (ap = (va_list)&arg + sizeof(arg))//

va_start指向可变参数的第一个参数地址用于获取函数参数的首地址,既最左边第一个参数的地址,栈顶位置。

#define va_arg(ap, type) (*(type*)((ap += sizeof(type)) - sizeof(type)))

va_arg指向可变参数的下一个参数地址,栈中。

#define reva_end(ap) (ap = (va_list)0)

将va_list指针指空,避免出现野指针。

va_copy是复制va_list指针 

重写printf函数源码

.h头文件

#ifndef _REPRINTF_H#define _REPRINTF_H #ifndef WIN32#define reva_list char* //参数指针#define reva_start(ap, arg) (ap = (va_list)&arg + sizeof(arg))//指向可变参数的第一个参数地址#define reva_arg(ap, type) (*(type*)((ap += sizeof(type)) - sizeof(type)))//指向可变参数的下一个参数地址#define reva_end(ap) (ap = (va_list)0)//指空 防止野指针#else#include 
#endif int reprintf(const char* format, ...); #endif

.c源码

/*	rePrintf 重写printf	可变参数*/#include "rePrintf.h"#include 
#include
#include
//写字符到FILE 流int refputc(char c, FILE* stream){ if (NULL == stream) return EOF; if (1 != fwrite(&c, 1, 1, stream)) return EOF;// EOF -1 else return c;}//写字符串到 FILE流int refputs(char* str, FILE* stream){ int len = strlen(str); if (NULL == str || NULL == stream)//参数校验 return EOF; if (len != fwrite(str, 1, len, stream)) return EOF; else return len;} //实现函数 - 简单的解析int char str三种数据int revfprintf(FILE* stream, const char* format, va_list arglist){ int translating = 0;//解析标志位 置1表明遇到% int count = 0;//输出数据 - 字节量计数 const char* p = NULL; char* str = NULL; char buffer[32] = "";//int转换str后的缓冲数组 for (p = format; '\0' != *p; p++) { switch (*p) { case '%': if (1 != translating)//解析标志位 置1 { translating = 1; } else//已置1 则表明%%叠加 { if (EOF != refputc(*p, stream))//输出'%' { count++;//计数++ translating = 0;//解析重置 } else return EOF;//输出失败 } break; case 'd'://输出int数据 if (translating)//如果需要解析 { translating = 0; _itoa_s(reva_arg(arglist, int), buffer, 32, 10);//10进制整数转字符串 _itoa_s函数是VS中的安全函数,原型是itoa函数 if (EOF != refputs(buffer, stream))//将转换后的数据写入I/O流 count += strlen(buffer);//计数++ else return EOF; } else if (EOF != refputc(*p, stream))//如不需要解析则直接输出'd' count++; else return EOF; break; case 'c'://输出char数据 if (translating) { translating = 0; if (EOF != refputc(reva_arg(arglist, char), stream))//输出字符 count++; else return EOF; } else if (EOF != refputc(*p, stream))//直接输出'c' count++; else return EOF; break; case 's'://输出str数据 if (translating) { translating = 0; str = reva_arg(arglist, const char*);//指向下一个参数,既str指针 if (EOF != refputs(str, stream)) count += strlen(str); else return EOF; } else if (EOF != refputc(*p, stream))//直接输出's' count++; else return EOF; break; default: if (translating)translating = 0; if (EOF != refputc(*p, stream))//直接按字符输出 count++; else return EOF; break; } } reva_end(arglist);//指空 释放va_list指针 return count;} //输出到系统标准输入输出流int reprintf(const char* format, ...){ reva_list arglist;//定义va_list参数指针 reva_start(arglist, format);//获取参数栈顶指针 return revfprintf(stdout, format, arglist);//输出到stdout}//输出到文件int refprintf(FILE* stream, const char* format, ...){ reva_list arglist; reva_start(arglist, format); return revfprintf(stream, format, arglist);//输出到stream 文件流}

具体实现,参考代码注释,每一步都做了详尽的说明。上述两个文件源码已在VS2015中跑通。至此,相信认真看完一遍后应该足以熟知并能自己实现可变参数函数了。
————————————————
版权声明:本文为CSDN博主「叔子衿」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/loveliyuyuan/article/details/102516656

你可能感兴趣的文章
朝花夕拾
查看>>
HGE系列之零 使用细究
查看>>
爬坑笔记
查看>>
再谈谈列表元素的删除
查看>>
小聊聊NGUI中Panel的Clip功能(之二)
查看>>
Thoughts On <To The Moon>
查看>>
也说棋类游戏
查看>>
疑难杂症小记
查看>>
foreach, 用还是不用,这是一个问题~
查看>>
关于Unity ParticleSystem的一些"冷"知识
查看>>
HGE系列之四 小试牛刀
查看>>
HGE系列之五 管中窥豹(基础类别)
查看>>
又一篇杂记
查看>>
HGE系列之六 管中窥豹(资源管理)
查看>>
Singleton模式小探
查看>>
HGE系列之七 管中窥豹(图形界面)
查看>>
HGE系列之八管中窥豹(粒子系统)
查看>>
“连连看”小析
查看>>
HGE系列之九 管中窥豹(精灵动画)
查看>>
HGE系列之十 管中窥豹(游戏字体)
查看>>