message-exclamationTipuri, constante, pointeri

Pointerii în C++ devin cu adevărat interesanți atunci când introducem în ecuație și cuvântul cheie const. Deși poate părea complicat la prima vedere, odată ce înțelegi logica din spatele fiecărei combinații, totul devine simplu. Să explorăm fiecare caz în parte, cu exemple concrete și diagrame care ilustrează exact ce se întâmplă în memorie. În cadrul acestor exemple, vom folosi ca exemplu tipul char, însă aceleași tehnici pot fi extrapolate și pentru restul tipurilor primitive (și nu numai) din C++.

Pentru o mai simplă reprezentare a datelor în memorie, am folosit următoarea convenție:

  • forme:

    • pătrat: variabilă

    • hexagon: pointer

  • culori:

    • albastru: variabilă mutabilă

    • portocaliu: variabilă constantă

  • bordurile pointerilor:

    • linie plină: pointerul este declarat ca indicând o variabilă mutabilă

    • linie punctată: pointerul este declarat ca indicând spre o constantă

Terminologie:

  • variabilă - zonă de memorie identificată prin nume, folosită pentru a stoca o valoare ce poate fi modificată

  • constantă - zonă de memorie identificată prin nume, folosită pentru a stoca o valoare ce nu poate fi modificată

  • pointer - variabilă folosită pentru a stoca adresa unei alte zone de memorie (a unei variabile sau constante), care poate fi atât variabil, cât și constant

  • mutabil - caracterul unei variabile de a putea fi modificată după ce a fost declarată

  • imutabil - caracterul unei variabile de a nu putea fi modificată după ce a fost declarată

Variabile simple: Fundamentele

Când declarăm o variabilă obișnuită, de exemplu char c = 'A', creăm un spațiu în memoria stack care stochează valoarea caracterului 'A'. Această variabilă are o adresă în memorie, iar noi putem modifica valoarea ei oricând dorim. Dacă apoi schimbăm valoarea lui c în 'B', valoarea se modifică, dar adresa rămâne aceeași. Iată o secvență de cod care declară o variabilă de tip char și îi atribuie o valoare.

Instrucțiunile de mai sus alocă o variabilă numită c la o adresă de memorie din stack, conform figurii de mai jos.

Cele două instrucțiuni afișază valoarea variabilei, precum și adresa la care este alocată aceasta. Următoarele instrucțiuni modifică valoarea aceleiași variabile.

Observăm cum, în urma executării acestor instrucțiuni, valoarea variabilei c s-a schimbat, însă adresa ei a rămas aceeași, precum în reprezentarea de mai jos.

În acest caz, avem libertate totală. Valoarea poate fi modificată oricând, iar memoria ocupată de variabilă rămâne la aceeași adresă pe toată durata vieții sale.

Constante: Valori imutabile

Atunci când adăugăm const în fața declarației unui tip, de exemplu const char c = 'A', transformăm variabila într-o constantă. Aceasta înseamnă că, odată ce am stabilit valoarea inițială, nu o mai putem schimba niciodată. Compilatorul va refuza orice încercare de modificare, generând o eroare. Este un mecanism de siguranță care ne protejează de modificări accidentale ale valorilor care ar trebui să rămână fixe.

Asemenea alocării unei variabile obișnuite (non-constante), se alocă în memorie o locație nouă care reține valoarea 'A', conform figurii de mai jos.

Însă, încercarea de a executa o instrucțiune precum cea de mai jos, generează eroare:

Deoarece variabila c este declarată ca fiind constantă, nu este permisă modificarea ei după ce atribuirea valorii inițiale a avut loc. Mai concret, odată stabilită, valoarea este blocată pentru întreg ciclul de viață al variabilei.

Pointeri simpli către variabile modificabile

Lucrurile devin mai interesante când introducem pointerii. Un pointer este, în esență, o variabilă care stochează adresa altei variabile. Când declarăm char* p = &c, creăm un pointer care reține adresa caracterului c. Acum avem două modalități de a accesa caracterul: direct prin c sau indirect prin *p (operatorul de dereferențiere).

În urma executării instrucțiunilor de mai sus, se alocă în memorie două variabile: una de tip char, denumită c, și alta de tip char*, denumită p, conform următoarei reprezentări.

Observăm cum adresa lui c este memorată ca valoare a pointerului p, iar caracterul 'A' se poate afișa atât direct, prin intermediul variabilei c, cât și indirect, prin dereferențierea pointerului p (operatorul *).

Deoarece variabila c nu este constantă, aceasta permite modificarea valorii reținute în aceasta. Așadar, prin executarea blocului de cod de mai jos, se poate modifica valoarea variabilei (atât în mod direct, cât și prin dereferențiere).

Observăm că valoarea variabilei c se modifică, însă atât adresa variabilei c, cât și a pointerului p, rămân neschimbate. De aceea, adresa de memorie reținută de p rămâne aceeași, deoarece modificarea s-a executat peste conținutul variabilei c, precum este reprezentat mai jos. Aceasta demonstrează caracterul mutabil al variabilei de tip char în acest context.

Totodată, și pointerul p are caracter mutabil. Astfel, se poate modifica și valoarea acestuia - concret, din moment ce pointerii rețin adrese, ei pot indica spre alte variabile de același tip. În blocul de cod de mai jos, pointerul p este instruit să indice spre variabila d.

Se poate observa cum variabila d, stocată la adresa 0020F9B7 și care reține valoarea 'D', este acum referențiată de către pointerul p. Astfel, adresa reținută în acesta este înlocuită cu adresa variabilei d, precum în figura de mai jos.

În această situație, am demonstrat că, atât valoarea variabilei de tip char, cât și pointerul către aceasta, își pot modifica valorile.

Pointeri către constante: Protejarea valorilor

În cazul anterior, pointerul a indicat către o variabilă mutabilă. Există situații când avem nevoie ca aceștia să poată indica spre constante, însă păstrând flexibilitatea de a schimba spre ce variabilă indică. Astfel, când sintaxa este sub forma const char* p, declarăm un pointer către un caracter constant. Acest lucru înseamnă că nu putem modifica valoarea caracterului prin intermediul pointerului, însă putem modifica spre ce caracter indică pointerul. Să luăm instrucțiunile de mai jos:

Observăm că am alocat o constantă c, căreia i-am atribuit valoarea 'A'. Deși întâlnim cuvântul cheie const în definiția pointerului p, acesta se referă la tipul variabilei spre care indică, și nu face pointerul în sine constant. Astfel, memoria alocată în acest caz este reprezentată mai jos.

Deoarece valoarea variabilei c este constantă, instrucțiuni precum cea de mai jos generează eroare, nefiind posibilă modificarea variabilei.

Totuși, deoarece pointerul p nu este constant, avem posibilatea de a indica spre o altă constantă de același tip, precum în instrucțiunile de mai jos:

În această situație, memoria este alocată astfel:

Pointer spre constantă căruia i se atribuie adresa unei variabile mutabile.

Există și o situație interesantă. După cum am menționat la începutul acestei secțiuni, un pointer către o constantă nu permite modificarea variabilei prin intermediul său, însă acest lucru nu impune obligativitatea ca pointerul să indice neapărat spre o constantă. Pot exista, astfel, și situații de acest tip:

Deși variabila e nu este constantă, un pointer spre o constantă poate indica spre aceasta, deoarece semnificația termenului "constantă" se referă la caracterul imutabil al variabilei prin intermediul pointerului, nu al variabilei în sine. Așadar, memoria alocată în această situație poate fi reprezentată astfel:

Din acest motiv, instrucțiuni precum cele de mai jos sunt perfect valabile:

Observăm că ambele adrese de memorie rămân neschimbate, schimbându-se doar valoarea reținută de variabila e, precum în figura de mai jos.

Cu toate acestea, deși acum pointerul indică în esență spre o variabilă mutabilă, deoarece acesta este declarat ca pointer spre constantă, modificarea variabilei prin dereferențierea sa produce eroare:

În concluzie, pointerul spre constantă nu implică faptul că variabila a cărei adresă este reținută în pointer este constantă, ci faptul că variabila spre care se indică are caracter imutabil atunci când este accesată prin dereferențiere.

Pointeri constanți către variabile: Legături permanente

Un pointer constant, declarat ca char* const p = &c, reprezintă cazul opus. Aici, pointerul în sine nu poate fi modificat după inițializare. El va indica mereu către aceeași variabilă. Cu toate acestea, putem modifica valoarea variabilei indicate, fie direct, fie prin dereferențiere. Exemplul de cod de mai jos exemplifică acest lucru.

Memoria alocată pentru instrucțiunile de mai sus poate fi reprezentată astfel:

Deoarece pointerul indică spre o variabilă mutabilă, aceasta poate fi modificată atât direct, cât și prin dereferențiere, precum în exemplul de mai jos:

Modificările în memorie în această situație pot fi reprezentate astfel:

Totuși, deoarece de această dată pointerul este constant, instrucțiunile de mai jos vor genera eroare, deoarece, odată stabilită, adresa reținută în pointer nu mai poate fi modificată.

Pointeri constanți către constante: Imutabilitate completă

Cel mai restrictiv caz este cel în care, atât variabila spre care se indică, cât și pointerul, sunt tipuri constante, folosindu-se sintaxă de tipul const char* const p. Această instrucțiune declară un pointer constant care indică către un caracter constant. Nici pointerul nu poate fi redirecționat, nici valoarea caracterului nu poate fi modificată prin dereferențiere. Este maximum de protecție și imutabilitate.

Memoria alocată în acest caz poate fi reprezentată astfel:

La fel ca în situația pointerului la constantă, pointerul constant la variabilă constantă nu se referă la faptul că tipul spre care indică pointerul trebuie să fie constant, ci că valoarea spre care pointerul indică nu poate fi modificată prin dereferențiere. Astfel, următoarele instrucțiuni sunt permise.

Observăm că, deși caracterul c nu este constant, C++ permite pointerului constant spre constantă acest tip de referențiere. Memoria, în acest caz, poate fi reprezentată astfel:

Deoarece variabila c este mutabilă, este permisă modificarea acesteia în mod direct, astfel:

Putem astfel observa că modificarea variabilei prin accesare directă este posibilă, iar pointerul constant spre constantă se comportă identic. Memoria este modificată în urma acestor instrucțiuni astfel:

Totuși, o instrucțiuni precum cele de mai jos vor genera eroare:

Astfel:

  • în primul caz, variabila c este constantă și nu se poate modifica

  • în al doilea caz, pointerul p nu poate indica spre o altă adresă, deoarece este constant

  • în al treilea caz, se încearcă modificarea variabilei mutabile e prin dereferențiere, lucru nepermis de caracterul de pointer constant spre constantă al lui q.

Rezumat

Declarație
Valoarea poate fi modificată direct?
Valoarea poate fi modificată prin dereferențiere?
Pointerul poate fi redirecționat?

char c

Da

-

-

const char c

Nu

-

-

char* p

Da

Da

Da

const char* p

Da, dacă pointerul indică spre variabilă mutabilă

Nu

Da

char* const p

Da

Da

Nu

const char* const p

Da, dacă pointerul indică spre variabilă mutabilă

Nu

Nu

Cum putem reține ușor sintaxa pentru toate aceste tipuri?

Există un truc simplu pentru a înțelege declarațiile cu pointeri și constante: citește-le de la dreapta la stânga, în engleză. De exemplu:

Declarație
Cum citim?

char c

c (c) is a character (char)

const char c

c (c) is a character (char) that's constant (const)

char* p

p (p) is a pointer (*) to a character (char)

const char* p

p (p) is a pointer (*) to a character (char) that's constant (const)

char* const p

p (p) is a constant (const) pointer (*) to a character (char)

const char* const p

p (p) is a constant (const) pointer (*) to a character (char) that's constant (const)

Înțelegerea acestor distincții este esențială pentru a scrie cod sigur și pentru a comunica intențiile tale ca programator. Fiecare combinație are scopul ei specific, iar alegerea corectă depinde de comportamentul pe care îl dorești în programul tău.

Last updated