Tablouri în parametrii funcției
Dacă o funcție primește ca parametru un tablou, atunci de fapt în acea funcție se transmite un pointer către primul element al tabloului. Adică, la fel ca în cazul pointerilor, avem acces la adresa unde putem modifica valorile. Prin urmare, următoarele declarații de funcții sunt în esență echivalente:
void print(int numbers[]);
void print(int *numbers);
Acum, transmitem funcției un tablou:
#include <iostream>
void print(int[]);
int main()
{
int nums[] {1, 2, 3, 4, 5};
print(nums);
}
void print(int numbers[])
{
std::cout << "First number: " << numbers[0] << std::endl; // First number: 1
}
Acum definim parametrul ca pointer:
#include <iostream>
void print(int*);
int main()
{
int nums[] {1, 2, 3, 4, 5};
print(nums);
}
void print(int *numbers)
{
std::cout << "First number: " << *numbers << std::endl;
}
Limitări
Pentru că parametrul definit ca tablou este de fapt tratat ca pointer către primul element, nu vom putea obține corect lungimea tabloului astfel:
void print(int numbers[])
{
int size = sizeof(numbers) / sizeof(numbers[0]);
// sau
// size_t size = std::size(nums);
std::cout << size << std::endl;
}
De asemenea, nu putem folosi un for-each pentru a parcurge acest tablou:
void print(int numbers[])
{
for (int n : numbers)
std::cout << n << std::endl;
}
Transmiterea unui marcator de sfârșit al tabloului
Pentru a parcurge corect un tablou și a ști unde se termină, se poate folosi un marcator special. O metodă este ca un element din tablou să semnalizeze finalul acestuia. De exemplu, un tablou de caractere (șir de caractere) se termină cu caracterul nul '\0':
#include <iostream>
void print(char[]);
int main()
{
char chars[] {"Hello"};
print(chars);
}
void print(char chars[])
{
for (unsigned i{}; chars[i] != '\0'; i++)
{
std::cout << chars[i];
}
}
Totuși, acest mod presupune că știm sigur că există acel caracter de terminare. De aceea, de obicei se preferă transmiterea mărimii tabloului în funcție:
#include <iostream>
void print(int[], size_t);
int main()
{
int nums[]{1, 2, 3, 4, 5};
size_t n {std::size(nums)};
print(nums, n);
}
void print(int numbers[], size_t n)
{
for(size_t i {}; i < n; i++)
{
std::cout << numbers[i] << "\t";
}
}
O altă metodă constă în transmiterea unui pointer la sfârșitul tabloului. Putem calcula manual acest pointer sau putem folosi std::begin() și std::end():
int nums[] { 1, 2, 3, 4, 5 };
int *begin {std::begin(nums)};
int *end {std::end(nums)};
end returnează adresa imediat după ultimul element.
Acum, aplicăm aceste funcții:
#include <iostream>
void print(int*, int*);
int main()
{
int nums[] { 1, 2, 3, 4, 5 };
int *begin {std::begin(nums)};
int *end {std::end(nums)};
print(begin, end);
}
void print(int *begin, int *end)
{
for (int *ptr {begin}; ptr != end; ptr++)
{
std::cout << *ptr << "\t";
}
}
Tablouri constante
Fiindcă prin transmiterea unui tablou se transmite un pointer la primul element, putem modifica elementele. Dacă nu dorim modificarea, parametrul trebuie definit ca constant:
#include <iostream>
void print(const int*, const size_t);
void twice(int*, const size_t);
int main()
{
int numbers[]{1, 2, 3, 4, 5};
size_t n = std::size(numbers);
print(numbers, n);
twice(numbers, n); // dublăm valorile
print(numbers, n);
}
void print(const int numbers[], const size_t n)
{
for(size_t i {}; i < n; i++)
{
std::cout << numbers[i] << "\t";
}
std::cout << std::endl;
}
void twice(int *numbers, const size_t n)
{
for(size_t i {}; i < n; i++)
{
numbers[i] = numbers[i] * 2;
}
}
Transmiterea tabloului prin referință
Putem transmite un tablou prin referință astfel:
void print(int (&)[], size_t);
Atenție la parantezele (&), care indică faptul că este o referință la tablou:
#include <iostream>
void print(int (&)[], size_t);
int main()
{
int nums[] {1, 2, 3, 4, 5};
size_t count = std::size(nums);
print(nums, count);
}
void print(int (&numbers)[], size_t count)
{
for(size_t i{}; i < count; i++)
{
std::cout << numbers[i] << "\t";
}
}
Putem de asemenea transmite referințe constante la tablouri:
void print(const int (&)[]);
Deși pare inutil, având în vedere că prin valoare se transmite adresa, folosirea referinței are avantaje. Nu se copiază adresa, iar dimensiunea poate fi restricționată la compilare:
#include <iostream>
void print(const int (&)[5]); // tablou cu exact 5 elemente
int main()
{
int nums1[] {1, 2, 3, 4, 5};
print(nums1);
}
void print(const int (&numbers)[5])
{
for(unsigned i{}; i < 5; i++)
{
std::cout << numbers[i] << "\t";
}
}
Dacă încercăm să transmitem un tablou cu alt număr de elemente, compilarea va eșua:
int nums2[] {1, 2, 3, 4, 5, 6};
print(nums2); // ! Eroare - 6 elemente
Transmiterea tablourilor multidimensionale
Un tablou 2D se transmite ca pointer la primul element. Fiindcă elementele sunt la rândul lor tablouri, pointerul va fi de fapt pointer la tablou. Dimensiunea celei de-a doua dimensiuni trebuie specificată, pentru că face parte din tipul elementului:
void print(int (*numbers)[3]);
Spre deosebire de:
void print(int *numbers[3]); // vector de pointeri
În acest caz parametrul este definit ca un tablou de pointeri, nu ca un pointer la un tablou.
Să analizăm utilizarea unui pointer la tablou ca parametru:
#include <iostream>
void print(const int(*)[3], const size_t);
int main()
{
int table[][3] { {1, 2, 3}, {4, 5, 6}, {7, 8, 9} };
// numărul de rânduri sau sub-tablouri
size_t rowsCount {std::size(table)};
print(table, rowsCount);
}
void print(const int (*rows)[3], const size_t rowsCount)
{
// numărul de coloane sau de elemente în fiecare sub-tablou
size_t columnsCount {std::size(*rows)};
for(size_t i{}; i < rowsCount; i++)
{
for (size_t j{}; j < columnsCount; j++)
{
std::cout << rows[i][j] << "\t";
}
std::cout << std::endl;
}
}
În funcția main este definit un tablou bidimensional – el constă din trei sub-tablouri. Fiecare sub-tablou are trei elemente.
În funcția print, împreună cu tabloul este transmis și numărul de rânduri – adică numărul de sub-tablouri. În funcția print obținem numărul de elemente din fiecare sub-tablou și cu ajutorul a două bucle parcurgem toate elementele. Cu expresia rows[0] accesăm primul sub-tablou din tabloul bidimensional, iar cu rows[0][0] – primul element al primului sub-tablou. Astfel, manipulând indicii putem parcurge întregul tablou bidimensional.
În final, vom obține următoarea ieșire în consolă:
1 2 3
4 5 6
7 8 9
De asemenea, am putea folosi notația tablourilor la declararea și definirea funcției print, care poate părea mai simplă decât notația cu pointeri. Însă și în acest caz trebuie indicată în mod explicit a doua dimensiune:
#include <iostream>
void print(const int[][3], const size_t);
int main()
{
int table[][3] { {1, 2, 3}, {4, 5, 6}, {7, 8, 9} };
// numărul de rânduri sau sub-tablouri
size_t rowsCount {std::size(table)};
print(table, rowsCount);
}
void print(const int rows[][3], const size_t rowsCount)
{
// numărul de coloane sau de elemente în fiecare sub-tablou
size_t columnsCount {std::size(rows[0])};
for( size_t i{}; i < rowsCount; i++)
{
for (size_t j{}; j < columnsCount; j++)
{
std::cout << rows[i][j] << "\t";
}
std::cout << std::endl;
}
}