本文部分内容参考此文: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指针
.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中跑通。至此,相信认真看完一遍后应该足以熟知并能自己实现可变参数函数了。