文章目录
- 概述
- 适用于做可变参数的数据类型
- 格式化字符串输出
- 用int做变长参数类型
- 用结构体指针做变长参数类型
- 用double做变长参数类型
- 用结构体直接做变长参数类型
- 变参函数与宏定义
- 符号 ... 不能透传
- 符号 ... 不接受ap做参数
- _VA_ARGS_ 代表可变参数
- 回调可变参数函数
- 取代变参函数的一些方法
- 小结
概述
本文介绍了C和C++语言中,可变参函数的正确的设计、实现、使用方法,使用场景和替代方案。文章 《语言基础 /C&C++ 可变参函数设计与实践,必须要指定可变参数的个数?YES》和《语言基础 /C&C++ 可变参函数设计与实践,va_ 系列实战详解(强制参数和变参数的参数类型陷阱)》算是对本文的某些具体方面的扩展,本文中的范例代码,也是基于上述两篇文章的实践结果的。
@History
C&C++ 可变参函数设计与实践,相关的几篇文章从2019年着笔,跨越到2023年,就一直烂在哪里,期间乱七八糟的填充了内容,近周内又是耗费了些许时间,简直要成为小王子的玫瑰了。我删减了许多之前的垃圾内容,删减了大部分踩坑和测验过程,重新整理了文章思路,凭借目前有限的知识储备,试图将可变参数函数的定义、实现、使用方法、浅层本质讲述明白。
转载请标明原文链接,
https://blog.csdn.net/quguanxin/category_6223029.html
适用于做可变参数的数据类型
在开始本小节前,先要点名的是,可变参函数并不只适用于格式化字符串输出。在《语言基础 /C&C++ 可变参函数设计与实践,va_ 系列实战详解》文中,我们已经讲明了,可变参函数强制参数(最后的具名参数)其数据类型的选择问题,在文章 《语言基础 /C&C++ 可变参函数设计与实践,必须要指定可变参数的个数?》中,我们已经讲明了,变参个数这一信息的必要性。本文中的测试例子中,无特殊情况下,都使用了 int 来做变参函数强制参数,当然如果是X64,最好使用 int64,或者你定义个宏,专门用以标记变参函数实现中的强制参数类型。
格式化字符串输出
void AflDebugWarn(const char *format, ...) {va_list ap;printf("[Warn] "); //附加信息va_start(ap, format);vprintf(format, ap);va_end(ap);printf("\r\n"); //附加信息
}
//Test in main or other function
AflDebugError("cant find Key %d,%d,%d", key->i1, key->i2, key->i3);//留意C函数vprintf以va_list类型做参数列表 //后续会详解
//int __CRTDECL vprintf(char const* const _Format, va_list _ArgList)
如上自定义的变参打印函数,几乎算是最简单的定义方式,其借用了C库 vprintf 函数(后续详解)。
用int做变长参数类型
//指定参数个数做可变函数强制参数
void print_Integers2(int param_count, ...) {va_list argptr;va_start(argptr, param_count); //初始化变参列表for (int i = 0; i < param_count; i++) {int value = va_arg(argptr, int);//do something .., or save first..qDebug("test2_Param%d:%d ", i+1, value); }va_end(argptr); //清空变参列表
}int main() { //Testprint_Integers2(3, 100, 200, 300);return 0;
}
如上变参函数正确实现了处理(测试过程为打印,可进行其他运算处理)任意个数整数变参的功能。
用结构体指针做变长参数类型
//这种结构体,不符合直接做可变参数类型的规则
typedef struct tagParam {short iSeg1;short iSeg2;
} TParam;
//
void Test_T_Pointer(int param_count, ...) {va_list ap;va_start(ap, param_count);for (int i = 0; i < param_count; i++) {TParam *ptValue = va_arg(ap, TParam*);qDebug("param_%d:value:%d,%d ", i+1, ptValue->iSeg1, ptValue->iSeg2);}va_end(ap);
}
//
int main(/*int argc, char *argv[]*/)
{TParam tArray[10] = { {100, 101}, {200, 201} };Test_T_Pointer(2, &tArray[0], &tArray[1]);return 0;
}
本质上,无论是 ( const char * ) 还是 ( TParam* ) ,都是一个地址,被 va_start 和 va_arg 当做 int64 整型类型来处理。
用double做变长参数类型
//执行成功/结果符合预期
void print_float2(int param_count, /*real param is float*/...) {va_list argptr;va_start(argptr, param_count); for (int i = 0; i < param_count; i++) {double value = va_arg(argptr, double); //note doubleqDebug("test_float2_param%d:%2.3f ", i+1, value);}va_end(argptr);
}int main() {//8, 4, 4, 8qDebug("sizeof(__int64):%d, sizeof(int):%d, sizeof(float):%d, sizeof(double):%d", sizeof(__int64), sizeof(int), sizeof(float), sizeof(double)); //print_float1(3, 1.234, 1.345, 1.567);//print_float2(3, 2.234, 2.345, 2.567);...
}
用结构体直接做变长参数类型
这是在彻底理解了 va_arg 宏的工作原理后,我们已经看到了理论上的支持。
//结构体尺寸必须要大于U64
typedef struct tagParamC {short iSeg1;short iSeg2;int a;short b;
} TParamC;//
void TestStructParamaABC(int param_count, /* param is TParamA/B/C */...) {va_list argptr;va_start(argptr, param_count);for (int i = 0; i < param_count; i++) {TParamC value = va_arg(argptr, TParamC);qDebug("test_struct_param%d:(%d,%d,%d,%d) ", i+1, value.iSeg1, value.iSeg2, value.a, value.b);}va_end(argptr);
}int main() {TParamC tArray[10] = {{100, 101, 102, 103}, {200, 201, 202, 203}, 0};TestStructParamaABC(2, &tArray[0], &tArray[1]);return 0;
}
变参函数与宏定义
符号 … 不能透传
首先,基于…形参定义的可变参函数,是不不能透传… 变参列表的,直接有编译错误。
//试图直接透传...形式的变参列表是不符合语法的
//void PrintfInfoA3(const char *format, ...) {
// SubtPrintfInfo(format, ...);
//} //大河qu @ CSDN
其次,va_list 是不能做…变参函数实参的,由于va_list本质是指针,这里并不编译报错。
void PrintfInfoA1(const char *format, ...) {//... vprintf(format, ap); ... //参照前文
}//试图直接将用va_list ap做...的实参,会有不可预期的结果
void PrintfInfoA2(const char *format, ...) {va_list ap;va_start(ap, format); PrintfInfoA1(format, ap); //这里的ap仅被当做一个U64的地址值va_end(ap);
}
//PrintfInfoA2("%x ", 100); //输出0x65FE84/即ap代表的地址值,实际的实参被丢失
符号 … 不接受ap做参数
//va_list _ArgListap 是可以透传给 vprintf
void PrintfInfoB1(const char *format, va_list _ArgListap) {vprintf(format, _ArgListap);printf("\r\n"); fflush(stdout);
}//va_list _ArgListap 形参可接收 ...变参列表
void PrintfInfoB2(const char *format, ...) {va_list ap;va_start(ap, format);PrintfInfoB1(format, ap);va_end(ap);
}
//PrintfInfoB2("大河qu @ CSDN %d", 100); //输出 大河qu @ CSDN 100
通过之前章节的测试采坑和整理总结,我们已经深刻地知道,在一个变长参数函数中,可以将(…)代表的参数通过 va_start 宏函数转换为 va_list 类型参数,但是我们不能将 va_list 传递给(…)参数,像PrintfInfoA2函数,更加不能像 PrintfInfoA3 函数那样,将…直接代入另个一变长参数函数。
VA_ARGS 代表可变参数
VA_ARGS 是一个预处理宏,用于在宏定义中表示可变数量的参数。在宏定义中,我们通常使用 … 表示宏的可变数量参数,而 VA_ARGS 则是一个特殊的标识符,用于代表宏参数列表中的可变参数部分,将可变数量的参数作为一个整体传递给宏定义中的处理部分。通过使用 VA_ARGS,我们可以在宏定义中对可变数量的参数进行处理,例如将它们作为参数传递给其他宏、展开为多个语句等等。
基本问题,
//
void print_Integers(void *puser, int param_count, ...) {va_list argptr;va_start(argptr, param_count);for (int i = 0; i < param_count; i++) {int value = va_arg(argptr, int);printf("test2_Param%d:%d ", i + 1, value);}va_end(argptr);
}
//错误的定义
#define LOG_DEFAULT2(param_count, ...) \print_Integers(NULL, param_count, ...) \
//正确的定义
#define LOG_DEFAULT1(param_count, ...) \print_Integers(NULL, param_count, __VA_ARGS__) \
int main() {//LOG_DEFAULT2(2, 100, 200);//LOG_DEFAULT1(2, 100, 200);///*system("pause");*/ return 0;
}
如下,LOG_DEFAULT2 无法通过编译,
要注意的是,如果LOG_DEFAULT2没有调用操作,上述错误不会暴露。
在项目的实际应用,
void LastError_SetRecord(unsigned char u8Module, unsigned char u8Unit, unsigned short u16ErrCode, const char * format, ...);
//变参函数定义为宏,以减少一些具名参数,方便调用
#define LASTERROR_BDSVR_DEFAULT(u16ErrCode, format, ...) \LastError_SetRecord(1, 0, u16ErrCode, format, __VA_ARGS__)
还可以这么用,
#define LOG(format, ...) printf(format, ##__VA_ARGS__)
int main() {LOG("Hello %s!", "world"); // 使用_LOG宏打印日志return 0;
}
也可以如下定义宏,给…传递一个实参,然后封装成宏函数,
//
#elif defined _M_X64void __cdecl __va_start(va_list* , ...);
//
#define __crt_va_start_a(ap, x) ((void)(__va_start(&ap, x)))
如下,是项目中的一些代码片段截取,使用到了 VA_ARGS 宏,
//使用宏定义封装va_list函数,并无什么特别
#define VSPRINTF_MR(mybuffer, bufferCount, myformat, arglist) \vsprintf_s(mybuffer, bufferCount, myformat, arglist)//无法封装sprintf_s/sprintf等使用...做形参的函数
//DALOS_API_EXPORT inline int AflSprintfElipsis(char* const _Buffer, size_t const _BufferCount, char const* const _Format, ...);//宏定义封装...形参的函数 //#if _MSC_VER >=1600
#define AflSprintfElipsis(_Buffer, _BufferCount, _Format, ...) \sprintf_s(_Buffer, _BufferCount, _Format, __VA_ARGS__)
回调可变参数函数
最早产生 ‘回调变参函数’ 这一怪诞想法,是在系统错误处理模块的实现和使用过程中,我当时的目标是,在顶层应用层中实现一个可变参函数,使得该函数可以在形如QTextEdit的对象上展示错误信息。我一开始期望的函数指针形式如下,
typedef void (*FuncDBSvrPrintf)(const char *format, ...);
上述定义在编译时未报错,但真的能用吗?
//定义...做形参的变参函数指针
typedef void (*FuncDBSvrPrintf)(const char *format, ...);//实现一个回调函数
void ABCPrintf(const char *format, ...) {va_list ap;va_start(ap, format);//Qt5.9.3\Tools\mingw530_32\i686-w64-mingw32\include\stdio.h//int __cdecl vprintf(const char * __restrict__ _Format, va_list _ArgList);vprintf(format, ap); fflush(stdout);va_end(ap);
}int main(int argc, char *argv[]) {setbuf(stdout, NULL);FuncDBSvrPrintf func = ABCPrintf;func("%s,%d", "https://blog.csdn.net/quguanxin/category_12345326.html ", 123);...
}
透过上述测试过程和结果,运行无误,可得出,… 作为特殊的语法结构,是可以用在函数指针定义中的。但是我们的最终目的是在形如 QTextEdit 的对象上展示错误信息,因此还要必须定义 ‘自己的 vprintf 函数’,后来的一个实践大约如下,
//回调函数的指针声明
typedef void (*FuncDalPrintf)(void *pvHandleUser, const char* format, va_list _ArgListap);//注册GUI等外部打印过程 /u8HandleFlagID-以支持多过程 /C接口不支持默认值
DALOS_API_PUBLIC signed int RegistDalPrintCallback(FuncDalPrintf pFuncDalPrintf, unsigned cahr u8HandleFlagID /*= 0*/){... s_arrayFuncDalPrintf[u8HandleFlag] = pFuncDalPrintf; ...
}//在客户端的回调函数实现,如MainHMI
static void CallbackPrintfInGui(void *pvHandleUsedByCallbackFunc, const char* format, va_list _ArgListap) {char outContext[1024] = { 0 };int ret = 0;#if _MSC_VER >=1600ret = vsprintf_s(outContext, 1024, format, _ArgListap);#elseret = vsprintf(outContext, format, _ArgListap);#endifassert(ret > 0);//fromLocal8Bit进行字符编码转换To QString(MainWindow::ms_pToolUiConsole)->ShowDebugInfo(QString::fromLocal8Bit(outContext));
}//在错误处理模块中执行回调
void AflPrintfInCallback(unsigned char u8HandleFlagID, const char *format, ...) {...va_list ap;va_start(ap, format);if (NULL == s_arrayFuncDalPrintf[u8HandleFlagID]) AflPrintToStderror("PrintfInCallback Has No Register");else s_arrayFuncDalPrintf[u8HandleFlagID](pUser, format, ap); //执行回调过程 va_end(ap);
}
虽然前文中验证了…变参函数签名是可以直接被定义为函数指针来正确使用的,但是实际项目中却很少这么用,尤其是用以格式化输出逻辑时。这是因为最终的格式化输出通常会依赖 vsprintf 等以va_list为形参的函数,如此,我们的回调函数具体实现,就不如直接使用 va_list 来做形参,而不是绕远路去使用…做形参。
取代变参函数的一些方法
通常函数中形式参数的数目是确定的,在调用时要依次给出与形参对应的所有实际参数,但在某些情况下希望函数的参数个数可以根据需要确定。于是可变参函数(Variadic Functions)应运而生,它是参数的数目可以改变的函数,也可以叫做 ‘参数个数可变函数’ 或 ‘变长参数函数’ 等。变长参数的函数,看似灵活性很高,其实不然!
首先,可变参数的访问和管理是非常依赖于编译器实现的,可移植性较差。另外,直觉上除了格式化字符串输出以外的其他场景,其适用性,或者不可替代性病不高,使用按位定义的或值、使用list/vector容器对象等传递不定数量的数据,往往便利性更好。
因为,在变参函数被调用的那一刻,它的客户是要明确知道,要传递的参数个数和具体参数的,
if (1 == paramcount) {DoSomthing("Hello World!", 1, &tArray[0]);
}
else if (2 == paramcount) {DoSomthing("Hello World!", 2, &tArray[0], &tArray[1]);
}
在…和va_list参数类型的比较过程中,我们提到,… 形参避免了头文件的引入,这个优势,同样的适用于与容器类型参数的比较。另外,在参数列表不是很长的情况下,如1-4个左右,则变参函数感觉上是有优势的,这使得客户端不用在调用函数时,定义额外的实参变量。但若用户期望变参数列表特别长、或者参数列表是可通过规则自动填充的,则容器传参数将更加灵活方便。如,某接口的功能是向通信远端设备写入n(约60~160)个使能状态的寄存器的数值,而寄存器的使能状态、寄存器的值等都可以从配置文件中加载。此时变参函数接口就有点抓虾,因为光是在代码里写这百十个实参就很头疼。
另外,使用按位定义的或值来传递 “可变数量的” 参数,也是很不错的选择,
//采集数据处理结果的类型
#define TM_TYPE_API_PUBLISH_COMPONENT unsigned int
//内部用数据维护管理/外部接口上使用或型值定义方便用户使用
#define API_LINE_COMPONENT_A /*1*/ (1 << INNER_LINE_COMPONENT_A /*0*/) //
#define API_LINE_COMPONENT_B /*2*/ (1 << INNER_LINE_COMPONENT_B /*1*/) //
#define API_LINE_COMPONENT_C /*4*/ (1 << INNER_LINE_COMPONENT_C /*2*/) //
#define API_LINE_COMPONENT_D /*8*/ (1 << INNER_LINE_COMPONENT_D /*3*/) //
...
//伪代码 //
void ToolLineBuffLayout::EnableComponent(TM_TYPE_API_PUBLISH_COMPONENT valueComponent) {...// 将用户的或值拆分 //计算指数的另一种方案 while (valueComponent> 1) { valueComponent/= 2; exponent++; } return exponent;for (int ipos = 0; ipos < INNER_LINE_COMPONENT_MAX /*10*/; ipos++) {// 记录当前对象是否包含某类数据结果m_ArrayAddrOfLineRltPart[ipos].u8PartEnable = ((1 << ipos) == (valueComponent & (1 << ipos))) ? 1 : 0;}...
}
小结
0、printf 是可变参函数,vprintf 不算是可变参函数。
1、变参函数必须要有一个强制的形参,在…代表的变参列表之前。
2、强制形参,可以是任意类型的参数,可以与变参类表参数数据类型不同或相同。
4、可变参函数的…形式和va_list形式,都是可以定义成回调函数的。
5、如printf函数,通过事先定义好的格式化占位符可得可变参数的类型及个数。注意实参可多不可少,否则可能造成程序崩溃。
6、不同于格式化占位符,在型如print_Integers2的可变函数形参中包含param_count参数个数来做强制参数,不光是应为可以简化变参列表的处理过程,而是必须的。
7、在大部分情况下,va_list 类型就是一个char指针类型。
8、va_start 的实参 param_4_start,可以是 int、long、void*、char*、T* 等,不期望是 char 、short 等非字节对齐的类型,但在默认提升规则作用下,并不会带来实质性错误。
9、va_arg 可以解析 int、long、double、T* 等数据类型,但是不接受 float 类型。在 IDE-x86平台配置下,它不接受结构体对象类型,在 IDE-x64 平台配置下,是可以接受结构体对象类型的,但要求满足 sizeof(T) > sizeof(_int64) 条件。