5. Typy złożone

5.1. Tablice

Tablica w C++ to ciąg obiektów jednego typu zajmujący ciągły obszar pamięci. Wielkość tablicy musi być stałą, znaną w czasie kompilacji (nie dotyczy tablic tworzonych dynamicznie za pomocą operatora new). Wymiar musi być znany na etapie kompilacji, ale nie musi być koniecznie jawnie podany – jeśli tylko da się go obliczyć na podstawie wartości inicjujących.

Tablica tworzona jest jako typ pochodny pomocą operatora [] z:

  • typów fundamentalnych (oprócz void)
  • typów wyliczeniowych
  • wskaźników
  • tablic (tablice wielowymiarowe)
  • klas

Przykłady definicji tablic:


// trójelementowa tablica liczb zmiennoprzecinkowych
float v[3];
// pięcioelementowa tablica wskaźników na znak
char *a[5];
// dwuwymiarowa tablica liczb całkowitych
int d2[2][10];
// trójwymiarowa tablica liczb całkowitych
int d3[2][2][2];

// obliczenie wielkości tablicy z listy inicjującej
int v1[] = { 1, 2, 3, 4, 5 };
// jeśli lista inicjująca jest zbyt krótka – zostanie automatycznie
// dopełniona zerami
int v2[10] {1, 2};

// tablica jest równoważna wskaźnikowi na pierwszy element.
int *pInt = v1;

Jak wspomnieliśmy, iteratorem dla tablicy w C++ jest wskaźnik. Zmienna tablicowa jest tożsama ze wskaźnikiem na jej pierwszy element, i jeśli wrócicie do arytmetyki wskaźników – jasne też okaże się, w jaki sposób można poruszać się po tablicy w alternatywny sposób. Alternatywny do klasycznego indeksowania z wykorzystaniem operatora [] - którym zajmiemy się na początek.

Ogólnie dostęp do i-tego elementu tablicy można uzyskać poprzez podanie jego indeksu w nawiasach kwadratowych po nazwie zmiennej, przy czym pamiętajcie:

Elementy tablicy są indeksowane (numerowane) zawsze od 0, ostatnim elementem tablicy jest więc element o indeksie n-1, jeśli n jest liczbą elementów w tablicy.

We wbudowanym typie tablicowym indeksami są zawsze liczby całkowite, przy czym, co również jest bardzo istotne:

C++ nie przeprowadza żadnej kontroli zakresów przy dostępie do elementów tablic.

Brak kontroli zakresu jest charakterystyczny nie tylko dla dostępu indeksowanego, ale także dla dostępu typu iteratorowego.

Oba podejścia do dostępu do elementów są w praktyce równoważne. Czasem możecie spotkać określenia że jeden jest szybszy czy drugi bardziej elegancki – w praktyce dzięki optymalizacjom kompilatorów różnice w czasie dostępu są pomijalne, natomiast co do elegancji – zadecydujcie sami:


int main()
{
  int t[20];
  // klasycznie (dostęp indeksowany)
  for (int i = 0; i < 20; ++i)
    tab[i] = 1;
  // alternatywnie (dostęp iteratorowy)
  for (int *x = t; x != t + 20;)
    *x++ = 2;
  // od C++11 - możliwe jest też stosowanie pętli zakresowej
  for (auto& elem : tab) 
     cout << elem; 
}

Statyczne tablice wielowymiarowe także są jednolitym obszarem pamięci. Jedynie zmienia się interpretacja znaczenia operatorów []. W przypadku tablic wielowymiarowych kod:


int tab[lr][lk]; 
...
tab[i][j] = 12; 

oznacza: weź wartość spod indeksu i*lk+j z jednowymiarowej tablicy o wymiarze lk*lr. Dlatego też - nie ma różnicy między jednowymiarową a wielowymiarową tablicą statyczną.

Zaprezentowane wyżej tablice statycznie są przechowywane na stosie. Istotnym ograniczeniem takiego podejścia jest konieczność obliczenia rozmiaru tablicy na etapie jej kompilacji. Jeśli rozmiar tablicy na tym etapie nie jest znany, koniecznym staje się wykorzystanie dynamicznej alokacji tablic. Do tego celu wykorzystuje się operator new[rozmiar], z rozmiarem tablicy podawanym wewnątrz nawiasów kwadratowych. Tak utworzone tablice następnie muszą być również ręcznie usunięte za pomocą operatora delete[]. Po utworzeniu i przed usunięciem jednowymiarowe tablice statyczne są w pełni równoważne (mogą być stosowane zamiennie) z jednowymiarowymi tablicami statycznymi.

Dynamiczne i statyczne tworzenie tablic jest możliwe również dla tablic wielowymiarowych, przy czym w tym przypadku przestaje obowiązywać równoważność tablic statycznych i dynamicznych. Wielowymiarowa tablica statyczna jest jednolitym obszarem pamięci, a dostęp do odpowiednich elementów tej tablicy jest możliwy dzięki wewnętrznemu przeliczaniu indeksów dwuwymiarowych na indeks jednowymiarowy. W przypadku tablic tworzonych dynamicznie mamy do czynienia z rzeczywistą „tablicą tablic”, co pokazuje choćby kod konieczny do utworzenia i skasowania takiej tablicy:


// dynamiczne tworzenie
double **a;

a = new double[w];
for (int i=0; i< w; i++)
  a[i] = new double[k];
...
 a[i][j] = 1.5;
...
for (int i=0; i<w; i++)
  delete[] a[i];
delete[] a;

Dokładniejszy opis działań w przypadku dynamicznego tworzenia i kasowania tablicy zamieściliśmy na poniższym rysunku:


Pamiętajcie – z punktu widzenia kompilatora, wielowymiarowe tablice statyczne i dynamiczne są zupełnie różnymi typami, nie można ich stosować zamiennie np. jako parametrów przekazywanych do funkcji.