我应该通过参数还是通过返回值初始化C结构?[关闭]


33

我工作的公司正在通过初始化函数来初始化所有数据结构,如下所示:

//the structure
typedef struct{
  int a,b,c;  
} Foo;

//the initialize function
InitializeFoo(Foo* const foo){
   foo->a = x; //derived here based on other data
   foo->b = y; //derived here based on other data
   foo->c = z; //derived here based on other data
}

//initializing the structure  
Foo foo;
InitializeFoo(&foo);

我遇到了一些尝试初始化我的结构的问题:

//the structure
typedef struct{
  int a,b,c;  
} Foo;

//the initialize function
Foo ConstructFoo(int a, int b, int c){
   Foo foo;
   foo.a = a; //part of parameter input (inputs derived outside of function)
   foo.b = b; //part of parameter input (inputs derived outside of function)
   foo.c = c; //part of parameter input (inputs derived outside of function)
   return foo;
}

//initialize (or construct) the structure
Foo foo = ConstructFoo(x,y,z);

一个人比另一个人有优势吗?
我应该怎么做,我将如何证明它是一种更好的做法?


4
@gnat这是有关结构初始化的明确问题。该线程体现了一些我希望看到的适用于此特定设计决策的基本原理。
Trevor Hickey

2
@Jefffrey我们在C语言中,所以我们实际上没有方法。它也不总是直接的一组值。有时初始化一个结构是为了获取值(以某种方式),并执行一些逻辑来初始化该结构。
Trevor Hickey

1
@JacquesB我得到了“您构建的每个组件都会有所不同。该结构的其他地方使用了Initialize()函数。从技术上来讲,将其称为构造函数会产生误导。”
Trevor Hickey

1
@TrevorHickey InitializeFoo()是构造函数。与C ++构造函数的唯一区别是,this指针是显式传递的,而不是隐式传递的。InitializeFoo()和相应的C ++ 的编译代码Foo::Foo()完全相同。
cmaster

2
更好的选择:停止在C ++上使用C。Autowin。
Thomas Eding

Answers:


25

在第二种方法中,您将永远不会有半初始化的Foo。将所有结构放在一个地方似乎是一个更明智和明显的地方。

但是...第一种方法还不错,并且在许多领域中经常使用(甚至讨论了依赖注入的最佳方法,如第一种方法的属性注入或第二种方法的构造函数注入) 。都没错。

因此,如果都没有错,并且公司的其他成员使用方法1,那么您应该适合现有的代码库,而不要通过引入新的模式来搞乱它。这确实是在这里发挥作用的最重要的因素,与您的新朋友和睦相处,不要试图做事特别的雪花。


好吧,似乎很合理。我的印象是,初始化一个对象而无法看到正在初始化的输入类型会导致混乱。我试图遵循数据输入/输出的概念来生成可预测和可测试的代码。用另一种方式执行此操作似乎增加了耦合,因为我的结构的源文件需要额外的依赖性才能执行初始化。但是,您是对的,因为除非一种方法比另一种方法更受青睐,否则我不想动摇船。
特雷弗·希基

4
@TrevorHickey:实际上,我想说的是您提供的示例之间有两个主要区别-(1)一个函数传递了一个指向要初始化的结构的指针,而另一个函数则返回了一个初始化的结构;(2)一种是将初始化参数传递给函数,而另一种则是隐式的。您似乎在询问有关(2)的更多信息,但此处的答案集中在(1)。您可能希望澄清一下-我怀疑大多数人会建议使用显式参数和指针将两者混合使用:void SetupFoo(Foo *out, int a, int b, int c)
psmears

1
第一种方法将如何导致“半初始化” Foo结构?第一种方法也可以在一处执行所有初始化。(或者您是否正在考虑将初始化的Foo结构“半初始化”?)
jamesdlin

1
@jamesdlin(在创建Foo的情况下,而InitialiseFoo被意外丢失)。只是描述两阶段初始化的一种语言,而无需输入冗长的描述。我认为人们会理解经验丰富的开发人员类型。
gbjbaanb 2015年

22

两种方法都将初始化代码捆绑到一个函数调用中。到目前为止,一切都很好。

但是,第二种方法存在两个问题:

  1. 第二个对象实际上并不构造生成的对象,而是初始化堆栈上的另一个对象,然后将其复制到最终对象中。这就是为什么我认为第二种方法略逊一筹的原因。您收到的回推可能是由于此无关的副本。

    当你派生类,这是更糟糕的Derived,从Foo用第二种方法,功能:(结构被大量地用于在C对象定向)ConstructDerived()将调用ConstructFoo(),复制生成的临时Foo对象在成的超时隙Derived对象; 完成Derived对象的初始化;只是将得到的对象在返回时再次复制。添加第三层,整个事情变得完全荒谬。

  2. 使用第二种方法 ConstructClass()功能无法访问正在构造的对象的地址。这使得在构造过程中无法链接对象,因为当一个对象需要向另一个对象注册自己以进行回调时就需要这样做。


最后,并不是所有structs的课程都是完全成熟的课程。有些structs有效地只是将一堆变量捆绑在一起,而对这些变量的值没有任何内部限制。typedef struct Point { int x, y; } Point;将是一个很好的例子。对于这些,使用初始化函数似乎有些过分。在这些情况下,复合文字语法可能会很方便(C99):

Point = { .x = 7, .y = 9 };

要么

Point foo(...) {
    //other stuff

    return (Point){ .x = n, .y = n*n };
}

5
我没想到会复制是因为一个问题en.wikipedia.org/wiki/Copy_elision
特雷弗·希基

5
编译器可以删除副本不会减轻您写下副本的事实。在C语言中,编写多余的操作并依靠编译器对其进行修复被认为是错误的。这与C ++不同,在C ++中,人们可以证明编译器可以从理论上消除嵌套模板所遗留的所有残骸而感到自豪。在C语言中,人们尝试准确地编写机器应执行的代码。无论如何,关于不可访问地址的问题仍然存在,复制省略无法帮助您。
cmaster

3
任何使用编译器的人都应该期望他们编写的代码会被编译器转换。除非他们运行硬件C解释器,否则即使很容易相信,他们编写的代码也不是他们执行的代码。如果他们确实了解编译器,他们也会理解省略号,这与int x = 3;不将字符串存储x在二进制文件中没有什么不同。地址和继承点都很好;假定的省略失败是愚蠢的。
Yakk

@Yakk:历史上,C的发明是用作系统编程的一种高级汇编语言。此后的几年中,其身份变得越来越模糊。有人希望它是一种优化的应用程序语言,但是由于没有更好的形式的高级汇编语言出现,因此仍然需要C担当后者的角色。我认为编写良好的程序代码即使经过最少的优化编译也应至少表现得不错,这一点没有错,尽管要使其真正起作用,就需要C添加一些它早已缺乏的东西。
超级猫

@Yakk:例如,具有伪指令,该伪指令将告诉编译器“在随后的一段代码中,以下变量可以安全地保存在寄存器中”,以及一种块复制类型的unsigned char方法,但不允许对其进行优化。严格的别名规则将不足以同时使程序员的期望更加明确。
超级猫

1

根据结构的内容和所使用的特定编译器,这两种方法都可能更快。一种典型的模式是,满足某些条件的结构可以在寄存器中返回。对于返回其他结构类型的函数,要求调用者为某个临时结构(通常在堆栈上)分配空间,并将其地址作为“隐藏”参数传递;如果函数的返回值直接存储到其地址没有任何外部代码保存的局部变量中,则某些编译器可能能够直接传递该变量的地址。

如果结构类型满足具有函数返回的特定实现要求的要在具有函数返回的寄存器中返回(例如,不大于一个机器字,或恰好填充两个机器字),则该结构可能比传递结构的地址快,尤其是因为将变量的地址暴露于可能保留其副本的外部代码可能会排除一些有用的优化方法。如果类型不满足此类要求,则为返回结构的函数生成的代码将类似于为接受目标指针的函数生成的代码;对于使用指针的表单,调用代码可能会更快,但是该表单放置会失去一些优化机会。

太糟糕了,C没有提供一种方法来禁止函数保留传入的指针的副本(语义类似于C ++引用),因为传递这样的受限指针将获得传递的直接性能优势指向预先存在的对象的指针,但同时避免了要求编译器将变量的地址视为“暴露”的语义开销。


3
到最后一点:C ++中没有什么可以阻止函数保留作为参考传递的指针的副本,该函数可以简单地获取对象的地址。也可以免费使用引用来构造另一个包含该引用的对象(不创建裸指针)。但是,对象中的指针副本或引用可能会超出其指向的对象的寿命,从而创建了悬空的指针/引用。因此,关于参考安全性的观点是相当沉默的。
cmaster

@cmaster:在通过将指针传递到临时存储返回结构的平台上,C编译器不提供通过任何方式访问该存储地址的被调用函数。在C ++中,可以通过引用获取变量的地址,但是除非调用者保证所传递的项的生存期(在这种情况下,通常它会传递一个指针),否则可能会导致未定义行为。
2015年

1

支持“输出参数”样式的一个论据是它允许函数返回错误代码。

struct MyStruct {
    int x;
    char *y;
    // ...
};

int MyStruct_init(struct MyStruct *out) {
    // ...
    char *c = malloc(n);
    if (!c) {
        return -1;
    }
    out->y = c;
    return 0;  // Success!
}

考虑到一些相关的结构集,如果初始化对它们中的任何一个都可能失败,那么为了一致性起见,让所有它们都使用输出参数样式可能是值得的。


1
虽然可以设置errno
Deduplicator 2015年

0

我假设您的重点是通过输出参数进行初始化而不是通过返回值进行初始化,而不是构造参数的提供方式上的差异。

请注意,第一种方法可以Foo是不透明的(尽管当前使用的方式不是这样),并且通常对于长期可维护性是理想的。例如,您可以考虑一个分配不透明Foo结构而不初始化它的函数。或者,您可能需要重新初始化Foo先前使用不同值初始化的结构。


唐纳德,关心解释吗?我说的话真的不正确吗?
jamesdlin
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.