C语言可变参数函数实现原理解析 - 重写printf

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

前题

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

你可能感兴趣的:(C/C++)