多次调用 Variadic Obj-C 方法导致出现无效参数

Calling variadic Obj-C method multiple times causes invalid arguments to appear

我有一个可变参数方法,我想将多个枚举值传递给:

typedef NS_ENUM(NSInteger, Enum) {
    Enum0 = 0,
    Enum1 = 1,
    Enum2 = 2,
    Enum3 = 3,
    Enum4 = 4,
    Enum5 = 5,
    Enum6 = 6,
};

@interface Variadics : NSObject
- (void)test:(Enum)enm, ...;
@end

这是实现

@implementation Variadics
- (void)test:(Enum)enm, ... {
    va_list args;
    va_start(args, enm);
    for (; enm != 0; enm = va_arg(args, Enum))
    {
        NSAssert(enm <= Enum6, @"invalid enum");
    }
    va_end(args);
}
@end

我是这样称呼它的:

Variadics* var = [[Variadics alloc] init];
[var test:Enum1, Enum2, Enum3, Enum4, Enum5, Enum6, 0];

效果很好!但是如果我把它改成这样:

Variadics* var = [[Variadics alloc] init];
[var test:Enum1, Enum2, Enum3, Enum4, Enum5, Enum6, 0];
[var test:Enum1, Enum2, Enum3, Enum4, Enum5, Enum6, 0];
[var test:Enum1, Enum2, Enum3, Enum4, Enum5, Enum6, 0];
[var test:Enum1, Enum2, Enum3, Enum4, Enum5, Enum6, 0];

突然间我想到了那个断言 enum == 4294967296。更奇怪的是,我在第一次调用 时就命中了断言 。后来的三个电话从来没有 运行.

这是怎么回事?

您在参数列表末尾的零终止作为 int(32 位)值传递给您的函数,但您的 va_arg 正在拉出一个 NSInteger(又名 long)来自堆栈的值。因此,您从堆栈中吸取了额外的 32 位,并将其全部视为一个 64 位值,其中一半是您想要的零,另一半是内存中与它相邻的任何值那个时候。

您必须这样做才能获得您想要的行为:

[var test:Enum1, Enum2, Enum3, Enum4, Enum5, Enum6, (NSInteger)0];

正如 Rob Mayoff 在下面的评论中阐明的那样,作为整数值的未转换的零文字被视为 int。适用普通 C 整数提升规则*;在可变参数列表中,因为参数没有声明类型,较小的整数类型被提升为--并作为--ints传递。因为编译器无法看到您希望在运行时看到的实际类型,所以 int 不会为您提升为 long,因此在堆栈中作为 int 结束.

通常,varargs 终止是隐式完成的(printf arg 数量和类型的样式预知)或使用像 nil 这样的常量,这将是正确的宽度,这些方法避免了这个问题.在旧的 32 位世界中,枚举值是 ints, NSIntegerint,默认整数提升是 还有int,这些区别被隐藏了。

实际上,这对您的代码设计可能意味着您可能会保留一个特殊的哨兵枚举值(不一定为零)用作列表终止符以保证它是正确的类型。或者您可能会修改以在函数调用前添加参数数量。

*参见 C18 标准 §6.3.1.1 ;-)


红利解说:为什么看到值enum == 4294967296

小数4294967296等于0x0000000100000000。第二个(下)一半是你放在那里的 32 位零。上半部分看起来只是数字 1。起初我假设这将是当前堆栈帧的其他(有效)部分,但一些调查(在 64 位 Mac 使用当前 llvm 和 Xcode 等)表明编译器通过使用 movq(移动四边形 =64 位)推送枚举参数和使用 movl(移动长 =32 位)推送最终零参数来设置对 -test:... 的调用。因为堆栈是 64 位对齐的,所以在倒数第二个和终端参数之间的堆栈内存中留下了 32 位的 "hole"(即,未被覆盖)。那是包含 0x1 的位置。所以你不是在阅读相邻的参数,或者其他有效但用于其他用途的值。您正在从某个早期函数调用的工作区中读取幻影值。在我的调试中,它似乎不是来自 Variable 的 alloc/init -- 它是先于手头的测试代码且无关的东西。