Documente Academic
Documente Profesional
Documente Cultură
Pentru un program C++, memoria unui calculator ca o succesiune de celule de memorie, fiecare dintre
ele ocupând un byte și având o adresă unică. Aceste celule de un byte sunt ordonate astfel încât
reprezentarea datelor mai largi de un byte să ocupe celule de memorie cu adrese consecutive.
În acest fel, fiecare celulă poate fi ușor localizată în memorie au ajutorul adresei unice. De exemplu,
celula de adresă 1776 este întotdeauna imediat după celula cu adresa 1775 și o precede pe cea cu
adresa 1777, fiind la distanță de exact o mie de celule după 776 și exact o mie de celule înainte de 2776.
Când se declară o variabilă, spațiul de memorie necesar pentru a stoca valoarea corespunzătoare este
rezervat la o anumită locație de memorie (adresa de memorie). În general, programele C++ nu decid
adresele exacte de memorie unde vor fi stocate variabilele. Din fericire, această sarcină revine mediului
în care va rula programul - în general, un sistem de operare care alocă spații de memorie la momentul
rulării programelor. Totuși, ar putea fi util ca un program să poată obțină adresa unei variabile în timpul
rulării, astfel încât să poată accesa celulele cu informații situate într-o anumită poziție față de aceasta.
Adresa unei variabile poate fi obținută punând în fața numelui variabilei un simbol ampersand (&),
cunoscut ca operatorul de adresare. De exemplu:
foo=&variabila_mea;
Adresa exactă a unei variabile din memorie nu poate fi cunoscută înainte de rularea programului, dar să
prespupunem, în vederea clarificării unor concepte, că variabila_mea se găsește în timpul rulării la
adresa 1776.
variabila_mea=25;
foo=&variabila_mea;
bar=variabila_mea;
Valorile conținute în fiecare variabilă după executarea acestei secvențe se pot vedea în următoarea
diagramă:
Variabila care memorează adresa unei alte variabile (ca foo în exemplul anterior) în C++ se
numește pointer. Pointerii sunt caracteristică foarte puternică a limbajului și au foarte multe
întrebuințări în programarea la nivel jos. Puțin mai târziu, vom vedea cum se declară și se folosesc
pointerii.
Tocmai am văzut că o variabilă care memorează adresa altei variabile se numește pointer. Se spune că
pointerii "pointează spre" variabilele ale căror adrese sunt memorate.
O proprietate interesantă a pointerilor este aceea că pot fi folosite pentru a accesa direct variabila spre
care pointează. Pentru aceasta, se precede numele pointerului cu operatorul de dereferențiere (*).
Operatorul însuși poate fi citit ca "valoarea spre care pointează".
De aceea, dăm mai jos valorile din exemplul anterior, cu următoarea instrucțiune:
baz=*foo;
Aceasta ar putea fi citită ca: "baz ia valoarea spre care pointează foo", iar instrucțiunea, de fapt,
atribuie valoarea 25 lui baz, când foo este 1776 și valoarea spre care pointează 1776 (conform
exemplului de mai sus) ar fi 25.
Este foarte important să înțelegem că foo se referă la valoarea 1776, în timp ce *foo (cu
asterisc * precedând identificatorul) se referă la valoarea memorată la adresa 1776, care, în acest caz,
este 25. Să remarcăm diferența între a include sau nu operatorul de dereferențiere (Am adăugat un
comentariu explicativ referitor la cum ar trebui citite aceste două expresii):
baz=foo; // baz ia valoarea lui foo (1776)
baz=*foo; // baz ia valoarea spre care pointează foo (25)
Deci, operatorii de referențiere și de dereferențiere sunt complementari:
& este operatorul adresă și poate fi citit pur și simplu ca "adresa lui"
* este operatorul de dereferențiere și poate fi citit ca "valoarea spre care
pointează"
variabila_mea=25;
foo=&variabila_mea;
Chiar după aceste două instrucțiuni, toate expresiile de mai jos au ca rezultat valoarea true:
variabila_mea == 25
&variabila_mea == 1776
foo == 1776
*foo ==
Prima expresie este foarte clară, având în vedere că operația de atribuire executată asupra
lui variabila_mea a fost variabila_mea=25. Cea de-a doua folosește operatorul de adresare
(&), care returnează adresa lui variabila_mea, care am presupus că are valoarea 1776. A treia
este, oarecum, evidentă, căci a doua expresie a fost adevărată și operația de atribuire realizată asupra
lui foo a fost foo=&variabila_mea. A patra expresie folosește operatorul de dereferențiere (*)
care poate fi citit ca "valoarea spre care pointează", iar valoarea spre care pointează foo este într-
adevăr 25.
Deci, după toate acestea, atât timp cât adresa spre care pointează foo rămâne neschimbată ,
următoarea expresie are tot valoarea true:
*foo ==
Declararea pointerilor
Datorită posibilității unui pointer de a accesa direct valoarea spre care pointează, un pointer are
proprietăți diferite când pointează spre un char față de un pointer care pointează spre un int sau
spre un float. Pentru dereferențiere, trebuie cunoscut tipul de dată. De aceea, declarația unui pointer
trebuie să includă tipul de dată spre care va pointa pointerul respectiv.
int * numar;
char * caracter;
double * zecimal;
Acestea sunt trei declarații de pointeri. Fiecare dintre ei are rolul de a pointa spre un tip de dată diferit,
dar, de fapt, toți sunt pointeri și toți ocupă același spațiu de memorie (spațiul de memorie ocupat de un
pointer depinde de platforma pe care rulează programul). Cu toate acestea, informațiile spre care ei
pointează nici nu ocupă același spațiu de memorie, nici nu sunt de acelasi tip: primul pointează la
un int, al doilea la char, iar ultimul la double. Așadar, deși aceste trei exemple de variabile sunt
pointeri, ele au tipuri diferite: int*, respectiv char* și double*, în funcție de tipul spre care
pointează.
Să observăm că asterisk-ul (*) folosit la declararea unui pointer semnifică numai că este pointer
(face parte din expresia specificatorului de tip) și nu trebuie confundat cu operatorul de
dereferențiere pe care l-am studiat ceva mai devreme, dar pentru care folosim, de asemenea, un
asterisk (*). Sunt, pur și simplu, două lucruri diferite reprezentate cu acelasi semn.
int main ()
{ int valoare_1, valoare_2;
int * pointerul_meu;
pointerul_meu = &valoare_1;
*pointerul_meu=10;
pointerul_meu=&valoare_2;
*pointerul_meu=20;
cout<<"valoare_1 este "<<valoare_1<<'\n';
cout<<"valoare_2 este "<<valoare_2<<'\n';
return 0;
}
Să observăm că deși nici valoare_1 nici valoare_2 nu au atribuite valori directe în program,
ambele vor avea o valoare atribuită indirect au ajutorul variabilei pointerul_meu. Iată ce se
întâmplă:
Pentru a demonstra că un pointer poate pointa spre mai multe variabile diferite pe parcursul unui
program, exemplul repetă procedeul cu valoare_2 și același pointer, pointerul_meu.
int main ()
{ int valoare_1=5, valoare_2=15;
int * p1, * p2;
Fiecare operație de atribuire include un comentariu despre modul în care ar trebui citită fiecare linie:
i.e., înlocuirea lui ampersand(&) cu "adresa lui", respectiv a lui asterisk(*) cu "valoarea spre
care pointeaza".
p1 ar fi fost de tip int*, dar p2 ar fi fost de tip int. Spațiile nu au nicio importanță în acest sens.
Oricum, este suficient să reținem să punem câte un asterisk pentru fiecare pointer atunci când declarăm
mai mulți pointeri într-o singură instrucțiune. Sau, poate mai simplu: folosirea unei instrucțiuni pentru
fiecare variabilă.
Pointeri și tablouri
Conceptul de tablou este strâns legat de pointeri. De fapt, un tablou poate fi convertit implicit într-un
pointer către tipul de bază al elementelor sale. De exemplu, să considerăm următoarele două declarații:
int tabloul_meu [20];
int * pointerul_meu;
int main ()
{ int numere[5];
int * p;
p=numere; *p=10;
p++; *p=20;
p=&numere[2]; *p=30;
p=numere + 3; *p=40;
p=numere; *(p+4)=50;
for (int n=0; n<5; n++)
cout<<numere[n]<<", ";
return 0;
}
Pointerii și tablourile suportă același set de operații, având aceleași semnificații pentru ambele. Singura
diferență o reprezintă faptul că pointerilor li se pot atribui noi adrese, în timp ce tablourilor nu.
În capitolul referitor la tablouri, parantezele drepte ([]) au fost explicate ca precizând indexul unui
element al tabloului. Ei bine, de fapt aceste paranteze sunt un operator de dereferențiere cunoscut
ca operatorul offset. Parantezele dereferențiază variabila pe care o succed exact cum face și *,
dar cuprind și un număr în interiorul lor, număr care precizează adresa ce trebuie dereferențiată. De
exemplu:
a[5]=0; // a [offset of 5]=0
*(a+5)=0; // pointed by (a+5)=0
Aceste două expresii sunt echivalente și valide, nu numai dacă a este un pointer, dar și dacă a este un
tablou. Să ne amintim că dacă este un tablou numele său poate fi folosit exact ca un pointer către primul
său element.
Inițializarea pointerilor
Starea variabilelor după acest cod este aceeași ca după a codului următor:
int variabila_mea;
int * pointerul_meu;
pointerul_meu=&variabila_mea;
Asteriscul (*) din declarația pointerului (linia 2) indică doar faptul că este un pointer și nu este
operatorul de dereferențiere (ca în linia 3). Este doar o coincidență folosirea aceluiași simbol: *. Ca de
obicei, spațiile nu sunt relevante și nu schimbă semnificația expresiei.
Pointerii pot fi inițializați atât cu adresa unei variabile (ca în cazul de mai sus), cât și cu valoarea unui alt
pointer (sau tablou):
int variabila_mea;
int *foo=&variabila_mea;
int *bar=foo;
Aritmetica pointerilor
Operațiile realizate cu pointeri sunt puțin diferite de cele realizate cu numere întregi. Aici numai
adunsarea și scăderea sunt permise; celelalte nu au sens în lucrul cu pointeri. Dar atât adunarea cât și
scăderea se comportă diferit cu pointerii, în funcție de tipul de dată spre care aceștia pointează.
Această regulă se aplică atât la adunarea, cât și la scăderea unui număr la un pointer.
S-ar fi întâmplat exact la fel dacă am fi scris:
mychar=mychar + 1;
myshort=myshort + 1;
mylong=mylong + 1;
In ceea ce privește operatorii de incrementare (++) și decremantare (--), ambii pot fi folosiți atăt ca
prefixe, cât și ca sufixe ale unei expresii, dar cu o ușoară diferență în comportament: ca prefix,
incrementarea se realizează înainte ca expresia să fie evaluată, iar ca sufix, incrementarea se face după
ce expresia este evaluată. La fel se aplică expresiilor de incrementare sau decrementare a pointerilor,
care pot aparține unor expresii mai complicate, conținând la rândul lor operatori de dereferențiere (*).
De la regulile de precedență pentru operatori, să ne amintim că operatorii postfixați, precum cel de
incrementare și decrementare, au prioritate față de operatorii prefixați precum operatorul de
dereferențiere (*).
De aceea, următoarea expresie:
*p++
este echivalentă cu *(p++). Iar efectul este este de a crește valoarea lui p (așa că el pointează acum
spre următorul element), dar deoarece ++ este folosit în forma postfixată, întreaga expresie este
evaluată cu valoarea spre care a pointat inițial (adresa spre care pointa înainte de a fi incrementat).
În esență, există patru combinații ale operatorului de deferențiere cu operatorul de incrementare atât în
forma prefixată cât și în cea postfixată (același lucru și pentru operatorul de decrementare):
O expresie tipică, dar nu chiar simplă, care implică acești operatori este:
*p++=*q++;
Deoarece ++ are prioritate față de *, atât p cât și q sunt incrementate, dar pentru că ambii operatori (++)
sunt folosiți în forma postfixată și nu prefixată, valoarea atribuită lui *p este *q înainte de a se
incrementa atât p cât și q. Apoi sunt incrementate ambele. Ar fi echivalent cu:
*p=*q;
++p;
++q;
Pointerii pot fi folosiți pentru a accesa o variabilă prin adresa sa, iar acest acces poate include și
modificarea valorii spre care pointează. De asemenea, este posibilă declararea de pointeri care să
pointeze spre o anumită valoare, dar fără să o modifice. Pentru aceasta, este suficientă marcarea tipului
spre care pointează cu const. De exemplu:
int x;
int y=10;
const int * p=&y;
x=*p; // ok: citirea lui p
*p=x; // eroare: modificarea lui p,
care este marcat cu const
Aici p pointează spre o variabilă, dar este marcat cu const, ceea ce înseamnă că poate citi valoarea
spre care pointează, însă nu o poate și modifica. Să remarcăm, de asemenea, că expresia &y este de
tip int*, dar este atribuită unui pointer de tip const int*. Acest lucru este permis: un pointer
către non-const poate fi convertit implicit la un pointer către const. Dar conversia nu merge și în
sens invers! Ca o măsură de siguranță, pointerii către const nu se convertesc implicit la pointeri către
non-const.
Unul din cazurile de folosire a pointerilor către elemente const îl reprezintă parametrii funcțiilor: o
funcție care are ca parametru un pointer către non-const poate modifica valoarea transmisă ca
parametru, în timp ce o funcție care are ca parametru un pointer către const nu poate schimba
valoarea parametrului.
// pointerii ca parametri: 11
#include <iostream> 21
using namespace std; 31
int main ()
{
int numere[]={10,20,30};
increment_all (numere,numere+3);
print_all (numere,numere+3);
return 0;
}
And this is where a second dimension to constness is added to pointers: Pointers can also be themselves
const. And this is specified by appending const to the pointed type (after the asterisk):
Să vedem o altă dimensiune a invarianței pointerilor: pointerii pot fi ei însuși constanți. Acest lucru se
poate specifica marcând cu modificatorul const chiar tipul de dată spre care pointează (după asterisc):
int x;
int * p1=&x; // non-const pointer către non-const int
const int * p2=&x; // non-const pointer către const int
int * const p3=&x; // const pointer către non-const int
const int * const p4=&x; // const pointer către const int
Sintaxa cu const și pointeri este foarte înșelătoare și identificarea cazului potrivit fiecărei situații
necesită ceva experiență. în orice caz, este important să acordăm invarianță pointerilor (și referințelor)
mai bine mai devreme decăt prea tărziu. Dar nu trebuie să vă faceți prea multe griji dacă este prima dată
când lucrați cu marcatorul const și pointeri. În capitolele următoare vom arăta mai multe exemple.
Ca și spațiile din jurul asteriscului, poziția lui const în acest caz este ține doar de stil. Acest capitol
folosește prefixul const doar pentru că în timp s-a dovedit a fi mai utilizat, dar formele sunt
echivalente. Avantajele fiecărui stil sunt încă intens dezbătute pe internet.
Pointeri și literali de tip string
Așa acum am arătat mai înainte, literalii string sunt tablouri conținând secvențe de caractere terminate
cu caracterul nul. În secțiunile anterioare, literalii string au fost folosiți pentru a fi inserați direct
în cout, pentru a inițializa string-uri și tablouri de caractere.
Dar pot fi accesați și direct. Literalii string sunt tablouri având ca tip de bază acel tip de dată care conține
șiruri de caractere terminate cu caracterul nul, iar fiecare element al tabloului este de
tipul const char (ca literali, ele nu pot fi modificate niciodată). De exemplu:
const char * foo="hello";
Această secvență declară un tablou care reprezintă literalul "hello", deci un pointer către primul său
element se atribuie lui foo. Dacă presupunem că "hello" este stocat într-o locație de memorie
începând cu adresa 1702, putem reprezenta declarația anterioară astfel:
Să observăm că aici foo este un pointer care conține valoarea 1702, nu este 'h' și nici "hello", deși
1702 este într-adevăr adresa amândurora.
Pointeri de pointeri
C++ permite folosrea pointerilor care pointează către pointeri, care, la răndul lor, pointează către o dată
(sau chiar către alți pointeri). Sintaxa implică doar un asterisc (*) pentru fiecare nivel de direcționare în
declarația pointerului:
char a;
char * b;
char ** c;
a='z';
b=&a;
c=&b;
Presupunând că au fost alese aleator locațiile de memorie pentru fiecare variabilă
la 7230, 8092 și 10502, s-ar putea reprezenta astfel:
unde valoarea fiecărei variabile este scrisă în interiorul fiecărei celule, iar adresa ocupată în memorie
este scrisă dedesubt.
Noutatea în acest exemplu o reprezintă variabila c, care este un pointer către un pointer și și poate fi
folosit în trei niveluri de direcționare, fiecare nivel corespunzând unei valori diferite:
c este de tip char** și are valoarea 8092
*c este de tip char* și are valoarea 7230
**c este de tip char și are valoarea 'z'
Pointeri void
Tipul void de pointer este un tip special de pointer. În C++, void reprezintă absența tipului de dată.
De aceea, pointerii void sunt pointeri care pointează către o valoare fără tip (și deci are lungime
nedeterminată și proprietăți de dereferențiere nedeterminate).
Aceasta dă pointerilor void o mare flexibilitate, căci sunt capabili să pointeze către orice tip de dată, de
la o valoare întreagă sau reală la un șir de caractere. În schimb, au o mare constrângere: informația
pointată de ei nu poate fi dereferențiată direct (ceea ce este logic, căci nu avem tip pe care să îl
dereferențiem) și din acest motiv orice adresă dintr-un pointer void trebuie să fie transformată într-un
alt tip de pointer care pointează către un tip de dată concret ce poate fi dereferențiat.
Una dintre posibilele utilizări ar fi transmiterea de parametri generici unei funcții. De exemplu:
// increaser y, 1603
#include <iostream>
using namespace std;
int main ()
{
char a='x';
int b=1602;
increase (&a,sizeof(a));
increase (&b,sizeof(b));
cout<<a<<", "<<b<<'\n';
return 0;
}
sizeof este un operator definit în limbajul C++ care returnează dimensiunea în bytes a argumentului.
Pentru tipurile de date nedinamice, această valoare este o constantă. De aceea, de
exemplu, sizeof(char) este 1, deoarece char are întotdeauna exact un byte.
În principiu, pointerii sunt concepuți pentru a pointa către adrese valide, precum adresa unei variabile
sau adresa unui element într-un tablou. Dar, de fapt, pointerii pot pointa către orice adresă, inclusiv
adrese care nu se referă la niciun element valid. Exemple tipice de acest fel sunt pointerii neinițializați și
pointeri către elemente inexistente ale unui tablou:
int * p; // pointer neinițializat (variabilă locală)
int tabloul_meu[10];
int * q=tabloul_meu+20; // element din afara limitei
Nici p nici q nu pointează către adrese cunoscute care să conțină valori, dar niciuna dintre instrucțiunile
de mai sus nu generează vreo eroare. În C++, pointerii pot să rețină orice adresă, indiferent dacă este sa
nu memorat ceva la acea adresă. Ceea ce ar putea cauza o eroare ar fi dereferențierea unui asemenea
pointer (adică, încercarea de a accesa valoarea către care pointează). Accesarea unui asemenea pointer
poate cauza un comportament imprevizibil, de la o eroare în timpul execuției până la accesarea unei
valori aleatorii.
Dar, uneori, avem nevoie de un pointer care să nu pointeze către ceva anume, nu doar către o adresă
invalidă. Pentru asemenea situații axistă o valoare specială pe care o poate lua un pointer indiferent de
tip: valoarea pointer nul. Această valoare poate fi exprimată în C++ în două moduri: fie prin valoarea
întreagă zero, fie prin cuvăntul cheie nullptr:
int * p=0;
int * q=nullptr;
Aici, atât p cât și q sunt pointeri nuli, ceea ce înseamnă că ei în mod explicit nu pointează către ceva
anume și chiar sunt egali înre ei: toți pointerii nuli sunt egali cu toți ceilalți pointeri nuli. De asemenea, în
programele mai vechi se obișnuia folosirea constantei NULL definită pentru a se referi valoarea pointer
nul:
int * r=NULL;
NULL este definită în câteva fișiere antet din biblioteca standard și reprezintă un alias pentru valoarea
constantă pointer nul (la fel ca 0 sau nullptr).
C++ permite operații cu pointeri către funcții. Tipică este transmiterea unei funcții ca parametru pentru
o altă funcție. Pointerii către funcții sunt declarați cu aceeași sintaxă ca a unei declarații obișnuite de
funcție, cu excepția faptului că numele funcției este scris între paranteze () și se inserează un asterisc (*)
înaintea numelui:
int main ()
{ int m,n;
int (*minus)(int,int)=scadere;
În exemplul de mai sus, minus este un pointer către o funcție care are doi parametri de tip int. Este
inițializat să pointeze către funcția scadere:
int (* minus)(int,int)=scadere;