Dlaczego wiele funkcji, które zwracają struktury w C, w rzeczywistości zwraca wskaźniki do struktur?

Jaka jest zaleta zwracania wskaźnika do struktury w przeciwieństwie do zwracania całej struktury w return instrukcja funkcji?

Mówię o funkcjach takich jak fopen i innych funkcjach niskiego poziomu, ale prawdopodobnie są też funkcje wyższego poziomu, które zwracają wskaźniki do struktur.

Uważam, że jest to bardziej kwestia projektu, a nie tylko kwestia programistyczna, i jestem ciekawy, aby dowiedzieć się więcej o zaletach i wadach tych dwóch metod.

Jedna z Powody, dla których uważałem, że byłoby korzystne zwrócenie wskaźnika do struktury, to możliwość łatwiejszego stwierdzenia, czy funkcja zawiodła, zwracając NULL wskaźnik.

Zwrócenie pełnej struktury, która jest NULL, byłoby trudniejsze lub mniej wydajne. Czy to ważny powód?

Komentarze

  • @ JohnR.Strohm Próbowałem i faktycznie działa. Funkcja może zwrócić strukturę… Więc jaki jest powód, dla którego nie jest to zrobione?
  • Przed standaryzacją C nie pozwalała na kopiowanie struktur lub przekazywanie ich przez wartość. Biblioteka standardowa C ma wiele obiektów z tamtej epoki, które dziś nie zostałyby napisane w ten sposób, np. usunięcie całkowicie źle zaprojektowanej funkcji gets() trwało do C11. Niektórzy programiści nadal mają awersję do kopiowania struktur, stare nawyki ciężko umierają.
  • FILE* jest właściwie nieprzezroczystym uchwytem. Kod użytkownika nie powinien przejmować się jego wewnętrzną strukturą.
  • Zwrot przez odniesienie jest rozsądną wartością domyślną tylko w przypadku czyszczenia pamięci.
  • @ JohnR.Strohm ” bardzo starszy ” w Twoim profilu wydaje się pochodzić sprzed 1989 r .;-) – kiedy ANSI C zezwalało na to, co K & RC didn ' t: Kopiowanie struktur w przypisaniach, przekazywanie parametrów i zwracanie wartości. K & R ' rzeczywiście została wyraźnie określona oryginalna książka (ja ' m parafrazując): ” możesz zrobić dokładnie dwie rzeczy ze strukturą, wziąć jej adres za pomocą & i uzyskać dostęp do członka za pomocą .. ”

Odpowiedź

Tam jest kilka praktycznych powodów, dla których funkcje takie jak fopen zwracają wskaźniki zamiast wystąpień typów struct:

  1. Chcesz ukryć przed użytkownikiem reprezentację typu struct;
  2. Przydzielasz obiekt dynamicznie;
  3. Ty „re odwoływanie się do pojedynczej instancji obiektu poprzez wiele odniesień;

W przypadku typów takich jak FILE *, dzieje się tak, ponieważ nie chcę ujawnić użytkownikowi szczegóły reprezentacji typu – a FILE * obje ct służy jako nieprzezroczysty uchwyt i po prostu przekazujesz ten uchwyt do różnych procedur we / wy (i chociaż FILE jest często implementowany jako struct wpisz, nie musi być).

Możesz więc wyeksponować niekompletne struct gdzieś wpisać nagłówek:

typedef struct __some_internal_stream_implementation FILE; 

Chociaż nie możesz zadeklarować instancji niekompletnego typu, możesz zadeklarować do niej wskaźnik. Mogę więc utworzyć FILE * i przypisać do niego za pomocą fopen, freopen itd. , ale nie mogę bezpośrednio manipulować obiektem, na który wskazuje.

Prawdopodobnie funkcja fopen przydziela FILE dynamicznie, używając malloc lub podobnego. W takim przypadku warto zwrócić wskaźnik.

Wreszcie, jest możliwe, że ponownie przechowujesz jakiś stan w obiekcie struct i będziesz musiał udostępnić ten stan w kilku różnych miejscach. Jeśli zwrócisz wystąpienia typu struct, będą one oddzielnymi obiektami w pamięci i ostatecznie stracą synchronizację. Zwracając wskaźnik do pojedynczego obiektu, wszyscy odnoszą się do tego samego obiektu.

Komentarze

  • Szczególna zaleta używania wskaźnika jako elementu nieprzezroczysty typ polega na tym, że sama struktura może się zmieniać między wersjami biblioteki i nie ' nie trzeba rekompilować wywołań.
  • @Barmar: Rzeczywiście, stabilność ABI to ogromna zaleta C i nie byłaby tak stabilna bez nieprzejrzystych wskaźników.

Odpowiedź

Istnieją dwa sposoby” zwracania struktury „. Możesz zwrócić kopię danych lub zwrócić do niej odniesienie (wskaźnik).Generalnie preferowane jest zwracanie (i generalnie przekazywanie) wskaźnika z kilku powodów.

Po pierwsze, kopiowanie struktury zajmuje dużo więcej czasu procesora niż kopiowanie wskaźnika. Jeśli to jest coś Twój kod robi to często, może to spowodować zauważalną różnicę w wydajności.

Po drugie, bez względu na to, ile razy skopiujesz wskaźnik, nadal wskazuje on tę samą strukturę w pamięci. Wszystkie modyfikacje zostaną odzwierciedlone w tej samej strukturze. Ale jeśli skopiujesz samą strukturę, a następnie dokonasz modyfikacji, zmiana pojawi się tylko w tej kopii . Każdy kod, który zawiera inną kopię, nie zobaczy zmiany. Czasami, bardzo rzadko, właśnie tego chcesz, ale w większości przypadków tak nie jest i może powodować błędy, jeśli zrobisz to źle.

Komentarze

  • Wada powrotu przez wskaźnik: teraz ' musisz śledzić własność tego obiektu i możliwe uwolnij to. Ponadto pośrednia zmiana wskaźnika może być bardziej kosztowna niż szybka kopia. Jest tu wiele zmiennych, więc używanie wskaźników nie jest uniwersalnie lepsze.
  • Ponadto wskaźniki są obecnie 64-bitowe na większości platform komputerowych i serwerowych. ' widziałem w swojej karierze więcej niż kilka struktur, które mieszczą się w 64 bitach. Możesz więc ' t zawsze powiedzieć, że skopiowanie wskaźnika kosztuje mniej niż skopiowanie struktury.
  • To jest w większości dobra odpowiedź , ale nie zgadzam się co do tej części, czasami, bardzo rzadko, właśnie tego chcesz, ale w większości przypadków ' nie – wręcz przeciwnie. Zwracanie wskaźnika pozwala na kilka rodzajów niepożądanych efektów ubocznych i kilka rodzajów nieprzyjemnych sposobów na błędne określenie własności wskaźnika. W przypadkach, gdy czas procesora nie jest tak ważny, wolę wariant kopiowania, jeśli jest to opcja, jest on znacznie podatny na błędy.
  • Należy zauważyć, że to naprawdę dotyczy tylko zewnętrznych interfejsów API. W przypadku funkcji wewnętrznych każdy, nawet marginalnie kompetentny kompilator ostatnich dziesięcioleci, przepisuje funkcję, która zwraca dużą strukturę, przyjmując wskaźnik jako dodatkowy argument i konstruując obiekt bezpośrednio w tym miejscu. Argumenty niezmienności vs mutowalności były wykonywane wystarczająco często, ale myślę, że wszyscy możemy się zgodzić, że twierdzenie, że niezmienne struktury danych prawie nigdy nie są tym, czego chcesz, nie jest prawdziwe.
  • Możesz również wspomnieć o ścianach ogniowych kompilacji jako profesjonalista za wskazówki. W dużych programach z szeroko udostępnianymi nagłówkami niekompletne typy z funkcjami zapobiegają konieczności ponownej kompilacji za każdym razem, gdy zmienia się szczegół implementacji. Lepsze zachowanie kompilacji jest w rzeczywistości efektem ubocznym hermetyzacji, które uzyskuje się, gdy interfejs i implementacja są oddzielone. Zwracanie (i przekazywanie, przypisywanie) według wartości wymaga informacji o implementacji.

Odpowiedź

Oprócz innych odpowiedzi , czasami warto zwrócić małą struct według wartości. Na przykład można zwrócić parę danych i związany z nimi kod błędu (lub sukcesu).

Na przykład fopen zwraca tylko jedno dane (otwarte FILE*) iw przypadku błędu podaje kod błędu za pośrednictwem errno zmienna pseudo-globalna. Ale być może lepiej byłoby zwrócić struct dwóch elementów: uchwytu FILE* i kodu błędu (który zostałby ustawiony, gdyby uchwyt pliku to NULL). Ze względów historycznych tak nie jest (a błędy są zgłaszane za pośrednictwem globalnego errno, który obecnie jest makrem).

Zwróć uwagę, że język Go ma ładną notację zwracającą dwie (lub kilka) wartości.

Zwróć również uwagę, że w systemie Linux / x86-64 ABI i konwencje wywoływania (patrz strona x86-psABI ) określa, że struct dwóch elementów skalarnych (np. wskaźnika i liczby całkowitej lub dwóch wskaźników lub dwóch liczb całkowitych) jest zwracanych przez dwa rejestry (jest to bardzo wydajne i nie przechodzi przez pamięć).

Dlatego w nowym kodzie C zwracanie małego C struct może być bardziej czytelne, przyjazne dla wątków i wydajniejsze.

Komentarze

  • Właściwie małe struktury są pakowane w rdx:rax. Tak więc struct foo { int a,b; }; jest zwracane w rax (np. Z shift / lub) i musi zostać rozpakowane za pomocą shift / mov. Tutaj ' jest przykładem na Godbolt . Ale x86 może używać niskich 32 bitów rejestru 64-bitowego do operacji 32-bitowych bez przejmowania się wysokimi bitami, więc ' jest zawsze źle, ale zdecydowanie gorsze niż używanie 2 rejestruje się przez większość czasu dla struktur składających się z 2 elementów.
  • Powiązane: bugs.llvm.org/show_bug.cgi? id = 34840 std::optional<int> zwraca wartość logiczną w górnej połowie rax, więc potrzebujesz 64-bitowej maski stała, aby przetestować ją z test. Możesz też użyć bt. Ale to jest do bani dla dzwoniącego i wywoływanego w porównaniu do używania dl, które kompilatory powinny zrobić dla ” prywatnego ” funkcje. Również powiązane: libstdc ++ ' s std::optional<T> isn ' t trywialne do skopiowania, nawet jeśli T jest , więc zawsze zwraca ukryty wskaźnik: stackoverflow.com/questions/46544019/… . (libc ++ ' s jest trywialnie kopiowalne)
  • @PeterCordes: twoje powiązane rzeczy to C ++, a nie C
  • Ups, racja. Cóż, to samo miałoby zastosowanie dokładnie do struct { int a; _Bool b; }; w C, gdyby wywołujący chciał przetestować wartość logiczną, ponieważ struktury C ++ do łatwego kopiowania używają tego samego ABI, co C.
  • Klasyczny przykład div_t div()

Odpowiedź

Jesteś na dobrej drodze

Oba powody, o których wspomniałeś, są słuszne:

Jeden z powodów, dla których myśl, że zwrócenie wskaźnika do struktury byłoby zaletą, jest łatwiejsze stwierdzenie, czy funkcja zawiodła, zwracając wskaźnik NULL.

Zwrócenie struktury FULL, która ma wartość NULL, byłoby trudniejsze. lub mniej wydajne. Czy to ważny powód?

Jeśli masz teksturę (na przykład) gdzieś w pamięci i chcesz odwołać się do tej tekstury w kilku miejscach w swoim program; nie byłoby rozsądne tworzenie kopii za każdym razem, gdy chciałbyś się do niej odwołać. Zamiast tego, jeśli po prostu przekażesz wskaźnik w celu odniesienia do tekstury, program będzie działał znacznie szybciej.

Jednak najważniejszym powodem to dynamiczna alokacja pamięci. Często podczas kompilacji programu nie masz pewności, ile dokładnie pamięci potrzebujesz dla określonych struktur danych. W takim przypadku ilość pamięci, której potrzebujesz, zostanie określona w czasie wykonywania. Możesz zażądaj pamięci za pomocą „malloc”, a następnie zwolnij ją po zakończeniu używania „free”.

Dobrym przykładem tego jest odczyt z pliku określonego przez użytkownika. W tym przypadku nie ma wyobraź sobie, jak duży może być plik podczas kompilacji programu. Możesz dowiedzieć się, ile pamięci potrzebujesz tylko wtedy, gdy program faktycznie działa.

Zarówno malloc, jak i wolne wskaźniki powrotu do lokalizacji w pamięci. Więc funkcje które wykorzystują dynamiczną alokację pamięci, zwrócą wskaźniki do miejsca, w którym utworzyły swoje struktury w pamięci.

Ponadto w komentarzach widzę, że pojawia się pytanie, czy można zwrócić strukturę z funkcji. Naprawdę możesz to zrobić. Powinno działać:

struct s1 { int integer; }; struct s1 f(struct s1 input){ struct s1 returnValue = xinput return returnValue; } int main(void){ struct s1 a = { 42 }; struct s1 b= f(a); return 0; } 

Komentarze

  • Jak można nie wiedzieć, ile pamięci pewna zmienna będzie potrzebna, jeśli masz już zdefiniowany typ struktury?
  • @JenniferAnderson C ma koncepcję niekompletnych typów: nazwa typu może być zadeklarowana, ale jeszcze nie zdefiniowana, więc ' s rozmiar jest niedostępny. Nie mogę zadeklarować zmiennych tego typu, ale mogę zadeklarować wskaźniki do tego typu, np. struct incomplete* foo(void). W ten sposób mogę zadeklarować funkcje w nagłówku, ale tylko zdefiniować struktury w pliku C, pozwalając w ten sposób na hermetyzację.
  • @amon A więc w ten sposób deklaruję nagłówki funkcji (prototypy / sygnatury) przed zadeklarowaniem ich praca jest faktycznie wykonywana w C? I można zrobić to samo ze strukturami i związkami w C
  • @JenniferAnderson deklarujesz funkcję prototypy (funkcje bez treści) w plikach nagłówkowych, a następnie możesz wywołać te funkcje w innym kodzie, bez znajomości treści funkcji, ponieważ kompilator musi tylko wiedzieć, jak ułożyć argumenty i jak zaakceptować zwracaną wartość. Zanim połączysz program, musisz znać definicję funkcji (tj. Z treścią), ale wystarczy przetworzyć ją tylko raz. Jeśli używasz nieprostego typu, musi on również znać strukturę tego typu ', ale wskaźniki są często tej samej wielkości i nie ' nie ma znaczenia dla prototypu ' użycia.

Odpowiedź

Coś w rodzaju FILE* nie jest tak naprawdę wskaźnikiem do struktury, jeśli chodzi o kod klienta, ale jest formą nieprzejrzystego identyfikatora związanego z jakimś inną jednostkę, taką jak plik. Gdy program wywołuje fopen, generalnie nie przejmuje się żadną zawartością zwracanej struktury – wszystko, o co go obchodzi, to że inne funkcje, takie jak fread, zrobią z nim wszystko, co trzeba.

Jeśli standardowa biblioteka przechowuje w FILE* informacje o np. aktualna pozycja odczytu w tym pliku, wywołanie fread wymagałoby zaktualizowania tych informacji. Dzięki fread otrzymaniu wskaźnika do FILE jest to łatwe. Jeśli zamiast tego fread otrzyma FILE, nie będzie możliwości zaktualizowania obiektu FILE trzymana przez dzwoniącego.

Odpowiedź

Ukrywanie informacji

Jaka jest zaleta zwracania wskaźnika do struktury w przeciwieństwie do zwracania całej struktury w instrukcji return w funkcja?

Najczęściej jest to ukrywanie informacji . C nie ma, powiedzmy, możliwości ustawiania pól elementu struct jako prywatnych, nie mówiąc już o udostępnianiu metod dostępu do nich.

Więc jeśli chcesz na siłę uniemożliwić programistom przeglądanie i modyfikowanie zawartości pointee, na przykład FILE, wtedy jedynym sposobem jest zapobieżenie ujawnieniu ich definicji poprzez potraktowanie wskaźnika jako nieprzezroczysty, którego rozmiar i definicja wskaźnika są nieznane światu zewnętrznemu. Definicja FILE będzie wtedy widoczna tylko dla tych, którzy wykonują operacje, które wymagają jej zdefiniowania, np. fopen, podczas gdy tylko deklaracja struktury będzie widoczna w publicznym nagłówku.

Zgodność binarna

Ukrycie definicji struktury może również pomóc w zachowaniu kompatybilności binarnej w interfejsach API dylib. Umożliwia implementatorom biblioteki zmianę pól w nieprzezroczystej strukturze ure bez naruszania zgodności binarnej z tymi, którzy używają biblioteki, ponieważ natura ich kodu musi tylko wiedzieć, co mogą zrobić ze strukturą, a nie jak duża jest ona i jakie ma pola.

Jako Na przykład, mogę obecnie uruchamiać niektóre starożytne programy zbudowane w erze Windows 95 (nie zawsze idealnie, ale zaskakująco wiele z nich nadal działa). Istnieje prawdopodobieństwo, że część kodu tych starożytnych plików binarnych wykorzystywała nieprzezroczyste wskaźniki do struktur, których rozmiar i zawartość zmieniły się od czasów Windows 95. Jednak programy nadal działają w nowych wersjach systemu Windows, ponieważ nie były narażone na zawartość tych struktur. Podczas pracy nad biblioteką, w której ważna jest zgodność binarna, to, na co klient nie jest narażony, zazwyczaj można zmienić bez przerywania kompatybilność wsteczna.

Wydajność

Zwracanie pełnej struktury o wartości NULL byłoby trudniejsze lub mniej wydajne. Czy to ważny powód?

Zwykle jest mniej efektywny, zakładając, że typ może praktycznie pasować i być przydzielony na stosie, chyba że zazwyczaj jest znacznie mniej uogólniony alokator pamięci jest używany za kulisami niż malloc, tak jak już przydzielona pula alokatora o stałym rozmiarze, a nie o zmiennej. Jest to w tym przypadku kompromis bezpieczeństwa. prawdopodobnie, aby umożliwić programistom bibliotek utrzymanie niezmienników (gwarancji koncepcyjnych) związanych z FILE.

To nie jest taki ważny powód, przynajmniej z punktu widzenia wydajności aby fopen zwróciło wskaźnik, ponieważ „d return NULL nie może otworzyć pliku. Oznaczałoby to optymalizację wyjątkowego scenariusza w zamian za spowolnienie wszystkich typowych ścieżek wykonywania. W niektórych przypadkach może istnieć ważny powód związany z produktywnością, aby uprościć projekty i uczynić z nich wskaźniki powrotu, aby umożliwić zwrócenie NULL w pewnych warunkach końcowych.

W przypadku operacji na plikach narzut jest stosunkowo dość trywialny w porównaniu z samymi operacjami na plikach, a i tak nie można uniknąć ręcznej potrzeby fclose. Więc nie jest tak, że możemy zaoszczędzić klientowi kłopotów związanych ze zwolnieniem (zamknięciem) zasobu, ujawniając definicję FILE i zwracając ją według wartości w fopen lub spodziewaj się znacznego wzrostu wydajności, biorąc pod uwagę względny koszt samych operacji na plikach, aby uniknąć alokacji sterty.

Punkty aktywne i poprawki

Jednak w innych przypadkach sprofilowałem dużo marnotrawnego kodu C w starszych bazach kodów z punktami aktywnymi w malloc i niepotrzebne, obowiązkowe pomyłki w pamięci podręcznej w wyniku zbyt częstego stosowania tej praktyki z nieprzezroczystymi wskaźnikami i niepotrzebnego przydzielania zbyt wielu rzeczy na stercie, czasami w dużych pętlach.

Alternatywną praktyką, której używam zamiast tego, jest ujawnianie definicji struktur, nawet jeśli klient nie ma ich modyfikować, używając standardu konwencji nazewnictwa, aby poinformować, że nikt inny nie powinien dotykać pól:

struct Foo { /* priv_* indicates that you shouldn"t tamper with these fields! */ int priv_internal_field; int priv_other_one; }; struct Foo foo_create(void); void foo_destroy(struct Foo* foo); void foo_something(struct Foo* foo); 

Jeśli w przyszłości pojawią się problemy ze zgodnością binarną, to stwierdziłem, że jest to wystarczająco dobre, aby po prostu zbytnio zarezerwować dodatkowe miejsce na przyszłe cele, na przykład:

struct Foo { /* priv_* indicates that you shouldn"t tamper with these fields! */ int priv_internal_field; int priv_other_one; /* reserved for possible future uses (emergency backup plan). currently just set to null. */ void* priv_reserved; }; 

Ta zarezerwowana przestrzeń jest trochę marnotrawna, ale może uratować życie, jeśli w przyszłości stwierdzimy, że musimy dodać więcej danych do Foo bez niszczenia plików binarnych, które używają naszej biblioteki.

Moim zdaniem ukrywanie informacji i zgodność binarna jest zazwyczaj jedynym przyzwoitym powodem, aby zezwolić tylko na alokację sterty struktury oprócz struktur o zmiennej długości (które zawsze tego wymagałyby lub przynajmniej byłyby trochę niewygodne w użyciu w przeciwnym razie, gdyby klient musiał przydzielić pamięć na stosie w trybie VLA jon do przydzielenia VLS). Nawet duże struktury są często tańsze do zwrócenia według wartości, jeśli oznacza to, że oprogramowanie działa znacznie bardziej z gorącą pamięcią na stosie. A nawet jeśli nie byłyby tańsze do zwrotu według wartości przy tworzeniu, można po prostu zrobić to:

int foo_create(struct Foo* foo); ... /* In the client code: */ struct Foo foo; if (foo_create(&foo)) { foo_something(&foo); foo_destroy(&foo); } 

… aby zainicjować Foo ze stosu bez możliwości wykonania zbędnej kopii. Lub klient ma nawet swobodę przydzielania Foo na stercie, jeśli z jakiegoś powodu chce.

Dodaj komentarz

Twój adres email nie zostanie opublikowany. Pola, których wypełnienie jest wymagane, są oznaczone symbolem *