Tablouri

Un vector (sau șir sau tablou unidimensional) reprezintă o serie de elemente de același tip, memorate în locații de memorie succesive care pot fi referite individual prin adăugarea unui index la un identificator unic.

Aceasta înseamnă că, de exemplu, cinci valori de tip int pot fi declarate ca un vector fără a fi necesară declararea a cinci variabile diferite (fiecare cu propriul identificator). Intr-adevăr, folosind un vector, cele cinci valori de tip int sunt stocate în locații de memorie continuă și toate cinci pot fi accesate folosind același identificator, dar cu index-ul potrivit.

De exemplu, un vector conținând cinci valori întregi de tip int denumit foo ar putea fi reprezentat astfel:


unde fiecare spațiu liber reprezintă un element al șirului. În acest caz, acestea sunt valori de tip int. Aceste elemente sunt numerotate de la 0 la 4, primul fiind pe poziția 0 și 4 indicându-l pe ultimul; în C++, primul element dintr-un vector este totdeauna numerotat cu zero (nu cu unu), indiferent de lungimea vectorului.

Ca și o variabilă obișnuită, un vector trebuie declarat înainte de a fi folosit. O declarație tipică pentru un tablou unidimensional în C++ este:

tip nume [nr_elemente];

unde tip este un tip de dată valid (precum int, float...), nume este un identificator valid și câmpul nr_elemente (care este întotdeauna închis între paranteze drepte []) precizează lungimea vectorului prin numărul de elemente.

De aceea, șirul foo, cu cinci elemente de tip int, poate fi declarat astfel:

 
int foo [5];

ATENȚIE: Câmpul nr_elemente dintre parantezele drepte [], reprezentând numărul de elemente ale vectorului, trebuie să fie o expresie constantă, deoarece vectorii sunt blocuri de memorie statică a căror dimensiune trebuie să fie cunoscută la momentul compilării, înainte ca programul să ruleze.

Inițializarea vectorilor

În mod implicit, vectorii obișnuiți cu domeniu local (de exemplu, cei declarați în interiorul unei funcții) sunt lăsați neinițializați. Aceasta înseamnă că niciunul dintre elementele sale nu are setată vreo valoare; conținutul lor este nedeterminat în momentul declarării vectorului.

Dar elementele dintr-un vector pot fi inițializate explicit cu anumite valori la declarație, punând acele valori între acolade {}. De exemplu:

 
int foo [5] = { 16, 2, 77, 40, 12071 }; 


Această instrucțiune declară un vector ce poate fi reprezentat astfel:


Numărul valorilor incluse între acolade {} nu ar trebui să fie mai mare decât numărul elementelor din vector. De exemplu, mai sus foo a fost declarat ca având 5 elemente (așa cum precizează numărul dintre parantezele drepte []) și acoladele {} conțin exact 5 valori, una pentru fiecare element. Dacă s-ar fi precizat mai puține, elementele rămase ar fi fost setate la valorile lor implicite (care pentru tipurile de date fundamentale înseamnă valori zero). De exemplu:

 
int bar [5] = { 10, 20, 30 }; 


va crea un vector ca și acesta:


Este posibil, chiar, ca inițializarea să nu conțină nicio valoare, doar acolade:

 
int baz [5] = { }; 


Aceasta crează un vector cu cinci valori de tip int, fiecare dintre ele fiind inițializată cu o valoare zero:


Când se furnizează valori de inițializare pentru un vector, C++ ne permite să lăsăm parantezele drepte [] necompletate. În acest caz, compilatorul va dimensiona automat vectorul la numărul de valori incluse între acolade {}:

 
int foo [] = { 16, 2, 77, 40, 12071 };


După această declarație, vectorul foo ar avea lungimea a cinci valori int, deoarece noi am furnizat cinci valori de inițializare.

Evoluția limbajului C++ a inclus, până la urmă, la adoptarea inițializării universale și pentru vectori. De aceea, nu mai este obligatoriu să punem semnul egal între declarație și inițializare. Cele două instrucțiuni de mai jos sunt echivalente:

1
2
int foo[] = { 10, 20, 30 };
int foo[] { 10, 20, 30 }; 


Vectorii statici și cei declarați direct în spațiile de nume (în afara oricărei funcții) sunt întotdeauna inițializați. Dacă nu se specifică o inițializare explicită, toate elementele sunt inițializate implicit (cu zero, pentru tipurile de date fundamentale).

Accesarea valorilor unui vector

Valorile elementelor unui vector pot fi accesate ca și valoarea unei variabile obișnuite de același tip. Sintaxa este:

nume[index]
Urmărind exemplul anterior în care vectorul foo avea 5 elemente și fiecare dintre aceste elemente era de tip int, numele folosit pentru referirea fiecărui element este următorul:


De exemplu, instrucțiunea următoare memorează valoarea 75 în al treilea element al lui foo:

 
foo [2] = 75;


și, de exemplu, următoarea copiază valoarea celui de-al treilea element al lui foo într-o variabilă numită x:

 
x = foo[2];


De aceea, expresia foo[2] este ea însăși o variabilă de tip int.

Să observăm că al treilea element al lui foo este precizat cu foo[2], deoarece primul este foo[0], cel de-al doilea este foo[1] și, de aceea, al treilea este foo[2]. Din același motiv, ultimul său element este foo[4]. De aceea, dacă am scrie foo[5], am putea să accesăm al șaselea element al lui foo, dar, de fapt, s-ar depăși dimensiunea vectorului.

În C++, este corect din punct de vedere sintactic să depășim domeniul valid de indici pentru un vector. Aceasta poate crea probleme, căci la accesatea de elemente din afara domeniului nu cauzeaza erori la compilare, ci la momentul execuției programului. Vom vedea motivul pentru care este eprmis așa ceva mai târziu, într-un capitol în care vor fi prezentați pointerii.

La acest moment, este important să distingem foarte clar între cele două forme de utilizare a parantezelor drepte [] în legătură cu vectorii. Se execută două sarcini diferite: una ajută la precizarea dimensiunii vctorilor când sunt declarați; cea de-a doua precizează indicii pentru anumite elemente ce trebuie accesate. Să nu confundați aceste două posibilități de folosire a parantezelor drepte [] cu vectori.

1
2
int foo[5];         // declaratia unui nou vector
foo[2] = 75;        // accesarea unui element al vectorului 


Principala diferență constă în faptul că declarația este precedată de tipul elementelor, în timp ce accesarea lor nu este precedată.

Alte operații valide cu vectori:

1
2
3
4
foo[0] = a;
foo[a] = 75;
b = foo [a+2];
foo[foo[a]] = foo[2] + 5;


De exemplu:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// exemplu cu vectori
#include <iostream>
using namespace std;

int foo [] = {16, 2, 77, 40, 12071};
int n, rezultat=0;

int main ()
{
  for ( n=0 ; n<5 ; ++n )
  {
    rezultat += foo[n];
  }
  cout << rezultat;
  return 0;
}
12206


Tablouri multidimensionale

Tablourile multidimensionale pot fi descrise ca "vectori de vectori". De exemplu, un tablou bidimensional poate fi imaginat ca un tabel bidimensional format cu elemente care au același tip de dată.


jimmy reprezintă un tablou bidimensional (matrice) de 3 vectori a câte 5 elemente de tip int. The C++ syntax for this is:

 
int jimmy [3][5];


și, de exemplu, modul de a ne referi la al doilea element vertical și al patrulea orizontal ar fi o expresie ca:

 
jimmy[1][3]



(să ne amintim că indicii unui vector încep întotdeauna cu zero).

Tablourile multidimensionale nu se limitează la doi indici (adică șa două dimensiuni). Ele pot conține atâția indici cât sunt necesari. Totuși, să fim atenți: cantitatea de memorie necesară pentru un tablou crește exponențial cu fiecare dimensiune. De exemplu:

 
char century [100][365][24][60][60];


declară un tablou cu un element de tip char pentru fiecare al doilea dintr-un century. Aceasta ajunge la mai mult de 3 bilioane de char! Astfel incât această declarație ar consuma mai mult de 3 gigabytes de memorie!

La sfârșit, tablourile multidimensionale sunt doar o abstractizare pentru programatori, deoarece aceleași rezultate pot fi obținute cu un vector simplu, prin înmulțirea indicilor săi:

1
2
int jimmy [3][5];   // este echivalent cu
int jimmy [15];     // (3 * 5 = 15)  


cu singura diferență că pentru tablourile multidimensionale compilatorul își reamintește automat adâncimea fiecărei dimensiuni imaginare. Următoarele două secvențe de cod produc exact același rezultat, dar una folosește un tablou bidimensional (matrice) în timp ce cealaltă folosește un tablou simplu (vector):

tablou multidimensionaltablou pseudo-multidimensional
#define WIDTH 5
#define HEIGHT 3

int jimmy [INALTIME][LATIME];
int n,m;

int main ()
{
  for (n=0; n<INALTIME; n++)
    for (m=0; m<LATIME; m++)
    {
      jimmy[n][m]=(n+1)*(m+1);
    }
}
#define LATIME 5
#define INALTIME 3

int jimmy [INALTIME * LATIME];
int n,m;

int main ()
{
  for (n=0; n<INALTIME; n++)
    for (m=0; m<LATIME; m++)
    {
      jimmy[n*LATIME+m]=(n+1)*(m+1);
    }
}

Niciuna dintre variantele de cod de mai sus nu produce nici o ieșire pe ecran, dar ambele atribuie valori blocului de memorie denumit jimmy în felul următor:


Să observăm că s-au folosit constante definite pentru lățime și înălțime, în loc de folosirea directă a unor valori numerice. Aceasta dă o mai mare claritate codului și face mai ușoară modificarea programului într-un singur loc.

Tablourile ca parametri

La un moment dat, vom avea nevoie să transmitem un tablou ca parametru al unei funcții. În C++, nu este posibil să transmitem unei funcții întregul bloc de memorie care conține un tablou direct ca argument. În schimb, putem să transmitem adresa sa. În practică, aceasta are cam același efect, fiind mult mai rapid și mai eficient.

Pentru ca o funcție să primească un parametru tablou ca argument, parametrii pot fi declarați ca fiind de tip tablou, dar fără nimic scris între parantezele drepte, adică fără precizarea dimensiunii tabloului. De exemplu:

 
void procedura (int arg[])


Această funcție acceptă un parametru de tip "tablou de int" denumit arg. Pentru a transmite acestei funcții un tablou, el trebuie declarat astfel:

 
int tabloul_meu [40];


ceea ce ar fi suficient pentru a scrie un apel ca:

 
procedura (tabloul_meu);


Mai jos avem un exemplu complet:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// tablouri ca parametri
#include <iostream>
using namespace std;

void afisare_tablou (int arg[], int lungime) {
  for (int n=0; n<lungime; ++n)
    cout << arg[n] << ' ';
  cout << '\n';
}

int main ()
{
  int tablou1[] = {5, 10, 15};
  int tablou2[] = {2, 4, 6, 8, 10};
  afisare_tablou (tablou1,3);
  afisare_tablou (tablou2,5);
}
5 10 15
2 4 6 8 10


În programul se mai sus, primul parametru (int arg[]) acceptă orice tablou ale cărui elemente sunt de tip int, indiferent de lungime. De aceea, am inclus un al doilea parametru care îi precizează funcției lungimea fiecărui tablou ce va fi transmis ca prim parametru. Aceasta face ca repetiția for care afișează tabloul să știe în ce interval să itereze tabloul, fără a-i depăși limitele.

Într-o definiție de funcție se pot include și tablouri multidimensionale. Formatul pentru un tablou tridimensional dat ca parametru este:

 
tip_baza[][adancime][adancime]


De exemplu, o funcție care are un tablou tridimensional ca parametru ar putea fi:

 
void procedura (int tabloul_meu[][3][4])


Sublinime că prima pereche de paranteze drepte [] nu are nimic completat, în timp ce în următoarele se precizează mărimile pentru respectivele dimensiuni. Acest lucru este necesar, astfel încât compilatorul să poată determina adâncimea pentru fiecare nouă dimensiune.

Într-un fel, transmiterea unui tablou ca argument pierde întotdeauna o dimensiune. Motivul, din motive istorice, este acela că tablourile nu pot fi copiate direct, ci se transmit prin intermediul pointerilor. Aceasta este o sursă uzuală de erori pentru programatorii începători. Deși, o înțelegere clară a pointerilor, explicată într-un capitol următor, ar ajuta foarte mult.

Biblioteca pentru tablouri

Tablourile explicate mai sus sunt implementate direct ca o caracteristică a limbajului, moștenită din limbajul C. Sunt grozave, dar prin restricționarea copierii lor și folosirea pointerilor, se simte o oarecare lipsă de optimizare.

Pentru a contracara aceasta modalitate de implementare a tablourilor, C++ furnizează o alternativă la tipul tablou printr-un container standard. Este un tip șablon (un șablon de clasă, de fapt) definit în fișierul antet <array>.

Containerele sunt o caracteristică de bibliotecă pe care nu am inclus-o în acest tutorial, motiv pentru care nu vom discuta aici despre această clasă. Este suficient să spunem că funcționează în mod similar cu tablourile predefinite, dar pot fi fi copiate (operație care, în fapt, este foarte costisitoare și ar trebui folosită cu grijă) și se apelează la pointeri numai când se precizează explicit să se facă așa (cu ajutorul membrului data).

Doar ca un exemplu, dăm mai jos două versiuni ale aceluiași program, una cu implementarea predefinită descrisă în acest capitol și cealaltă cu containerele din bibliotecă:

tablouri predefinitetablouri container
#include <iostream>

using namespace std;

int main()
{
  int tabloul_meu[3] = {10,20,30};

  for (int i=0; i<3; ++i)
    ++tabloul_meu[i];

  for (int elem : tabloul_meu)
    cout << elem << '\n';
}
#include <iostream>
#include <array>
using namespace std;

int main()
{
  array<int,3> tabloul_meu {10,20,30};

  for (int i=0; i<tabloul_meu.size(); ++i)
    ++tabloul_meu[i];

  for (int elem : tabloul_meu)
    cout << elem << '\n';
}

După cum se poate vedea, ambele tablouri folosesc aceeași sintaxă pentru accesarea elementelor: tabloul_meu[i]. În rest, principala diferență constă în declarația tabloului și includerea suplimentară a unui fișier antet biblioteca array. De asemenea, observați cât este de ușor de accesat dimensiunea tabloului de bibliotecă.
Index
Index