Trudno dziś wyobrazić sobie programowanie bez znajomości tych struktur. Czym są tablice?! W najprostszym ujęciu są one strukturą złożoną z wielu elementów tego samego typu. Co to oznacza? Można wyobrazić sobie następującą sytuację, gdy potrzebujemy przechować wartości dla 5 zmiennych typu int w następujący sposób:

int a1, a2, a3, a4, a5;

a1=1;
a2=4;
a3=0;
a4=a3-a1;
a5=a3+a2;

Osiągnęliśmy zamierzony cel, ale… czy jest to wygodne?! To pokaże następujący przykład: załóżmy, że potrzebujemy wyświetlić wartości tych zmiennych, kod wtedy przybrać może taką postać:

printf("%d, %d, %d, %d, %d\n\r",a1,a2,a3,a4,a5);

Dało się? Dało… a teraz kolejny przykład – wyświetlić te dane w postaci uporządkowanej od najmniejszej do największej:

int a1, a2, a3;

a1=1;
a2=7;
a3=3;

if (a1<a2 && a2<a3) printf("%d, %d, %d",a1,a2,a3);
 else if (a1>a2 && a1<a3 && a2<a3) printf("%d, %d, %d",a2,a1,a3);
  else if (a1>a2 && a1>a3 && a2<a3) printf("%d, %d, %d",a2,a3,a1);
   else if (a1<a2 && a2>a3 && a3<a1) printf("%d, %d, %d",a3,a1,a2);
    else if (a1>a2 && a3<a1 && a3<a2) printf("%d, %d, %d",a3,a2,a1);	     
     else printf("%d, %d, %d",a1,a3,a2);
     

Działa, prawda? Dla 3 zmiennych program jest całkiem zgrabny, ale dla 10? W temacie 5 o wyrażeniach warunkowych było podobne zadanie do samodzielnego wykonania.

Tablice jednowymiarowe

Tutaj przychodzi z pomocą idea tablic – po pierwsze znacznie łatwiej odwoływać się do każdej kolejnej wartości umieszczonej w takiej tablicy – nie mają odrębnych nazw zmiennych, a indeksy. Indeksy wskazują na kolejne elementy tablicy, trzeba jedynie pamiętać o fakcie, iż najmniejszym indeksem tablicy, wskazującym na pierwszy element tablicy jest indeks o wartości 0 – czyli numerowanie elementów tablicy rozpoczyna się od 0. Tablicę wieloelementową definiujemy podając ilość elementów jaka w niej ma się znaleźć i tutaj od razu uwaga – w języku C/C++ nie ma kontroli zakresów indeksów, co zostało w obowiązkach programisty. Tak więc odwołując się do 10 elementowej tablicy musimy wiedzieć, że poprawnymi indeksami są wartości od 0 do 9. Program jednak nie “oburzy się” jeśli spróbujemy odwołać się do elementu 101 takiej tablicy. Są pewne szczególne przypadki, gdy taki zabieg jest prawidłowy, ale w olbrzymiej większości przypadków jest to po prostu błąd programisty, który prowadzi do nieoczekiwanego działania programu – bądź poprzez pobranie wartości, które są… bezsensowne (przy odczycie z tabeli) do zapisu do takiego elementu tablicy wartości, która absolutnie nie powinna być tam zapisywana i która właśnie w tym momencie nadpisuje jakąś ważną wartość w strukturze programu – co zazwyczaj prowadzi do jego zawieszenia (co jest wyjątkowo dobrą reakcją), lub do nadzwyczaj trudnych do wykrycia błędów, gdy program w losowych momentach zachowuje się nieoczekiwanie z powodu zmiany (nadpisania) poprawnych danych w jego pamięci w czasie działania zupełnie innego jego fragmentu.

Tak więc wiadomo już, że tablice są wręcz niezbędne do optymalizacji kodu programu i nadzwyczaj przydatne do realizacji niektórych postawionych zadań programistycznych. Poniżej przedstawiono sposób deklarowania zmiennych tablicowych o różnych rozmiarach i typach elementów składowych, oraz przykładowe ich inicjalizacje.

int tab1[0];
int tab2[256];
int tab3[]={1,3,2,6,4,3,7,8,9,0};
int tab4[100]={1,3,2,6,4,3,7,8,9,0};

tab2[100]=99;
tab2[200]=-100;

int z=tab2[50];

char tChar1[11];
char tChar2[]={'A','l','a',' ','m','a',' ','k','o','t','a'};
char tChar3[12]="Ala ma kota";

tChar1[0]='A';
tChar1[1]='l';
tChar1[2]='a';
tChar1[3]=' ';
tChar1[4]='m';
tChar1[5]='a';
tChar1[6]=' ';
tChar1[7]='k';
tChar1[8]='o';
tChar1[9]='t';
tChar1[10]='a';

tChar1[1000]='r';

const int rozmiar=100;
bool tabBool[rozmiar];

string tabS[10];

Należy zwrócić uwagę, na to w jaki sposób są deklarowane zmienne tablicowe i w jaki sposób nadawane są ich elementom wartości.

Jak widać w pierwszej linijce stworzyliśmy tablicę o nazwie tab1, gdzie elementami są wartości typu int. Jednakże podaliśmy też, że ilość elementów, które możemy przypisać do tablicy jest równa… 0. Nie jest to błąd, można stworzyć zmienną tablicową o zerowej ilości elementów, nie da się jednak do niej (poprawnie) przypisać ani jednego elementu, gdyż każdy element będzie przekroczeniem zakresu jej indeksów (nawet tab1[0], bo to jest już tablica jednoelementowa).

W drugiej linijce zadeklarowaliśmy zmienną tablicową tab2 o 256 elementach typu int, nie przypisaliśmy do niej póki co żadnych danych, choć kilka linijek niżej jest już przypisanie takich wartości do indeksu 100 i 200 tablicy. A w kolejnej mamy użycie wartości umieszczonej w elemencie tablicy tab2 o indeksie 50 (nie w 50 elemencie tablicy, a w elemencie o indeksie 50!!!, 50 element tablicy ma indeks 49!!!)

W trzecim przykładzie nie zadeklarowaliśmy wprost wielkości tablicy, ale zarządziliśmy jej inicjalizację podaną grupą liczb – kompilator przeliczył liczbę tych elementów i stworzył tablicę o dokładnie takim rozmiarze, aby wszystkie te liczby wypełniły ją w pełni.

Czwarty przykład ma znów podaną wartość wskazującą na oczekiwany rozmiar tablicy, ale też i listę elementów – widać na pierwszy rzut oka, że lista jest znacznie krótsza niż rozmiar tablicy – nie jest to błąd, do tablicy zostaną po kolei, aż do wyczerpania listy, wpisane te wartości do komórek od najmniejszego indeksu, reszta pozostanie zarezerwowana, ale nie zainicjalizowana.

Kolejne przykłady pokazują sposób zapisu do tablicy wartości typu char i jeśli należało się spodziewać poprawności w pierwszych dwu linijkach to ciekawostką jest linijka trzecie, gdzie do tChar3[] o rozmiarze 12 elementów typu char wpisujemy wartości poprzez podanie string‘a “Ala ma kota”. Taki zabieg możliwy jest TYLKO podczas deklaracji zmiennej wraz z jednoczesną jej inicjalizacją sekwencją elementów zebranych w ciąg znaków (czyli string). Ciekawostką tutaj jest i sama możliwość takiej inicjalizacji, ale też i fakt, że taka tablica musi mieć o 1 element więcej zadeklarowany (pytanie do studentów: dlaczego?). Widać to szczególnie na tym przykładzie, gdzie została ilość elementów tablicy z premedytacją wpisana przy deklaracji, gdy tymczasem string “Ala ma kota” ma ile liter?!

Ostatnią kwestią jaką poruszymy co do deklaracji i inicjalizacji tablic jest powrót do informacji o obowiązku programisty co do dbania o właściwe odwoływanie się (wartości indeksów) do tablic. Na listingu jest linia, gdzie do tChar1 do elementu wskazywanego przez indeks o wartości 1000 (czyli 1001 elementu tablicy!) próbujemy przypisać wartość litery ‘r’. Program nie przestrzeże przed taką próbą, ale może zachować się nieoczekiwanie (w sumie to dla zaawansowanych programistów jak najbardziej oczekiwanie) i ulegnie zawieszeniu, bowiem w ten sposób mogliśmy nadpisać newralgiczną wartość specyficznej i używanej w programie komórki pamięci, leżącej znacznie poza pamięcią zarezerwowaną dla tablicy tChar1. Należy o tym pamiętać.

Nasz przykład z początku niniejszego tematu o wyświetlaniu wartości znacznie się upraszcza z użyciem tablic, co widać na poniższym listingu:

int tab[]={1,3,2,6,4,3,7,8,9,0};

for (int i=0;i<10;i++) printf("%d, ",tab[i]);

Zdefiniowaliśmy tablicę i umieściliśmy w niej aż 10 różnych wartości typu int (zamiast tylko 5 jak we wspomnianym przykładzie). Stworzyliśmy JEDNĄ zmienną – zmienną tablicową – do której możemy się odwoływać do każdego elementu indywidualnie używając jego indeksu, czyli miejsca w tablicy na którym wartość prosta (element tablicy) została umieszczona.

W przypadku tablic nie sposób nie wspomnieć o strukturze pętli, która jest wręcz stworzona do wyświetlenia wszystkich elementów takiej tablicy – a mianowicie pętla for-each, przykład jej użycia i jej konstrukcję znajdziemy poniżej:

int tab[]={1,3,2,6,4,3,7,8,9,0};

for (int ii:tab) printf("%d, ",ii);

Pętla działa dla WSZYSTKICH ELEMENTÓW danej struktury (tutaj zmiennej tablicowej tab), które to elementy są po kolei przypisywane w każdej iteracji pętli do zmiennej ii, będącej tak samo jak elementy tablicy typem int (koniecznie musi być zachowany taki sam typ danych).

Tablice w C/C++ są obarczone pewną wadą, bowiem nie posiadają wewnętrznego licznika ilości elementów – za zarządzanie nimi odpowiada programista. Dlatego więc struktura pętli for-each jest przydatna do wyliczenia ilości elementów takiej tablicy, wystarczy zastosować poniższy wzór:

bool tabBool[jakis_rozmiar];

int licznik=0;
for(bool b:tabBool) licznik++; 

printf("Ilość elementów tablicy wynosi: %d.",licznik);

Po zrealizowaniu tego kodu w zmiennej licznik znajduje się wartość wskazująca na ilość elementów tablicy.

Innym sposobem jest odniesienie się do pamięci jaką rezerwuje dana struktura i przy podziale tej wartości przez dzielnik będący ilością pamięci jaką zajmuje pojedynczy element uzyskujemy ilość tych elementów, tak jak w przykładzie poniżej:

printf("Ilość elementów pamięci tablicy wynosi %d.", sizeof(tabBool)/sizeof(tabBool[0]));  //elementem tablicy jest np. element spod indeksu [0].

//lub

printf("Ilość elementów pamięci tablicy wynosi %d.", sizeof(tabBool)/sizeof(bool));   //elementem tablicy jest typ bool

Teraz możemy się odnieść do drugiego przykładu, gdzie dla 3 zmiennych nasz program w celu ich posortowania zaczął wyglądać dość niezrozumiale, a przy wzięciu pod uwagę większej liczby zmiennych byłby prawie niemożliwy do bezbłędnego napisania. Także i tutaj tablice powodują, że sortowanie jest znacznie łatwiejsze do zaimplementowania (dla zabawy kodem pominiemy gotowe funkcje sortujące, jak np. sort z biblioteki algorithm). Tak więc nasz kod może wyglądać następująco:

#include<cstdio>

void sortuj(int *tab,int ile) { 
	bool posortowane;
	int tmp;
	do
	 {
	 	posortowane=true;
	 	for (int ii=0;ii<ile-1;ii++) if (tab[ii]>tab[ii+1]) {
	 		tmp=tab[ii];
			tab[ii]=tab[ii+1];
			tab[ii+1]=tmp;
	 		posortowane=false;
								};
	 }
	while (!posortowane);
}

int main(){
	int i_tab[]={10,5,3,21,11,4,23,8,-45,0};
	printf("Elementy tablicy nieposortowane :");
	for (int i:i_tab) printf("%5d, ",i);

	sortuj(i_tab,sizeof(i_tab)/sizeof(int));  //wywołanie procedury

	printf("\r\nElementy tablicy posortowane    :");
	for (int i:i_tab) printf("%5d, ",i);
	return 0;
    }

Jak widać teraz sortowanie działa efektywnie niezależnie od ilości elementów w tablicy – zmiana ich ilości nie wymusza zmiany kodu programu. W programie można wyróżnić cztery bloki funkcjonalne (poza częściami stałymi, czy tak oczywistymi jak zadeklarowanie i inicjalizacja tablicy wartościami), w tym trzy w funkcji uruchomieniowej main. Są to:

  • wyświetlenie tablicy nieposortowanej:
printf("Elementy tablicy nieposortowane :");
	for (int i:i_tab) printf("%5d, ",i);
  • wyświetlenie tablicy posortowanej (w zasadzie taki sam fragment kodu jak wyżej)
  • wywołanie procedury sortującej (oczywiście można było kilka linijek kodu z procedury wpisać w funkcji main, ale wtedy nie byłoby wartości dydaktycznej pokazującej jak pisać funkcje i procedury 😉 )
sortuj(i_tab,sizeof(i_tab)/sizeof(int)); 
  • oraz sama procedura sortująca, korzystająca z metody sortowania bąbelkowego, gdzie jako parametry procedury są przekazywane: wskaźnik na tablicę (będzie o tym mowa przy omawianiu wskaźników) i ilość elementów w tablicy podlegających sortowaniu.
void sortuj(int *tab,int ile) { 
	bool posortowane;
	int tmp;
	do
	 {
	 	posortowane=true;
	 	for (int ii=0;ii<ile-1;ii++) if (tab[ii]>tab[ii+1]) {
	 		tmp=tab[ii];
			tab[ii]=tab[ii+1];
			tab[ii+1]=tmp;
	 		posortowane=false;
								};
	 }
	while (!posortowane);
}

Procedura kończy pracę, gdy wszystkie elementy są już posortowane. Nie zwraca ona żadnych wartości, gdyż pracowaliśmy na tablicy i_tab, wobec czego wszelkie zmiany w kolejności elementów tablicy były od razu nanoszone bezpośrednio w niej, mimo iż można mieć wrażenie, że w procedurze pracujemy na zmiennej lokalnej tab. Poniekąd to prawda, tyle, że zmienna tab jest referencją (wskaźnikiem) do zmiennej i_tab, tak więc wszystkie operacje wykonywane na niej de facto odnoszą się do zmiennej na którą wskazuje, czyli w tym przypadku jest nią wspomniana tablica i_tab (o referencjach i wskaźnikach będzie nieco później).
Z uwagi na wprowadzone poprawki kodu (flaga posortowane i pętla do-while) uzyskaliśmy optymalizację działania sortowania bąbelkowego, które w klasycznej wersji ma złożoność obliczeniową (n2).

Tablice wielowymiarowe

Powoływanie do życia tablic wielowymiarowych polega na umiejętnym używaniu nawiasów kwadratowych i zrozumieniu w jakiej kolejności są tworzone kolejne wymiary i jak wygląda struktura tablicy wielowymiarowej. Najprostsze przykłady użycia takich tablic można zaczerpnąć z kodu poniżej:

int tTab01[100][1][1];

int tTab02[10][3]={{1,5,3},{23,5,-10},{100,1000,10000},{0,0,0},{-10,-6,-12}};

int tTab03[][3]={{1,5,3},{23,5,-10},{100,1000,10000},{0,0,0},{-10,-6,-12}};

int tTab04[5][3][2]={{{23,20},{0,9},{14,3}},{{10,10},{-10,-10},{0,0}},{{16,0},{100,100},{1000,10000}}};

tTab04[4][2][0]=-17;
tTab04[4][2][1]=17;

Podsumowując zagadnienie tablic nie sposób nie docenić ich roli, jednakże jedną z najpoważniejszych ich wad jest np. zasada niezmiennego ich rozmiaru. Raz zadeklarowane nie mogą zmieniać ilości elementów jakie przechowują. Kopiowanie tablic również nastręcza pewnych problemów, o których będzie mowa w temacie o wskaźnikach. Wady te stały się podwalinami dla innych struktur programistycznych, które pozbyły się ograniczeń tablic statycznych i przy minimalnie większym skomplikowaniu dały nieporównanie większą elastyczność stosowania.

Zadanie do samodzielnego wykonania: Zadeklaruj tablice o różnych rozmiarach, różnych wymiarach, różnych typach – spróbuj zapisać do nich wartości podczas części deklaratywnej (jednoczesna deklaracja i inicjalizacja różnymi wartościami), jak i podczas trwania już programu. Spróbuj odczytać wartości z tablic i sprawdź, czy zgadzają się z wcześniej zapisanymi. Przetestuj i przećwicz posługiwanie się indeksami i pętlami (w tym pętlą for-each). Spróbuj świadomie przekraczać zakresy tablic odczytując czy zapisując wartości poza zadeklarowane pojemności tablicy – co otrzymujesz, co się dzieje z programem?

Zadanie do samodzielnego wykonania: Napisz program deklarujący tablicę 5-cio wymiarową o maksymalnie 10 elementach w każdym wymiarze. Zainicjuj jej elementy losowo wygenerowanymi wartościami (rand), a następnie zastanów się i zrealizuj sposób wyświetlenia takiej tablicy na ekranie, aby przedstawione wartości były zaprezentowane w czytelny sposób. Na koniec podaj wielkość całej struktury danych w ilości elementów oraz w zajętości pamięci w bajtach.