Variabile, referințe și pointeri
Comportamentul obișnuit al operatorului de atribuire (=
) în C++ este să copieze valoarea unei variabile în alta. Asta înseamnă că există 2 blocuri de memorie complet separate alocate și fiecare variabilă poate fi modificată independent de cealaltă.
Referințe
Acest comportament nu este singurul posibil. Cel mai probabil că sunteți deja familiari cu noțiunea de "referințe":
void f(int x, int &y) {
x++;
y++;
cout << x << ' ' << y << '\n';
}
...
int main() {
int a = 0;
int b = 5;
f(a, b);
}
În acest exemplu, e impropriu să spunem că y
își poate modifica valoarea în afara funcției. Atât x
cât și y
își modifică valoarea în interiorul funcției, dar există 4 variabile în total.
Snippetul de cod de mai devreme este echivalent cu:
int main() {
int a = 0;
int b = 5;
// Începe "f"
int x = a;
int &y = b;
cout << x << ' ' << y << '\n';
}
** TODO desen 2 referințe pe aceeași adresă **
Poate că acel int &y = x
arată mai puțin cunoscut decât exemplul din semnătura funcției, dar se aplică același concept. y
este o referință la variabila b
. Există un singur bloc de memorie de 4 bytes alocat, și ambele variabile lucrează pe ea la comun. Așadar, dacă ar fi să executăm y = 10
după ultimul snippet, și b
ar avea valoarea 10
.
O scurtă introducere în concepte de memorie
Pentru a putea vorbi despre pointeri, trebuie pusă in context puțin noțiunea de memorie. Vom revizita subiectul în mult mai mult detaliu în cursurile următoare, dar pentru moment, ce trebuie să știți e că fiecare variabilă ocupă o spațiu în RAM.
RAM, disk, și cum lucrăm cu fiecare
Atunci când spunem memorie, mereu ne referim la RAM. Laptopurile moderne au, de obicei, pe undeva între 8GB si 16GB de RAM, cu unele mai performante aflându-se pe la 32GB. Aici se declară și variabile indiferent dacă sunt scalari, tablouri, declarate local, global sau alocate dinamic. Reamintim și conversiile între unitățile de măsura a spațiului de stocare:
- 8b (bit) = 1B (Byte)
- 1000B (Byte) = 1KB (Kilobyte)
- 1000KB (Kilobyte) = 1MB (Megabyte)
- 1000MB (Megabyte) = 1GB (Gigabyte)
- 1000GB (Gigabyte) = 1TB (Terabyte)
Spațiul pe disk este unde instalați aplicații și țineți directoare (folder) si fișiere (file). Pentru a interacționa cu diskul, trebuie deschise fișiere care pot fi citite, scrise, mutate, sau orice altceva ne permite sistemul de operare să facem cu ele.
Tipuri de date și dimensiunile lor
Tipurile de date cu care suntem obișnuiți: int
, long long
, float
și așa mai departe nu trebuie să aibă neapărat o dimensiune fixă în memorie. Nu scrie nicăieri în standardele de C sau C++ că aceste tipuri de date trebuie să fie de o anumită dimensiune, ceea ce înseamnă că o arhitectură nouă de procesor sau chiar și un compilator mai exotic pe arhitecturile cunoscute ar putea să nu respecte dimensiunile cu care suntem noi obișnuiți (mai mult despre asta în cursurile următoare). Acestea fiind zise, în mediul în care scrieți cod la liceu e aproape sigur că veți avea de a face cu următoarele dimensiuni:
bool
,char
= 1Bshort
,unsigned short
= 2Bint
,unsigned
,float
= 4Blong long
,unsigned long long
,double
= 8B
Atunci când declarăm o variabilă, sistemul de operare ne alocă (en: allocate) spațiul necesar. Din cum sunt gândite sistemele de operare uzuale (MacOS, Windows, Linux și cel mai probabil orice ați mai atins vreodată), ele permit indexarea memoriei la dimensiune de minim 1B. Acesta e motivul pentru care tipul de date bool
consumă 8 biți întregi, chiar dacă ar avea în teorie nevoie de unul singur. Pur și simplu nu putem cere mai puțină memorie. Un lucru important de menționat este că atunci când declarăm orice fel de tablouri, memoria alocată este contiguă (en: contiguous) - elementele sunt la rând, unul după celălalt. De exemplu, dacă declarăm int v[100]
, va fi un singur bloc de 400B.
TODO desen bloc contiguu de memorie
Pointeri
Un pointer este practic o variabilă de tip întreg (dimensiunea diferă) ce reține o adresă de memorie. Putem declara un pointer astfel int *ptr = &x
, unde x
este o variabilă de tip int
.
TODO desen pointer la un int
Anatomia declarării unei variabile
Declararea de mai sus introduce câteva elemente noi. În primul rând, *
și &
sunt simboluri cu câte 2 semnificații. Hai să le detaliem:
- Declararea unei referințe:
&
înint &x = y
simbolizează căx
este o referință la variabilay
de tipint
- Referențiere:
&
în&x
este adresa de memorie a primului byte utilizat în reprezentarea valorii din variabilax
- Declararea unui pointer:
*
înint *ptr = &x
simbolizează căptr
reține adresa de memorie de la care începe reprezentarea valorii din variabilax
de tipint
- Dereferențiere:
*
în*ptr
obține valoarea de la adresa reținută în pointerulptr
int
ul din față nu este tipul pointerului, ci tipul de date al valorii de la adresa indicată de pointer. În esență, dereferențierea unui pointer la int
va forma un int
din cei 4B începând cu adresa reținută. Asta înseamnă că după o secvență de instrucțiuni de forma:
unsigned int x = 1094795585; // 01000001 01000001 01000001 01000001
char *ptr = (char *) &x; // pointer la primul byte din x
char ch = *ptr; // valoarea primului byte din x
cout << ch << '\n';
se va afișa caracterul cu codul ASCII 65, 'A'.
Un detaliu important de menționat este că există și pointeri de tip void *
. Nu înseamnă ca rețin adresa unei variabile de tip void
, asta nu ar avea sens, ci se comportă ca un placeholder pentru atunci când tipul de date nu e cunoscut. Un pointer de tip void *
nu poate fi dereferențiat.
Orice pointer poate avea ca valoare o adresă speciala, nullptr
. Este un placeholder care înseamnă ca pointerul nu arată către nimic. De obicei, pointerii ar trebui inițializați pe nullptr
, și funcțiile care întorc pointeri de multe ori întorc nullptr
dacă eșuează. Un pointer care conține această valoare nu poate fi dereferențiat.
Exerciții
-
Menționați ce afișează următoarea secvență sau dacă are erori de compilare sau de runtime:
int x = 6; int *ptr1 = &x; int *ptr2 = ptr1; *ptr2 = 4; cout << x << ' ' << *ptr2 << '\n';
R: 4 4
-
Menționați ce afișează următoarea secvență sau dacă are erori de compilare sau de runtime:
int *ptr = (int *) 2; cout << ptr << '\n';
R: 0x2
-
Menționați ce afișează următoarea secvență sau dacă are erori de compilare sau de runtime:
int *ptr = (int *) 2; cout << *ptr << '\n';
R: Undefined behaviour
-
Menționați ce afișează următoarea secvență sau dacă are erori de compilare sau de runtime:
int x = 2; int y = 4; int *ptr1 = &x; int *&ptr2 = ptr1; ptr2 = &y; cout << *ptr1 << '\n';
R: 4