Pointeri

În capitolele anterioare, variabilele au fost explicate ca locații în memoria calculatorului ce pot fi accesate cu ajutorul indentificatorilor (numele lor). În acest fel, programul nu mai trebuie să aibă grijă de adresele fizice ale datelor din memorie; pur și simplu folosește identificatorul de fiecare dată când are nevoie să se refere la variabilă.

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.

Operatorul de adresare (&)

Adresa unei variabile poate fi obținută punând în fața numelui variabilei un simbol ampersand (&), cunoscut ca operatorul de adresare. De exemplu:

1
foo = &variabila_mea;

Aceasta atribuie lui 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.

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.

În acest caz, să considerăm următoarea secvență de cod:

1
2
3
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ă:



În primul rând, am atribuit valoarea 25 lui variabila_mea (o variabilă a cărei adresă de memorie am presupus că este 1776).

A doua instrucțiune atribuie lui foo adresa lui variabila_mea, pe care am presupus-o a fi 1776.

În fine, a treia instrucțiune atribuie valoarea memorată în 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.

Principala diferență între a doua și a treia instrucțiune este apariția operatorului de adresare (&).

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.

Operatorul de deferențiere (*)

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:

1
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):

1
2
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ă"

Așadar, ei au sensuri opuse: O adresă obținută cu & poate fi dereferențiată cu *.

Mai devreme, am executat următoarele două operații:

1
2
variabila_mea = 25;
foo = &variabila_mea;

Chiar după aceste două instrucțiuni, toate expresiile de mai jos au ca rezultat valoarea true:

1
2
3
4
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:

1
*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.

Declarația pointerilor respectă următoarea sintaxă:

tip* nume;

unde 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:

1
2
3
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.

Să vedem un exemplu cu pointeri:

// primul meu pointer
#include <iostream>
using namespace std;

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;
}
valoare_1 este 10
valoare_2 este 20

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ă:

Mai întâi, 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.

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.

Iată un exemplu puțin mai complicat:

// mai multi pointeri
#include <iostream>
using namespace std;

int main ()
{
  int valoare_1 = 5, valoare_2 = 15;
  int * p1, * p2;

  p1 = &valoare_1;  // p1 = adresa lui valoare_1
  p2 = &valoare_2;  // p2 = adress lui valoare_2
  *p1 = 10;         // valoarea spre care pointeaza p1 = 10
  *p2 = *p1;        // valoarea spre care pointeaza p2 = valoarea spre care pointeaza p1
  p1 = p2;          // p1 = p2 (este copiata valoarea pointer-ului)
  *p1 = 20;         // valoarea spre care pointeaza p1 = 20
  
  cout << "valoare_1 este " << valoare_1 << '\n';
  cout << "valoare_2 este " << valoare_2 << '\n';
  return 0;
}
valoare_1 este 10
valoare_2 este 20

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".

Să remarcăm că avem expresii cu pointerii 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).

Un alt lucru de care trebuie să aveți grijă este linia:

1
int * p1, * p2;

Aceasta declară cei doi pointeri utilizați în exemplul anterior. Dar să observăm că este câte un asterisk (*) 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:

1
int * p1, p2;

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ă.

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:

1
2
int tabloul_meu [20];
int * pointerul_meu;

Următoarea operație de atribuire este validă:

1
pointerul_meu = tabloul_meu;

Acum, 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ă:

1
tabloul_meu = pointerul_meu;

Să vedem un exemplu care folosește și tablouri și pointeri:

// mai multi pointeri
#include <iostream>
using namespace std;

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;
}
10, 20, 30, 40, 50, 

Pointerii și tablourile suportă același set de operații, având aceleași semnificații pentru ambele. Singura diferență o reprezintă faptul ca 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:

1
2
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

Pointerii pot fi inițializați chiar în momentul în care sunt definiți:

1
2
int variabila_mea;
int * pointerul_meu = &variabila_mea;

Starea variabilelor după acest cod este aceeași ca după a codului următor:

1
2
3
int variabila_mea;
int * pointerul_meu;
pointerul_meu = &variabila_mea;

La inițializarea unui pointer se inițializează de fapt adresa spre care pointează el (i.e., 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:

1
2
3
int variabila_mea;
int * pointerul_meu;
*pointerul_meu = &variabila_mea;

Care oricum nu prea are sens (și nu este o secvență validă).

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):

1
2
3
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ă.

Când am introdus tipurile de date fundamentale, am văzut că ele au mărimi diferite. De exemplu: 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.

Să presupunem acum că definim trei pointeri în acest compilator:

1
2
3
char *mychar;
short *myshort;
long *mylong;

și știm că pointează către locațiile de memorie 1000, 2000 și respectiv 3000.

De aceea, dacă scriem:

1
2
3
++mychar;
++myshort;
++mylong;

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ă.


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:

1
2
3
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:

1
*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):

1
2
3
4
*p++   // la fel ca *(p++): incrementeaza pointerul și  dereferentiaza adresa neincrementata
*++p   // la fel ca *(++p): incrementeaza pointerul si dereferentiaza adresa incrementata
++*p   // la fel ca ++(*p): dereferentiaza pointerul si incrementeaza valoarea spre care pointeaza
(*p)++ // dereferentiaza pointerul si post-incrementeaza valoarea spre care pointeaza 

O expresie tipică, dar nu chiar simplă, care implică acești operatori este:

1
*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:

1
2
3
*p = *q;
++p;
++q;

Ca de obicei, parantezele permit eliminarea confuziilor aducând lizibilitate expresiilor.

Pointeri și constante

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:

1
2
3
4
5
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:
#include <iostream>
using namespace std;

void increment_all (int* start, int* stop)
{
  int * current = start;
  while (current != stop) {
    ++(*current);  // incrementeaza valoarea pointata
    ++current;     // incrementeaza pointerul
  }
}

void print_all (const int* start, const int* stop)
{
  const int * current = start;
  while (current != stop) {
    cout << *current << '\n';
    ++current;     // incrementeaza pointerul
  }
}

int main ()
{
  int numere[] = {10,20,30};
  increment_all (numere,numere+3);
  print_all (numere,numere+3);
  return 0;
}
11
21
31

Să observăm că 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ă.

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):

1
2
3
4
5
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 să adăugăm și mai multă încurcătură la sintaxa pointerilor cu const, marcatorul const poate precede sau urma tipului de dată către care pointează, dar sensul rămâne același:

1
2
const int * p2a = &x;  //      non-const pointer către const int
int const * p2b = &x;  // tot non-const pointer către const int 

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:

1
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.

Pointerul 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:

1
2
*(foo+4)
foo[4]

Ambele expresii au valoarea 'o' (al cincilea element al tabloului).

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:

1
2
3
4
5
6
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ăriar fi transmiterea de parametri generici unei funcții. De exemplu:

// increaser
#include <iostream>
using namespace std;

void increase (void* data, int psize)
{
  if ( psize == sizeof(char) )
  { char* pchar; pchar=(char*)data; ++(*pchar); }
  else if (psize == sizeof(int) )
  { int* pint; pint=(int*)data; ++(*pint); }
}

int main ()
{
  char a = 'x';
  int b = 1602;
  increase (&a,sizeof(a));
  increase (&b,sizeof(b));
  cout << a << ", " << b << '\n';
  return 0;
}
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.

Pointeri invalizi și pointeri nuli

Î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:

1
2
3
4
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:

1
2
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:

1
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).

Să nu confundăm pointerii nuli cu pointerii 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ă.

Pointeri către funcții

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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// pointer către funcții
#include <iostream>
using namespace std;

int adunare (int a, int b)
{ return (a+b); }

int scadere (int a, int b)
{ return (a-b); }

int operation (int x, int y, int (*functocall)(int,int))
{
  int g;
  g = (*functocall)(x,y);
  return (g);
}

int main ()
{
  int m,n;
  int (*minus)(int,int) = scadere;

  m = operation (7, 5, adunare);
  n = operation (20, m, minus);
  cout <<n;
  return 0;
}
8

Î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:

1
int (* minus)(int,int) = scadere;
Index
Index