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
.&
), cunoscut ca operatorul de adresare. De exemplu:
|
|
foo
adresa variabilei variabila_mea
; punând în fața numelui variabilei variabila_mea
operatorul de adresare (&
), nu mai atribuim conținutul variabilei, ci chiar adresa ei lui foo
.variabila_mea
se găsește în timpul rulării la adresa 1776
.
|
|
25
lui variabila_mea
(o variabilă a cărei adresă de memorie am presupus că este 1776
).foo
adresa lui variabila_mea
, pe care am presupus-o a fi 1776
.variabila_mea
lui bar
. Aceasta este o operație obișnuită de atribuire, așa cum am făcut de mai multe ori în capitolele anterioare.&
).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.*
). Operatorul însuși poate fi citit ca "valoarea spre care pointează".
|
|
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
.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):
|
|
&
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ă"&
poate fi dereferențiată cu *
.
|
|
|
|
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
.foo
rămâne neschimbată , următoarea expresie are tot valoarea true:
|
|
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.tip* nume;
tip
este tipul de dată spre care pointează pointerul. Acesta nu este tipul pointerului însuși, ci tipul datei spre care pointează acesta. De exemplu:
|
|
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ă.*
) 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.
|
|
valoare_1 este 10 valoare_2 este 20 |
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ă:pointerul_meu
primește adresa lui valoare_1 prin folosirea operatorului adresă (&
). Apoi, variabilei spre care pointează pointerul_meu
i se atribuie valoarea 10
. Deoarece, în acest moment, pointerul_meu
pointează spre zona de memorie a lui valoare_1
, se va schimba chiar valoarea lui valoare_1
.valoare_2
și același pointer, pointerul_meu
.
|
|
valoare_1 este 10 valoare_2 este 20 |
&
) cu "adresa lui", respectiv a lui asterisk (*
) cu "valoarea spre care pointeaza".p1
și p2
, cu și fără operatorul de deferențiere (*
). Semnificația unei expresii care folosește operatorul de dereferențiere (*) este foarte diferită față de una care nu îl folosește. Când acest operator precede numele pointerului, expresia se referă la valoarea spre care se pointează, în timp ce numele unui pointer fără acest operator se referă chiar la valoarea pointerului (adică, adresa pe care o indică pointerul respectiv).
|
|
*
) pentru fiecare pointer, astfel încât ambii să fie tip int*
(pointer spre int
). Este necesar datorită regulilor de precedență. Să remarcăm că dacă am fi avut codul:
|
|
p1
ar fi fost de tip int*
, dar p2
ar fi fost de tip int
. Spațiile nu au nici o 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ă.
|
|
|
|
pointerul_meu
și tabloul_meu
ar putea fi echivalente și ar avea proprietăți asemănătoare. Principala diferență constă în faptul că pointerul_meu
poate primi o nouă adresă,în timp ce tabloul_meu
nu-și poate schimba adresa și va reprezenta întotdeauna același bloc de 20 de elemente de tip int
. De aceea, atribuirea următoare nu este validă:
|
|
|
|
10, 20, 30, 40, 50, |
[]
) 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
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.
|
|
|
|
pointerul_meu
), nu valoarea reținută la acea adresă (i.e., *pointerul_meu
). De aceea, să nu confundăm codul de mai sus cu următorul:
|
|
*
) 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.
|
|
char
ocupă întotdeauna 1 byte, short
este în general mai larg de atăt, iar int
și long
sunt chiar mai largi; dimensiunea exactă depinde de sistem. De exemplu, să ne imaginăm că într-un anumit sistem, char
are 1 byte, short
are 2 bytes și long
are 4.
|
|
1000
, 2000
și respectiv 3000
.
|
|
mychar
, așa cum ne așteptăm, va conține valoarea 1001. Dar nu la fel de clar, myshort
ar putea conține valoarea 2002 și mylong
ar conține 3004, chiar dacă fiecare dintre ele a fost incrementată o singură dată. Motivul este că adunând unu la un pointer, el va pointa către următorul element de același tip și, de aceea, se adaugă numărul de octeți (bytes) ocupați de tipul spre care pointează.
|
|
++
) ș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++)
. 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).
|
|
|
|
++
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:
|
|
const
. De exemplu:
|
|
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
.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.
|
|
11 21 31 |
print_all
folosește pointeri care pointează către elemente constante. Acești pointeri nu pot schimba conținutul, dar ei însuși nu sunt constanți: adică, pointerii pot fi incrementați și li se pot atribui diverse adrese, deși nu pot schimba valorile memorate la adresele spre care pointează.
|
|
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.const
, marcatorul const
poate precede sau urma tipului de dată către care pointează, dar sensul rămâne același:
|
|
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.cout
, pentru a inițializa string-uri și tablouri de caractere.const char
(ca literali, ele nu pot fi modificate niciodată). De exemplu:
|
|
"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: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.foo
pointează către o secvență de caractere. Si pentru că pointerii și tablourile se comportă asemănător în expresii, foo
poate fi folosit pentru a accesa caracterele în același fel ca tablourile de secvențe de caractere terminate cu caracterul nul. De exemplu:
|
|
'o'
(al cincilea element al tabloului).*
) pentru fiecare nivel de direcționare în declarația pointerului:
|
|
7230
, 8092
și 10502
, s-ar putea reprezenta astfel: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'
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).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.
|
|
y, 1603 |
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.
|
|
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.nullptr
:
|
|
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:
|
|
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
).void
! Un pointer nul este o valoare pe care o ia orice pointer care nu pointează nicăieri, în timp ce pointerul void
este un tip de pointer care poate pointa undeva, fără a i se asocia un anumit tip de dată. Unul se referă la valoarea memorată în pointerm iar celălalt la tipul de dată către care pointează.*
) înaintea numelui:
|
|
8 |
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
:
|
|
Previous: Șiruri de caractere | Index | Next: Memoria dinamică |