Care este avantajul returnării unui pointer la o structură spre deosebire de returnarea întregii structuri în return
declarația funcției?
Vorbesc despre funcții precum fopen
și alte funcții de nivel scăzut, dar probabil că există funcții de nivel superior care returnează pointerii și structurilor.
Cred că aceasta este mai mult o alegere de proiectare decât o întrebare de programare și sunt curios să aflu mai multe despre avantajele și dezavantajele celor două metode.
Una dintre motive pentru care am crezut că este un avantaj pentru a returna un pointer la o structură este să poți spune mai ușor dacă funcția a eșuat prin returnarea NULL
pointer.
Returnarea unei structuri complete care este NULL
ar fi mai dificil, presupun sau mai puțin eficient. Este acesta un motiv valid?
Comentarii
Răspuns
Acolo sunt mai multe motive practice pentru care funcții precum fopen
returnează pointeri în loc de instanțe de tipuri struct
:
- Doriți să ascundeți reprezentarea tipului
struct
de la utilizator; - Alocați din nou un obiect în mod dinamic;
- Sunteți re referindu-se la o singură instanță a unui obiect prin referințe multiple;
În cazul unor tipuri precum FILE *
, este „pentru că nu” doriți să expuneți utilizatorului detalii despre reprezentarea tipului – o FILE *
obje ct servește ca un handle opac și pur și simplu îl treceți la diverse rutine I / O (și în timp ce FILE
este adesea implementat ca struct
tastați, nu trebuie să fie trebuie ).
Deci, puteți expune un tip incomplet struct
într-un antet undeva:
typedef struct __some_internal_stream_implementation FILE;
În timp ce nu puteți declara o instanță de tip incomplet, puteți declara un indicator către aceasta. Așa că pot crea un FILE *
și îi pot atribui prin fopen
, freopen
etc. , dar nu pot manipula direct obiectul către care indică.
De asemenea, este probabil ca funcția fopen
să aloce un FILE
obiect dinamic, folosind malloc
sau similar. În acest caz, are sens să returnăm un indicator.
În cele din urmă, este posibil să stocați un fel de stare într-un obiect struct
și trebuie să faceți această stare disponibilă în mai multe locuri diferite. Dacă ați returnat instanțe de tip struct
, aceste instanțe ar fi obiecte separate în memorie una de cealaltă și, în cele din urmă, ar ieși din sincronizare. Prin returnarea unui pointer la un singur obiect, toată lumea se referă la același obiect.
Comentarii
- Un avantaj deosebit al utilizării indicatorului ca tipul opac este acela că structura în sine se poate schimba între versiunile bibliotecii și nu ‘ nu trebuie să recompilați apelanții.
- @Barmar: Într-adevăr, stabilitatea ABI este imensul punct de vânzare al lui C și nu ar fi la fel de stabil fără indicatori opaci.
Răspuns
Există două moduri de„ returnare a unei structuri. ”Puteți returna o copie a datelor sau puteți returna o referință (pointer) la aceasta.În general, este de preferat să returnați (și să treceți în general) un pointer, din câteva motive.
În primul rând, copierea unei structuri necesită mult mai mult timp CPU decât copierea unui pointer. Dacă acesta este ceva codul dvs. face frecvent, poate provoca o diferență vizibilă de performanță.
În al doilea rând, indiferent de câte ori copiați un pointer în jur, acesta indică în continuare aceeași structură din memorie. Toate modificările aduse acestuia vor fi reflectate pe aceeași structură. Dar dacă copiați structura în sine și apoi faceți o modificare, modificarea apare doar pe copia respectivă . Orice cod care conține o copie diferită nu va fi modificat. Uneori, foarte rar, asta vrei, dar de cele mai multe ori nu este posibil și poate provoca erori dacă greșești.
Comentarii
- Dezavantajul revenirii prin pointer: acum ‘ trebuie să urmăriți proprietatea asupra acelui obiect și posibil eliberează-l. De asemenea, indirectarea indicatorului poate fi mai costisitoare decât o copie rapidă. Există o mulțime de variabile aici, astfel încât utilizarea pointerelor nu este universal mai bună.
- De asemenea, pointerele din zilele noastre sunt pe 64 de biți pe majoritatea platformelor desktop și server. Am ‘ am văzut mai mult de câteva structuri în cariera mea care s-ar încadra în 64 de biți. Deci, poți ‘ t întotdeauna spune că copierea unui pointer costă mai puțin decât copierea unei structuri.
- Acesta este în mare parte un răspuns bun , dar nu sunt de acord cu partea uneori, foarte rar, asta vrei, dar de cele mai multe ori nu ‘ nu – chiar dimpotrivă. Returnarea unui indicator permite mai multe tipuri de efecte secundare nedorite și mai multe tipuri de modalități urâte de a greși proprietatea asupra unui indicator. În cazurile în care timpul procesorului nu este atât de important, prefer varianta de copiere, dacă aceasta este o opțiune, este mult mai puțin predispusă la erori.
- Trebuie remarcat faptul că acest lucru este într-adevăr se aplică numai pentru API-urile externe. Pentru funcții interne, fiecare compilator, chiar marginal, din ultimele decenii, va rescrie o funcție care returnează o structură mare pentru a lua un pointer ca argument suplimentar și pentru a construi obiectul direct acolo. Argumentele despre imutabil vs mutabil au fost făcute suficient de des, dar cred că putem fi de acord cu toții că afirmația că structurile de date imuabile nu sunt aproape niciodată ceea ce vrei nu este adevărată.
- De asemenea, ai putea menționa pereții de foc de compilare ca un profesionist pentru indicatori. În programele mari cu antete partajate pe scară largă, tipurile incomplete cu funcții împiedică necesitatea recompilării de fiecare dată când se modifică un detaliu de implementare. Un comportament mai bun de compilare este de fapt un efect secundar al încapsulării care se obține atunci când interfața și implementarea sunt separate. Revenirea (și trecerea, atribuirea) în funcție de valoare necesită informații de implementare.
Răspuns
În plus față de alte răspunsuri , uneori returnarea unui mic struct
după valoare merită. De exemplu, s-ar putea returna o pereche de date și un cod de eroare (sau de succes) legat de acestea.
Pentru a lua un exemplu, fopen
returnează doar o dată (FILE*
deschisă) și în caz de eroare, dă codul de eroare prin errno
pseudo-variabilă globală. Dar ar fi probabil mai bine să returnăm un struct
a doi membri: mânerul FILE*
și codul de eroare (care ar fi setat dacă mânerul fișierului este NULL
). Din motive istorice, nu este cazul (iar erorile sunt raportate prin errno
global, care astăzi este o macro).
Observați că Go language are o notație plăcută pentru a returna două (sau câteva) valori.
Observați, de asemenea, că pe Linux / x86-64 ABI și convențiile de apelare (consultați pagina pagina x86-psABI ) specifică faptul că un struct
din doi membri scalari (de exemplu, un pointer și un număr întreg, sau doi indicatori, sau două numere întregi) este returnat prin două registre (și acest lucru este foarte eficient și nu merge prin memorie).
Deci, în noul cod C, returnarea unui C struct
mic poate fi mai ușor de citit, mai ușor de utilizat și mai eficient.
Comentarii
- De fapt, structurile mici sunt împachetate în
rdx:rax
. Deci,struct foo { int a,b; };
este returnat ambalat înrax
(de exemplu, cu shift / or) și trebuie despachetat cu shift / mov. Aici ‘ este un exemplu despre Godbolt . Dar x86 poate utiliza cei 32 de biți mici ai unui registru pe 64 de biți pentru operațiuni pe 32 de biți fără să-i pese de biții mari, așa că ‘ este întotdeauna prea rău, dar cu siguranță mai rău decât utilizarea 2 înregistrează de cele mai multe ori pentru structuri cu 2 membri. - Corelate: bugs.llvm.org/show_bug.cgi? id = 34840
std::optional<int>
returnează booleanul în jumătatea superioară arax
, deci aveți nevoie de o mască pe 64 de biți constantă pentru a-l testa cutest
. Sau puteți utilizabt
. Dar e de rau pentru apelant și apelat să se compare cu utilizareadl
, ceea ce ar trebui să facă compilatoarele pentru ” private ” funcții. De asemenea, legat de: libstdc ++ ‘ sstd::optional<T>
isn ‘ t trivial copiat chiar și atunci când T este , deci revine întotdeauna prin pointer ascuns: stackoverflow.com/questions/46544019/… . (libc ++ ‘ s poate fi copiat în mod banal) - @PeterCordes: lucrurile dvs. conexe sunt C ++, nu C
- Hopa, corect. Ei bine, același lucru s-ar aplica exact la
struct { int a; _Bool b; };
în C, dacă apelantul ar dori să testeze booleanul, deoarece structurile C ++ care pot fi copiate în mod banal folosesc același ABI ca C. - Exemplu clasic
div_t div()
Răspuns
Sunteți pe drumul cel bun
Ambele motive pe care le-ați menționat sunt valabile:
Unul dintre motivele pentru care am cred că asta ar fi un avantaj pentru a returna un pointer la o structură este să poți spune mai ușor dacă funcția a eșuat prin returnarea pointerului NULL.
Returnarea unei structuri FULL care este NUL ar fi mai dificil, presupun sau mai puțin eficiente. Este acesta un motiv valid?
Dacă aveți o textură (de exemplu) undeva în memorie și doriți să faceți referire la textura respectivă în mai multe locuri din program; nu ar fi înțelept să faci o copie de fiecare dată când ai vrut să o faci referință. În schimb, dacă treci pur și simplu în jurul unui indicator pentru a face referire la textură, programul tău va rula mult mai repede.
este alocarea dinamică a memoriei. De multe ori, când este compilat un program, nu sunteți sigur exact de câtă memorie aveți nevoie pentru anumite structuri de date. Când se întâmplă acest lucru, cantitatea de memorie pe care trebuie să o utilizați va fi determinată în timpul rulării. Puteți solicitați memorie folosind „malloc” și apoi eliberați-o când ați terminat de utilizat „liber”.
Un bun exemplu în acest sens este citirea dintr-un fișier specificat de utilizator. În acest caz, nu aveți idee cât de mare poate fi fișierul atunci când compilați programul. Puteți afla doar câtă memorie aveți nevoie atunci când programul rulează efectiv.
Atât indicatorii malloc, cât și cei liberi de întoarcere la locațiile din memorie. care folosesc alocarea dinamică a memoriei vor întoarce indicii către locul unde și-au creat structurile în memorie >
De asemenea, în comentarii văd că există o întrebare dacă puteți returna o structură dintr-o funcție. Puteți într-adevăr să faceți acest lucru. Următorul ar trebui să funcționeze:
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; }
Comentarii
- Cum este posibil să nu știți câtă memorie o anumită variabilă va avea nevoie dacă aveți deja tipul struct definit?
- @ JenniferAnderson C are un concept de tipuri incomplete: un nume de tip poate fi declarat, dar nu este încă definit, deci nu este disponibilă. Nu pot declara variabile de acest tip, dar pot declara pointeri la acel tip, de ex.
struct incomplete* foo(void)
. În acest fel pot declara funcții într-un antet, dar pot defini doar structurile dintr-un fișier C, permițând astfel încapsularea. - @amon Deci așa se declară antetele funcției (prototipuri / semnături) înainte de a declara modul în care acestea se lucrează de fapt în C? Și este posibil să faceți același lucru structurilor și uniunilor din C
- @JenniferAnderson declarați funcția prototipuri (funcții fără corpuri) în fișierele antet și apoi puteți apela acele funcții în alt cod, fără a cunoaște corpul funcțiilor, deoarece compilatorul trebuie doar să știe cum să aranjeze argumentele și cum să accepte valoarea returnată. Până când conectați programul, trebuie să cunoașteți funcția definiție (adică cu un corp), dar trebuie să procesați asta o singură dată. Dacă utilizați un tip non-simplu, trebuie să știe și structura de tip ‘, dar indicatorii au adesea aceeași dimensiune și nu ‘ nu contează utilizarea unui prototip ‘.
Răspuns
Ceva ca un FILE*
nu este într-adevăr un pointer către o structură în ceea ce privește codul clientului, ci este în schimb o formă de identificator opac asociat cu unele altă entitate ca un fișier. Când un program apelează fopen
, în general, nu-i pasă de niciunul dintre conținutul structurii returnate – tot ceea ce îi va pasa este că alte funcții precum fread
vor face tot ce trebuie să facă cu aceasta.
Dacă o bibliotecă standard păstrează în FILE*
informații despre de ex. poziția de citire curentă din acel fișier, un apel către fread
ar trebui să poată actualiza aceste informații. Dacă primiți fread
un pointer către FILE
, este ușor. Dacă fread
ar primi în schimb un FILE
, nu ar avea nicio modalitate de actualizare a obiectului FILE
deținut de apelant.
Răspuns
Ascunderea informațiilor
Care este avantajul returnării unui pointer la o structură spre deosebire de returnarea întregii structuri în declarația return a funcția?
Cea mai comună este ascunderea informațiilor . C nu are, să zicem, capacitatea de a face câmpurile private struct
, darămite să furnizăm metode pentru a le accesa.
Deci, dacă doriți să forțați împiedicați dezvoltatorii să poată vedea și manipula conținutul unui pointee, cum ar fi FILE
, atunci singura modalitate este de a preveni expunerea la definiția sa prin tratarea indicatorului ca opac a cărui dimensiune și definiție a pointei sunt necunoscute lumii exterioare. Definiția FILE
va fi atunci vizibilă numai celor care implementează operațiunile care necesită definirea acesteia, cum ar fi fopen
, în timp ce numai declarația de structură va fi vizibilă pentru antetul public.
Compatibilitate binară
Ascunderea definiției structurii poate ajuta, de asemenea, să ofere spațiu de respirație pentru a păstra compatibilitatea binară în API-urile dylib. Permite implementatorilor de biblioteci să schimbe câmpurile din structura opacă nu trebuie să rupă compatibilitatea binară cu cei care folosesc biblioteca, deoarece natura codului lor trebuie doar să știe ce pot face cu structura, nu cât de mare este sau ce câmpuri are.
de exemplu, pot rula de fapt câteva programe antice construite astăzi în epoca Windows 95 (nu întotdeauna perfect, dar surprinzător, multe încă funcționează). Șansele sunt că unele dintre codurile pentru acele binare antice au folosit indicatori opaci pentru structuri a căror dimensiune și conținut s-au schimbat de la era Windows 95. Cu toate acestea, programele continuă să funcționeze în versiuni noi de Windows, deoarece acestea nu au fost „expuse la conținutul acelor structuri. Când lucrați la o bibliotecă în care compatibilitatea binară este importantă, ceea ce clientul nu este expus este permis în general să se schimbe fără a se rupe compatibilitate înapoi.
Eficiență
Întoarcerea unei structuri complete care este NULL ar fi mai dificil, presupun sau mai puțin eficientă. Este acesta un motiv valabil?
Este de obicei mai puțin eficient presupunând că tipul se poate potrivi practic și poate fi alocat pe stivă, cu excepția cazului în care există, de obicei, mult mai puțin alocatorul de memorie generalizat fiind folosit în spatele scenei decât malloc
, ca un alocator de memorie de dimensiuni fixe mai degrabă decât variabile, alocând deja memoria. Este un compromis de siguranță în acest caz, majoritatea probabil, pentru a permite dezvoltatorilor de biblioteci să mențină invarianți (garanții conceptuale) legate de FILE
.
Nu este un motiv atât de valid, cel puțin din punct de vedere al performanței pentru a face ca fopen
să returneze un pointer, deoarece singurul motiv pentru care „d returnează NULL
este dacă nu se deschide un fișier. Aceasta ar fi optimizarea unui scenariu excepțional în schimbul încetinirii tuturor căilor de execuție obișnuite. S-ar putea să existe un motiv de productivitate valid în unele cazuri pentru a face proiectele mai simple pentru a le face să returneze pointeri pentru a permite returnarea NULL
în anumite condiții post.
Pentru operațiile de fișiere, cheltuielile generale sunt relativ destul de banale în comparație cu operațiile de fișiere în sine, iar necesitatea manuală de a fclose
nu poate fi oricum evitată. Deci, nu este de parcă am putea salva clientul dificultatea de a elibera (închide) resursa expunând definiția FILE
și returnând-o după valoare în fopen
sau vă așteptați la o mare creștere a performanței, având în vedere costul relativ al operațiunilor de fișiere în sine, pentru a evita o alocare a heap-ului.
Hotspots and Fixes
Totuși, pentru alte cazuri, am profilat o mulțime de cod C risipitor în bazele de cod vechi cu hotspoturi în malloc
și cache-ul obligatoriu inutil ratează ca urmare a utilizării acestei practici prea frecvent cu indicatori opaci și a alocării inutile a mai multor lucruri în grămadă, uneori în bucle mari.
O practică alternativă pe care o folosesc în schimb este să expun definițiile structurii, chiar dacă clientul nu este menit să le manipuleze, utilizând un standard de convenție de denumire pentru a comunica că nimeni altcineva nu ar trebui să atingă câmpurile:
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);
Dacă există probleme de compatibilitate binară în viitor, atunci am considerat că este suficient de bun să rezerve superfluu un spațiu suplimentar pentru scopuri viitoare, cum ar fi:
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; };
Acel spațiu rezervat este puțin risipitor, dar poate fi un economisitor de viață dacă descoperim în viitor că trebuie să adăugăm câteva date la Foo
fără a sparge binarele care utilizează biblioteca noastră.
În opinia mea, ascunderea informațiilor și compatibilitatea binară sunt de obicei singurul motiv decent pentru a permite doar alocarea heap a structuri pe lângă structuri cu lungime variabilă (care ar necesita întotdeauna acest lucru, sau cel puțin ar fi puțin cam incomod pentru a fi utilizate altfel dacă clientul ar trebui să aloce memorie pe stivă într-o fash VLA ion pentru a aloca VLS). Chiar și structurile mari sunt adesea mai ieftine de returnat în funcție de valoare dacă acest lucru înseamnă că software-ul funcționează mult mai mult cu memoria fierbinte din stivă. Și chiar dacă nu ar fi „mai ieftin de returnat în funcție de valoare la creație, s-ar putea face pur și simplu acest lucru:
int foo_create(struct Foo* foo); ... /* In the client code: */ struct Foo foo; if (foo_create(&foo)) { foo_something(&foo); foo_destroy(&foo); }
… pentru a inițializa Foo
din stivă fără posibilitatea unei copii inutile. Sau clientul are chiar și libertatea de a aloca Foo
pe heap dacă doresc dintr-un anumit motiv.
gets()
complet greșită să fie eliminată. Unii programatori au în continuare o aversiune față de copierea structurilor, obiceiurile vechi mor greu.FILE*
este efectiv un mâner opac. Codului utilizatorului nu ar trebui să îi pese de structura sa internă.&
și accesați un membru cu.
. ”