【问题标题】:In C, how would I choose whether to return a struct or a pointer to a struct?在 C 中,我将如何选择是返回结构还是指向结构的指针?
【发布时间】:2017-03-03 05:29:19
【问题描述】:

最近研究我的 C 肌肉并浏览了我一直在使用的许多库,这无疑让我对什么是好的实践有了一个很好的了解。我没有看到的一件事是返回结构的函数:

something_t make_something() { ... }

据我所知,这是“正确”的做法:

something_t *make_something() { ... }
void destroy_something(something_t *object) { ... }

代码 sn-p 2 中的架构比 sn-p 1 更受欢迎。所以现在我问,为什么我会直接返回一个结构,就像在 sn-p 1 中一样?在这两个选项之间进行选择时,我应该考虑哪些差异?

此外,这个选项比较如何?

void make_something(something_t *object)

【问题讨论】:

  • 我看到的重要区别是复制与不复制以及堆与堆栈。
  • 不要同时标记 C 和 C++。两种语言的同一个问题的答案非常不同。选择一个。
  • 我进行了一些编辑,试图避免这个问题被标记为基于意见。 “最佳实践”可能是一条非常模糊的界限,一种选择比另一种更好的想法可能是一种主观的想法。如果问题的编辑版本与您想知道的内容相去甚远,请告诉我。
  • 如果结构太大而无法像正常返回值一样返回(例如,在寄存器中),绝大多数 ABI 要求编译器将第一种形式转换为第二种形式,有效地传递一个make_something 函数将填充的隐藏指针。因此,从目标代码的角度来看,这两种形式基本上是相同的,唯一的区别是您希望您的 API 在客户端看起来像什么。出于这个原因,绝大多数时候我会选择表格#1,因为它要简单得多。让编译器完成传递指针的脏活。
  • @CodyGray:除了... ABI 兼容性可以通过使用不透明类型(Lundin 的回答)来实现,这需要通过指针传递。当它重要时,它真的很重要。

标签: c pointers struct malloc


【解决方案1】:

something_t 很小(阅读:复制它与复制指针一样便宜)并且您希望它默认为堆栈分配时:

something_t make_something(void);

something_t stack_thing = make_something();

something_t *heap_thing = malloc(sizeof *heap_thing);
*heap_thing = make_something();

something_t 很大或者您希望它被堆分配时:

something_t *make_something(void);

something_t *heap_thing = make_something();

不管something_t 的大小,如果你不在乎它的分配位置:

void make_something(something_t *);

something_t stack_thing;
make_something(&stack_thing);

something_t *heap_thing = malloc(sizeof *heap_thing);
make_something(heap_thing);

【讨论】:

  • 这个线程中有很多很好的答案。这个用最少的词有效地解释了最多。
  • 除了正确考虑对象的大小之外,后一个示例对于易用性和简单性也是一个很好的建议。也许你可以返回一个int 并带有一些执行错误代码。
  • 字数太少了。
  • 如果您选择了选项 2,您还需要提供free_something
  • 样式 3 在相反的情况下也很有用,在这种情况下,您实际上确实关心 确切 分配对象的位置,可能是因为它的身份很重要。
【解决方案2】:

这几乎总是与 ABI 稳定性有关。库版本之间的二进制稳定性。在它不是的情况下,它有时是关于动态大小的结构。很少是关于非常大的structs 或性能。


在堆上分配 struct 并返回它几乎与按值返回它一样快,这是非常罕见的。 struct 必须很大。

真的,速度不是技术 2 背后的原因,即按指针返回,而不是按值返回。

技术 2 用于 ABI 稳定性。如果您有一个struct,并且您的下一个版本的库向其中添加了另外 20 个字段,那么您之前版本的库的使用者是二进制兼容的,前提是他们收到了预先构造的指针。他们知道的struct 末尾之外的额外数据是他们不必知道的。

如果你将它返回到堆栈中,调用者正在为它分配内存,他们必须同意你的大小。如果您的库自上次重建后更新,您将丢弃堆栈。

技术 2 还允许您在返回的指针之前和之后隐藏额外的数据(将数据附加到结构末尾的版本是一种变体)。你可以用一个可变大小的数组来结束这个结构,或者在指针前面加上一些额外的数据,或者两者兼而有之。

如果您想在稳定的 ABI 中堆栈分配 structs,几乎所有与 struct 对话的函数都需要传递版本信息。

所以

something_t make_something(unsigned library_version) { ... }

库使用library_version 来确定预期返回的something_t 的哪个版本,并且它更改了它操作的堆栈数量。这使用标准 C 是不可能的,但是

void make_something(something_t* here) { ... }

是。在这种情况下,something_t 可能将version 字段作为其第一个元素(或大小字段),并且您需要在调用make_something 之前填充它。

采用something_t 的其他库代码然后会查询version 字段以确定他们正在使用的something_t 的版本。

【讨论】:

    【解决方案3】:

    根据经验,您永远不应该按值传递struct 对象。在实践中,只要它们小于或等于 CPU 在单条指令中可以处理的最大大小,就可以这样做。但从风格上讲,即使在那时,人们通常也会避免使用它。如果您从不按值传递结构,您可以稍后将成员添加到结构中,这不会影响性能。

    我认为void make_something(something_t *object) 是在 C 中使用结构的最常见方式。您将分配留给调用者。它很有效,但并不漂亮。

    但是,面向对象的 C 程序使用 something_t *make_something(),因为它们是使用 不透明类型 的概念构建的,这迫使您使用指针。返回的指针是指向动态内存还是其他东西取决于实现。具有不透明类型的 OO 通常是设计更复杂的 C 程序的最优雅和最好的方法之一,但遗憾的是,很少有 C 程序员知道/关心它。

    【讨论】:

    • 一个答案涉及不透明类型,这是 ABI 稳定性的基石。谢谢你,先生。
    • -1 表示“小于或等于您的 CPU 在单条指令中可以处理的最大大小”。 Malloc 需要的不仅仅是一条指令。没有确切的方法来确定结构必须有多大才能通过引用传递它(因为这取决于它的使用方式)而不是堆分配它,但实际上它比 sizeof(void*) 大很多对于大多数用例。例如,大多数游戏会按值传递 4x4 矩阵。
    • @Lundin 当您说“面向对象”时,您指的是在 C 语言建立之后流行的编程范式,还是其他什么?我并不是说不可能在 C 中使用 OOP,我只是想确保我正在考虑正确的概念。
    • @GordonGustafson 面向对象被广泛认为是正确设计程序的好方法。语言的选择并不重要。 OO 由 3 部分组成:实现和数据的私有封装(有些重要)、模块化编程,其中每个类都是自治的并且只关注其自己指定的目的(非常重要)和具有/不具有多态性的继承(有时可能有用)。即使是优秀程序员编写的古代 C 程序也使用了一种面向对象,尽管这些类当时被称为“ADT”。
    • 在 C 中实现纯私有封装的唯一方法是通过不透明类型(使用 static 数据可以实现半角版本,但这不允许类的多个实例,也不是线程安全的)。不透明类型也可以用来实现继承和多态。
    【解决方案4】:

    第一种方法的一些优点:

    • 编写的代码更少。
    • 更习惯于返回多个值的用例。
    • 适用于没有动态分配的系统。
    • 对于小型或小型物体可能更快。
    • 不会因为忘记free而导致内存泄漏。

    一些缺点:

    • 如果对象很大(例如,一兆字节),可能会导致堆栈溢出,或者如果编译器没有很好地优化它,可能会很慢。
    • 可能会让那些在 1970 年代学习 C 的人感到惊讶,因为这是不可能的,也没有跟上时代。
    • 不适用于包含指向自身一部分的指针的对象。

    【讨论】:

    • “可能会让那些在 1970 年代学习 C 的人感到惊讶,因为这是不可能的,而且还没有跟上时代的步伐。”这不是一件好事吗? :)
    • “不会因为忘记释放而导致内存泄漏” - 内存泄漏更容易制造,只需在 make 和 destroy 之间添加一些 return/break/goto 语句即可。
    【解决方案5】:

    我有点惊讶。

    不同之处在于示例 1 在堆栈上创建结构,示例 2 在堆上创建结构。在 C 或实际上是 C 的 C++ 代码中,在堆上创建大多数对象是惯用且方便的。在 C++ 中不是,大多数情况下它们都在堆栈上。原因是如果你在栈上创建一个对象,析构函数会被自动调用,如果你在堆上创建,它必须显式调用。所以确保没有内存泄漏和处理异常要容易得多。一切都在堆栈上。在 C 中,无论如何都必须显式调用析构函数,并且没有特殊析构函数的概念(当然,您有析构函数,但它们只是名称如 destroy_myobject() 的普通函数)。

    现在 C++ 中的例外是低级容器对象,例如向量、树、哈希图等。这些确实保留了堆成员,并且它们具有析构函数。现在大多数内存密集型对象由一些直接数据成员组成,这些成员给出大小、id、标签等,然后是 STL 结构中的其余信息,可能是像素数据的向量或英文单词/值对的映射。所以大部分数据实际上都在堆上,即使在 C++ 中也是如此。

    而现代 C++ 就是这样设计的

    class big
    {
        std::vector<double> observations; // thousands of observations
        int station_x;                    // a bit of data associated with them
        int station_y; 
        std::string station_name; 
    }  
    
    big retrieveobservations(int a, int b, int c)
    {
        big answer;
        //  lots of code to fill in the structure here
    
        return answer;
    }
    
    void high_level()
    {
       big myobservations = retriveobservations(1, 2, 3);
    }
    

    将编译为非常高效的代码。大型观察员不会生成不必要的临时副本。

    【讨论】:

    • 说 C++ 使用堆比 C 少是愚蠢的。首先,RAII 并不一定意味着本地类不使用堆——它只是意味着它不会轻易泄漏内存。如果它不使用堆,那么为什么需要析构函数? “3 规则”。几乎每个 C++ 标准库容器都使用堆,包括 std::string。 C 和 C++ 之间的一个主要区别在于,在 C 中,您只在需要时才使用堆,而在 C++ 中,您经常在不知不觉中使用它。这实际上是 C++ 在嵌入式系统开发中不受欢迎的主要原因之一。
    【解决方案6】:

    与其他一些语言(如 Python)不同,C 没有tuple 的概念。例如,以下在 Python 中是合法的:

    def foo():
        return 1,2
    
    x,y = foo()
    print x, y
    

    函数foo返回两个值作为一个元组,分别赋值给xy

    由于 C 没有元组的概念,因此从函数返回多个值很不方便。解决此问题的一种方法是定义一个结构来保存值,然后返回该结构,如下所示:

    typedef struct { int x, y; } stPoint;
    
    stPoint foo( void )
    {
        stPoint point = { 1, 2 };
        return point;
    }
    
    int main( void )
    {
        stPoint point = foo();
        printf( "%d %d\n", point.x, point.y );
    }
    

    这只是您可能会看到函数返回结构的一个示例。

    【讨论】:

    • 好的,这很好,但是在返回类型的所有差异之间,是否总是取决于偏好?
    • 这不能回答问题。返回多个值也可以通过返回指向具有多个成员的结构的指针来实现。这个问题理所当然地认为为什么要返回一个结构,并超越这个问题,询问哪些因素决定了在按值返回还是按指针返回它之间的选择。这根本不是我们为什么要返回一个结构。
    • @user3386109 在堆栈上返回结构的“不便”并非真实存在。正如更好的答案中所解释的,它取决于结构的大小。小的可以很容易地在没有指针参数的情况下“复制”进和出栈,而较大的,除了需要更多的堆栈空间外,可能需要更多的“工作”才能将它们弹出。
    猜你喜欢
    • 2013-08-02
    • 1970-01-01
    • 2014-09-09
    • 1970-01-01
    • 1970-01-01
    • 2022-06-15
    • 2017-06-13
    • 2018-07-28
    • 2017-08-22
    相关资源
    最近更新 更多