C/C++省略符形参原理和使用
前言
最近重新翻了一遍《C++ Primer》,看了一下省略符形参的相关内容,发现只有半页左右的内容(可能后面还有),只是提了一下有这种形参语法,作用是兼容C语言,但没有提及如何使用这种形参,以及要注意什么。个人也很好奇不定量形参的使用以及实现方法,不定形参在最近个人的使用中也比较常见,比如格式化输出,就测试了一下,说点粗陋的发现。
测试环境
测试基于window10环境,使用的编译器是VS2019,在32和64位的编译配置下都测试过。
省略符形参的实现
个人使用到的几个用到省略符形参的方法都有一套类似的结构, 函数的所有参数都是原变量被拷贝后赋值到新的内存中的,因而所有参数在内存中是连续的,这是可以使用省略符形参的前提。 参数a是形参列表中最后一个明确的参数,va_list是char*类型。
va_start
va_start的作用是获取参数a后面第一个未知参数的地址,并赋值给args参数。
#define _crt_va_start(ap,v) ( ap = (va_list)_ADDRESSOF(v) + _INTSIZEOF(v) ) #define _INTSIZEOF(n) ( (sizeof(n) + sizeof(int) - 1) & ~(sizeof(int) - 1) ) #define _ADDRESSOF(v) ( &reinterpret_cast<const char &>(v) )
在32位的编译环境中,va_start的底层实现如上。
_ADDRESSOF(v)
这个方法获取明确变量的地址并强制转换为char*类型处理,因为char类型的大小为1字节,等于字节大小单元,更方便寻址处理。
_INTSIZEOF(v)
这个宏定义用于获取不小于变量v(在实际使用中是参数a,亦即形参列表中最后一个明确类型的参数)类型大小的 4的 整数倍的字节长度,结果是4的整数倍。具体为什么这样设计我还在查询,但可能和指针以及内存地址有关,在32位的系统中,内存地址长度以及指针变量长度为4字节(int);而在64位系统中,这一长度也随着内存地址大小扩展变成了8字节(__int64),并且,当实际数据大于指针大小时,转换为指针处理。
va_arg
#define _crt_va_arg(ap,t) ( *(t *)((ap += _INTSIZEOF(t)) - _INTSIZEOF(t)) )
va_arg宏定义用于获取未知参数存储内存中的第一个参数数据,提取时类型为t,同时,也让args指向这个参数后的位置,用于获取(可能存在的)下一个未知参数,可以通过可控的遍历安全获取实际参数列表。
如 假设省略符形参的实参为
int a, float b, TypeNameA* c
则,可以通过依次顺序调用以下语句获取实参数据:
va_arg(args,int);//返回结果为 a va_arg(args,float);//返回结果为 b va_arg(args,TypeNameA*);//返回结果为 c
但一旦使用了错误的类型或者顺序提取参数,可能会造成未知的错误。
va_end
va_end将参数列表清零
#define _crt_va_end(ap) ( ap = (va_list)0 )
省略符参数的使用
首先做个小实验,在函数的实现中不通过实参而获取它对应的值。
void show(int a, char b); int main() { int a = 10; char b = v; show(a, b); system("pause"); return 0; } void show(int a, char b) { int* aptr = &a; char b_ = e; #ifdef _M_X64 b_ = *(char*)((char*)aptr+sizeof(__int64)); #elif defined(_M_IX86) b_ = *(char*)((char*)(aptr) + sizeof(int)); #endif cout << b_ << endl; }
在show方法中,我并没有显示的调用形参b,最终却依旧会输出显示b的值。 说回正题: 使用省略符形参的关键在于确定省略形参的数量和类型,因此你需要通过一定的标志去确定这两项数据。 通常使用的方法有, 1、在明确的参数中添加数量参数,让函数循环有限次数,省略参数的类型一般有默认要求 2、最后一个省略符参数显式标记为null或其他标志性数据,确保能在参数范围内停止。 3、格式化输出时,一般会明确类型为char*或者string的format参数,在format字符串中会有特殊字符标记嵌入位置、嵌入数量和嵌入类型。标准库中也有自带的vsnprintf函数,可用于代替处理va_arg的过程。