首页 > 解决方案 > 在多个函数中使用时 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++ 编译器一起使用。

标签: 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_startva_argva_endva_copy. 如果需要访问不同的参数,被调用的函数应声明一个类型为 的对象(通常ap在本子条款中指代)va_list。该对象ap可以作为参数传递给另一个函数;如果该函数va_arg使用参数调用宏ap,则调用函数中的值ap是不确定的,应在对 的va_end任何进一步引用之前传递给宏ap253)

253)允许创建指向 a 的指针va_list并将该指针传递给另一个函数,在这种情况下,原始函数可以在其他函数返回后进一步使用原始列表。


推荐阅读