Wat is het voordeel van het retourneren van een pointer naar een structuur in tegenstelling tot het retourneren van de hele structuur in de return
verklaring van de functie?
Ik heb het over functies zoals fopen
en andere functies op laag niveau, maar waarschijnlijk zijn er functies op een hoger niveau die ook verwijzingen naar structuren retourneren.
Ik geloof dat dit meer een ontwerpkeuze is dan alleen een programmeervraag en ik ben benieuwd naar de voor- en nadelen van de twee methoden.
Een van de redenen waarvan ik dacht dat het een voordeel zou zijn om een pointer naar een structuur te retourneren, is om gemakkelijker te kunnen zien of de functie is mislukt door NULL
pointer te retourneren.
Het teruggeven van een volledige structuur die NULL
is, zou moeilijker zijn, denk ik, of minder efficiënt. Is dit een geldige reden?
Reacties
Antwoord
Daar zijn verschillende praktische redenen waarom functies zoals fopen
verwijzingen retourneren in plaats van instanties van struct
typen:
- U wilt de weergave van het
struct
type verbergen voor de gebruiker; - U “wijst een object dynamisch toe;
- U” bent verwijzen naar een enkele instantie van een object via meerdere referenties;
In het geval van typen als FILE *
, is dat omdat je dat niet doet wil details van de representatie van het type aan de gebruiker tonen – een FILE *
obje ct dient als een ondoorzichtige handle, en je geeft die handle gewoon door aan verschillende I / O-routines (en hoewel FILE
vaak wordt geïmplementeerd als een struct
type, het hoeft niet “t te zijn).
U kunt dus een onvolledig struct
type ergens in een koptekst weergeven:
typedef struct __some_internal_stream_implementation FILE;
Hoewel u een instantie van een onvolledig type niet kunt declareren, kunt u er wel een verwijzing naar declareren. Dus ik kan een FILE *
maken en eraan toewijzen via fopen
, freopen
, enz. , maar ik kan het object waarnaar het verwijst niet rechtstreeks manipuleren.
Het is ook waarschijnlijk dat de functie fopen
een object dynamisch, gebruikmakend van malloc
of iets dergelijks. In dat geval is het logisch om een aanwijzer terug te sturen.
Ten slotte is het mogelijk dat je een soort staat opslaat in een struct
object, en je moet die staat op verschillende plaatsen beschikbaar maken. Als u instanties van het type struct
zou retourneren, zouden die instanties afzonderlijke objecten in het geheugen van elkaar zijn en uiteindelijk niet meer synchroon lopen. Door een pointer naar een enkel object te retourneren, verwijst iedereen naar hetzelfde object.
Opmerkingen
- Een bijzonder voordeel van het gebruik van de aanwijzer als een ondoorzichtig type is dat de structuur zelf kan veranderen tussen bibliotheekversies en je hoeft de ‘ niet opnieuw te compileren.
- @Barmar: Inderdaad, ABI Stability is het enorme verkoopargument van C, en het zou niet zo stabiel zijn zonder ondoorzichtige aanwijzingen.
Answer
Er zijn twee manieren om” een structuur te retourneren “. U kunt een kopie van de gegevens retourneren, of u kunt er een verwijzing (pointer) naar retourneren.Het heeft over het algemeen de voorkeur om een pointer terug te sturen (en in het algemeen door te geven), om een aantal redenen.
Ten eerste kost het kopiëren van een structuur veel meer CPU-tijd dan het kopiëren van een pointer. Als dit iets is je code vaak doet, kan het een merkbaar prestatieverschil veroorzaken.
Ten tweede, het maakt niet uit hoe vaak je een pointer rond kopieert, het verwijst nog steeds naar dezelfde structuur in het geheugen. Alle wijzigingen eraan zullen worden weerspiegeld in dezelfde structuur. Maar als u de structuur zelf kopieert en vervolgens een wijziging aanbrengt, wordt de wijziging alleen op die kopie weergegeven. Elke code die een andere kopie bevat, zal de verandering niet zien. Soms, zeer zelden, is dit wat je wilt, maar meestal is het dat niet, en het kan bugs veroorzaken als je het fout doet.
Reacties
- Het nadeel van retourneren per pointer: nu moet je ‘ het eigendom van dat object bijhouden en mogelijk bevrijd het. Ook kan indirecte aanwijzer duurder zijn dan een snelle kopie. Er zijn hier veel variabelen, dus het gebruik van pointers is niet universeel beter.
- Ook zijn pointers tegenwoordig 64 bits op de meeste desktop- en serverplatforms. Ik ‘ heb in mijn carrière meer dan een paar structuren gezien die in 64 bits zouden passen. Dus je kunt ‘ t altijd zeggen dat het kopiëren van een pointer minder kost dan het kopiëren van een struct.
- Dit is meestal een goed antwoord , maar ik ben het oneens over het deel soms, zeer zelden, dit is wat je wilt, maar meestal is het ‘ niet – integendeel. Als u een aanwijzer retourneert, zijn er verschillende soorten ongewenste bijwerkingen en verschillende soorten vervelende manieren om het eigendom van een aanwijzer verkeerd te krijgen. In gevallen waar de CPU-tijd niet zo belangrijk is, geef ik de voorkeur aan de kopie-variant, als dat een optie is, is deze veel minder foutgevoelig.
- Opgemerkt moet worden dat dit echt is alleen van toepassing op externe APIs. Voor interne functies zal elke zelfs marginaal competente compiler van de afgelopen decennia een functie herschrijven die een grote struct retourneert om een pointer als een extra argument te nemen en het object daar direct in construeren. De argumenten van onveranderlijk versus veranderlijk zijn vaak genoeg gedaan, maar ik denk dat we het er allemaal over eens kunnen zijn dat de bewering dat onveranderlijke datastructuren bijna nooit zijn wat je wilt, niet waar is.
- Je zou ook compilatie vuurmuren kunnen noemen als een pro voor aanwijzingen. In grote programmas met breed gedeelde headers voorkomen onvolledige typen met functies de noodzaak om elke keer dat een implementatiedetail verandert, opnieuw te compileren. Het betere compilatiegedrag is eigenlijk een neveneffect van de inkapseling die wordt bereikt wanneer interface en implementatie worden gescheiden. Het retourneren (en doorgeven, toewijzen) op waarde heeft de implementatie-informatie nodig.
Antwoord
Naast andere antwoorden , soms is het de moeite waard om een kleine struct
waarde te retourneren. Men zou bijvoorbeeld een paar van één gegevens kunnen retourneren, en een foutcode (of succescode) die eraan gerelateerd is.
Om een voorbeeld te nemen, fopen
geeft alleen één gegevens (de geopende FILE*
) en in geval van een fout, geeft de foutcode door de errno
pseudo-globale variabele. Maar het zou misschien beter zijn om een struct
van twee leden te retourneren: de FILE*
handle en de foutcode (die wordt ingesteld als de bestandsingang is NULL
). Om historische redenen is dit niet het geval (en fouten worden gerapporteerd via de errno
global, die tegenwoordig een macro is).
Merk op dat de Go-taal heeft een mooie notatie om twee (of een paar) waarden te retourneren.
Merk ook op dat onder Linux / x86-64 de ABI en aanroepconventies (zie x86-psABI pagina) geeft aan dat een struct
van twee scalaire leden (bijv. een pointer en een geheel getal, of twee aanwijzers, of twee gehele getallen) wordt geretourneerd via twee registers (en dit is zeer efficiënt en gaat niet door het geheugen).
Dus in nieuwe C-code kan het retourneren van een kleine C struct
leesbaarder, thread-vriendelijker en efficiënter zijn.
Opmerkingen
- Eigenlijk worden kleine structuren verpakt in
rdx:rax
. Dusstruct foo { int a,b; };
wordt teruggestuurd verpakt inrax
(bijv. Met shift / of), en moet worden uitgepakt met shift / mov. Hier ‘ is een voorbeeld van Godbolt . Maar x86 kan de lage 32 bits van een 64-bits register gebruiken voor 32-bits bewerkingen zonder rekening te houden met de hoge bits, dus het is ‘ altijd jammer, maar zeker erger dan 2 registreert zich meestal voor 2-ledige structs. - Gerelateerd: bugs.llvm.org/show_bug.cgi? id = 34840
std::optional<int>
retourneert de booleaanse waarde in de bovenste helft vanrax
, dus je hebt een 64-bits masker nodig constante om het te testen mettest
. Of u kuntbt
gebruiken. Maar het is waardeloos voor de beller en de aangeroepen persoon in vergelijking met het gebruik vandl
, wat compilers zouden moeten doen voor ” privé ” functies. Ook gerelateerd: libstdc ++ ‘ sstd::optional<T>
isn ‘ t triviaal te kopiëren, zelfs als T is , dus het keert altijd terug via een verborgen aanwijzer: stackoverflow.com/questions/46544019/… . (libc ++ ‘ s is triviaal kopieerbaar) - @PeterCordes: je gerelateerde zaken zijn C ++, niet C
- Oeps, juist. Hetzelfde zou exact van toepassing zijn op
struct { int a; _Bool b; };
in C, als de beller de boolean wilde testen, omdat triviaal kopieerbare C ++ -structuren dezelfde ABI gebruiken als C. - Klassiek voorbeeld
div_t div()
Antwoord
Je bent op de goede weg
Beide redenen die je noemde zijn geldig:
Een van de redenen waarom ik dacht dat het een voordeel zou zijn om een pointer naar een structuur te retourneren, is om gemakkelijker te kunnen zien of de functie is mislukt door een NULL-pointer te retourneren.
Het retourneren van een FULL-structuur die NULL is, zou moeilijker zijn, denk ik of minder efficiënt. Is dit een geldige reden?
Als u een textuur (bijvoorbeeld) ergens in het geheugen heeft, en u wilt naar die textuur verwijzen op verschillende plaatsen in uw programma; het zou niet verstandig zijn om elke keer dat je ernaar zou willen verwijzen een kopie te maken. Als je in plaats daarvan gewoon een aanwijzer doorgeeft om naar de textuur te verwijzen, zal je programma veel sneller werken.
De grootste reden echter is dynamische geheugentoewijzing. Wanneer een programma wordt gecompileerd, weet u vaak niet precies hoeveel geheugen u nodig heeft voor bepaalde gegevensstructuren. Wanneer dit gebeurt, wordt de hoeveelheid geheugen die u moet gebruiken tijdens runtime bepaald. vraag geheugen aan met behulp van malloc en maak het vervolgens vrij als u klaar bent met het gebruik van gratis.
Een goed voorbeeld hiervan is het lezen van een bestand dat door de gebruiker is opgegeven. In dit geval heeft u geen idee hoe groot het bestand kan zijn wanneer u het programma compileert. U kunt alleen achterhalen hoeveel geheugen u nodig heeft wanneer het programma daadwerkelijk wordt uitgevoerd.
Zowel malloc- als free return-verwijzingen naar locaties in het geheugen. Dus functies die gebruik maken van dynamische geheugentoewijzing zullen verwijzingen teruggeven naar waar ze hun structuren in het geheugen hebben gemaakt.
Ook zie ik in de opmerkingen dat er een vraag is of je een struct van een functie kunt retourneren. U kunt dit inderdaad doen. Het volgende zou moeten werken:
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; }
Reacties
- Hoe is het mogelijk om niet te weten hoeveel geheugen heb je een bepaalde variabele nodig als je het struct-type al hebt gedefinieerd?
- @JenniferAnderson C heeft een concept van onvolledige typen: een typenaam kan worden gedeclareerd maar nog niet gedefinieerd, dus ‘ s maat is niet beschikbaar. Ik kan geen variabelen van dat type declareren, maar wel pointers naar dat type, bijv.
struct incomplete* foo(void)
. Op die manier kan ik functies in een header declareren, maar alleen de structs binnen een C-bestand definiëren, waardoor inkapseling mogelijk is. - @amon Dus dit is hoe functiekoppen (prototypes / handtekeningen) worden gedeclareerd voordat ze aangeven hoe ze werk wordt eigenlijk gedaan in C? En het is mogelijk om hetzelfde te doen met de structuren en vakbonden in C
- @JenniferAnderson je declareert functie prototypes (functies zonder body) in header-bestanden en kunt dan die functies aanroepen in andere code, zonder de body van de functies te kennen, omdat de compiler alleen hoeft te weten hoe de argumenten moeten worden gerangschikt en hoe de retourwaarde moet worden geaccepteerd. Tegen de tijd dat je het programma koppelt, moet je de functie definition (dus met een body) eigenlijk kennen, maar dat hoef je maar één keer te verwerken. Als u een niet-eenvoudig type gebruikt, moet het ook de structuur van het type ‘ weten, maar aanwijzers hebben vaak dezelfde grootte en ‘ is niet van belang voor een prototype ‘ s gebruik.
Antwoord
Iets als een FILE*
is niet echt een verwijzing naar een structuur wat betreft de clientcode, maar is in plaats daarvan een vorm van ondoorzichtige identificatie die is gekoppeld aan een of andere andere entiteit zoals een bestand. Wanneer een programma fopen
aanroept, geeft het over het algemeen niets om de inhoud van de geretourneerde structuur – het enige waar het om gaat is dat andere functies zoals fread
zullen doen wat ze ermee moeten doen.
Als een standaardbibliotheek binnen een FILE*
informatie houdt over bijv. de huidige leespositie in dat bestand, een aanroep naar fread
zou die informatie moeten kunnen bijwerken. Door fread
een pointer te laten ontvangen naar de FILE
, is dat gemakkelijk. Als fread
in plaats daarvan een FILE
zou ontvangen, zou het object FILE
niet kunnen worden bijgewerkt vastgehouden door de beller.
Antwoord
Informatie verbergen
Wat is het voordeel van het retourneren van een pointer naar een structuur in plaats van het retourneren van de hele structuur in de retourinstructie van de functie?
De meest voorkomende is het verbergen van informatie . C heeft bijvoorbeeld niet de mogelijkheid om velden van een struct
privé te maken, laat staan methoden te bieden om er toegang toe te krijgen.
Dus als je krachtig wilt te voorkomen dat ontwikkelaars de inhoud van een pointee kunnen zien en ermee kunnen knoeien, zoals FILE
, dan is de enige manier om te voorkomen dat ze worden blootgesteld aan de definitie door de aanwijzer te behandelen als ondoorzichtig waarvan de grootte en definitie van de pointe voor de buitenwereld onbekend zijn. De definitie van FILE
is dan alleen zichtbaar voor degenen die de bewerkingen uitvoeren waarvoor de definitie ervan vereist is, zoals fopen
, terwijl alleen de structuurverklaring zichtbaar zal zijn voor de openbare header.
Binaire compatibiliteit
Het verbergen van de structuurdefinitie kan ook helpen ademruimte te bieden om de binaire compatibiliteit in dylib-APIs te behouden. Het stelt bibliotheekimplementeerders in staat om de velden in de ondoorzichtige structuur te wijzigen zeker zonder de binaire compatibiliteit te verbreken met degenen die de bibliotheek gebruiken, aangezien de aard van hun code alleen hoeft te weten wat ze kunnen doen met de structuur, niet hoe groot het is of welke velden het heeft.
Als een Ik kan bijvoorbeeld een aantal oude programmas uitvoeren die vandaag tijdens het Windows 95-tijdperk zijn gebouwd (niet altijd perfect, maar verrassend genoeg werken er nog steeds veel). De kans is groot dat een deel van de code voor die oude binaire bestanden ondoorzichtige verwijzingen gebruikte naar structuren waarvan de grootte en inhoud zijn veranderd ten opzichte van het Windows 95-tijdperk. Toch blijven de programmas werken in nieuwe versies van Windows, aangezien ze niet werden blootgesteld aan de inhoud van die structuren. Wanneer je aan een bibliotheek werkt waar binaire compatibiliteit belangrijk is, mag datgene waaraan de client niet wordt blootgesteld, over het algemeen veranderen zonder te breken. achterwaartse compatibiliteit.
Efficiëntie
Het teruggeven van een volledige structuur die NULL is, zou moeilijker zijn, denk ik, of minder efficiënt. Is dit een geldige reden?
Het is doorgaans minder efficiënt, ervan uitgaande dat het type praktisch kan passen en op de stapel kan worden toegewezen, tenzij er doorgaans veel minder is gegeneraliseerde geheugentoewijzing die achter de schermen wordt gebruikt dan malloc
, zoals een allocator-poolgeheugen met een vaste grootte in plaats van een variabele grootte die al is toegewezen. Het is in dit geval een veiligheidsafweging, de meeste waarschijnlijk, om de bibliotheekontwikkelaars in staat te stellen invarianten (conceptuele garanties) te handhaven met betrekking tot FILE
.
Het is niet zon geldige reden, tenminste niet vanuit het oogpunt van prestaties om fopen
een pointer te laten retourneren, aangezien de enige reden waarom het “d terug NULL
terugkomt, is wanneer een bestand niet kan worden geopend. Dat zou het optimaliseren van een uitzonderlijk scenario zijn in ruil voor het vertragen van alle gangbare uitvoeringspaden. In sommige gevallen kan er een geldige productiviteitsreden zijn om ontwerpen eenvoudiger te maken zodat ze verwijzingen retourneren zodat NULL
kan worden geretourneerd onder een bepaalde post-conditie.
Voor bestandsbewerkingen is de overhead relatief tamelijk triviaal vergeleken met de bestandsbewerkingen zelf, en de handmatige behoefte aan fclose
kan sowieso niet worden vermeden. Het is dus niet zo dat we de klant het gedoe van het vrijmaken (sluiten) van de bron kunnen besparen door de definitie van FILE
bloot te leggen en deze op waarde te retourneren in fopen
of verwacht veel van een prestatieverbetering gezien de relatieve kosten van de bestandsbewerkingen zelf om een heap-toewijzing te vermijden.
Hotspots en fixes
Voor andere gevallen heb ik “veel verkwistende C-code geprofileerd in verouderde codebases met hotspots in malloc
en onnodige verplichte cache-missers als gevolg van het te vaak gebruiken van deze praktijk met ondoorzichtige pointers en het onnodig toewijzen van teveel dingen op de heap, soms in grote lussen.
Een alternatieve praktijk die ik in plaats daarvan gebruik, is om structuurdefinities bloot te leggen, zelfs als het niet de bedoeling is dat de klant ermee knoeit, door een standaard voor naamgeving te gebruiken om te communiceren dat niemand anders de velden mag aanraken:
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);
Als er problemen zijn met binaire compatibiliteit in de toekomst, dan heb ik het goed genoeg gevonden om gewoon overtollig wat extra ruimte te reserveren voor toekomstige doeleinden, zoals:
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; };
Die gereserveerde ruimte is een beetje verkwistend, maar kan levens redden als we in de toekomst ontdekken dat we wat meer gegevens moeten toevoegen aan Foo
zonder de binaire bestanden die onze bibliotheek gebruiken te doorbreken.
Naar mijn mening is informatie verbergen en binaire compatibiliteit meestal de enige goede reden om alleen heap-toewijzing van structuren naast structs met variabele lengte (die het altijd nodig zouden hebben, of anders een beetje lastig te gebruiken zijn als de client geheugen op de stapel moest toewijzen in een VLA-fash ion om de VLS toe te wijzen). Zelfs grote structuren zijn vaak goedkoper om te retourneren op waarde als dat betekent dat de software veel meer werkt met het hete geheugen op de stapel. En zelfs als ze “niet goedkoper waren om op waarde terug te geven bij creatie, zou je dit eenvoudig kunnen doen:
int foo_create(struct Foo* foo); ... /* In the client code: */ struct Foo foo; if (foo_create(&foo)) { foo_something(&foo); foo_destroy(&foo); }
… om van de stapel zonder de mogelijkheid van een overbodige kopie. Of de cliënt heeft zelfs de vrijheid om Foo
aan de stapel toe te wijzen als ze dat willen om wat voor reden dan ook.
gets()
-functie was verwijderd. Sommige programmeurs hebben nog steeds een afkeer van het kopiëren van structuren, oude gewoonten zijn hardnekkig.FILE*
is in feite een ondoorzichtig handvat. Het maakt gebruikerscode niet uit wat de interne structuur is.&
en open een lid met.
. ”