Parametri C++: valoare, referință și tablouri
Compară transmiterea prin valoare și prin referință în C++. Vezi cum modifici o variabilă și cum trimiți tablouri clasice sau std::vector unei funcții.
În acest material
Când apelezi o funcție, contează dacă îi trimiți o copie a unei valori sau îi permiți să lucreze cu variabila originală. Alegerea face parte din contractul funcției. Cititorul trebuie să poată înțelege dacă apelul doar calculează un rezultat sau modifică datele primite.
Parcurge întâi definirea și apelul funcțiilor. Începem cu întregi, apoi vedem de ce tablourile clasice au un comportament diferit de cel al unui parametru simplu de tip int.
Prin valoare: funcția primește o copie
În int cresteCopie(int x), parametrul x este o variabilă locală inițializată cu valoarea argumentului. Instrucțiunea ++x schimbă această copie. Variabila din funcția apelantă își păstrează valoarea. Funcția întoarce copia mărită prin return x, iar apelantul poate păstra acest rezultat separat.
Pentru a schimba originalul folosind rezultatul, apelantul ar trebui să îi atribuie explicit valoarea întoarsă. Transmiterea prin valoare nu împiedică obținerea unui rezultat nou; doar evită modificarea directă a variabilei originale prin parametrul local.
Prin referință: funcția lucrează cu originalul
În void cresteOriginal(int& x), simbolul & face din x un parametru referință. Tipul void arată că această funcție nu întoarce o valoare. Parametrul x este un alt nume pentru variabila transmisă. Dacă îl mărești, modificarea este vizibilă și după încheierea apelului.
Aceeași instrucțiune ++x are efect diferit în funcție de declararea parametrului.
Programul citește n, între -1000000000 și 1000000000. Pe prima linie afișează copia mărită și originalul rămas după apelul prin valoare. Pe a doua linie afișează originalul după apelul prin referință. Limita permite incrementarea fără depășirea unui int de 32 de biți.
#include <iostream>
int cresteCopie(int x) {
++x;
return x;
}
void cresteOriginal(int& x) {
++x;
}
int main() {
int n = 0;
std::cin >> n;
int copieCrescuta = cresteCopie(n);
std::cout << copieCrescuta << ' ' << n << '\n';
cresteOriginal(n);
std::cout << n << '\n';
return 0;
}
Pentru n = 4, prima linie este 5 4, iar a doua este 5. Ambele linii sunt afișate în main: prima compară rezultatul returnat cu originalul încă neschimbat. Astfel verificăm exact efectul care rămâne la apelant.
O referință neconstantă la int trebuie legată de o variabilă potrivită. Apelul cresteOriginal(4) nu este valid: literalul nu este o variabilă pe care funcția să o poată modifica în acest mod. Nu confunda & din declarația parametrului cu o operație pe biți într-o expresie.
Tablouri clasice: trimitem și lungimea
Într-un parametru de funcție, declarația const int a[] este ajustată la un pointer către elemente. Nu se copiază automat întregul tablou și nu se transmite automat numărul de elemente utile. Trimitem separat n. Calificatorul const interzice modificarea elementelor prin acest parametru.
Următorul program citește n, cu 0 <= n <= 1000, apoi n valori între -1000000 și 1000000. Funcția întoarce suma în long long. Pentru un tablou fără elemente utile, suma este zero. Capacitatea și lungimea logică trebuie respectate de apelant.
Varianta cu tablou clasic
#include <iostream>
long long suma(const int a[], int n) {
long long rezultat = 0;
for (int i = 0; i < n; ++i) {
rezultat += a[i];
}
return rezultat;
}
int main() {
int n = 0;
std::cin >> n;
int a[1000]{};
for (int i = 0; i < n; ++i) {
std::cin >> a[i];
}
std::cout << suma(a, n) << '\n';
return 0;
}
Nu folosi sizeof(a) în corpul funcției pentru a afla lungimea tabloului original. Acolo parametrul nu mai este obiectul tablou declarat în main. Fără const, o atribuire către a[i] ar putea modifica elementul din tabloul apelantului, chiar dacă nu am scris o referință cu &.
Varianta cu std::vector
Transmitem const std::vector<int>&: referința evită copierea containerului, iar const permite doar citirea prin acel parametru. Funcția poate afla lungimea din container, deci nu mai are nevoie de n separat. În main păstrăm exact aceleași limite și același format de intrare.
#include <iostream>
#include <vector>
long long suma(const std::vector<int>& a) {
long long rezultat = 0;
for (int x : a) {
rezultat += x;
}
return rezultat;
}
int main() {
int n = 0;
std::cin >> n;
std::vector<int> a(n);
for (int i = 0; i < n; ++i) {
std::cin >> a[i];
}
std::cout << suma(a) << '\n';
return 0;
}
Alegerea potrivită și costul
Pentru întregi mici, transmiterea prin valoare este potrivită când funcția nu trebuie să modifice argumentul. Folosește o referință neconstantă când modificarea este chiar scopul operației. Pentru un container citit fără modificări, referința constantă evită o copie inutilă și exprimă intenția.
Funcțiile de incrementare au timp și spațiu O(1). Suma parcurge n elemente în O(n) și folosește un acumulator, deci O(1) spațiu suplimentar față de date. Transmiterea vectorului prin valoare ar adăuga copierea celor n elemente; varianta prezentată evită acel cost.
Exersează
- Ce afișează primul program pentru
-3? - Poate
sumamodificaa[0]în variantele prezentate? - De ce tabloul clasic primește separat
n? - Ce sumă întoarce funcția pentru
n = 0?
Răspunsuri: -2 -3, apoi -2; nu prin parametrul declarat const; lungimea logică nu este inclusă în parametrul pointer; zero. Un apel cu lungimea mai mare decât capacitatea ar încălca precondiția și poate produce acces invalid.
Continuă cu apeluri recursive.
Un pas mai departe
Se încarcă progresul…