Hvorfor returnerer mange funksjoner som returnerer strukturer i C, pekere til strukturer?

Hva er fordelen med å returnere en peker til en struktur i motsetning til å returnere hele strukturen i return uttalelse om funksjonen?

Jeg snakker om funksjoner som fopen og andre funksjoner på lavt nivå, men sannsynligvis er det funksjoner på høyere nivå som også viser pekere til strukturer.

Jeg tror at dette er mer et designvalg i stedet for bare et programmeringsspørsmål, og jeg er nysgjerrig på å vite mer om fordelene og ulempene med de to metodene.

En av grunner til at jeg trodde det ville være en fordel å returnere en peker til en struktur, er å kunne fortelle lettere om funksjonen mislyktes ved å returnere NULL peker.

Å returnere en full struktur som er NULL ville være vanskeligere antar jeg eller mindre effektiv. Er dette en gyldig årsak?

Kommentarer

  • @ JohnR.Strohm Jeg prøvde det, og det fungerer faktisk. En funksjon kan returnere en struktur …. Så hva er grunnen til at det ikke gjøres?
  • Pre-standardisering C tillot ikke at kopier av strukturer eller ble overført med verdi. C-standardbiblioteket har mange holdouts fra den tiden som ikke ville blitt skrevet slik i dag, f.eks. det tok til C11 før den helt feildesignede gets() -funksjonen ble fjernet. Noen programmerere har fortsatt en aversjon mot å kopiere strukturer, gamle vaner dør hardt.
  • FILE* er effektivt et ugjennomsiktig håndtak. Brukerkoden skal ikke bry seg hva den interne strukturen er.
  • Retur som referanse er bare en rimelig standard når du har søppeloppsamling.
  • @ JohnR.Strohm » veldig eldre » i profilen din ser ut til å gå tilbake før 1989 😉 – da ANSI C tillot det K & RC gjorde ikke ‘ t: Kopier strukturer i oppgaver, parameteroverføring og returverdier. K & R ‘ s opprinnelige bok uttales faktisk eksplisitt (I ‘ m omskrivning): » du kan gjøre nøyaktig to ting med en struktur, ta adressen med & og få tilgang til et medlem med .. »

Svar

Der er flere praktiske grunner til at funksjoner som fopen returnerer pekere til i stedet for forekomster av struct typer:

  1. Du vil skjule representasjonen av struct -typen for brukeren;
  2. Du tildeler et objekt dynamisk;
  3. Du «re refererer til en enkelt forekomst av et objekt via flere referanser;

Når det gjelder typer som FILE *, er det fordi du ikke ønsker å avsløre detaljer om typen «representasjon for brukeren – en FILE * obje ct fungerer som et ugjennomsiktig håndtak, og du gir det håndtaket til forskjellige I / O-rutiner (og mens FILE ofte implementeres som en struct type, trenger den ikke å være).

Så du kan avsløre en ufullstendig struct -type i en overskrift et sted:

typedef struct __some_internal_stream_implementation FILE; 

Selv om du ikke kan erklære en forekomst av en ufullstendig type, kan du erklære en peker til den. Så jeg kan opprette en FILE * og tildele den gjennom fopen, freopen osv. , men jeg kan ikke direkte manipulere objektet det peker på.

Det er også sannsynlig at fopen -funksjonen tildeler en FILE objekt dynamisk, ved hjelp av malloc eller lignende. I så fall er det fornuftig å returnere en peker.

Endelig er det mulig at du lagrer en slags tilstand i et struct -objekt, og du må gjøre den tilstanden tilgjengelig flere forskjellige steder. Hvis du returnerte forekomster av struct -typen, ville disse forekomstene være separate objekter i minnet fra hverandre og til slutt komme ut av synkronisering. Ved å returnere en peker til et enkelt objekt, refererer alle til det samme objektet.

Kommentarer

  • En spesiell fordel ved å bruke pekeren som en ugjennomsiktig type er at selve strukturen kan skifte mellom biblioteksversjoner, og at du ikke trenger ‘ du trenger ikke å kompilere innringerne på nytt.
  • @Barmar: ABI Stability er faktisk det enorme salgsargumentet til C, og det ville ikke vært like stabilt uten ugjennomsiktige pekere.

Svar

Det er to måter å» returnere en struktur. «Du kan returnere en kopi av dataene, eller du kan returnere en referanse (peker) til den.Det er generelt foretrukket å returnere (og generelt sende) en peker av et par grunner.

Først tar det å kopiere en struktur mye mer CPU-tid enn å kopiere en peker. Hvis dette er noe koden din ofte, kan den føre til en merkbar ytelsesforskjell.

For det andre, uansett hvor mange ganger du kopierer en peker rundt, peker den fremdeles på samme struktur i minnet. Alle modifikasjoner på den vil gjenspeiles i den samme strukturen. Men hvis du kopierer selve strukturen, og deretter gjør en endring, vises endringen bare på den kopien . Enhver kode som inneholder en annen kopi, vil ikke se endringen. Noen ganger, veldig sjelden, er dette det du vil ha, men mesteparten av tiden er det ikke, og det kan forårsake feil hvis du tar feil.

Kommentarer

  • Ulempen med å returnere med pekeren: nå må du ‘ spore eierskap til det objektet og mulig frigjør den. Pekerindireksjon kan også være dyrere enn en rask kopi. Det er mange variabler her, så bruk av pekere er ikke universelt bedre.
  • Også pekere er i disse dager 64 bits på de fleste stasjonære og serverplattformer. Jeg ‘ har sett mer enn noen få strukturer i karrieren min som ville passe i 64 biter. Så du kan ‘ t alltid si at kopiering av en peker koster mindre enn å kopiere en struktur.
  • Dette er stort sett et godt svar , men jeg er uenig om delen noen ganger, veldig sjelden, er dette det du vil ha, men mesteparten av tiden er det ‘ ikke – snarere tvert imot. Å returnere en peker tillater flere typer uønskede bivirkninger, og flere slags stygge måter å få eierskapet til en peker feil. I tilfeller der CPU-tid ikke er så viktig, foretrekker jeg kopivarianten. Hvis det er et alternativ, er det mye mindre utsatt for feil.
  • Det bør bemerkes at dette virkelig gjelder bare for eksterne API-er. For interne funksjoner vil hver til og med marginalt kompetente kompilator fra de siste tiårene omskrive en funksjon som returnerer en stor struktur for å ta en peker som et tilleggsargument og konstruere objektet direkte der inne. Argumentene om uforanderlig vs foranderlig har blitt gjort ofte nok, men jeg tror vi alle kan være enige i at påstanden om at uforanderlige datastrukturer nesten aldri er det du vil, ikke er sant.
  • Du kan også nevne kompilering av brannvegger. som en proff for pekere. I store programmer med vidt delte overskrifter forhindrer ufullstendige typer funksjoner nødvendigheten av å kompilere hver gang en implementeringsdetalj endres. Jo bedre kompilasjonsatferd er faktisk en bivirkning av innkapslingen som oppnås når grensesnitt og implementering skilles. Å returnere (og passere, tildele) etter verdi trenger implementeringsinformasjonen.

Svar

I tillegg til andre svar , noen ganger er det verdt å returnere en liten struct. For eksempel kan man returnere et par data, og en feilkode (eller suksess) som er relatert til den.

For å ta et eksempel, returnerer fopen bare én data (den åpnede FILE*) og i tilfelle feil, gir feilkoden gjennom errno pseudo-global variabel. Men det ville kanskje være bedre å returnere et struct av to medlemmer: FILE* -håndtaket, og feilkoden (som ville bli satt hvis filhåndtaket er NULL). Av historiske grunner er det ikke tilfelle (og feil rapporteres gjennom errno global, som i dag er en makro).

Legg merke til at Go språk har en fin notasjon for å returnere to (eller noen få) verdier.

Legg også merke til at på Linux / x86-64 ABI og anropskonvensjoner (se x86-psABI side) angir at en struct av to skalarelementer (f.eks. en peker og et heltall, eller to pekere eller to heltall) returneres gjennom to registre (og dette er veldig effektivt og går ikke gjennom minnet).

Så i ny C-kode kan retur av en liten C struct være mer lesbar, mer trådvennlig og mer effektiv.

Kommentarer

  • Egentlig små pakker er pakket i rdx:rax. Så struct foo { int a,b; }; returneres pakket inn i rax (f.eks. Med shift / eller), og må pakkes ut med shift / mov. Her er ‘ et eksempel på Godbolt . Men x86 kan bruke de lave 32 bitene i et 64-biters register for 32-biters operasjoner uten å bry seg om de høye bitene, så det er ‘ alltid så ille, men definitivt verre enn å bruke 2 registrerer mesteparten av tiden for 2-medlems strukturer.
  • Relatert: bugs.llvm.org/show_bug.cgi? id = 34840 std::optional<int> returnerer boolesk i den øverste halvdelen av rax, så du trenger en 64-biters maske konstant for å teste den med test. Eller du kan bruke bt. Men det suger for innringeren og callee sammenlignet med å bruke dl, hvilke kompilatorer som skal gjøre for » private » funksjoner. Også relatert: libstdc ++ ‘ s std::optional<T> isn ‘ t trivielt kopierbar selv når T er , så den returnerer alltid via skjult peker: stackoverflow.com/questions/46544019/… . (libc ++ ‘ s kan trivielt kopieres)
  • @PeterCordes: dine relaterte ting er C ++, ikke C
  • Ups, ikke sant. Vel, det samme vil gjelde nøyaktig struct { int a; _Bool b; }; i C, hvis den som ringer ville teste boolesk, fordi trivielt kopierbare C ++ -strukturer bruker samme ABI som C.
  • Klassisk eksempel div_t div()

Svar

Du er på rett spor

Begge årsakene du nevnte er gyldige:

En av grunnene til at jeg trodde det ville være en fordel å returnere en peker til en struktur, er å kunne fortelle lettere om funksjonen mislyktes ved å returnere NULL-pekeren.

Å returnere en FULL struktur som er NULL ville være vanskeligere antar jeg eller mindre effektiv. Er dette en gyldig årsak?

Hvis du har en tekstur (for eksempel) et eller annet sted i minnet, og du vil referere til denne strukturen flere steder i program; det ville ikke være lurt å lage en kopi hver gang du ønsket å referere til den. Hvis du i stedet bare går rundt en peker for å referere til teksturen, vil programmet kjøre mye raskere.

Den største grunnen til er dynamisk minnetildeling. Ofte når et program er kompilert, er du ikke sikker på nøyaktig hvor mye minne du trenger for visse datastrukturer. Når dette skjer, vil mengden minne du trenger å bli bestemt ved kjøretid. Du kan be om minne ved hjelp av malloc og frigjør det når du er ferdig med å bruke gratis.

Et godt eksempel på dette er å lese fra en fil som er spesifisert av brukeren. I dette tilfellet har du ingen ide om hvor stor filen kan være når du kompilerer programmet. Du kan bare finne ut hvor mye minne du trenger når programmet faktisk kjører.

Både malloc og gratis returpekere til steder i minnet. Så fungerer som bruker dynamisk minnetildeling, vil peke tilbake til der de har opprettet sine strukturer i minnet.

I kommentarene ser jeg også at det er et spørsmål om du kan returnere en struktur fra en funksjon. Du kan virkelig gjøre dette. Følgende skal fungere:

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

  • Hvordan er det mulig å ikke vite hvor mye minne vil en viss variabel trenge hvis du allerede har definert strukturtypen?
  • @JenniferAnderson C har et begrep om ufullstendige typer: et typenavn kan erklæres, men ikke definert ennå, så det ‘ størrelse er ikke tilgjengelig. Jeg kan ikke erklære variabler av den typen, men kan erklære pekere til den typen, f.eks. struct incomplete* foo(void). På den måten kan jeg erklære funksjoner i en overskrift, men bare definere strukturer i en C-fil, og dermed tillate innkapsling.
  • @amon Så dette er hvordan deklarere funksjonsoverskrifter (prototyper / signaturer) før jeg erklærer hvordan de arbeidet er faktisk gjort i C? Og det er mulig å gjøre det samme med strukturene og fagforeningene i C
  • @JenniferAnderson du erklærer funksjon prototyper (funksjoner uten kropper) i headerfiler og kan da kalle disse funksjonene i annen kode, uten å vite kroppens funksjoner, fordi kompilatoren bare trenger å vite hvordan man skal ordne argumentene, og hvordan man aksepterer returverdien. Når du kobler programmet, må du faktisk vite funksjonen definisjon (dvs. med en kropp), men du trenger bare å behandle det en gang. Hvis du bruker en ikke-enkel type, må den også vite at typen ‘ s struktur, men pekere er ofte av samme størrelse, og den ‘ t saken for en prototype ‘ s bruk.

Svar

Noe som en FILE* er egentlig ikke en peker til en struktur når det gjelder klientkode, men er i stedet en form for ugjennomsiktig identifikator tilknyttet noen annen enhet som en fil. Når et program kaller fopen, vil det vanligvis ikke bry seg om noe av innholdet i den returnerte strukturen – alt det vil bry seg om er at andre funksjoner som fread vil gjøre hva de trenger å gjøre med det.

Hvis et standardbibliotek holder innenfor en FILE* informasjon om f.eks. den gjeldende leseposisjonen i den filen, vil et anrop til fread trenge å kunne oppdatere denne informasjonen. Å ha fread motta en peker til FILE gjør det enkelt. Hvis fread i stedet mottok et FILE, ville det ikke være noen måte å oppdatere FILE -objektet holdes av den som ringer.

Svar

Informasjon Skjuler

Hva er fordelen med å returnere en peker til en struktur i motsetning til å returnere hele strukturen i returoppgaven av funksjonen?

Den vanligste er skjuling av informasjon . C har ikke si, si, muligheten til å gjøre felt av en struct private, enn si å gi metoder for å få tilgang til dem.

Så hvis du vil med kraft hindre utviklere fra å kunne se og tukle med innholdet i en pointee, som FILE, så er den eneste måten å forhindre at de blir utsatt for definisjonen ved å behandle pekeren som ugjennomsiktig hvis poengstørrelse og definisjon er ukjent for omverdenen. Definisjonen av FILE vil da bare være synlig for de som implementerer operasjonene som krever definisjonen, som fopen, mens bare strukturdeklarasjonen vil være synlig for den offentlige overskriften.

Binær kompatibilitet

Skjuling av strukturdefinisjonen kan også bidra til å gi pusterom for å bevare binær kompatibilitet i dylib APIer. Det gjør det mulig for biblioteksimplementører å endre felt i den ugjennomsiktige strukturen ure uten å bryte binær kompatibilitet med de som bruker biblioteket, siden kodenes art bare trenger å vite hva de kan gjøre med strukturen, ikke hvor stor den er eller hvilke felt den har.

Som en for eksempel kan jeg faktisk kjøre noen eldgamle programmer som ble bygget i løpet av Windows 95-tiden i dag (ikke alltid perfekt, men overraskende mange fungerer fremdeles). Sjansen er at noen av koden for de gamle binærfilmene brukte ugjennomsiktige pekere til strukturer hvis størrelse og innhold har endret seg fra Windows 95-tiden. Likevel fortsetter programmene å fungere i nye versjoner av windows siden de ikke ble utsatt for innholdet i disse strukturene. Når du arbeider med et bibliotek der binær kompatibilitet er viktig, kan det som klienten ikke utsettes for generelt endres uten å bryte bakoverkompatibilitet.

Effektivitet

Å returnere en full struktur som er NULL ville være vanskeligere antar jeg eller mindre effektiv. Er dette en gyldig årsak?

Det er vanligvis mindre effektivt forutsatt at typen praktisk talt kan passe og tildeles på bunken med mindre det vanligvis er mye mindre generalisert minnetildeling som brukes bak kulissene enn malloc, som en allokering med fast størrelse i stedet for variabel størrelse som allerede er tildelt minne. Det er en sikkerhetsavveining i dette tilfellet, de fleste sannsynligvis for å tillate biblioteksutviklerne å opprettholde invarianter (konseptuelle garantier) relatert til FILE.

Det er ikke en så gyldig grunn i det minste fra et ytelsesperspektiv. for å få fopen til å returnere en peker siden den eneste grunnen til at «d return NULL ikke har åpnet en fil. Det ville være å optimalisere et eksepsjonelt scenario i bytte for å bremse alle vanlige kjøringsveier. I noen tilfeller kan det være en gyldig produktivitetsgrunn for å gjøre design enklere for å få dem til å returnere pekere slik at NULL kan returneres i noen ettertilstander.

For filoperasjoner er overhead relativt ganske trivielt i forhold til selve filoperasjonene, og det manuelle behovet for fclose kan uansett ikke unngås. Så det er ikke slik at vi kan spare klienten for bryet med å frigjøre (lukke) ressursen ved å eksponere definisjonen av FILE og returnere den med verdi i fopen eller forvent mye av en ytelsesforbedring gitt de relative kostnadene for filoperasjonene selv for å unngå en bunktildeling.

Hotspots and Fixes

For andre tilfeller har jeg imidlertid profilert mye bortkastet C-kode i eldre kodebaser med hotspots i malloc og unødvendig obligatorisk hurtigbuffer som et resultat av å bruke denne øvelsen for ofte med ugjennomsiktige pekere og tildele for mange ting unødvendig på haugen, noen ganger i store løkker.

En alternativ praksis jeg bruker i stedet er å avsløre strukturdefinisjoner, selv om klienten ikke er ment å tukle dem, ved å bruke en standardkonfigurasjonsstandard for å kommunisere at ingen andre skal berøre feltene:

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

Hvis det er bekymringer for binær kompatibilitet i fremtiden, så har jeg funnet det bra nok til å bare reservere litt ekstra plass til fremtidige formål, slik:

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

Den reserverte plassen er litt sløsende, men kan være en livredder hvis vi i fremtiden finner ut at vi trenger å legge til litt mer data til Foo uten å bryte binærfiler som bruker biblioteket vårt.

Etter min mening er skjuling av informasjon og binær kompatibilitet vanligvis den eneste anstendige grunnen til å bare tillate haugetildeling strukturer i tillegg til strukturer med variabel lengde (som alltid vil kreve det, eller i det minste være litt vanskelig å bruke ellers hvis klienten måtte tildele minne på stakken i et VLA-mote ion for å tildele VLS). Selv store strukturer er ofte billigere å returnere etter verdi hvis det betyr at programvaren jobber mye mer med det varme minnet på bunken. Og selv om de ikke var billigere å returnere etter verdien ved skapelsen, kunne man ganske enkelt gjøre dette:

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

… for å initialisere Foo fra bunken uten mulighet for overflødig kopi. Eller klienten har til og med friheten til å tildele Foo på bunken hvis de vil av en eller annen grunn.

Legg igjen en kommentar

Din e-postadresse vil ikke bli publisert. Obligatoriske felt er merket med *