Miért sok függvény, amely C-ben adja vissza a struktúrákat, valójában visszaadja a struktúrák mutatóit?

Mi az az előny, ha egy mutatót viszünk vissza egy struktúrához, szemben az egész struktúra visszaadásával a return utasítás a függvényről?

Olyan függvényekről beszélek, mint a fopen és más alacsony szintű függvények, de valószínűleg vannak olyan magasabb szintű függvények, amelyek a struktúrákra is mutatókat adnak vissza.

Úgy gondolom, hogy ez inkább dizájn választás, nem csupán programozási kérdés, és kíváncsi vagyok arra, hogy többet megtudjak a két módszer előnyeiről és hátrányairól.

Az egyik Azok az okok, amelyekről azt gondoltam, hogy előnyös lenne egy mutató visszaküldése egy struktúrához, az az, hogy könnyebben meg tudjuk mondani, ha a függvény meghiúsult az NULL mutató visszaadásával.

Feltételezem, hogy egy teljes struktúra NULL visszaadása nehezebb, vagy kevésbé hatékony. Ez érvényes ok?

Megjegyzések

  • @ JohnR.Strohm Kipróbáltam, és valóban működik. Egy függvény visszaadhat egy struktúrát …. Tehát mi az oka annak, hogy nem történt meg?
  • A C előstandardizálás nem tette lehetővé a struktúrák másolását vagy érték szerinti továbbítását. A C szabványos könyvtárban sok olyan letartóztatás található abból a korból, amelyet ma nem így írnának, pl. C11-ig tartott, amíg a teljesen rosszul megtervezett gets() függvényt eltávolították. Egyes programozók még mindig idegenkednek a struktúrák másolásától, a régi szokások keményen meghalnak.
  • FILE* gyakorlatilag átlátszatlan fogantyú. A felhasználói kódnak nem szabad érdekelnie, hogy mi a belső szerkezete.
  • A hivatkozással történő visszaküldés csak ésszerű alapértelmezés, ha szemétszállítás van.
  • @ JohnR.Strohm A nagyon idősebb ” 1989 előtt nyúlik vissza 😉 – amikor az ANSI C engedélyezte azt, amit K & RC didn ‘ t: Struktúrák másolása hozzárendelésekben, paraméter-átadás és visszatérési értékek. K & R ‘ eredeti könyv valóban kifejezetten kimondta (I ‘ m átfogalmazás): ” pontosan két dolgot tehet meg egy struktúrával, felveheti annak címét a & paranccsal, és hozzáférhet a . ”

Válasz

Ott számos gyakorlati oka annak, hogy a struct típusok helyett a függvények, például a fopen visszatérnek a mutatókhoz:

  1. El szeretné rejteni a struct típus reprezentációját a felhasználó elől;
  2. dinamikusan lefoglal egy objektumot;
  3. újra egy objektum egyetlen példányára hivatkozás több hivatkozás révén;

Olyan típusoknál, mint a FILE *, ez azért van, mert nem meg akarja tárni a felhasználó típusának ábrázolásának részleteit – a FILE * obje A ct átlátszatlan fogantyúként szolgál, és ezt a fogantyút csak átadja különféle I / O rutinoknak (és míg a FILE -et gyakran struct type, ennek nem “kell lennie”.

Tehát egy hiányos struct típust kitehet egy fejlécbe valahova:

typedef struct __some_internal_stream_implementation FILE; 

Bár nem deklarálhat hiányos típusú példányt, deklarálhat egy mutatót rá. Így létrehozhatok egy FILE * fájlt, és hozzárendelhetem hozzá a fopen, freopen stb. , de nem tudom közvetlenül manipulálni az objektumot, amelyre mutat.

Az is valószínű, hogy a fopen függvény egy FILE objektum dinamikusan, a malloc vagy hasonló használatával. Ebben az esetben van értelme egy mutató visszaadásával.

Végül lehetséges, hogy valamilyen állapotot tárol egy struct objektumban, és ezt az állapotot több különböző helyen kell elérhetővé tenni. Ha visszaküldi a struct típusú példányokat, akkor ezek a példányok különálló objektumok lesznek a memóriában egymástól, és végül kikerülnek a szinkronból. A mutató visszaadásával egyetlen objektumra mindenki ugyanarra az objektumra hivatkozik.

Megjegyzések

  • A mutató mint Az átlátszatlan típus az, hogy maga a struktúra változhat a könyvtár verziói között, és nem kell ‘ nem kell újrafordítania a hívókat.
  • @Barmar: Valóban, az ABI stabilitása a C óriási eladási pontja, és nem lenne olyan stabil átlátszatlan mutatók nélkül.

Válasz

Kétféle módon lehet” struktúrát visszaadni “. Visszaadhatja az adatok másolatát, vagy visszaküldhet egy hivatkozást (mutatót).Általában inkább egy mutató visszaküldése (és átadása általában), néhány okból kifolyólag.

Először is, egy struktúra másolása sokkal több CPU-időt vesz igénybe, mint egy mutató másolása. Ha ez valami a kódod gyakran, észrevehető teljesítménykülönbséget okozhat.

Másodszor, függetlenül attól, hogy hányszor másol egy mutatót, akkor is ugyanarra a struktúrára mutat a memóriában. Minden módosítása ugyanazon a struktúrán fog tükröződni. De ha átmásolja magát a struktúrát, majd módosít, akkor a változás csak azon a másolaton jelenik meg . Bármely kód, amely más másolatot tartalmaz, nem fogja látni a változást. Néha, nagyon ritkán, ezt akarja, de legtöbbször nem, és hibákat okozhat, ha téved.

Megjegyzések

  • A mutatóval történő visszatérés hátránya: Most ‘ neked nyomon kell követned az adott objektum tulajdonjogát és lehetséges szabadítsd fel. Ezenkívül a mutató közvetlen keresése költségesebb lehet, mint a gyors másolat. Nagyon sok változó van itt, ezért a mutatók használata nem általánosan jobb.
  • Emellett manapság a mutatók 64 bitesek a legtöbb asztali és szerver platformon. ‘ több dolgot láttam a karrierem során, amelyek 64 bitbe illeszkednének. Tehát ‘ t mindig elmondhatja, hogy a mutató másolása kevesebbe kerül, mint egy struktúra másolása.
  • Ez többnyire jó válasz , de én nem értek egyet néha, nagyon ritkán, ezt akarod, de legtöbbször ‘ nem – éppen ellenkezőleg. A mutató visszaküldése többféle nem kívánt mellékhatást és többféle csúnya módot tesz lehetővé a mutató rossz tulajdonjogának megszerzésére. Azokban az esetekben, amikor a CPU ideje nem olyan fontos, inkább a másolási változatot részesítem előnyben, ha ez opció, akkor sokkal kevesebb a hibára való hajlam.
  • Meg kell jegyezni, hogy ez valóban csak a külső API-kra vonatkozik. A belső függvényeknél az elmúlt évtizedek minden marginálisan kompetens fordítója átír egy olyan függvényt, amely egy nagy struktúrát ad vissza, hogy egy mutatót vegyen figyelembe további argumentumként, és közvetlenül az objektumot konstruálja oda. Az immutable vs mutable érvei elég gyakran megtörténtek, de úgy gondolom, mindannyian egyetértünk abban, hogy nem igaz az az állítás, miszerint az ingatlan adatstruktúrák szinte soha nem azok, amire vágysz.
  • Említhetnél fordítási tűzfalakat is mint a mutatók profi. A széles körben megosztott fejlécű, nagy programokban a hiányos típusok a funkciókkal megakadályozzák az újrafordítás szükségességét, valahányszor egy végrehajtási részlet megváltozik. A jobb összeállítási magatartás tulajdonképpen a kapszulázás mellékhatása, amelyet akkor érnek el, ha az interfészt és a megvalósítást elkülönítik. Az értékek szerinti visszatéréshez (és átadáshoz, hozzárendeléshez) a végrehajtási információkra van szükség.

Válasz

Egyéb válaszok mellett , néha érdemes egy kicsi struct értéket visszaadni. Például vissza lehet adni egy pár adatot, és néhány hibakódot (vagy sikerességi kódot).

Példaként a fopen csak egy adat (a megnyitott FILE*) és hiba esetén megadja a hibakódot a errno ál-globális változó. De talán jobb lenne egy struct tagot visszaadni két tagból: a FILE* fogantyúból és a hibakódból (amelyet akkor állítunk be, ha a fájlkezelő NULL). Történelmi okokból nem ez a helyzet (és a hibákat a errno globálison keresztül jelentik, amely ma makró).

Figyelje meg, hogy a Go nyelv szép jelöléssel rendelkezik, hogy két (vagy néhány) értéket adjon vissza.

Vegye figyelembe azt is, hogy Linux / x86-64 rendszeren a ABI és a hívási konvenciók (lásd: x86-psABI oldal) megadja, hogy egy struct két skaláris tagból (pl. egy mutató és egy egész, vagy két mutató, vagy két egész szám) két regiszter adódik vissza (és ez nagyon hatékony, és nem megy át a memóriába).

Tehát az új C-kódban egy kis C struct visszaadása olvashatóbb, szálbarátabb és hatékonyabb lehet.

Megjegyzések

  • Valójában a kis struktúrák be vannak csomagolva rdx:rax. Tehát a struct foo { int a,b; }; a rax csomagolásba kerül vissza (pl. Shift / orval), és a shift / mov paranccsal kell kicsomagolni. Itt ‘ példa a Godboltra . De az x86 a 64 bites regiszter alacsony 32 bitjét használhatja 32 bites műveletekhez anélkül, hogy törődne a magas bitekkel, ezért ‘ mindig rossz, de határozottan rosszabb, mint a használata 2 legtöbbször regisztrálja a 2 tagú struktúrákat.
  • Kapcsolódó: bugs.llvm.org/show_bug.cgi? id = 34840 std::optional<int> a logikai értéket adja vissza a rax felső felében, tehát 64 bites maszkra van szüksége állandó a test használatával. Vagy használhatja az bt parancsot. De nagyon szíves a hívó fél és a hívott fél összehasonlítása a dl használatával, amelyet a fordítóknak meg kell tenniük a ” private ” funkciókat. Szintén kapcsolódó: libstdc ++ ‘ s std::optional<T> nem ‘ t triviálisan másolható akkor is, ha T , így mindig rejtett mutatóval tér vissza: stackoverflow.com/questions/46544019/… . (libc ++ ‘ s triviálisan másolható)
  • @PeterCordes: a kapcsolódó dolgok C ++, nem C
  • Hoppá, ugye. Nos, ugyanez vonatkozik pontosan a struct { int a; _Bool b; }; re a C-ben, ha a hívó tesztelni szeretné a logikai értéket, mert a triviálisan másolható C ++ struktúrák ugyanazt az ABI-t használják, mint C.
  • Klasszikus példa div_t div()

Válasz

Jó úton jársz

Mindkét ok, ami igaz, érvényesek:

Az egyik oka annak, hogy úgy gondoltam, hogy ez előnyös lenne egy mutató visszaküldéséhez egy struktúrához, ha könnyebben meg tudod mondani, ha a függvény meghiúsult a NULL mutató visszaadásával.

FULL feltételezem, hogy NEM NAGY FULL struktúrát adunk vissza. vagy kevésbé hatékony. Ez érvényes ok?

Ha van egy textúra (például) valahol a memóriában, és a textúrára több helyen is hivatkozni szeretne. program; nem lenne bölcs dolog másolatot készíteni minden alkalommal, amikor hivatkozni szeretne rá. Ehelyett, ha egyszerűen áthelyez egy mutatót a textúra hivatkozására, akkor a program sokkal gyorsabban fog futni.

A legnagyobb ok mégis dinamikus memória-allokáció. Gyakran előfordul, hogy egy program fordításakor nem biztos abban, hogy pontosan mennyi memória szükséges bizonyos adatstruktúrákhoz. Amikor ez megtörténik, a felhasználandó memória mennyisége futás közben kerül meghatározásra. kérjen memóriát a „malloc” használatával, majd szabadítsa fel, amikor befejezte az „free” használatát.

Jó példa erre a felhasználó által megadott fájlból való olvasás. Ebben az esetben nincs ötlet, hogy mekkora lehet a fájl a program fordításakor. Csak akkor tudhatja meg, mennyi memóriára van szüksége, amikor a program ténylegesen fut.

Mind a malloc, mind az ingyenes visszatérési mutatók a memória helyeire. Tehát függvények amelyek dinamikus memória-allokációt használnak, visszatérnek a mutatók oda, ahol a memóriában létrehozták struktúrájukat.

A megjegyzésekben azt is látom, hogy kérdés merül fel, hogy visszaadhat-e egy függvényből egy struktúrát. Ezt valóban megteheti. A következőknek működniük kell:

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

Megjegyzések

  • Hogyan lehet nem tudni, mennyi memória van egy bizonyos változóra szükség lesz, ha már megadta a struktúra típust?
  • @JenniferAnderson C-nél hiányos típusok fogalma van: egy típusnév deklarálható, de még nem definiálható, tehát A “>

méret nem érhető el. Nem deklarálhatom az ilyen típusú változókat, de az adott típus mutatóit t, pl.struct incomplete* foo(void). Így deklarálhatom a függvényeket egy fejlécben, de csak a C fájlban definiálhatom a struktúrákat, ezáltal lehetővé téve a beágyazást.

  • @amon Tehát így deklarálom a függvényfejléceket (prototípusokat / aláírásokat), mielőtt deklarálnám, hogyan a munkát valójában C-ben végzik? És ugyanezt megteheti a C struktúráival és szakszervezeteivel is.
  • @JenniferAnderson a fejlécfájlokban deklarálja a függvény prototípusait (test nélküli funkciók), majd meghívhatja ezeket a függvényeket más kódban, a függvények törzsének ismerete nélkül, mert a fordítónak csak tudnia kell, hogyan kell elrendezni az argumentumokat, és hogyan kell elfogadni a visszatérési értéket. Mire összekapcsolja a programot, valójában ismernie kell a Definíció függvényt (azaz testtel), de ezt csak egyszer kell feldolgoznia. Ha nem egyszerű típust használ, akkor tudnia kell a ‘ típusú struktúrát is, de a mutatók gyakran azonos méretűek, és nem ‘ t számít a prototípus ‘ s használatának.
  • Válasz

    Valami, mint például egy FILE*, nem igazán mutat egy struktúrára az ügyfélkódot illetően, hanem egy átlátszatlan azonosító formája, amely társul néhány más entitás, mint egy fájl. Amikor egy program hívja a fopen -t, általában nem törődik a visszaküldött struktúra semmilyen tartalmával – csak az fog érdekelni, hogy hogy más olyan funkciók, mint a fread, mindent megtesznek, amit csak kell.

    Ha egy szabványos könyvtár FILE* információn belül tart pl. a fájl jelenlegi olvasási pozícióját, a fread címre küldött hívásnak képesnek kell lennie arra, hogy frissítse ezeket az információkat. Ha a fread mutatót kap a FILE -re, az ezt megkönnyíti. Ha a fread helyett egy FILE kapott, akkor nem lenne módja a FILE objektum frissítésére. a hívó birtokában van.

    Válasz

    Információ elrejtése

    Mi az az előnye, ha egy mutatót viszünk vissza egy struktúrához, szemben a teljes struktúra visszatérésével a a függvény?

    A leggyakoribb az információ elrejtése . A C mondjuk nem képes arra, hogy a struct mezőket priváttá tegye, nem beszélve a hozzáférés módszereiről.

    Tehát ha erőszakosan akarja megakadályozza a fejlesztőket abban, hogy lássák és manipulálják a pointee tartalmát, például FILE, akkor az egyetlen mód az, hogy megakadályozzák őket, hogy a mutató kezelésével kitegyék őket a definíciójának mint átlátszatlan, amelynek pointee mérete és meghatározása a külvilág számára ismeretlen. A FILE meghatározása akkor csak azok számára lesz látható, amelyek végrehajtják a definíciót igénylő műveleteket, például fopen, míg a nyilvános fejlécben csak a struktúra deklaráció lesz látható.

    Bináris kompatibilitás

    A bináris kompatibilitás megszakítása nélkül a könyvtárat használókkal, mivel a kódjuk jellegének csak azt kell tudnia, hogy mit tehetnek a struktúrával, nem pedig azt, hogy mekkora vagy milyen mezőkkel rendelkezik.

    Mint Például néhány ősi programot futtathatok ma a Windows 95 korszakában (nem mindig tökéletesen, de meglepően sok még mindig működik). Valószínű, hogy az ősi bináris fájlok egy része átlátszatlan mutatókat használt olyan struktúrákra, amelyek mérete és tartalma a Windows 95 korszakához képest megváltozott. Mégis, a programok továbbra is működnek a Windows új verzióiban, mivel nincsenek kitéve az adott struktúrák tartalmának. Ha olyan könyvtárban dolgozunk, ahol a bináris kompatibilitás fontos, akkor az, amit az ügyfél nem tesz ki, általában megengedhető, hogy ne törjön meg visszafelé kompatibilitás.

    Hatékonyság

    Egy teljes NULL struktúra visszaküldése feltételezhetően nehezebb vagy kevésbé hatékony. Ez érvényes ok?

    Általában kevésbé hatékony, ha azt feltételezzük, hogy a típus gyakorlatilag elfér és felosztható a veremben, hacsak nincsenek sokkal kevesebbek általánosított memória-elosztót használnak a kulisszák mögött, mint a malloc -t, mint egy fix méretű, nem pedig a változó méretű allokátor, a már kiosztott memóriát. Ez ebben az esetben a legtöbb esetben biztonsági kompromisszumot jelent. valószínűleg lehetővé teszi a könyvtár fejlesztőinek, hogy fenntartsák a (z) FILE -hez kapcsolódó invariantusokat (fogalmi garanciákat).

    Ez nem olyan érvényes ok, legalábbis teljesítmény szempontjából hogy a fopen visszaadjon egy mutatót, mivel az egyetlen oka annak, hogy “d return NULL a fájl megnyitásának elmulasztása. Ez egy kivételes forgatókönyv optimalizálása lenne az összes általános végrehajtási útvonal lassításáért cserébe. Bizonyos esetekben érvényes termelékenységi ok lehet arra, hogy a terveket egyszerűbbé tegyék, hogy visszatérési mutatókká tegyék őket, hogy lehetővé tegyék a NULL visszatérését valamilyen utólagos feltétel esetén.

    A fájlműveleteknél az általános költségek viszonylag meglehetősen triviálisak magukhoz a fájlműveletekhez képest, és a kézi fclose igényt egyébként sem lehet elkerülni. Tehát nem mintha megmenthetnénk az ügyfélnek az erőforrás felszabadításának (bezárásának) a problémáját azáltal, hogy kitesszük a FILE definíciót, és értékként adjuk vissza a vagy nagy teljesítménynövekedésre számíthat, tekintve a fájlműveletek relatív költségeit, hogy elkerülje a halomallokációt.

    Hotspotok és javítások

    Más esetekben azonban sok pazarló C-kódot profiloztam a régi kódbázisokban, ahol hotspotok találhatók a malloc és felesleges kötelező gyorsítótár hiányzik, mivel ezt a gyakorlatot átlátszatlan mutatókkal túl gyakran használják, és túl sok dolgot feleslegesen osztanak fel a kupacra, néha nagy hurokban.

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

    Ha a jövőben bináris kompatibilitási problémák merülnek fel, akkor elég jónak találtam, hogy csak feleslegesen foglaljak le némi extra helyet a jövőbeni célokra, például:

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

    Ez a fenntartott hely kissé pazarló, de életmentő lehet, ha a jövőben úgy találjuk, hogy még néhány adatot kell hozzáadnunk a Foo a könyvtárunkat használó bináris fájlok feltörése nélkül.

    Véleményem szerint az információ elrejtése és a bináris kompatibilitás általában az egyetlen megfelelő ok arra, hogy csak struktúrák a változó hosszúságú struktúrák mellett (amihez mindig szükség lenne, vagy legalábbis kissé kényelmetlen lenne másként használni, ha az ügyfélnek memóriát kellene lefoglalnia a veremben egy VLA fash-ban ion a VLS kiosztására). Még a nagy struktúrákat is olcsóbban visszaadni érték szerint, ha ez azt jelenti, hogy a szoftver sokkal jobban dolgozik a verem forró memóriájával. És még ha nem is olcsóbb visszatérni értékük alapján a létrehozáskor, egyszerűen megtehetnénk ezt:

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

    … a a veremből felesleges másolat lehetősége nélkül. Vagy az ügyfélnek még szabadsága van arra is, hogy Foo kiosztja a kupacon, ha valamilyen okból akarja.

    Vélemény, hozzászólás?

    Az email címet nem tesszük közzé. A kötelező mezőket * karakterrel jelöltük