4

我想子类化一个在 init 标头中具有省略号语法的对象。IE

-(void) initObjectWith:(NSString*)argument arguments:(NSString*)someArgument,...;

在这种情况下,我不确定如何传递参数数组。我怀疑它会是这样的:

- (void) initObjectWithCustomInitializer:(NSString*)argument additionalArgument:(NSString*)additionalArgument argument:(NSString*) someArgument,... {
  self = [super initObjectWith:argument arguments:someArgument,...];
  if (self) {
     //custom init code here
  }
  return self
}

这可以编译,但以 nil 结尾的“参数”数组只得到第一个参数。如何传递零终止数组的对象?

4

3 回答 3

9

声明可变参数初始化器的超类也应该声明一个接受 a 的非可变参数va_list(例如,类似于 how printfhas vprintf)。假设这种情况,超类同时具有:

-(void)init:(id)a arguments:(id)b, ...;

-(void)init:(id)a arguments:(id)b variadicArgs:(va_list)args;

你会做这样的事情:

- (void)myInit:(id)a newArg:(id)c arguments:(id)b, ...
{
    va_list v;
    va_start(v, b);

    self = [super init:a arguments:b variadicArgs:v];
    if (self) {
        //custom init code here
    }

    va_end(v);
    return self;
}

当然,您也应该确保拥有新初始化程序的非可变版本!

于 2013-01-25T22:18:40.563 回答
7

...由于 varargs 的实际实现方式以及 C 语言的限制,如果没有-taking函数来调用,则不可能将 args 传递到调用链va_list中,除非您:

  1. 使用适用于您的代码可能运行的每个平台的汇编语言
  2. 了解编译器如何实现va_list等的详细信息,或
  3. 尝试编写一个函数,以某种方式计算参数类型的每种可能组合并手动传递它们。

在这些选项中,(3) 在任何实际情况下显然是不切实际的,(2) 可能随时更改,恕不另行通知。这给我们留下了(1),用于您的代码运行的每个平台的汇编语言。

在内部,可变参数以特定于 ABI 的方式为每个架构实现。从概念上讲,...“我将传递我想要的所有参数,就好像我正在调用一个接受这些参数的函数一样,由你决定从哪里获取每个参数。” 让我们以在堆栈上传递其所有参数的架构为例,例如i386在 OS X 和 iOS 模拟器上。

给定以下函数原型和调用:

void f(const char * const format, ...);
/* ... */
f("lUf", 0L, 1ULL, 1.0);

编译器将生成以下程序集(由我编写;真正的编译器可能会产生一些不同的调用序列,但效果相同):

leal L_str, %eax
pushl %eax
movl $0x3f800000, %eax
pushl %eax
movl $0x00000000, %eax
pushl %eax
movl $0x00000001, %eax
pushl %eax
movl $0x00000000, %eax
pushl %eax
call _f

这样做的效果是将每个参数以相反的顺序压入堆栈。这是秘诀:如果这样声明,编译器也会做同样的事情f()

void f(const char * const format, long arg1, unsigned long long arg2, float arg3);

这意味着如果您的函数可以复制堆栈的参数区域并调用 vararg-taking 函数,则 args 将简单地通过。问题:没有通用的方法来计算这个参数区域有多大!On i386,在具有帧指针的函数中,该函数也从具有帧指针的函数调用,您可以作弊和复制rbp - *rbp字节,但这效率低下并且不适用于所有情况(尤其是带struct参数或返回structs 的函数)。

然后你有像armv6and这样的架构armv7,其中大多数参数在必须小心保存的x86_64寄存器中传递,其中参数在寄存器中传递并且xmm寄存器计数被传递%al,并且ppc,其中堆栈位置和寄存器映射到参数!

在不使用 a 的情况下转发参数的唯一防弹方法va_list是使用每个架构的程序集在代码中重新实现整个架构 ABI 逻辑,就像编译器一样。

这也是本质上objc_msgSend()解决的相同问题。

“所以等等!” 你现在说。“我为什么不能直接打电话objc_msgSend而不是这样乱组装?!”

回答:因为你无法告诉编译器,“不要破坏堆栈上的任何东西,也不要清除任何你看不到我使用的寄存器”。您仍然必须编写一个汇编例程,将调用转发到超类实现 -在您的子类实现中执行任何工作之前- 然后返回到您的,同时注意相同的事情objc_msgSend(),例如需要_stret_fpret变体和在至少三种架构上实现(armv7, i386, x86_64- 并且取决于您对向后和向前兼容性的需要,也可能ppc是 , ppc64,armv6armv7s)。

对于普通的可变参数,编译器在创建va_list. C 不能直接访问任何此类信息。并且objc_msgSend()是 Objective-C 编译器和运行时再次重做这一切,因此您可以编写方法调用而无需va_list一直使用。(此外,在某些架构上,能够将参数传递给已知调用列表比使用可变参数约定更有效)。

因此,不幸的是,如果不付出比可能值得付出更多的努力,您就无法做到这一点。类实现者,让这成为你的一个教训 -每当你提供一个接受可变参数的方法时,也提供一个接受 ava_list代替...的相同方法的版本。NSString是一个很好的例子,使用initWithFormat:and initWithFormat:arguments:

于 2013-01-26T05:10:53.083 回答
0

我想出了一个答案,对于任何好奇的人!长话短说,我使用了 stock 'init' 初始化器,然后通过常规 NSArray 传递参数并使用超类设置器。

- (id) initWithCustomInitializer:(NSString *)argument arguments:(NSArray*)moreArguments {

    self = [super init];
    if (self) {
        self.argument = argument;

        for (int i = 0; i < [moreArguments count]; i++) {
            [self addArgument:[moreArguments objectAtIndex:i]];
        }
    }
    return self;
}

这被称为:

NSArray *moreArguments = [NSArray arrayWithObjects:@"argument0", @"argument1", @"argument2", nil];

CustomObject *myObject = [[CustomObject alloc] initWithCustomInitializer:@"argument" arguments:moreArguments];

注意:卡尔在下面提出了一些很好的观点。这个解决方案可能不是通用的,简单的 init 并不总是像 initWith... 方法那样做额外的初始化。不过,这个解决方案对我有用。

于 2013-01-25T22:35:02.700 回答