Aritmetica pointerilor
Pointerii pot participa la operații aritmetice (adunare, scădere, incrementare, decrementare). Totuși, aceste operații se efectuează puțin diferit față de cele cu numere, iar comportamentul depinde în mare măsură de tipul pointerului.
La un pointer se poate adăuga un număr întreg, la fel cum se poate scădea un număr întreg din el. În plus, se poate scădea un pointer din alt pointer de același tip.
Să analizăm mai întâi operațiile de incrementare și decrementare, folosind un pointer la un obiect de tip int:
#include <iostream>
int main()
{
int n{10};
int *pn {&n};
std::cout << "address=" << pn << "\tvalue=" << *pn << std::endl;
pn++;
std::cout << "address=" << pn << "\tvalue=" << *pn << std::endl;
pn--;
std::cout << "address=" << pn << "\tvalue=" << *pn << std::endl;
}
Оperația de incrementare ++ mărește valoarea cu o unitate. În cazul unui pointer, incrementarea cu o unitate va însemna mărirea adresei stocate în pointer cu dimensiunea tipului pointerului. Adică, în acest caz, pointerul este către tipul int, iar dimensiunea obiectelor int pe majoritatea arhitecturilor este de 4 bytes. Prin urmare, incrementarea unui pointer de tip int cu o unitate înseamnă creșterea adresei din pointer cu 4. Astfel, în cazul meu, ieșirea în consolă arată în felul următor:
address=0x81315ffd84 value=10
address=0x81315ffd88 value=828374408
address=0x81315ffd84 value=10
Aici se poate observa că, după incrementare, valoarea pointerului a crescut cu 4: de la 0x81315ffd84 la 0x81315ffd88. Iar după decrementare, adică micșorare cu o unitate, pointerul a revenit la adresa anterioară în memorie. De fapt, incrementarea cu o unitate înseamnă că dorim să trecem la următorul obiect în memorie, care se află imediat după cel curent, la care pointează pointerul. Scăderea cu o unitate înseamnă revenirea la obiectul anterior din memorie.
După modificarea adresei, putem obține valoarea aflată la noua adresă, însă această valoare poate fi nedeterminată, așa cum s-a arătat în exemplul de mai sus.
În cazul unui pointer de tip int, incrementarea/decrementarea cu o unitate înseamnă modificarea adresei cu 4. Similar, pentru un pointer de tip short, aceste operații ar modifica adresa cu 2, iar pentru un pointer de tip char — cu 1.
#include <iostream>
int main()
{
double d {10.6};
double *pd {&d};
std::cout << "Pointer pd: address:" << pd << std::endl;
pd++; // creștere a adresei cu 8 bytes – dimensiunea tipului double
std::cout << "Pointer pd: address:" << pd << std::endl;
short n {5};
short *pn {&n};
std::cout << "Pointer pn: address:" << pn << std::endl;
pn++; // creștere a adresei cu 2 bytes – dimensiunea tipului short
std::cout << "Pointer pn: address:" << pn << std::endl;
}
În cazul meu, ieșirea în consolă va arăta în felul următor:
Pointer pd: address:0x2731bffd58
Pointer pd: address:0x2731bffd60
Pointer pn: address:0x2731bffd56
Pointer pn: address:0x2731bffd58
După cum se vede din ieșirea în consolă, incrementarea cu o unitate a unui pointer de tip double a dus la creșterea adresei stocate în el cu 8 unități (dimensiunea unui obiect double este 8 bytes), iar incrementarea cu o unitate a unui pointer de tip short a dus la creșterea adresei cu 2 (dimensiunea tipului short este 2 bytes).
În mod similar, pointerul va fi modificat și atunci când se adună/scade nu cu 1, ci cu orice alt număr întreg.
#include <iostream>
int main()
{
double d {10.6};
double *pd {&d};
std::cout << "Pointer pd: address:" << pd << std::endl;
pd = pd + 2; // creștere a adresei cu 16 bytes – 2 obiecte double
std::cout << "Pointer pd: address:" << pd << std::endl;
short n {5};
short *pn {&n};
std::cout << "Pointer pn: address:" << pn << std::endl;
pn = pn - 3; // scădere a adresei cu 6 bytes – 3 obiecte short
std::cout << "Pointer pn: address:" << pn << std::endl;
}
Adunarea numărului 2 la un pointer de tip double:
pd = pd + 2;
înseamnă că dorim să trecem cu două obiecte double înainte, ceea ce implică modificarea adresei cu 2 * 8 = 16 bytes.
Scăderea numărului 3 dintr-un pointer de tip short:
pn = pn - 3;
înseamnă că dorim să ne întoarcem cu trei obiecte short înapoi, ceea ce implică modificarea adresei cu 3 * 2 = 6 bytes.
În cazul meu, voi obține următoarea ieșire în consolă:
Pointer pd: address:0xb88d5ffbe8
Pointer pd: address:0xb88d5ffbf8
Pointer pn: address:0xb88d5ffbe6
Pointer pn: address:0xb88d5ffbe0
Spre deosebire de adunare, operația de scădere poate fi aplicată nu doar între un pointer și un număr întreg, ci și între doi pointeri de același tip:
#include <iostream>
int main()
{
int a{10};
int b{23};
int *pa {&a};
int *pb {&b};
auto ab {pa - pb};
std::cout << "pa: " << pa << std::endl;
std::cout << "pb: " << pb << std::endl;
std::cout << "ab: " << ab << std::endl;
}
Conform standardului, diferența dintre doi pointeri este de tip std::ptrdiff_t, care, în realitate, este un alias pentru tipurile int, long sau long long. Tipul concret utilizat pentru stocarea diferenței depinde de platformă. De exemplu, pe Windows 64x acesta este long long.
De aceea, variabila ab, care stochează diferența dintre adrese, este declarată cu ajutorul operatorului auto.
Ieșirea în consolă în cazul meu:
pa: 0x6258fffab4
pb: 0x6258fffab0
ab: 1
Rezultatul diferenței dintre cei doi pointeri reprezintă distanța dintre ei. De exemplu, în cazul de mai sus, adresa din primul pointer este cu 4 mai mare decât adresa din al doilea pointer (0x6258fffab0 + 4 = 0x6258fffab4). Deoarece dimensiunea unui obiect de tip int este 4 bytes, distanța dintre pointeri va fi:
(0x6258fffab4 - 0x6258fffab0) / 4 = 1
Unele particularități ale operațiilor
Când lucrăm cu pointeri, este important să facem distincția între operațiile efectuate asupra pointerului însuși și cele efectuate asupra valorii de la adresa la care pointează pointerul.
int a {10};
int *pa {&a};
int b {*pa + 20}; // operație asupra valorii la care pointează pointerul
pa++; // operație asupra pointerului însuși
std::cout << "b: " << b << std::endl; // 30
Adică, în acest caz, prin operația de dereferențiere *pa obținem valoarea la care pointează pointerul pa, adică numărul 10, și efectuăm o operație de adunare. Deci este o operație obișnuită de adunare între două numere, deoarece expresia *pa reprezintă un număr.
Totuși, există și unele particularități, în special legate de operațiile de incrementare și decrementare. Acest lucru se datorează faptului că operatorii *, ++ și -- (în forma prefixată) au același nivel de prioritate și, atunci când sunt alăturați, se evaluează de la dreapta la stânga.
De exemplu, să efectuăm o incrementare prefixată:
int a {10};
int *pa {&a};
std::cout << "pa: address=" << pa << "\tvalue=" << *pa << std::endl;
int b {++*pa}; // incrementarea valorii de la adresa pointerului
std::cout << "b: value=" << b << std::endl;
std::cout << "pa: address=" << pa << "\tvalue=" << *pa << std::endl;
În expresia b {++*pa};, mai întâi are loc dereferențierea pointerului — obținem valoarea de la adresa stocată în pa, adică numărul 10. Apoi, la această valoare se adaugă 1. Și în cazul meu, rezultatul execuției este următorul:
pa: address=0x7ff7b31bd8b8 value=10
b: value=11
pa: address=0x7ff7b31bd8b8 value=11
Modificăm expresia:
int b{*++pa}; // incrementarea adresei pointerului urmată de dereferențiere
Acum, mai întâi se incrementează adresa pointerului (adică adresa se mărește cu 4, deoarece pointerul este de tip int), apoi se face dereferențierea — se obține valoarea aflată la noua adresă, care este atribuită variabilei b.
Valoarea obținută în acest caz poate fi nedeterminată, deoarece nu avem garanția că la acea adresă se află un obiect valid.
Ieșire:
pa: address=0x7ff7b13d78b8 value=10
b: value=0
pa: address=0x7ff7b13d78bc value=0
Spre deosebire de prefixatele de incrementare și decrementare, versiunile postfixate ale operațiilor au o prioritate mai mare decât operația de dereferențiere *. De exemplu, să luăm următorul program:
int a {10};
int *pa {&a};
std::cout << "pa: address=" << pa << "\tvalue=" << *pa << std::endl;
int b{*pa++}; // incrementarea adresei pointerului cu dereferențiere ulterioară
std::cout << "b: value=" << b << std::endl;
std::cout << "pa: address=" << pa << "\tvalue=" << *pa << std::endl;
Deoarece incrementarea postfixată are o prioritate mai mare, în expresia *pa++ mai întâi se mărește adresa pointerului pa cu o unitate (din nou, de fapt cu 4, deoarece pointerul este de tip int), iar apoi se obține valoarea de la adresă. Totuși, deoarece incrementarea postfixată returnează valoarea de dinaintea creșterii, în variabila b vom obține valoarea care era la adresă înainte de incrementare. De exemplu, ieșirea în consolă în cazul meu:
pa: address=0x7ff7b55288b8 value=10
b: value=10
pa: address=0x7ff7b55288bc value=0
Modificăm expresia:
b {(*pa)++};
Parantezele schimbă ordinea operațiilor. Aici mai întâi se execută operația de dereferențiere și obținere a valorii, apoi această valoare este mărită cu 1. Acum, la adresa stocată în pointer se află numărul 11. Iar apoi, deoarece incrementarea este postfixată, variabila b primește valoarea de dinaintea incrementării, adică din nou numărul 10. Astfel, spre deosebire de cazul anterior, toate operațiile se efectuează asupra valorii de la adresa stocată în pointer, și nu asupra pointerului propriu-zis. Prin urmare, rezultatul programului va fi diferit:
pa: address=0x7ff7b7b268b8 value=10
b: value=10
pa: address=0x7ff7b7b268b8 value=11