簡體   English   中英

在C中,我如何選擇是返回結構還是指向結構的指針?

[英]In C, how would I choose whether to return a struct or a pointer to a struct?

最近在我的C肌肉上工作,並查看我一直在使用它的許多圖書館,這當然讓我很好地了解了什么是好的做法。 我沒見過的一件事是一個返回結構的函數:

something_t make_something() { ... }

從我所吸收的,這是“正確”的方式:

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

代碼片段2中的體系結構比片段1更受歡迎。所以現在我問,為什么我會直接返回一個結構,就像在代碼片段1中一樣? 當我在兩種選擇中做出選擇時,我應該考慮哪些差異?

此外,該選項如何比較?

void make_something(something_t *object)

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);

這幾乎總是關於ABI的穩定性。 庫版本之間的二進制穩定性。 在不是的情況下,有時候會有動態大小的結構。 很少是關於非常大的struct或性能。


在堆上分配一個struct並返回它幾乎與返回它的值一樣快是非常罕見的。 struct必須是巨大的。

實際上,速度並不是技術2背后的原因,而是按指針返回,而不是按值返回。

技術2存在ABI穩定性。 如果您有一個struct並且您的下一個版本的庫會向其添加另外20個字段,那么如果它們是預先構造的指針,那么您之前版本的庫的使用者將是二進制兼容的 超出他們所知的struct末尾的額外數據是他們不必了解的。

如果你在堆棧上返回它,調用者正在為它分配內存,他們必須同意你的大小。 如果您的庫自上次重建后更新,您將要刪除堆棧。

技術2還允許您在返回指針之前和之后隱藏額外數據(將數據附加到結構末尾的版本是變體)。 您可以使用可變大小的數組結束結構,或者在指針前添加一些額外數據,或者兩者都添加。

如果您希望在穩定的ABI中使用堆棧分配的struct ,幾乎所有與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版本。

根據經驗,您不應該按值傳遞struct對象。 實際上,只要它們小於或等於CPU在單個指令中可以處理的最大大小,就可以這樣做。 但在風格上,人們通常會避免它。 如果您從未按值傳遞結構,則稍后可以向結構添加成員,這不會影響性能。

我認為void make_something(something_t *object)是在C中使用結構的最常用方法。您將分配留給調用者。 它很有效但不漂亮。

但是,面向對象的C程序使用something_t *make_something()因為它們是使用opaque類型的概念構建的,這會強制您使用指針。 返回的指針指向動態內存還是其他內容取決於實現。 具有opaque類型的OO通常是設計更復雜的C程序的最優雅和最好的方法之一,但遺憾的是,很少有C程序員知道/關心它。

第一種方法的一些優點:

  • 少寫代碼。
  • 更多慣用於返回多個值的用例。
  • 適用於沒有動態分配的系統。
  • 對於小型或小型物體來說可能更快。
  • 由於忘記free而沒有內存泄漏。

一些缺點:

  • 如果對象很大(例如,兆字節),可能導致堆棧溢出,或者如果編譯器沒有很好地優化它,則可能會很慢。
  • 可能會讓那些在20世紀70年代學會C的人感到驚訝,因為這是不可能的,並且沒有及時更新。
  • 不適用於包含指向其自身一部分的指針的對象。

我有些驚訝。

不同之處在於示例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);
}

將編譯為非常高效的代碼。 大型觀察成員不會產生不必要的制作副本。

與其他一些語言(如Python)不同,C沒有元組的概念。 例如,以下內容在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 );
}

這只是一個示例,您可能會看到一個函數返回一個結構。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM