本文共 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.c源码#endif int reprintf(const char* format, ...); #endif
/* rePrintf 重写printf 可变参数*/#include "rePrintf.h"#include具体实现,参考代码注释,每一步都做了详尽的说明。上述两个文件源码已在VS2015中跑通。至此,相信认真看完一遍后应该足以熟知并能自己实现可变参数函数了。 ———————————————— 版权声明:本文为CSDN博主「叔子衿」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。 原文链接:https://blog.csdn.net/loveliyuyuan/article/details/102516656#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 文件流}