Varför returnerar många funktioner som returnerar strukturer i C, pekare till strukturer?

Vad är fördelen med att returnera en pekare till en struktur i motsats till att returnera hela strukturen i return uttalande om funktionen?

Jag pratar om funktioner som fopen och andra funktioner på låg nivå men förmodligen finns det funktioner på högre nivå som också ger tillbaka pekare till strukturer.

Jag tror att det här är mer ett designval snarare än bara en programmeringsfråga och jag är nyfiken på att veta mer om fördelarna och nackdelarna med de två metoderna.

En av skäl som jag trodde att det skulle vara en fördel att returnera en pekare till en struktur är att kunna berätta lättare om funktionen misslyckades genom att returnera NULL -pekaren.

Att returnera en full struktur som är NULL skulle vara svårare antar jag eller mindre effektiv. Är detta ett giltigt skäl?

Kommentarer

  • @ JohnR.Strohm Jag försökte det och det fungerar faktiskt. En funktion kan returnera en struktur …. Så vad är anledningen till att det inte görs?
  • Förstandardisering C tillät inte att strukturer kopierades eller skickades med värde. C-standardbiblioteket har många hållplatser från den tiden som inte skulle skrivas på det sättet idag, t.ex. det tog till C11 innan den helt felaktigt designade gets() -funktionen togs bort. Vissa programmerare har fortfarande en motvilja mot att kopiera strukturer, gamla vanor dör hårt.
  • FILE* är i själva verket ett ogenomskinligt handtag. Användarkoden bör inte bry sig om dess interna struktur.
  • Retur med referens är bara en rimlig standard när du har insamling av skräp.
  • @ JohnR.Strohm ” mycket äldre ” i din profil verkar gå tillbaka före 1989 😉 – när ANSI C tillät vad K & RC gjorde inte ’ t: Kopiera strukturer i tilldelningar, parameteröverföring och returvärden. K & R ’ s originalbok angav verkligen uttryckligen (jag ’ m omskrivning): ” du kan göra exakt två saker med en struktur, ta dess adress med & och få tillgång till en medlem med .. ”

Svar

Där är flera praktiska skäl till varför funktioner som fopen pekar tillbaka till istället för instanser av struct -typer:

  1. Du vill dölja representationen av struct -typen för användaren.
  2. Du tilldelar ett objekt dynamiskt;
  3. Du ”re hänvisar till en enda instans av ett objekt via flera referenser;

När det gäller typer som FILE * beror det på att du inte t vill avslöja information om typen ”representation för användaren – en FILE * obje ct fungerar som ett ogenomskinligt handtag, och du skickar bara det handtaget till olika I / O-rutiner (och medan FILE ofta implementeras som en struct typ behöver den inte ha att vara).

Så du kan exponera en ofullständig struct typ i en rubrik någonstans:

typedef struct __some_internal_stream_implementation FILE; 

Även om du inte kan deklarera en instans av ofullständig typ kan du deklarera en pekare till den. Så jag kan skapa en FILE * och tilldela den genom fopen, freopen, etc. , men jag kan inte direkt manipulera objektet det pekar på.

Det är också troligt att fopen -funktionen tilldelar en FILE objekt dynamiskt med malloc eller liknande. I så fall är det vettigt att returnera en pekare.

Slutligen är det möjligt att du lagrar någon form av tillstånd i ett struct -objekt, och du måste göra detta tillstånd tillgängligt på flera olika platser. Om du returnerade förekomster av typen struct skulle dessa förekomster vara separata objekt i minnet från varandra och så småningom komma ur synkronisering. Genom att returnera en pekare till ett enda objekt hänvisar alla till samma objekt.

Kommentarer

  • En särskild fördel med att använda pekaren som en ogenomskinlig typ är att själva strukturen kan växla mellan biblioteksversioner och att du inte behöver ’ behöver inte kompilera de som ringer igen. i> den enorma försäljningsargumentet för C, och det skulle inte vara lika stabilt utan ogenomskinliga pekare.

Svar

Det finns två sätt att” returnera en struktur. ”Du kan returnera en kopia av data, eller så kan du returnera en referens (pekare) till den.Det är i allmänhet föredraget att returnera (och skicka i allmänhet) en pekare av några anledningar.

För det första tar det mycket mer CPU-tid att kopiera en struktur än att kopiera en pekare. Om detta är något din kod gör det ofta kan det orsaka en märkbar skillnad i prestanda.

För det andra, oavsett hur många gånger du kopierar en pekare runt, pekar den fortfarande på samma struktur i minnet. Alla ändringar av den kommer att återspeglas i samma struktur. Men om du kopierar själva strukturen och sedan gör en ändring, visas ändringen bara på den kopian . Alla koder som innehåller en annan kopia kommer inte att se förändringen. Ibland, mycket sällan, är det här du vill ha, men oftast är det inte, och det kan orsaka fel om du får fel.

Kommentarer

  • Nackdelen med att återvända med pekaren: nu måste du ’ spåra ägandet av det objektet och möjliga frigör det. Pekaren kan också vara dyrare än en snabb kopia. Det finns många variabler här, så att använda pekare är inte allmänt bättre.
  • Pekare är idag 64 bitar på de flesta stationära och serverplattformar. Jag ’ har sett mer än några få strängar i min karriär som skulle passa i 64 bitar. Så du kan ’ t alltid säga att kopiering av en pekare kostar mindre än att kopiera en struktur.
  • Detta är oftast ett bra svar , men jag håller inte med om delen ibland, mycket sällan, det här är vad du vill ha, men oftast är det ’ inte – tvärtom. Att returnera en pekare tillåter flera typer av oönskade biverkningar och flera typer av otäcka sätt att få äganderätten till en pekare fel. I fall där CPU-tid inte är så viktig föredrar jag kopian, om det är ett alternativ är det mycket mindre felbenäget.
  • Det bör noteras att detta verkligen gäller endast externa API: er. För interna funktioner kommer varje jämnt kompetent kompilator under de senaste decennierna att skriva om en funktion som returnerar en stor struktur för att ta en pekare som ett ytterligare argument och konstruera objektet direkt där inne. Argumenten för oföränderlig mot föränderlig har gjorts tillräckligt ofta, men jag tror att vi alla kan vara överens om att påståendet att oföränderliga datastrukturer nästan aldrig är vad du vill inte är sant.
  • Du kan också nämna sammanställning av eldväggar som ett proffs för pekare. I stora program med allmänt delade rubriker förhindrar ofullständiga typer med funktioner behovet av att kompilera om varje gång en implementeringsdetalj ändras. Det bättre sammanställningsbeteendet är faktiskt en bieffekt av inkapslingen som uppnås när gränssnitt och implementering separeras. Att returnera (och skicka, tilldela) efter värde behöver implementeringsinformationen.

Svar

Förutom andra svar , ibland att returnera en liten struct är värdefullt. Till exempel kan man returnera ett par av en data och någon felkod (eller framgång) relaterad till den.

För att ta ett exempel returnerar fopen bara en data (den öppna FILE*) och vid fel, ger felkoden genom errno pseudo-global variabel. Men det vore kanske bättre att returnera ett struct av två medlemmar: FILE* -handtaget och felkoden (som skulle ställas in om filhandtaget är NULL). Av historiska skäl är det inte fallet (och fel rapporteras genom errno global, vilket idag är ett makro).

Observera att Go-språket har en bra notation för att returnera två (eller några) värden.

Observera också att på Linux / x86-64 ABI och anropskonventioner (se x86-psABI sida) anger att en struct av två skalära medlemmar (t.ex. en pekare och ett heltal, eller två pekare eller två heltal) returneras genom två register (och detta är mycket effektivt och går inte igenom minne).

Så i ny C-kod kan det vara mer läsbart, mer trådvänligt och mer effektivt att returnera en liten C struct.

Kommentarer

  • Egentligen små strukturer packas i rdx:rax. Så struct foo { int a,b; }; returneras packad i rax (t.ex. med shift / eller) och måste packas upp med shift / mov. Här ’ är ett exempel på Godbolt . Men x86 kan använda de låga 32 bitarna i ett 64-bitarsregister för 32-bitarsoperationer utan att bry sig om de höga bitarna, så det är ’ alltid så dåligt, men definitivt värre än att använda 2 registrerar mestadels för strängar med två medlemmar.
  • Relaterat: bugs.llvm.org/show_bug.cgi? id = 34840 std::optional<int> returnerar boolean i den övre halvan av rax, så du behöver en 64-bitars mask konstant för att testa det med test. Eller så kan du använda bt. Men det suger för den som ringer och callee jämför med att använda dl, vilka kompilatorer ska göra för ” privat ” -funktioner. Även relaterat: libstdc ++ ’ s std::optional<T> isn ’ t trivialt kopierbar även när T är , så det återgår alltid via dold pekare: stackoverflow.com/questions/46544019/… . (libc ++ ’ s kan trivialt kopieras)
  • @PeterCordes: dina relaterade saker är C ++, inte C
  • Oj, höger. Samma sak skulle gälla exakt för struct { int a; _Bool b; }; i C, om den som ringer vill testa den booleska, eftersom trivialt kopierbara C ++ -strukturer använder samma ABI som C.
  • Klassiskt exempel div_t div()

Svar

Du är på rätt väg

Båda skälen du nämnde är giltiga:

En av anledningarna till att jag trodde att det skulle vara en fördel att returnera en pekare till en struktur är att kunna säga lättare om funktionen misslyckades genom att returnera NULL-pekaren.

Att returnera en FULL struktur som är NULL skulle vara svårare antar jag eller mindre effektiv. Är detta ett giltigt skäl?

Om du har en struktur (till exempel) någonstans i minnet och vill referera till den strukturen på flera ställen i din program; det skulle inte vara klokt att göra en kopia varje gång du ville referera till den. Om du istället bara passerar en pekare för att referera till strukturen kommer ditt program att köras mycket snabbare.

Den största anledningen är är dynamisk minnestilldelning. Ofta, när ett program sammanställs, är du inte säker på exakt hur mycket minne du behöver för vissa datastrukturer. När detta händer bestäms mängden minne du behöver använda vid körning. Du kan begära minne med ”malloc” och frigör det sedan när du är klar med ”gratis”.

Ett bra exempel på detta är att läsa från en fil som har specificerats av användaren. I det här fallet har du ingen idé hur stor filen kan vara när du kompilerar programmet. Du kan bara ta reda på hur mycket minne du behöver när programmet faktiskt körs.

Både malloc och gratis returpekare till platser i minnet. Så fungerar som använder sig av dynamisk minnestilldelning återgår pekare till var de har skapat sina strukturer i minnet.

I kommentarerna ser jag också att det är en fråga om du kan returnera en struktur från en funktion. Du kan verkligen göra det här. Följande ska fungera:

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

Kommentarer

  • Hur är det möjligt att inte veta hur mycket minne en viss variabel kommer att behövas om du redan har definierat strukturtypen?
  • @JenniferAnderson C har ett koncept av ofullständiga typer: ett typnamn kan deklareras men ännu inte definierat, så det ’ storlek är inte tillgänglig. Jag kan inte deklarera variabler av den typen, men kan deklarera pekare för den typen, t.ex. struct incomplete* foo(void). På det sättet kan jag deklarera funktioner i en rubrik, men bara definiera strukturerna i en C-fil, vilket möjliggör inkapsling.
  • @amon Så här deklarerar funktionsrubriker (prototyper / signaturer) innan jag förklarar hur de arbetar man faktiskt i C? Och det är möjligt att göra samma sak mot strukturerna och fackföreningarna i C
  • @JenniferAnderson du förklarar funktion prototyper (funktioner utan kroppar) i sidhuvudfiler och kan sedan anropa dessa funktioner i annan kod, utan att känna till kroppens funktioner, eftersom kompilatorn bara behöver veta hur man ordnar argumenten och hur man accepterar returvärdet. När du länkar programmet måste du faktiskt känna till funktionen definition (dvs. med en kropp), men du behöver bara bearbeta det en gång. Om du använder en icke-enkel typ måste den också veta att typen ’ s struktur, men pekare är ofta av samma storlek och den ’ t är viktigt för en prototyp ’ s användning.

Svar

Något som en FILE* är inte riktigt en pekare till en struktur vad gäller klientkod, men är istället en form av ogenomskinlig identifierare associerad med en del annan enhet som en fil. När ett program ringer till fopen, bryr det sig vanligtvis inte om något av innehållet i den returnerade strukturen – allt det kommer att bry sig om är att andra funktioner som fread gör vad de behöver för att göra med det.

Om ett standardbibliotek håller inom en FILE* information om t.ex. den aktuella läspositionen i den filen, ett samtal till fread skulle behöva kunna uppdatera den informationen. Att ha fread ta emot en pekare till FILE gör det enkelt. Om fread istället fick en FILE skulle det inte ha något sätt att uppdatera FILE -objektet hålls av den som ringer.

Svar

Information Döljer

Vad är fördelen med att returnera en pekare till en struktur i motsats till att returnera hela strukturen i returuttalandet av funktionen?

Den vanligaste är informationsgömning . C har inte, säg, förmågan att göra fält av en struct privat, än mindre ge metoder för att komma åt dem.

Så om du vill kraftfullt förhindra utvecklare att kunna se och manipulera innehållet i en pointee, som FILE, då är det enda sättet att förhindra att de utsätts för sin definition genom att behandla pekaren som ogenomskinlig vars pointerstorlek och definition är okända för omvärlden. Definitionen av FILE kommer då bara att vara synlig för dem som genomför operationerna som kräver dess definition, som fopen, medan endast strukturdeklarationen kommer att vara synlig för den offentliga rubriken.

Binär kompatibilitet

Att dölja strukturdefinitionen kan också ge andningsrum för att bevara binär kompatibilitet i dylib-API: er. Det gör det möjligt för biblioteksimplementörer att ändra fälten i den ogenomskinliga strukturen utan att bryta binär kompatibilitet med dem som använder biblioteket, eftersom deras kod bara behöver veta vad de kan göra med strukturen, inte hur stor den är eller vilka fält den har.

Som en till exempel kan jag faktiskt köra några gamla program som byggts under Windows 95-eran idag (inte alltid perfekt, men förvånansvärt många fungerar fortfarande). Chansen är att en del av koden för de gamla binärfilerna använde ogenomskinliga pekare till strukturer vars storlek och innehåll har förändrats från Windows 95-eran. Men programmen fortsätter att fungera i nya versioner av windows eftersom de inte utsattes för innehållet i dessa strukturer. När man arbetar på ett bibliotek där binär kompatibilitet är viktigt får det som kunden inte exponeras för ändras i allmänhet utan att bryta bakåtkompatibilitet.

Effektivitet

Att returnera en fullständig struktur som är NULL skulle vara svårare antar jag eller mindre effektiv. Är detta ett giltigt skäl?

Det är vanligtvis mindre effektivt om man antar att typen praktiskt taget kan passa och tilldelas på stacken såvida det vanligtvis inte är mycket mindre generaliserad minnesallokerare används bakom kulisserna än malloc, som en allokerad minnesgrupp i fast storlek snarare än allokering med variabel storlek. Det är en säkerhetsavvägning i detta fall, de flesta sannolikt att låta biblioteksutvecklarna behålla invarianter (konceptuella garantier) relaterade till FILE.

Det är inte en sådan giltig anledning åtminstone ur prestationssynpunkt för att få fopen att returnera en pekare eftersom den enda anledningen till att ”d return NULL inte går att öppna en fil. Det skulle vara att optimera ett exceptionellt scenario i utbyte mot att sakta ner alla vanliga exekveringsvägar. Det kan finnas en giltig produktivitetsskäl i vissa fall för att göra mönster enklare för att få dem att peka på så att NULL kan returneras under vissa förhållanden.

För filoperationer är omkostnaderna relativt ganska triviala jämfört med själva filoperationerna, och det manuella behovet av fclose kan inte undvikas i alla fall. Så det är inte så att vi kan spara klienten besväret med att frigöra (stänga) resursen genom att exponera definitionen av FILE och returnera den med värde i fopen eller förvänta dig mycket av en prestationsförbättring med tanke på den relativa kostnaden för själva filoperationerna för att undvika en högtilldelning.

Hotspots och fixar

För andra fall har jag dock profilerat mycket slösaktig C-kod i äldre kodbaser med hotspots i malloc och onödiga obligatoriska cache-missar som ett resultat av att använda denna övning för ofta med ogenomskinliga pekare och fördela för många saker onödigt på högen, ibland i stora öglor.

En alternativ praxis som jag använder istället är att exponera strukturdefinitioner, även om klienten inte är tänkt att manipulera dem, genom att använda en standard för namngivning för att kommunicera att ingen annan ska röra fälten:

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

Om det finns binära kompatibilitetsproblem i framtiden, har jag tyckte att det var tillräckligt bra att bara överflödigt reservera lite extra utrymme för framtida ändamål, så:

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

Det reserverade utrymmet är lite slösaktigt men kan vara en livräddare om vi i framtiden finner att vi behöver lägga till lite mer data till Foo utan att bryta binärfilerna som använder vårt bibliotek.

Enligt min mening är information gömd och binär kompatibilitet vanligtvis den enda anständiga anledningen att bara tillåta högtilldelning av strukturer förutom strukturer med variabel längd (som alltid skulle kräva det, eller åtminstone vara lite besvärligt att använda annars om klienten var tvungen att allokera minne på stacken i ett VLA-mode för att tilldela VLS). Även stora strukturer är ofta billigare att returnera efter värde om det betyder att programvaran arbetar mycket mer med det heta minnet på stacken. Och även om de inte var billigare att returnera genom värdet på skapelsen kunde man helt enkelt göra detta:

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

… för att initialisera Foo från stacken utan möjlighet till överflödig kopia. Eller klienten har till och med friheten att tilldela Foo på högen om de vill av någon anledning.

Lämna ett svar

Din e-postadress kommer inte publiceras. Obligatoriska fält är märkta *