语言基础 /CC++ 可变参函数设计与实践,va_ 系列实战详解(强制参数和变参数的参数类型陷阱)

news2024/9/28 21:20:34

文章目录

  • 概述
  • va_ 系列定义
  • va_list 类型
  • va_start 宏
    • 从变参函数的强制参数谈起
    • 宏 va_start 对 char 和 short 类型编译告警
    • 宏 va_start 源码分析
    • 猜测 __va_start 函数实现
  • va_arg 宏
    • 宏 va_arg 无法接受 char 和 short
    • 为啥va_arg可解析int却不能解析float类型?
    • 宏 va_arg 源码解析
    • 变参数类型: sizeof(结构体) > sizeof(_int64)
  • va_end 宏
  • 小结

概述

本文基于丰富的可变参函数设计、实现、使用实践经验,进一步结合对 va_list、va_start 、va_arg、va_end 源码分析,剖析了变参列表的处理过程,重新回答和解释了变参函数实现和使用过程中遇到的诸多问题和陷阱,如,
为什么 va_start 宏函数不愿接受 char或short 类型的参数变量?
为什么 va_arg 宏函数不可接受 char或short 等类型名?
实践过 int、long是可以做变参类型的,那么, float、double 可以做变参类型吗?
为什么 va_arg 宏函数可以正确解析 int 类型,却不肯接受 float 类型?
实践过结构体指针类型做变参类型,那么,结构体对象类型可做变参类型吗?

@History
上述问题已埋在CSDN草稿中太久太久。
@关联
@《语言基础 /C&C++ 可变参函数设计与实践,必须要指定可变参数的个数?》
@关联
@《语言基础 /C&C++ 可变参函数设计与实践,变参函数的实现、使用、替代方法》

转载请标明原文链接,
https://blog.csdn.net/quguanxin/category_6223029.html

va_ 系列定义

在 C/C++ 语言中,提供了一组以 va_ 为前缀的宏和类型定义,用以在函数内部迭代访问可变数量的参数,可称它们为 “va_list系列” ,它们的定义位于stdarg.h头文件中。一个简短的介绍如下:
va_list类型:
va_list 是一个用于存储可变参数信息的类型,通常是一个指向内部结构的指针,在可变参数函数中使用va_list类型的变量来迭代访问参数列表。
va_start宏:
va_start 宏用于初始化 va_list 类型的变量,以开始对参数列表的访问。它接受两个参数,第一个是 va_list 类型的变量,第二个是可变参数函数中最后一个具名参数的名称,它会根据具名参数的位置和大小来计算参数列表的起始位置。
va_arg宏:
va_arg 宏用于获取参数列表中的下一个参数值。它接受两个参数,第一个是 va_list 类型的变量,第二个是要获取的参数的类型,它会返回指定类型的下一个参数值,并将 va_list 变量 ap 更新为指向下一个参数的位置。
va_end宏:
va_end 宏用于结束对参数列表的访问。它接受一个va_list 类型的变量,它会执行一些清理操作,以确保参数列表的访问结束。

早些年定义和实现可变参函数时,使用 mingW 编译环境,
在这里插入图片描述
如上,并不能进行更深层次的源码跳转,更没去研究过mingW工具集相关组件的源码。而在 MSVC 下,可查看详细定义,

在 Microsoft Visual Studio 14.0\VC\include\stdarg.h 其顶层定义为,
在这里插入图片描述
相关的_crt_va_ 宏可进一步跳转代码到 vadefs.h 查看详细定义,
-/-/-
在这里插入图片描述
-/-/-
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
如上,除了 _M_X64 模式下的 va_start 宏定义,不能查阅全源码,其他情况下的定义,算是完整的。后文将逐一展开讲解。

va_list 类型

在可变参函数实践过程中,已经认识到,类型 va_list 定义如下,
在这里插入图片描述
如上,分别是在VS2017、MingW7.3、百科给出的 va_list 类型定义,它们定义在各自版本的 vadefs.h 这个C语言标准库头文件中。故,在大部分情况下,va_list 类型就是一个char指针类型,这是下文中理解一些现象的关键因素。

还要纠正之前的一个错误,

int __CRTDECL  printf(char const* const format, ...);
int __CRTDECL vprintf(const char* format, va_list argptr);

函数 printf 是变参函数,但是 vprintf 不是变参函数,因为 va_list 是一个具体的类型,argptr 是具名参数。

va_start 宏

在变参函数的实践中,最早遇到的问题是 va_start 相关的告警,本节便从它开始谈起。先了解强制参数的定义,再细细研究为什么强制参数是 char 或 short 类型时,会有编译警告。

从变参函数的强制参数谈起

在代表变参列表的语法符号… 前,可以存在多个确定的参数,其中,紧挨着…的那个确定参数,也即最后一个确定(具名)参数,一般称为变参函数的强制参数,它是必须存在的,或者说是至少要存在的。在《语言基础 /C&C++ 可变参函数设计与实践,必须要指定可变参数的个数?YES》 文中,我们实际验证了 va_start 宏函数是不接收 NULL参数的,至少在QtCreator+MSVC环境下那会导致编译器崩溃,也证明了把 ‘变参数个数’ 作为变参函数强制参数是不错的选择。

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); 
}

宏 va_start 对 char 和 short 类型编译告警

宏函数 va_start 不仅不接受 null,它也不愿意接受short 和 char 类型的参数。以前的草稿记录中有写下过:char和short等类型不可以作为可变参函数的强制参数类型,注意不是说指针类型哈。这种说法倒是没有什么很大的错误,只是不太确切,我们应该更加直接将此类问题安置在 va_start 和 va_arg 宏函数本身上来继续讨论。

最早的相关记录,是在某嵌入式项目中,Keil下C编程,遇到编译告警如下,
在这里插入图片描述
其他类似告警如下,只有简单记录,已找不到出处,未再设法复现,

//warning:  #1256-D: "MR_U8" would have been promoted to "int" when passed through the ellipsis parameter; use the latter type instead

还有一次类似的记录,当时没写IDE类型,后来在 QtCreator+MSVC2015 下复现,如下,
在这里插入图片描述

va_start(argptr, count);
//warning: passing an object that undergoes default argument promotion to 'va_start' has undefined behavior
//note: parameter of type 'short' is declared here`

要说明的一点是,虽然上述函数在 va_start 处存在编译告警,但上述函数是运行正常的。在VS集成开发环境下,同样测试上述 print_Integers3 函数实现,没有告警。
在这里插入图片描述

再后来,在一些机缘巧合之下,我发现:QtCreator + MSVC2015 集成开发环境下,va_start 告警提示,以及下文中提到的 va_arg 参数类型告警提示、对 va_arg 取地址操作时的错误提示,竟然是可以控制打开和关闭的。在开启 QtCreator 插件配置 ClangCodeModel 的情况下,通过 Clang 的检测机制,上述异常会报告,否则,没有任何提示。虽然上述告警并不会产生实质性的错误,但我还是深追了下来,毕竟前期被它吊了很久。

宏 va_start 源码分析

宏函数 va_start 可以接受 int、long 类型的参数,但却不太愿接受 unsigned char、char、short类型,这是为啥?所以,总想着找点理论上的支撑。接下来的文章中,我们将在QtCreator + MSVC2015_64 环境下,完成相关探究过程。

一开始我的研究方向有点跑偏,
由于上文告警中的,default argument promotion,其含义是默认参数提升。它是C语言中的一种隐式类型转换规则,用于将函数调用中的较小的整数类型和浮点类型参数提升为较大的整数类型和浮点类型。在《C语言程序设计》第2版 2.7 类型转换一节中,有提到,
在这里插入图片描述
在这里插入图片描述
但总觉得有些说不通,
以上文 print_Integers2(short param_count, …) 函数为例,这里的 param_count 是一个具参数,它不属于变参列表,它是明确声明的,这里也没有多元操作符,也不是什么无函数原型的情况,它怎么能被提升呢?
后来,得知,
根据C语言标准,可变参数的传递方式要求参数按照一定的字节对齐方式进行压栈。这种对齐方式要求参数的大小必须是特定字节的倍数。对于 va_start 宏的第二个参数,该具名参数的类型决定了可变参数列表的起始位置和对齐方式。由于 char和short类型的大小不能直接满足对齐要求,因此会告警。
再后来,
透过 _M_IX86 下 __crt_va_start_a 中使用的 _INTSIZEOF(n) 定义,也可以证明了上述猜测,

    #define _INTSIZEOF(n)          ((sizeof(n) + sizeof(int) - 1) & ~(sizeof(int) - 1))
    #define __crt_va_start_a(ap, v) ((void)(ap = (va_list)_ADDRESSOF(v) + _INTSIZEOF(v)))

计算过程1,sizeof(int) - 1:
这样做是为了确保后续的对齐操作不会超过 int 类型的大小。
计算过程2,(sizeof(n) + sizeof(int) - 1):
在上一过程基础上,得到一个比类型 n 大的值。
计算过程3,~(sizeof(int) - 1):
用于生成一个掩码,将最后 sizeof(int) 个比特位设置为 1,其余比特位设置为 0。
((sizeof(n) + sizeof(int) - 1) & ~(sizeof(int) - 1)):
将过程2和过程3的结果进行按位与操作,得到 sizeof(n) 的倍数并对齐到 int 类型的边界

最后,
这里倒是可以借用上文《C语言程序设计》中的那句话。对于可变参数函数中的强制参数(即形参类表中最后一个确定的参数),即使你的实参为char或short等,也请把它声明为int或long类型。这可能是个好习惯。
另外要注意的是,在C语言中,va_start 宏并不会直接申请可变参数列表的空间,当然 va_arg 也不会。宏 va_start 的主要作用是初始化一个 va_list 类型的变量,以便在函数内部访问可变参数。实际上可变参数列表的空间是由编译器自动管理的,在函数调用时,编译器会根据函数原型中的参数信息和函数调用时提供的实际参数,来为可变参数列表分配合适的内存空间。

猜测 __va_start 函数实现

//大河qu // vadefs.h 
#define __crt_va_start(ap, x) ((void)(__vcrt_va_start_verify_argument_type<decltype(x)>(), __crt_va_start_a(ap, x)))
//其中__vcrt_va_start_verify_argument_type其内容是静态断言,旨在强制"va_start参数不能具有引用类型,也不能用括号括起来"

//大河qu 
#elif defined _M_X64
    void __cdecl __va_start(va_list* , ...);
    #define __crt_va_start_a(ap, x) ((void)(__va_start(&ap, x)))
	...

前文提到过,在 _M_X64 模式下的 va_start 宏定义,不能查阅全源码,那么,就来猜测下它的实现过程。(纯属以前的自己闲的蛋疼)

void __cdecl __va_start(va_list* ap, ...) {
    // 获取可变参数函数中最后一个具名参数的地址
    void* last_arg = &ap;
    // 根据具名参数的地址计算可变参数列表的起始地址
    *ap = (va_list)last_arg + sizeof(void*);
    // 将起始地址按照平台相关的规则对齐 //如8字节
    *ap = (va_list)(((uintptr_t)(*ap) + 7) & ~7);
}

为此还设计了一个测试用例,

void print_Integers7(char param_count, /*real param is int */...) {
    qDebug("test_int7_forcedparam_addr:0x%.16llx, value:%d ", (int64_t)&param_count, param_count);
    va_list argptr;
    va_start(argptr, param_count);
    for (int i = 0; i < param_count; i++) {
        int *pvalue = &va_arg(argptr, int);
        qDebug("test_int7_param%d_addr:0x%.16llx, value:%d ", i+1, (int64_t)pvalue, *pvalue);
    }
    va_end(argptr);
}

在这里插入图片描述
如上结果显示,即使传递了 char 类型给 va_start 宏,变参函数的地址也会被设置为8字节对齐。这里还有个插曲:在 QtCreator 软件,开启 ClangCodeModel 插件选项时, &va_arg(argptr, int) 操作并标识为错误代码,提示,
在这里插入图片描述
类似异常的分析,已经记录在其他文章中,此处不再讨论。只了解到,某些编译器可能提供一些扩展或非标准功能,允许对右值进行取地址操作,如未开启 ClangCodeModel 的QtCreator,如 未特殊配置过的 Visual Studio…

va_arg 宏

前文我们已经贴出了,在 MSVC 2015 下的完整的 va_arg 宏函数定义。对 va_arg 宏的研究始于,那段 “想方设法不不给变参函数传递参数个数信息” 的尴尬历程,再后来,读懂了 va_arg 在 MSVC 源码定义,一切才迎刃而解。

宏 va_arg 无法接受 char 和 short

在Keil中,
在这里插入图片描述
在 QtCreator + MSVC2015 中,

int value = va_arg(argptr, short);
//warning: second argument to 'va_arg' is of promotable type 'short'; this va_arg has undefined behavior because arguments will be promoted to 'int'
//stdarg.h:35:50: note: expanded from macro 'va_arg'

与 va_start 差不多,va_arg 宏函数也不待见 char 和 short 类型。不同的是,前者针对这种情况,会执行默认提升,不会造成大问题,而后者,诚不欺你,发生了无法预计的行为,可能会在 va_arg 执行时奔溃掉。所以还是那个建议,请直接使用 int 或 long…

为啥va_arg可解析int却不能解析float类型?

首先验证,va_arg 是否可以解析浮点数,float 类型 或 double 类型,

//执行失败/结果不符合预期
void print_float1(int param_count, /*real param is float*/...) {
    va_list argptr;
    va_start(argptr, param_count);     
    for (int i = 0; i < param_count; i++) {
        float value = va_arg(argptr, float);  //note float
        qDebug("test_float1_param%d:%2.3f ", i+1, value);
    }
    va_end(argptr);
}
//执行成功/结果符合预期
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 double
        qDebug("test_float2_param%d:%2.3f ", i+1, value);
    }
    va_end(argptr);
}

int main() {
	//8, 4, 4, 8
    qDebug("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 解析 float 类型的操作,失败了。那么,就产生了一个新问题,结合在 x64下 va_arg 定义,无论是int或float类型,它们都是4字节,实际运行中,其都将执行定义中的第二个分支。但,va_arg 可以正确解析 int 类型,却不能正确解析 float 类型,这是什么鬼?

//__crt_va_arg(ap, t)   //in defined _M_X64
 *(t* )((ap += sizeof(__int64)) - sizeof(__int64)))

结合上文,我们设计并调试查看 va_arg 宏返回值 value 的地址信息,

//
void print_Integers7(int param_count, /*real param is int */...) {
    va_list argptr;
    va_start(argptr, param_count);
    for (int i = 0; i < param_count; i++) {
        int *pvalue = &va_arg(argptr, int);
        qDebug("test_int7_param%d_addr:0x%.16llx, value:%d ", i+1, (int64_t)pvalue, *pvalue);
    }
    va_end(argptr);
}
//
void print_float7(int param_count, /*real param is float*/...) {
    va_list argptr;
    va_start(argptr, param_count);
    for (int i = 0; i < param_count; i++) {
        float *pvalue = &va_arg(argptr, float);
        qDebug("test_float7_param%d_addr:0x%.16llx, value:%2.3f ", i+1, (int64_t)pvalue, *pvalue);
    }
    va_end(argptr);
}

在这里插入图片描述
再细细品鉴 va_arg 的源码定义,无论是int还是float类型,指针偏移都按照8字节的sizeof(int64)来执行,也可以想到,int和float数据类型在调用变参函数的那一刻,已经分别被默认的提升为 int64和double类型。再回到float为啥不行的问题上,其实这是一个常规错误,常见于新手代码。即"指针类型强转并解引用"非常危险。函数 print_float7 实际上执行了,用 float* 类型 去强转 double* 类型,然后在接下来的解引用操作中,用float去解析double的内存,这肯定是错误的。一个干净的例子如下,

int main() {
	//
    double dvalue = 1.2345;
    float fResult = *((float*)&dvalue);
    //float value from double by float pointer:0.00000
    qDebug("float value from double by float pointer:%2.5f ", fResult);

    int64_t i64value = 12345;
    int iResult = *((int*)&i64value);
    //int value from int64 by int pointer:12345 
    qDebug("int value from int64 by int pointer:%d ", iResult);
    
    return 0;
}

在这里插入图片描述
解引用操作具有一定的风险,如果解引用的指针无效或未初始化,或者指针指向的内存位置不符合指针类型的要求,那么解引用操作可能会导致未定义的行为。在 IEEE 754 标准中,double 类型的内存布局按照以下顺序排列:
在这里插入图片描述
而单精度浮点数float,按照上述布局,分别占用1-8-23bit位。由于 float和double 数据类型的内存构造的上述差异,按照float类型去解引用一个实际存储double数值的内存地址,自然是不符合规则的。对于一个正整数,int指针强转int64指针类型,由于整数数据的内存构造,在数值32bit大小以内的情况下,碰巧不会有问题,但这也是应该被禁止的操作。

宏 va_arg 源码解析

宏 va_arg 的主要作用是使得我们能够在函数中按照参数的类型和顺序获取变长参数列表中的参数值,从而实现对可变数量参数的处理。下文枚举了 _M_IX86 和 _M_X64 两种模式下的 va_arg 定义,前者容易理解,后者可能要费点小心思。

_M_IX86

//Microsoft Visual Studio 14.0\VC\include\vadefs.h
#elif defined _M_IX86
    #define _INTSIZEOF(n)          ((sizeof(n) + sizeof(int) - 1) & ~(sizeof(int) - 1))
    #define __crt_va_arg(ap, t)     (*(t*)((ap += _INTSIZEOF(t)) - _INTSIZEOF(t)))

理解它的关键是,+=操作是对ap自身进行修改的,而减法操作是不会的。故,+=操作的目的是为了改变ap指针,使得其指向下一个参数,而(变更后的)ap 减 _INTSIZEOF(t) 是为了访问当前参数的指针。
(ap += _INTSIZEOF(t)):
ap 指针会根据参数类型的大小 _INTSIZEOF(t) 进行偏移,以便指向下一个参数的内存位置。
(ap - _INTSIZEOF(t)):
ap 指针会减去参数类型 t 的大小,以便回到当前参数的内存位置。如果你没有理解+=会改变自身的特性,那么这可能看上去很诡异。因为ap 已经被 va_start 初始化过,已经指向了第一个变参的地址,因此下一步直接解引用就可以得到第一个参数,但是大神程序员为了少写一行递增指针的代码…
((t)((ap - _INTSIZEOF(t))):
通过将指针转换为指定类型 t 的指针,并通过解引用操作 * 来获取参数的值。

_M_X64

#elif defined _M_X64
    #define __crt_va_arg(ap, t)                                               \
        ((sizeof(t) > sizeof(__int64) || (sizeof(t) & (sizeof(t) - 1)) != 0) \
            ? **(t**)((ap += sizeof(__int64)) - sizeof(__int64))             \
            :  *(t* )((ap += sizeof(__int64)) - sizeof(__int64)))
...

条件 (sizeof(t) & (sizeof(t) - 1)) != 0 的含义是检查参数类型 t 的大小是否是2的幂次方。
补充,
在计算机中,2的幂次方的二进制表示形式是只有一个位为1,其他位都为0。因此,如果 sizeof(t) 是2的幂次方,那么 sizeof(t) - 1 的二进制表示形式将会是所有位都为1。而按位与运算符 & 将两个操作数的对应位进行逻辑与操作,只有在对应的位都为1时,结果位才为1。
因此,如果 (sizeof(t) & (sizeof(t) - 1)) 的结果等于0,那么说明 sizeof(t) 是2的幂次方,即参数类型 t 的大小是2的幂次方。
回到源码分析上,
(sizeof(t) > sizeof(__int64) || (sizeof(t) & (sizeof(t) - 1)) != 0
不等于 0,即表示为 true。即条件1或条件2,其中之一为true即可。
也就是说,如果参数类型的大小大于 sizeof(__int64) 或者 参数类型的大小不是2的幂次方值,也即不是1、2、4、8 等2的幂次方值,不是char、short、int、long 等,如一些单字节对齐的结构体。此时执行分支1。
否则,也就是,参数类型的大小 <= sizeof(__int64) 且大小是2的幂次方值,也即是 char、short、int、long 等单数据类型。将执行第2个条件分支。

对于第一个分支,其使用双指针将 ap 指针转换为指向指针类型 t** 的指针,然后通过两次解引用操作 ** 来获取指针指向的值,即参数的值,其应该是主要针对结构体类型的。对于非结构体类型,包含指针类型,统一的以 sizeof(__int64) 字节长度来处理。结构体类型的参数可能具有不同的大小和内存布局,因此需要特殊处理,相当于是把一个不确定的长度转换成了一个确定的长度,否则没有信息来执行地址的偏移。通过对指针进行一次解引用操作 ,我们得到指向 t* 类型的指针,以此可访问结构体参数的地址。再次对指针进行解引用操作 ,可以获取指针指向的值,即结构体类型的参数。

其他特别需要注意的是,
实践和理论上都可表明,va_arg 并不能按照预期识别出实参列表的结尾。这也是《语言基础 /C&C++ 可变参函数设计与实践,必须要指定可变参数的个数?YES》 重点要表达的。

变参数类型: sizeof(结构体) > sizeof(_int64)

基于对 va_arg 宏函数的理解,结构体对象参数理论上是可以被变参函数解析过程 va_arg 接受的,至少在 x64 平台下是有可能的。先在 QtCreator + MSVC2015_64 环境下测试,

//测试1
typedef struct tagParamA {
    short iSeg1;
    short iSeg2;
} TParamA;

//测试2
typedef struct tagParamB {
    short iSeg1;
    short iSeg2;
    int   a;
} TParamB;

//结构体尺寸必须要大于U64
typedef struct tagParamC {
    short iSeg1;
    short iSeg2;
    int   a;
    short b;
} TParamC;

//
void TestStructParamaB(int param_count, /* param is TParamA/B */...) {
    va_list argptr;
    va_start(argptr, param_count);
    for (int i = 0; i < param_count; i++) {
        TParamB value = va_arg(argptr, TParamB);  //note TParamB
        qDebug("test_struct_param%d:(%d,%d,%d,%d) ", i+1, value.iSeg1, value.iSeg2, value.a);
    }
    va_end(argptr);
}

//
void TestStructParamaC(int param_count, /* param is TParamC */...) {
    va_list argptr;
    va_start(argptr, param_count);
    for (int i = 0; i < param_count; i++) {
        TParamC value = va_arg(argptr, TParamC);  //note 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()  {
	//sizeof(TParamA)---4, sizeof(TParamB)---8, sizeof(TParamC) ---12
	//
	//TParamB tArray[10] = {{100, 101, 102}, {200, 201, 202}, 0};
	//TestStructParamaB(2, &tArray[0], &tArray[1]);
	//
    TParamC tArray[10] = {{100, 101, 102, 103}, {200, 201, 202, 203}, 0};
    TestStructParamaC(2, &tArray[0], &tArray[1]);
    /*system("pause");*/ return 0;
}

Test B
在这里插入图片描述
Test C
在这里插入图片描述
如上在X64平台下,经过测试 TParamA 和 TParamB 对象直接作为变参函数的解析类型和实参类型时,变参函数不能输出正确的结果,只有当结构体的尺寸大小大于 sizeof(__int64) 字节后,相关函数实现和运行过程才是正确的。
将同样的程序配置成x86平台,编译运行(VS 2017 x86配置),
在这里插入图片描述
如上,在Windows X86平台配置下,结构体对象是不能直接被 va_arg 解析的。

以 TParamA/B为例,其实现函数中,执行的是 __crt_va_arg 宏定义的第二个分支,代入,

*(TParamA *)((ap += 8) - 8))  //这个错误很好理解
*(TParamB *)((ap += 8) - 8))  //这个我没有理解透彻

TParamA的情况下,会导致指针偏移错误,进而解析混乱,这并不难理解。但是 TParamB 的测试结果,我并没有理解清楚,不明白它为啥会输出错误结果。

//VS2017 X64 
void TestStructParamaC(int param_count, /* param is TParamC */...)
{
    printf("test_struct_fixparam:(Addr:0x%.16llx)\r\n ", (int64_t)(&param_count));
    va_list argptr;
    va_start(argptr, param_count);
    for (int i = 0; i < param_count; i++) {
        TParamC *pValue = &va_arg(argptr, TParamC);  //note TParamC
        printf("test_struct_param%d(Addr:0x%.16llx, &Addr:0x%.16llx) \r\n", i + 1, (int64_t)pValue, (int64_t)(&pValue));
        printf("test_struct_param%d:(%d,%d,%d,%d) \r\n", i + 1, pValue->iSeg1, pValue->iSeg2, pValue->a, pValue->b);
    }
    va_end(argptr);
}

在这里插入图片描述
通过如上结果,可分析得到,当使用结构体对象直接做可变参数时,其初始地址并不是具名参数偏移sizeof(_int64),这应该与 __va_start(va_list* ap, …) 函数的实际行为有关。我没有多余时间继续研究下去,不过至此,我们最初的目的都已经达到,就这样吧。

va_end 宏

    #define __crt_va_end(ap)        ((void)(ap = (va_list)0))

略。

小结

先这样吧!不总结了。有问题请留言。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1663057.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Linux 第二十七章

&#x1f436;博主主页&#xff1a;ᰔᩚ. 一怀明月ꦿ ❤️‍&#x1f525;专栏系列&#xff1a;线性代数&#xff0c;C初学者入门训练&#xff0c;题解C&#xff0c;C的使用文章&#xff0c;「初学」C&#xff0c;linux &#x1f525;座右铭&#xff1a;“不要等到什么都没有了…

答辩PPT不会做?试试这些AI工具,一键生成

在我原本的认知里面&#xff0c;答辩PPT是要包含论文各个章节的&#xff0c;在答辩时需要方方面面都讲到的&#xff0c;什么摘要、文献综述、实证分析、研究结果样样不落。但是&#xff0c;这大错特错&#xff01; 答辩PPT环节时长一般不超过5分钟&#xff0c;老师想要的答辩P…

【JavaSE】/*初识Java*/

目录 一、了解 Java 语言 二、Java 语言的重要性 2.1 使用程度 2.2 工作领域 三、Java 语言的特性 四、Java 的基础语法 五、可能遇到的错误 六、第一个 java 程序代码解析 七、Java 注释 八、Java 标识符 九、Java 关键字 一、了解 Java 语言 Java 是由 Sun Micr…

2023年建筑特种作业人员安全生产知识试题

100分题库提供安全员考试试题、建筑安全员考试预测题、建筑安全员ABC考试真题、安全员证考试题库等&#xff0c;提供在线做题刷题&#xff0c;在线模拟考试&#xff0c;助你考试轻松过关。 判断题&#xff08;1-20&#xff09; 1.《建筑工程安全生产管理条例》是我国第一部关于…

mac苹果电脑卡顿反应慢如何解决?2024最新免费方法教程

苹果电脑以其稳定的性能、出色的设计和高效的操作系统&#xff0c;赢得了广大用户的喜爱。然而&#xff0c;随着时间的推移&#xff0c;一些用户会发现自己的苹果电脑开始出现卡顿、反应慢等问题。这不仅影响使用体验&#xff0c;还会影响工作效率。那么&#xff0c;面对这些问…

2024年旅游行业薪酬报告

来源&#xff1a;薪智 近期历史回顾&#xff1a; 2024年中国健康家电消费洞察及趋势研究报告.pdf 2024巴菲特股东大会5万字完整版.pdf 2024年全国大学生新媒体直播大赛.pdf 2024北京市高级别自动驾驶示范区数据安全治理白皮书.pdf 2024年第一季度开发者健康调查报告.pdf 2024年…

计算机毕业设计 | vue+springboot线上考试 在线测试系统(附源码)

1&#xff0c;项目介绍 项目背景 在线考试借助于网络来进行&#xff0c;传统考试所必备的考场和监考对于在线考试来说并不是必要项目&#xff0c;因此可以有效减少组织考试做需要的成本以及设施。同时&#xff0c;由于在线考试系统本身具有智能阅卷的功能&#xff0c;也大大减…

记录文件上传exists方法遇到的坑

1、问题 判断文件是否存在使用exist方法&#xff0c;官方的注释是这样的 百度翻译结果&#xff1a;true&#xff0c;当且仅当由该抽象路径名表示的文件或目录存在时&#xff1b;否则为false 2、实际返回 注意&#xff1a;实际上exsits方法的返回值与其官方注释的返回结果是相…

NSSCTF中的web学习(md5())

目录 MD5的学习 [BJDCTF 2020]easy_md5 [LitCTF 2023]Follow me and hack me [LitCTF 2023]Ping [SWPUCTF 2021 新生赛]easyupload3.0 [NSSCTF 2022 Spring Recruit]babyphp MD5的学习 md5()函数&#xff1a; md5($a)&#xff1a;返回a字符串的散列值 md5($a,TRUE)&…

一套全新的PACS医学存档影像系统源码 RIS和PACS系统分别在哪些方面发挥作用

RIS和PACS系统分别在哪些方面发挥作用 RIS系统的作用 放射信息系统&#xff08;RIS&#xff09;主要用于管理和调度患者的放射检查流程。它的主要功能包括患者管理、检查预约、报告生成等。RIS系统通常作为独立系统运行&#xff0c;侧重于临床流程管理&#xff0c;并优化放射…

关于docker network网络

首先,我们来看看Docker默认的网络模式,即docker0网桥。 每当你安装Docker时,它会创建一个名为docker0的虚拟网桥,并设置一个IP地址范围供它进行端口映射等工作。所有Docker容器在创建时,都会自动连接到这个docker0网桥,并分配一个虚拟IP地址。这样,容器与主机之间,以及容器与容…

3d里如何做螺旋状模型?---模大狮模型网

螺旋状模型在3D设计中常常被运用&#xff0c;不仅可以用于创造独特的装饰品和艺术品&#xff0c;还可以用于建筑设计、工程模拟等领域。然而&#xff0c;对于初学者而言&#xff0c;如何在3D软件中创建螺旋状模型可能是一个挑战。在本文中&#xff0c;我们将分享几种简单而有效…

Qt——信号 和 槽

目录 概述 信号和槽的使用 自定义信号和槽 带参数的信号和槽 概述 在Linux系统中&#xff0c;我们也介绍了信号的产生、信号的检测以及信号的处理机制&#xff0c;它就是系统内部的通知机制&#xff0c;也可以是一种进程间通信的方式。在系统中有很多信号&#xff0c;我们可…

镜舟科技亮相2024中国移动算力网络大会、Qcon、DTC等多项活动

在刚刚过去的 4 月份&#xff0c;镜舟科技受邀参与一系列技术交流活动&#xff0c;与移动云、金科创新社、infoQ、墨天轮、开科唯识等媒体及合作伙伴展开积极交流&#xff0c;并分享其在数据技术、金融等垂直行业领域的创新实践&#xff0c;从产业侧、业务侧、技术侧洞察需求、…

React:Router-3.路由懒加载

在 React&#xff1a;Router-1.BrowserRouter组件式 和 React&#xff1a;Router-2. createBrowserRouter函数式 两篇文章中我们已经完成了路由的创建。但是这种方式&#xff0c;会全量加载路由资源&#xff0c;如果项目较复杂&#xff0c;会产生性能问题。 为了优化项目性能&…

螺栓扭矩设计多大?原来如此简单!——SunTorque智能扭矩系统

智能扭矩系统-智能拧紧系统-扭矩自动控制系统-SunTorque 螺栓扭矩设计&#xff0c;看似复杂&#xff0c;实则遵循一定的科学原理和实践经验。本文将深入浅出地探讨螺栓扭矩设计的原理和方法&#xff0c;帮助读者轻松掌握这一技能。 首先&#xff0c;我们需要了解螺栓扭矩的基…

Simulink从0搭建模型04-练习_一阶低通滤波器的实现

Simulink从0搭建模型04-练习_一阶低通滤波器的实现 1. 前言1.1. 参考1.2. 好习惯&#xff08;初始设置&#xff09; 2. 一阶低通滤波的实现2.1. 根据公式在Simulink中搭模型2.1.1. 一阶低通滤波公式2.1.2. 搭建一阶低通滤波 2.2. 把模型装进子系统的2种方式2.2.1. 方式12.2.2. …

【联通支付注册/登录安全分析报告】

联通支付注册/登录安全分析报告 前言 由于网站注册入口容易被黑客攻击&#xff0c;存在如下安全问题&#xff1a; 暴力破解密码&#xff0c;造成用户信息泄露短信盗刷的安全问题&#xff0c;影响业务及导致用户投诉带来经济损失&#xff0c;尤其是后付费客户&#xff0c;风险巨…

26、Qt使用QFontDatabase类加载ttf文件更改图标颜色

一、图标下载 iconfont-阿里巴巴矢量图标库 点击上面的链接&#xff0c;在打开的网页中搜索自己要使用的图标&#xff0c;如&#xff1a;最大化 找到一个自己想用图标&#xff0c;选择“添加入库” 点击“购物车”图标 能看到刚才添加的图标&#xff0c;点击“下载代码”(需要…

安全工程师基础模拟试题

安全工程师基础模拟试题作为一名安全工程师&#xff0c;掌握基本的安全知识和技能是必不可少的。下面是一些基础模拟试题&#xff0c;帮助您检验自己的安全工程师能力。1.在网络安全中&#xff0c;什么是… 1安全工程师基础模拟试题 作为一名安全工程师&#xff0c;掌握基本的…