Pointer versus referință — RexDac Academy

RexDac AcademyNote tehnice · Ajută să ai câteva noțiuni de bază
0%Note publicate citite · pe acest dispozitiv

Informatică

Pointer versus referință

Note originale de SunDiaL · Vezi sursa în engleză

%% Sursă: https://www.reddit.com/r/cpp/comments/18186it/comment/kab1in7/?utm_source=share&utm_medium=web3x&utm_name=web3xcss&utm_term=1&utm_content=share_button %% Cred că problema de care te lovești aici este că aceste citate amestecă într-o anumită măsură semantica și implementarea. Să le separăm mai explicit; astfel va deveni mai clar de ce citatele nu sunt, de fapt, incompatibile sau contradictorii:

Semantica pointerului

Un pointer este un tip valoare distinct. Are o dimensiune definită pe o anumită platformă, care poate fi obținută prin sizeof(void*). Pe o platformă cu un spațiu de adrese real de 64 de biți, rezultatul va fi 8. Toți pointerii către orice tip de obiect vor avea aceeași dimensiune: sizeof(T*) == sizeof(U*). Valoarea unui pointer este o locație de memorie efectivă, reală. O poți inspecta direct: dacă afișezi un pointer fără să îl dereferențiezi, valoarea afișată este adresa de memorie pe care o stochează.

În esență, pointerii sunt tipuri valoare „autentice”, de sine stătătoare, și pot exista independent de un obiect către care să indice. De aceea poți avea un pointer nul. Și, fiind tipuri valoare autentice, au propria definiție a următoarelor operații, independent de tipul către care indică:

  • Compararea unui pointer înseamnă compararea adresei de memorie, nu a conținutului aflat la acea adresă.

  • Copierea/atribuirea unui pointer înseamnă copierea/atribuirea adresei de memorie, nu a conținutului aflat la acea adresă.

  • Obținerea adresei unui pointer produce adresa obiectului pointer însuși, nu a conținutului către care indică.

  • „Dereferențierea” unui pointer produce o referință la conținutul către care indică. Știu, este derutant. Avea mai mult sens în C, unde nu exista un element distinct numit referință; de aceea se spunea că pointerii au „semantică de referință”, iar „dereferențierea” lor producea conținutul propriu-zis.

Când spunem că un tip este „asemănător unui pointer”, ne referim la faptul că modelează proprietățile de mai sus. De aceea pointerii inteligenți se comportă așa. Tot de aceea std::span este atât de derutant. std::span arată ca un container, așa că te aștepți să se comporte ca o referință la un container: compararea lui ar trebui să compare elementele din span. Dar poți reatribui span-uri, iar atribuirea nu reatribuie elementele de dedesubt, ci doar pointerul de dedesubt, astfel încât acesta să indice către alte elemente. Prin urmare, pentru consecvență, standardul a ales răul mai mic și a făcut ca span să modeleze un pointer, având semantică de pointer în toate privințele. Când compari un span, întrebi „oferă aceste span-uri o vedere asupra exact aceluiași conținut din memorie?”, la fel ca atunci când compari un pointer. În C++20, această idee de „API de referință la container, dar semantică de pointer” a fost formalizată prin conceptul view.

Semantica referinței

O referință nu este, de fapt, un tip distinct, exact cum spune al doilea citat: nu există un obiect propriu-zis care să „reprezinte” o referință. Nu poți obține adresa referinței în sine. Dacă folosești sizeof(T&), vei primi același rezultat ca pentru sizeof(T). Așadar, cel mai exact este să spunem că o referință este un alias pentru instanța propriu-zisă a unui obiect.

Deoarece referințele nu sunt tipuri valoare autentice, de sine stătătoare, ci aliasuri pentru instanță, au următoarea semantică:

  • Trebuie „legate” de o instanță la inițializare. O referință nu poate fi niciodată „nulă”.

  • Nu pot fi reatribuite. Atribuirea către o referință după ce a fost „legată” de o instanță este echivalentă cu atribuirea către instanța legată, nu către referință.

  • Compararea referințelor este echivalentă cu compararea instanțelor la care se referă.

  • Obținerea adresei unei referințe oferă adresa instanței de care este legată.

Implementări

După cum am discutat mai sus, un pointer este un tip valoare definit explicit. Pointerii ocupă memorie pe stivă/heap.

În schimb, o referință nu este un tip valoare definit explicit. Deoarece o referință nu este un tip valoare „real”, ci este definită ca alias pentru instanța de care este legată, compilatorul este liber să implementeze referințele cum dorește, atât timp cât păstrează semantica.

Aceasta înseamnă că un compilator este liber să elimine complet o referință prin optimizare. Compilatorul nu poate face acest lucru în mod inerent cu un pointer: pointerul trebuie să existe efectiv, deoarece îi poți obține adresa, îl poți reatribui etc. Totuși, compilatorul este la fel de liber să implementeze o referință ca pointer (în sensul de la nivelul mașinii, vezi mai jos) și adesea o va face dacă referința trece între funcții care nu pot fi expandate inline.

De ce este atât de derutant?

Din două motive:

Confundarea pointerului cu referențierea indirectă

Când oamenii vorbesc despre pointeri, folosesc adesea termenul ca sinonim pentru noțiunea de adresă de memorie și referențiere indirectă de la nivelul mașinii, deoarece asta modelează un pointer (brut). Dar sunt, de fapt, două lucruri distincte. Pointerul ca tip C/C++ este o noțiune abstractă de adresă de memorie care poate fi folosită pentru referențiere indirectă. Are un sens independent de modul în care o anumită platformă implementează referențierea indirectă; în limbajul de asamblare nu există un tip „pointer”, ci doar instrucțiuni care efectuează operații indirect, printr-o adresă de memorie. Așadar, când cineva spune „o referință și un pointer sunt același lucru”, se gândește la codul de asamblare generat pentru referențiere indirectă și la felul în care acesta poate stoca adresele folosite.

Expresia colocvială „semantică de referință”

Se folosește frecvent expresia colocvială potrivit căreia atât pointerii, cât și referințele ar avea „semantică de referință”. Prin aceasta, oamenii înțeleg că numele unei variabile reprezintă indirect o instanță, fără a fi un tip valoare al acelei instanțe. Permite manipularea instanței prin numele variabilei și transmiterea instanței între funcții fără copierea ei. Totuși, această expresie este improprie. Un pointer nu are, de fapt, semantică de referință. Este un tip valoare. Acel tip valoare modelează semantica de referință prin operatorii * și ->.

Vei auzi adesea această distincție când cineva întreabă „Python/Java are semantică de referință sau semantică de valoare?”. Este o întrebare-capcană: răspunsul corect este „au semantică de valoare, dar singurul tip valoare pe care îl poți atribui direct unui nume este un pointer”. Prin urmare, lipsa unei referințe autentice sau a unui tip valoare atribuibil diferit de pointer este esențială pentru a înțelege de ce Java și Python se comportă așa.