c++ - 在多个函数中使用时 va_list 的意外行为
问题描述
在我的项目中,我正在制作一个 C 类层次结构,与 Ansi C 中的 Axel-Tobias Schreiner 面向对象编程相似但不同,请参见https://www.cs.rit.edu/~ats/books/ooc。 .pdf _ 例如。
我正在初始化我的对象,这与 Axel 有点不同。当我在多个初始化函数之间传递 va_list 对象时,我遇到了麻烦。假设我有一个对象二,它派生自对象一。然后在初始化两个对象时,我需要先初始化一个部分,然后再初始化两个部分。因此,我有 1 个 init 函数调用公共初始化函数,其参数初始化两个对象的一部分,而参数仅初始化扩展一个对象的两个部分。
我正在创建的库非常大,但我从中提取了一个演示相同问题的迷你项目:
CMakeLists.txt:
cmake_minimum_required(VERSION 3.5)
project (var_args
VERSION "0.0"
LANGUAGES C CXX
)
set(HEADERS "init.h")
set(SOURCES init.c program.c)
add_executable(program ${SOURCES} ${HEADERS})
if (NOT MSVC)
target_compile_options(program PRIVATE -W -Wall -Wextra -pedantic)
else()
target_compile_options(program PRIVATE /W4)
endif()
初始化.h:
typedef struct _one {
int a;
const char* msg;
} one_t;
/* this struct "derives" from struct _one */
typedef struct _two {
one_t parent;
double pi;
double e;
}two_t;
enum status_t {
STATUS_SUCCES,
STATUS_INVALID_ARGUMENT,
STATUS_ERROR
};
enum init_one_flags {
INIT_ONE_A, // 2nd argument should be of type int
INIT_ONE_MSG, // 3rd argument should be const char*
INIT_ONE_FINISHED, // takes no arugment, but rather tell init1 should be finished.
INIT_ONE_SENTINAL // Keep at the last position.
};
enum init_two_flags {
INIT_TWO_PI = INIT_ONE_SENTINAL, // 2nd arugument should be of type double.
INIT_TWO_E, // 2nd argument shoudl be double.
INIT_TWO_FINISHED, // takes no arugment, but rather tell init2 should be finished.
INIT_TWO_SENTINAL, // for init3...
};
#ifdef __cplusplus
extern "C" {
#endif
int init_two(two_t* two, ...);
//void init_one(one_t* one, ...);
#ifdef __cplusplus
}
#endif
初始化.c:
#include <stdarg.h>
#include "init.h"
static int priv_init1(one_t* one, va_list list)
{
// use default values;
int a = 0;
const char* msg = "";
int selector, ret = STATUS_SUCCES;
while ((selector = va_arg(list, int)) != INIT_ONE_FINISHED) {
switch (selector) {
case INIT_ONE_A:
a = va_arg(list, int);
break;
case INIT_ONE_MSG:
msg = va_arg(list, const char*);
break;
default:
// unknown argument
return STATUS_INVALID_ARGUMENT;
}
}
one->a = a;
one->msg = msg;
return ret;
}
static int priv_init2(two_t* two, va_list list)
{
double pi = 3.1415, e=2.7128;
int selector, ret = STATUS_SUCCES;
ret = priv_init1((one_t*)two, list);
if (ret)
return ret;
while ((selector = va_arg(list, int)) != INIT_TWO_FINISHED) {
switch (selector) {
case INIT_TWO_PI:
pi = va_arg(list, double);
break;
case INIT_TWO_E:
pi = va_arg(list, double);
break;
default:
return STATUS_INVALID_ARGUMENT;
}
}
two->pi = pi;
two->e = e;
return STATUS_SUCCES;
}
int init_two(two_t* two, ...)
{
int ret;
va_list list;
va_start(list, two);
ret = priv_init2(two, list);
va_end(list);
return ret;
}
程序.c:
#include <stdio.h>
#include "init.h"
int main() {
int ret;
two_t two;
ret = init_two(
&two,
INIT_ONE_A, 1,
INIT_ONE_MSG, "Hello, world",
INIT_ONE_FINISHED,
INIT_TWO_PI, 2 * 3.1415,
INIT_TWO_FINISHED
);
if (ret) {
fprintf(stderr, "unable to init two...\n");
printf("a=%d\tmsg=%s\tpi=%lf\te%lf\n",
two.parent.a,
two.parent.msg,
two.pi,
two.e
);
return 1;
}
else {
printf("a=%d\tmsg=%s\tpi=%lf\te%lf\n",
two.parent.a,
two.parent.msg,
two.pi,
two.e
);
return 0;
}
}
现在我遇到的问题是,这段代码的行为正是我在使用 gcc 或 clang 在调试和发布版本的 Linux 上所期望的。不幸的是,该代码在带有 Visual Studio 17 的 Windows 上失败。
所以程序的输出应该是这样的:
a=1 msg=你好,世界 pi=6.283000 e2.712800
这正是我在 Linux 上使用 gcc (5.4.0-6) 获得的
在 Windows 上我得到:
a=1 msg=你好,这里是世界 pi=jiberish e2=这里是jiberish。
并且该函数init_two
确实返回该函数在 Linux 上是成功的,而在 Windows 上是不成功的。我还可以看到 one_t 部分的两个部分已成功初始化,而 two_t 部分未成功初始化。如果有人指出问题出在哪里,我将不胜感激。va_list 在 Linux 上是按引用传递的,而 va_list 在 Windows 上是按值传递的吗?枚举值是否在 Linux 上提升为 int,而在 Windows 上作为 char 传递?最后,我将问题标记为 C 和 C++,因为我知道我演示的代码是 C,但我希望它也可以与 C++ 编译器一起使用。
解决方案
不同编译器的实现va_list
可能会有很大差异。
例如,gcc 可能将其实现为指针,因此通过值将其传递给另一个函数仍会修改底层结构,以便调用函数中的更改在调用函数中可见,而 MSVC 将其实现为结构,因此更改调用函数在调用者中不可见。
您可以通过将指向初始va_list
实例的指针传递给所有需要它的函数来解决此问题。然后内部状态将在所有功能中保持一致。
// use pointer to va_list
static int priv_init1(one_t* one, va_list *list)
{
// use default values;
int a = 0;
const char* msg = "";
int selector, ret = STATUS_SUCCES;
while ((selector = va_arg(*list, int)) != INIT_ONE_FINISHED) {
switch (selector) {
case INIT_ONE_A:
a = va_arg(*list, int);
break;
case INIT_ONE_MSG:
msg = va_arg(*list, const char*);
break;
default:
// unknown argument
return STATUS_INVALID_ARGUMENT;
}
}
one->a = a;
one->msg = msg;
return ret;
}
// use pointer to va_list
static int priv_init2(two_t* two, va_list *list)
{
double pi = 3.1415, e=2.7128;
int selector, ret = STATUS_SUCCES;
ret = priv_init1((one_t*)two, list);
if (ret)
return ret;
while ((selector = va_arg(*list, int)) != INIT_TWO_FINISHED) {
switch (selector) {
case INIT_TWO_PI:
pi = va_arg(*list, double);
break;
case INIT_TWO_E:
pi = va_arg(*list, double);
break;
default:
return STATUS_INVALID_ARGUMENT;
}
}
two->pi = pi;
two->e = e;
return STATUS_SUCCES;
}
int init_two(two_t* two, ...)
{
int ret;
va_list list;
va_start(list, two);
ret = priv_init2(two, &list); // pass pointer
va_end(list);
return ret;
}
C 标准的第 7.16p3 节明确提到了这种用法:
声明的类型是
va_list
这是一个完整的对象类型,适合保存宏
va_start
、va_arg
、va_end
和va_copy
. 如果需要访问不同的参数,被调用的函数应声明一个类型为 的对象(通常ap
在本子条款中指代)va_list
。该对象ap
可以作为参数传递给另一个函数;如果该函数va_arg
使用参数调用宏ap
,则调用函数中的值ap
是不确定的,应在对 的va_end
任何进一步引用之前传递给宏ap
。253)253)允许创建指向 a 的指针
va_list
并将该指针传递给另一个函数,在这种情况下,原始函数可以在其他函数返回后进一步使用原始列表。
推荐阅读
- reactjs - formik + yup 和验证子反应组件中的字段
- r - ggplot, sf package, 如何在地图上制作简单的饼图
- python - 如何使用 ALAudioDevice“订阅”方法调用“处理”函数
- c# - 如何从列表中删除项目
在使用多个线程枚举列表时? - python - 写入新文件时找不到文件
- react-native - Xcode 构建错误:PhaseScriptExecution Strip\ Frameworks /appName.build/Debug-iphonesimulator/PROD.build/Script-8CBD27422B744FC9C0407AA3.sh
- python - 访问我有权访问的 Amazon S3 上某些子文件夹(前缀)下的对象
- sql - 查询以获取 Schema/DB 中具有特定列 VALUE 的所有表
- javascript - 如何在 JavaScript 中编辑组合框的默认值
- css - 许多 CSS 变量对性能的影响