首页 > 其他分享 >使用链表而不是 stdarg 实现可变参数函数

使用链表而不是 stdarg 实现可变参数函数

时间:2023-10-15 23:56:03浏览次数:34  
标签:char int long 链表 参数 可变 argChainHead stdarg

Qidi 2023.10.15


0. 需要使用可变参数函数的场景

常见的场景是类似于 printf(char *fmt, ...) 函数,输入的参数个数和类型都是未知的,此时除了需要 ... 表示可变参数列表,还需要用 fmt 参数说明参数的个数和类型。

还有另一种场景,假设我们要实现一个音频控制功能的程序。在初始设计中,需要实现以下 3 个函数:

int init(char *amplifierType);
int connect(long long audioSource, int speakerMask, float sourceGain);
int setVolume(long long audioSource, int volumeStep);

为了 便于编写测试工具便于用有限状态机管理程序状态,我们需要将函数/接口形式统一,这时也要用到可变参数函数。

本文主要讨论第二种场景。

1. stdarg 的局限

  • va_start() 必须知道可变参数列表的前一个参数,用作查询可变参数列表的起始地址;
  • va_arg() 等宏的实现位于编译器内部,不便于阅读研究、了解代码细节。

2. stdarg 的工作原理

程序运行时,函数参数或存于寄存器、或存于栈。因为寄存器是已知的,且栈上的参数位于连续内存地址上,所以 stdarg 可以通过 读取寄存器 和通过 栈上起始地址 获得所有参数。
关于 stdarg 原理的更多介绍,推荐阅读 《AArch64中va_list/va_start/va_arg/...的实现》

3. 使用链表实现可变参数列表

为了摆脱 va_start() 对参数列表起始地址的依赖,我们可以把函数参数按照从左往右的顺序,依次存储于一个动态创建的链表中。

3.1 链表设计

链表节点要设计得足够通用,可以容纳常用数据类型。比如:

typedef union {
	long long  m_ll;
	char  *m_s;
	int    m_i;
	float  m_f;
} FuncArgItem;

struct FuncArgChainItem {
	FuncArgItem mArgVal;
	struct FuncArgChainItem *mpNext;
};
typedef struct FuncArgChainItem FuncArgChainItem_t;

3.2 可变参数函数声明

采用 stdarg 实现可变参数函数时,函数声明的形式类似下方这样:

int init(char *fmt, ...);
int connect(char *fmt, ...);
int setVolume(char *fmt, ...);

或者

int init(char *fmt, va_list ap);
int connect(char *fmt, va_list ap);
int setVolume(char *fmt, va_list ap);

在本文开头,我们其实就已经清楚了 init()connect()setVolume() 函数各自能接收的参数个数和类型,所以使用 stdarg 方式传递可变参数时,就算没有 fmt 参数,我们也能在各个函数的函数体里依次取出参数。这里之所以要写出 fmt 参数,完全是因为 stdarg 的相关语法导致。在我们讨论的场景中,fmt 完全是个冗余参数。

采用链表来传递可变参数,就可以规避冗余参数的问题。基于 3.1 节的链表设计,可变参数函数声明应该改为:

int init(FuncArgChainItem_t *argChainHead);  // "char *amplifierName"
int connect(FuncArgChainItem_t *argChainHead);  // "long long audioSource int speakerMask, float sourceGain"
int setVolume(FuncArgChainItem_t *argChainHead);  // "long long audioSource, int volumeStep"

其中 argChainHead 参数是链表头。链表中存储任意多个任意类型的参数。不同函数的参数个数和参数类型可以不同。
此时我们发现,采用可变参数链表替换掉原先的参数后,函数形式变得统一。每个函数都可以用以下函数指针进行表示:

typedef int(*AudioFuncPtr)(FuncArgChainItem_t *);

3.3 函数读取可变参数的方式

函数读取传入的可变参数的方式非常简单。
因为我们早已清楚每个函数需要接收的参数个数和类型,所以在函数体中可以直接从链表里读取各个参数。比如:

int init(FuncArgChainItem_t *argChainHead)  // "char *amplifierName"
{
	char *amplifierType = argChainHead->mArgVal.m_s;

	printf("%s got args: amplifierType(%s)\n", __func__, amplifierType);
	return RET_OK;
}

int connect(FuncArgChainItem_t *argChainHead)  // "long long audioSource int speakerMask, float sourceGain"
{
	long long audioSource = argChainHead->mArgVal.m_ll;
	int channelMask = argChainHead->mpNext->mArgVal.m_i;
	float sourceGain = argChainHead->mpNext->mpNext->mArgVal.m_f;

	printf("%s got args: audioSource(%lld), channelMask(%d), sourceGain(%f)\n", __func__, audioSource, channelMask, sourceGain);
	return RET_OK;
}

int setVolume(FuncArgChainItem_t *argChainHead)  // "long long audioSource, int volumeStep"
{
	long long audioSource = argChainHead->mArgVal.m_ll;
	int volumeStep = argChainHead->mpNext->mArgVal.m_i;
	
	printf("%s got args: audioSource(%lld), volumeStep(%d)\n", __func__, audioSource, volumeStep);
	return RET_OK;
}

3.4 如何向函数写入可变参数

写入可变参数的方式稍微复杂一点点。
向各个函数写入的可变参数通过链表存储。各个函数所接收的参数个数和类型不同,因此我们需要动态构造拥有不同长度和节点类型的链表。
为了便于动态构造链表,以及让代码更加通用,我们将每个函数和它所需的真实参数信息记录在一张表里。如下(称之为表,实际是个数组):

typedef enum {
	CASE_INIT,
	CASE_CONNECT,
	CASE_SETVOLUME
} AudioFuncCase;

typedef struct {
	int mFuncCase;  // AudioFuncCase enumerations
	char *mFuncStr;
	AudioFuncPtr mFuncPtr;
	char *mFuncArg;
	char *mFuncArgDesc;
} AudioFuncTableItem;

const AudioFuncTableItem gAudioFuncTable[] = {
	// CAUTION: argument descriptions must be extra '\0' terminated, else program may crash!!!

	{CASE_INIT,				"init",				init,			"s",		"amplifierType\0"},
	{CASE_CONNECT,			"connect",			connect,		"ll,i,f",	"audioSource\0channelMask\0sourceGain\0"},
	{CASE_SETVOLUME,		"setVolume",		setVolume,		"ll,i",		"audioSource\0volumeStep\0"}
};
const int gAudioFuncTableSize = sizeof(gAudioFuncTable)/sizeof(AudioFuncTableItem);

其中 mFuncArg 表示各函数真实需要的参数个数和类型。s 表示 char *ll 表示 long longi 表示 intf 表示 `float``。

然后再编写一段代码,根据表里信息为每个函数自动生成可变参数链表,并传入参数。
可变参数列表中,每个参数的值可以从其它地方读取/赋值,也可以是来自手工输入。以手工输入参数值为例,这段代码类似下方:

#define NEW_ARG_ITEM(argVal, argShortType, argChainHead, argChainCurrent) \
																		  \
	FuncArgChainItem_t *argChainItem_##argShortType = (FuncArgChainItem_t *)malloc(sizeof(FuncArgChainItem_t)); \
	memset(argChainItem_##argShortType, 0, sizeof(FuncArgChainItem_t)); \
	argChainItem_##argShortType->mArgVal.m_##argShortType = argVal; \
																    \
	if (argChainHead == NULL) { \
		argChainHead = argChainItem_##argShortType; \
	} else { \
		argChainCurrent->mpNext = argChainItem_##argShortType; \
	} \
	argChainCurrent = argChainItem_##argShortType;



int callAudioFunc()
{
	int ret = RET_ERROR;

	FuncArgChainItem_t *argChainHead = NULL, *argChainCurrent = NULL;

	char *origArgList = gAudioFuncTable[testCaseChoice].mFuncPtrArg;
	int origArgListSize = strlen(origArgList) + 1;
	char *copiedArgList = (char *)malloc(origArgListSize);
	memcpy(copiedArgList, origArgList, origArgListSize);

	char *nextArgDesc = gAudioFuncTable[testCaseChoice].mFuncArgDesc;

	char *subArgType, *subArgType_saved;
	subArgType = strtok_r(copiedArgList, ",", &subArgType_saved);
	while (subArgType != NULL) {
		if (strcmp(subArgType, "ll") == 0) {
			long long input_ll;
			scanf("%lld", &input_ll);
			NEW_ARG_ITEM(input_ll, ll, argChainHead, argChainCurrent);

		} else if (strcmp(subArgType, "i") == 0) {
			int input_i;
			scanf("%d", &input_i);
			NEW_ARG_ITEM(input_i, i, argChainHead, argChainCurrent);

		} else if (strcmp(subArgType, "f") == 0) {
			float input_f;
			scanf("%f", &input_f);
			NEW_ARG_ITEM(input_f, f, argChainHead, argChainCurrent);

		} else if (strcmp(subArgType, "s") == 0) {
			char input_s[MAX_KVPAIR_SIZE] = {0};
			LIMITED_STR_SCANF(MAX_KVPAIR_SIZE-1, input_s);
			NEW_ARG_ITEM(input_s, s, argChainHead, argChainCurrent);

		} else {
			printf("illegal argument type \'%s\'\n", subArgType);
			return RET_ERROR;
		}
		subArgType = strtok_r(NULL, ",", &subArgType_saved);
	}
	free(copiedArgList);

	ret = gAudioFuncTable[testCaseChoice].mFuncPtr(argChainHead);
	
	return ret;
}

至此,我们不使用 stdarg,而是使用链表实现的可变参数函数就可以工作了。

标签:char,int,long,链表,参数,可变,argChainHead,stdarg
From: https://www.cnblogs.com/qidi-huang/p/implement-variadic-function-with-chained-list-instead

相关文章

  • C#内存缓存链表BytesListBuffer
    C#自带MemoryStream,可以作为内存缓存使用,用来存储byte[]数据,但是MemoryStream的扩展机制是通过获取整块连续内存来缓存数据,当需要缓存较大数据时,虽然空闲内存可能足够,但是可能找不到足够大的整块连续内存而导致扩展失败产生outofmemory的异常。另外,对于很多缓存场景,重新分配整块......
  • Java基础 不可变集合详解
    如果不想让别人修改集合中的内容,只想让别人仅能够查询数据,就可以用不可变集合 在List、Set、Map接口中,都存在静态的of方法,可以获取一个不可变的集合eg:List<String>list=List.of("张三","李四");......
  • kotlin的函数关于可变参数使用vararg
    前提:kotlin在编译的时候会转换成对应的java一、java的可变参数类型:java类型的类似:voidfunc(Integer...values){}   那么对应的kotlin的类型类似:funfunc(varargvalues:Int?){}  注意:这里我使用的是Int?是可空的意思,那么到java层的时候会转换成Integer,......
  • 面试必刷TOP101:3、链表中的节点每k个一组翻转
    一、题目将给出的链表中的节点每k 个一组翻转,返回翻转后的链表如果链表中的节点数不是k的倍数,将最后剩下的节点保持原样你不能更改节点中的值,只能更改节点本身。二、题解publicclassSolution{/****@paramheadListNode类*@paramkint整型......
  • 面试必刷TOP101:2、链表内指定区间反转
    一、题目将一个节点数为size链表m 位置到n位置之间的区间反转,要求时间复杂度O(n),空间复杂度O(1)。例如:importjava.util.*;/**publicclassListNode{*intval;*ListNodenext=null;*}*/publicclassSolution{/****@paramhea......
  • 力扣19.删除链表的倒数第 N 个结点
    给你一个链表,删除链表的倒数第 n 个结点,并且返回链表的头结点。 示例1:输入:head=[1,2,3,4,5],n=2输出:[1,2,3,5] 示例2:输入:head=[1],n=1输出:[] 示例3:输入:head=[1,2],n=1输出:[1]  提示:链表中结点的数目为 sz1<=sz<=300<=N......
  • 面试必刷TOP101:1、反转链表
    一、题目给定一个单链表的头结点pHead(该头节点是有值的,比如在下图,它的val是1),长度为n,反转该链表后,返回新链表的表头。示例:输入:{1,2,3}返回值:{3,2,1}二、题解2.1使用栈求解栈是先进后出的。实现原理就是把链表节点一个个入栈,当全部入栈完之后再一个个出栈,出栈的时候在把出栈的结点......
  • python 链表
    fromtypingimportOptionalclassListNode:def__init__(self,val=0,next=None):self.val=valself.next=nextclassSolution:defpartition(self,head:Optional[ListNode],x:int)->Optional[ListNode]:lessListPre......
  • 03_变量与值可变性
    rust定义变量let关键字用于声明变量:let(mut)变量名变量值是否改变默认变量值不可变(Immutable),在声明变量时加上mut才为可变变量。letx=5;x=6;letmuty=6;y=8;letmuty:u32=9;第二行编译报错,上边变量声明时未标注类型的,默认是i32类型。rust定义......
  • 《剑指offer》面试题的Java实现-从尾到头打印链表
    输⼊⼀个链表的头节点,按链表从尾到头的顺序返回每个节点的值(⽤数组返回)。⽐如下⾯的链表: publicstaticclassLinkNode{intvalue;LinkNodenext;LinkNode(intvalue){this.value=value;}}//思路:将链表进行遍历,在遍历的过程中记录元素的个数,//然......