BAZA WIEDZY
KURSY
Bazy danych w PHP
Kurs AdvancedAJAX
Kurs ASP
Kurs ASP.NET
Kurs C++
Kurs CSS
Kurs HTML
Kurs HTML drugi
Kurs JavaScript
Kurs MySQL
Kurs PHP
Kurs RSS
Kurs XHTML
Obiekty DOM
MANUALE
CSS1 - W3C
DOM - w budowie
PHP 2005
PHP 2006
Wyrażenia regularne
SHOUTBOX
STAT
Online: 8 | UU: 523

Tablice

Jeżeli nasz zestaw danych składa się z wielu drobnych elementów tego samego rodzaju , jego najbardziej naturalnym ekwiwalentem w programowaniu będzie tablica .

Tablica (ang. array ) to zespół równorzędnych zmiennych, posiadających wspólną nazwę. Jego poszczególne elementy są rozróżnianie poprzez przypisane im liczby - tak zwane indeksy .

Każdy element tablicy jest więc zmienną należącą do tego samego typu. Nie ma tutaj żadnych ograniczeń: może to być liczba (w matematyce takie tablice nazywamy wektorami), łańcuch znaków (np. lista uczniów lub pracowników), pojedynczy znak, wartość logiczna czy jakikolwiek inny typ danych.

W szczególności, elementem tablicy może być także. inna tablica! Takimi podwójnie złożonymi przypadkami zajmiemy się nieco dalej.

Po tej garści ogólnej wiedzy wstępnej, czas na coś przyjemniejszego - czyli przykłady :)

Proste tablice

Zadeklarowanie tablicy przypomina analogiczną operację dla zwykłych (skalarnych) zmiennych. Może zatem wyglądać na przykład tak:

int aKilkaLiczb[ 5 ];

Jak zwykle, najpierw piszemy nazwę wybranego typu danych, a później oznaczenie samej zmiennej (w tym przypadku tablicy - to także jest zmienna). Nowością jest tu para nawiasów kwadratowych, umieszczona na końcu deklaracji. Wewnątrz niej wpisujemy rozmiar tablicy, czyli ilość elementów, jaką ma ona zawierać. U nas jest to 5 , a zatem z tylu właśnie liczb (każdej typu int ) będzie składała się nasza świeżo zadeklarowana tablica.

Skoro żeśmy już wprowadzili nową zmienną, należałoby coś z nią uczynić - w końcu niewykorzystana zmienna to zmarnowana zmienna :) Nadajmy więc jakieś wartości jej kolejnym elementom:

aKilkaLiczb[ 0 ] = 1 ;
aKilkaLiczb[ 1 ] = 2 ;
aKilkaLiczb[ 2 ] = 3 ;
aKilkaLiczb[ 3 ] = 4 ;
aKlikaLiczb[ 4 ] = 5 ;

Tym razem także korzystamy z nawiasów kwadratowych. Teraz jednak używamy ich, aby uzyskać dostęp do konkretnego elementu tablicy, identyfikowanego przez odpowiedni indeks . Niewątpliwie bardzo przypomina to docieranie do określonego znaku w zmiennej tekstowej (typu std::string ), aczkolwiek w przypadku tablic możemy mieć do czynienia z dowolnym rodzajem danych.

Analogia do łańcuchów znaków przejawia się w jeszcze jednym fakcie - są nim oczywiście indeksy kolejnych elementów tablicy. Identycznie jak przy napisach, liczymy je bowiem od zera ; tutaj są to kolejno 0 , 1 , 2 , 3 i 4 . Na postawie tego przykładu możemy więc sformułować bardziej ogólną zasadę:

Tablica mieszcząca n elementów jest indeksowana wartościami 0 , 1 , 2 , . , n - 2 , n - 1 .

Z regułą tą wiąże się też bardzo ważne ostrzeżenie:

W tablicy n -elementowej nie istnieje element o indeksie równym n . Próba dostępu do niego jest bardzo częstym błędem, zwanym przekroczeniem indeksów (ang. subscript out of bounds ).

Poniższa linijka kodu spowodowałaby zatem błąd podczas działania programu i jego awaryjne zakończenie:

aKilkaLiczb[ 5 ] = 6 ; // BŁĄD!!!

Pamiętaj więc, byś zwracał baczną uwagę na indeksy tablic, którymi operujesz.

Przekroczenie indeksów to jeden z przedstawicieli licznej rodziny błędów, noszących wspólne miano "pomyłek o jedynkę". Większość z nich dotyczy właśnie tablic, inne można popełnić choćby przy pracy z liczbami pseudolosowymi: najwredniejszym jest chyba warunek w rodzaju rand() % 10 == 10 , który nigdy nie może być spełniony (pomyśl, dlaczego 1 !).

Krytyczne spojrzenie na zaprezentowany kilka akapitów wyżej kawałek kodu może prowadzić do wniosku, że idea tablic nie ma większego sensu. Przecież równie dobrze możnaby zadeklarować 5 zmiennych i zająć się każdą z nich osobno - podobnie jak czynimy to teraz z elementami tablicy:

int nLiczba1, nLiczba2, nLiczba3, nLiczba4, nLiczba5;
nLiczba1 = 1 ;
nLiczba2 = 2 ;
// itd.

Takie rozumowanie jest pozornie słuszne. ale na szczęście, tylko pozornie! :D Użycie pięciu instrukcji - po jednej dla każdego elementu tablicy - nie było bowiem najlepszym rozwiązaniem. O wiele bardziej naturalnym jest odpowiednia pętla for :

for ( int i = 0 ; i < 5 ; ++i) // drugim warunkiem może być też i <= 4
aKilkaLiczb[i] = i + 1 ;

Jej zalety są oczywiste: niezależnie od tego, czy nasza tablica składa się z pięciu, pięciuset czy pięciu tysięcy elementów, przytoczona pętla jest w każdym przypadku niemal identyczna!

Tajemnica tego faktu tkwi rzecz jasna w indeksowaniu tablicy licznikiem pętli, i . Przyjmuje on odpowiednie wartości (od zera do rozmiaru tablicy minus jeden), które pozwalają zająć się całością tablicy przy pomocy jednej tylko instrukcji!

Taki manewr nie byłby możliwy, gdybyśmy używali tutaj pięciu zmiennych, zastępujących tablice. Ich "indeksy" (będące de facto częścią nazw) musiałyby być bowiem stałymi wartościami, wpisanymi bezpośrednio do kodu. Nie dałoby się zatem skorzystać z pętli for w podobny sposób, jak to uczyniliśmy w przypadku tablic.

1 Reszta z dzielenia przez 10 może być z nazwy równa jedynie liczbom 0 , 1 , ..., 8 , 9 , zatem nigdy nie zrówna się z samą dziesiątką. Programista chciał tu zapewne uzyskać wartość z przedziału < 1 ; 10 > , ale nie dodał jedynki do wyrażenia - czyli pomylił się o nią :)

Inicjalizacja tablicy

Kiedy w tak szczegółowy i szczególny sposób zajmujemy się tablicami, łatwo możemy zapomnieć, iż w gruncie rzeczy są to takie same zmienne, jak każde inne. Owszem, składają się z wielu pojedynczych elementów ("podzmiennych"), ale nie przeszkadza to w wykonywaniu nań większości znanych nam operacji. Jedną z nich jest inicjalizacja.

Dzięki niej możemy chociażby deklarować tablice będące stałymi.

Tablicę możemy zainicjalizować w bardzo prosty sposób, unikając przy tym wielokrotnych przypisań (po jednym dla każdego elementu):

int aKilkaLiczb[ 5 ] = { 1 , 2 , 3 , 4 , 5 };

Kolejne wartości wpisujemy w nawiasie klamrowym, oddzielając je przecinkami. Zostaną one umieszczone w następujących po sobie elementach tablicy, poczynając od początku. Tak więc aKilkaLiczb[ 0 ] będzie miał wartość 1 , aKilkaLiczb[ 1 ] - 2 , itd. Uzyskamy identyczny efekt, jak w przypadku poprzednich pięciu przypisań.

Interesującą nowością w inicjalizacji tablic jest możliwość pominięcia ich rozmiaru:

std::string aSystemyOperacyjne[] = { "Windows" , "Linux" , "BeOS" , "QNX" };

W takiej sytuacji kompilator " domyśli się " prawidłowej wielkości tablicy na podstawie ilości elementów, jaką wpisaliśmy wewnątrz nawiasów klamrowych (w tzw. inicjalizatorze ). Tutaj będą to oczywiście cztery napisy.

Inicjalizacja jest więc całkiem dobrym sposobem na wstępne ustawienie wartości kolejnych elementów tablicy - szczególnie wtedy, gdy nie jest ich zbyt wiele i nie są one ze sobą jakoś związane. Dla dużych tablic nie jest to jednak efektywna metoda; w takich wypadkach lepiej użyć odpowiedniej pętli for .

Przykład wykorzystania tablicy

Wiemy już, jak teoretycznie wygląda praca z tablicami w języku C++, zatem naturalną koleją rzeczy będzie teraz uważne przyglądnięcie się odpowiedniemu przykładowi. Ten (spory :)) kawałek kodu wygląda następująco:

// Lotto - użycie prostej tablicy liczb
const unsigned ILOSC_LICZB = 6 ;
const int MAKSYMALNA_LICZBA = 49 ;
void main()
{
// deklaracja i wyzerowanie tablicy liczb
unsigned aLiczby[ILOSC_LICZB];
for ( int i = 0 ; i < ILOSC_LICZB; ++i)
aLiczby[i] = 0 ;
// losowanie liczb
srand ( static_cast < int >(time(NULL)));
for ( int i = 0 ; i < ILOSC_LICZB; )
{
// wylosowanie liczby
aLiczby[i] = rand() % MAKSYMALNA_LICZBA + 1 ;
// sprawdzenie, czy się ona nie powtarza
bool bPowtarzaSie = false ;
for ( int j = 0 ; j < i; ++j)
{
if (aLiczby[j] == aLiczby[i])
{
bPowtarzaSie = true ;
break ;
}
}
// jeżeli się nie powtarza, przechodzimy do następnej liczby
if (!bPowtarzaSie) ++i;
}
// wyświetlamy wylosowane liczby
std::cout << "Wyniki losowania:" << std::endl;
for ( int i = 0 ; i < ILOSC_LICZB; ++i)
std::cout << aLiczby[i] << " " ;
// czekamy na dowolny klawisz
getch();
}

Huh, trzeba przyznać, iż z pewnością nie należy on do elementarnych :) Nie jesteś już jednak zupełnym nowicjuszem w sztuce programowania, więc zrozumienie go nie przysporzy ci wielkich kłopotów. Na początek spróbuj zobaczyć tę przykładową aplikację w działaniu:


Screen 25. Wysyłanie kuponów jest od dzisiaj zbędne ;-)

Nie potrzeba przenikliwości Sherlocka Holmesa, by wydedukować, że program ten dokonuje losowania zestawu liczb według zasad znanej powszechnie gry loteryjnej. Te reguły są determinowane przez dwie stałe, zadeklarowane na samym początku kodu:

const unsigned ILOSC_LICZB = 6 ;
const int MAKSYMALNA_LICZBA = 49 ;

Ich nazwy są na tyle znaczące, iż dokumentują się same. Wprowadzenie takich stałych ma też inne wyraźne zalety, o których wielokrotnie już wspominaliśmy. Ewentualna zmiana zasad losowania będzie ograniczała się jedynie do modyfikacji tychże dwóch linijek, mimo że te kluczowe wartości są wielokrotnie używane w całym programie.

Najważniejszą zmienną w naszym kodzie jest oczywiście tablica, która przechowuje wylosowane liczby. Deklarujemy i inicjalizujemy ją zaraz na wstępie funkcji main() :

unsigned aLiczby[ILOSC_LICZB];
for ( int i = 0 ; i < ILOSC_LICZB; ++i)
aLiczby[i] = 0 ;

Posługując się tutaj pętlą for , ustawiamy wszystkie jej elementy na wartość 0 . Zero jest dla nas neutralne, gdyż losowane liczby będą przecież wyłącznie dodatnie.

Identyczny efekt (wyzerowanie tablicy) można uzyskać stosując funkcję memset() , której deklaracja jest zawarta w nagłówku memory.h . Użylibyśmy jej w następujący sposób:
memset (aLiczby, 0 , break (aLiczby));
Analogiczny skutek spowodowałaby także specjalna funkcja ZeroMemory() z windows.h :
ZeroMemory (aLiczby, break (aLiczby));
Nie użyłem tych funkcji w kodzie przykładu, gdyż wyjaśnienie ich działania wymaga wiedzy o wskaźnikach na zmienne, której jeszcze nie posiadasz. Chwilowo jesteśmy więc zdani na swojską pętlę :)

Po wyzerowaniu tablicy przeznaczonej na generowane liczby możemy przystąpić do właściwej czynności programu, czyli ich losowania. Rozpoczynamy je od niezbędnego wywołania funkcji srand() :

srand ( static_cast < int >(time(NULL)));

Po dopełnieniu tej drobnej formalności możemy już zająć się po kolei każdą wartością, którą chcemy uzyskać. Znowuż czynimy to poprzez odpowiednią pętlę for :

for ( int i = 0 ; i < ILOSC_LICZB; )
{
// ...
}

Jak zwykle, przebiega ona po wszystkich elementach tablicy aLiczby . Pewną niespodzianką może być tu nieobecność ostatniej części tej instrukcji, którą jest zazwyczaj inkrementacja licznika. Jej brak spowodowany jest koniecznością sprawdzania, czy wylosowana już liczba nie powtarza się wśród wcześniej wygenerowanych. Z tego też powodu program będzie niekiedy zmuszony do kilkakrotnego "obrotu" pętli przy tej samej wartości licznika i losowania za każdym razem nowej liczby, aż do skutku.

Rzeczone losowane przebiega tradycyjną i znaną nam dobrze drogą:

aLiczby[i] = rand() % MAKSYMALNA_LICZBA + 1 ;

Uzyskana w ten sposób wartość jest zapisywana w tablicy aLiczby pod i -tym indeksem, abyśmy mogli ją później łatwo wyświetlić. W powyższym wyrażeniu obecna jest także stała, zadeklarowana wcześniej na początku programu.

Wspominałem już parę razy, że konieczna jest kontrola otrzymanej tą metodą wartości pod kątem jej niepowtarzalności. Musimy po prostu sprawdzać, czy nie wystąpiła już ona przy poprzednich losowaniach. Jeżeli istotnie tak się stało, to z pewnością znajdziemy ją we wcześniej "przerobionej" części tablicy. Niezbędne poszukiwania realizuje kolejny fragment listingu:

bool bPowtarzaSie = false ;
for ( int j = 0 ; j < i; ++j)
{
if (aLiczby[j] == aLiczby[i])
{
bPowtarzaSie = true ;
break ;
}
}
if (!bPowtarzaSie) ++i;

Wprowadzamy tu najpierw pomocniczą zmienną (flagę) logiczną, zainicjalizowaną wstępnie wartością false (fałsz). Będzie ona niosła informację o tym, czy faktycznie mamy do czynienia z duplikatem którejś z wcześniejszych liczb.

Aby się o tym przekonać, musimy dokonać ponownego przeglądnięcia części tablicy. Robimy to poprzez, a jakże, kolejną pętlę for :) Aczkolwiek tym razem interesują nas wszystkie elementy tablicy występujące przed tym aktualnym, o indeksie i . Jako warunek pętli wpisujemy więc j < i ( j jest licznikiem nowej pętli).

Koncentrując się na niuansach zagnieżdżonej instrukcji for nie zapominajmy, że jej celem jest znalezienie ewentualnego bliźniaka wylosowanej kilka wierszy wcześniej liczby. Zadanie to wykonujemy poprzez odpowiednie porównanie:

if (aLiczby[j] == aLiczby[i])

aLiczby[i] ( i -ty element tablicy aLiczby ) reprezentuje oczywiście liczbę, której szukamy; jak wiemy doskonale, uzyskaliśmy ją w sławetnym losowaniu :D Natomiast aLiczby[j] ( j -ta wartość w tablicy) przy każdym kolejnym przebiegu pętli oznacza jeden z przeszukiwanych elementów. Jeżeli zatem wśród nich rzeczywiście jest wygenerowana, "aktualna" liczba, niniejszy warunek instrukcji if z pewnością ją wykryje.

Co powinniśmy zrobić w takiej sytuacji? Otóż nic skomplikowanego - mianowicie, ustawiamy naszą zmienną logiczną na wartość true (prawda), a potem przerywamy pętlę for :

bPowtarzaSie = true ;
break ;

Jej dalsze działanie nie ma bowiem najmniejszego sensu, gdyż jeden duplikat liczby w zupełności wystarcza nam do szczęścia :)

W tym momencie jesteśmy już w posiadaniu arcyważnej informacji, który mówi nam, czy wartość wylosowana na samym początku cyklu głównej pętli jest istotnie unikatowa, czy też konieczne będzie ponowne jej wygenerowanie. Ową wiadomość przydałoby się teraz wykorzystać - robimy to w zaskakująco prosty sposób:

if (!bPowtarzaSie) ++i;

Jak widać, właśnie tutaj trafiła brakująca inkrementacja licznika pętli, i . Zatem odbywa się ona wtedy, kiedy uzyskana na początku liczba losowa spełnia nasz warunek niepowtarzalności. W innym przypadku licznik zachowuje swą aktualną wartość, więc wówczas będzie przeprowadzona kolejna próba wygenerowania unikalnej liczby. Stanie się to w następnym cyklu pętli.

Inaczej mówiąc, jedynie fałszywość zmiennej bPowtarzaSie uprawnia pętlę for do zajęcia się dalszymi elementami tablicy. Inna sytuacja zmuszą ją bowiem do wykonania kolejnego cyklu na tej samej wartości licznika i , a więc także na tym samym elemencie tablicy wynikowej. Czyni to aż do otrzymania pożądanego rezultatu, czyli liczby różnej od wszystkich poprzednich.

Być może nasunęła ci się wątpliwość, czy takie kontrolowanie wylosowanej liczby jest aby na pewno konieczne. Skoro prawidłowo zainicjowaliśmy generator wartości losowych (przy pomocy srand() ), to przecież nie powinien on robić nam świństw, którymi z pewnością byłyby powtórzenia wylosowywanych liczb. Jeżeli nawet istnieje jakaś szansa na otrzymanie duplikatu, to jest ona zapewne znikomo mała.
Otóż nic bardziej błędnego! Sama potencjalna możliwość wyniknięcia takiej sytuacji jest wystarczającym powodem, żeby dodać do programu zabezpieczający przed nią kod. Przecież nie chcielibyśmy, aby przyszły użytkownik (niekoniecznie tego programu, ale naszych aplikacji w ogóle) otrzymał produkt, który raz działa dobrze, a raz nie!
Inna sprawa, że prawdopodobieństwo wylosowania powtarzających się liczb nie jest tu wcale takie małe. Możesz spróbować się o tym przekonać 1 .

Na finiszu całego programu mamy jeszcze wyświetlanie uzyskanego pieczołowicie wyniku. Robimy to naturalnie przy pomocy adekwatnego for 'a, który tym razem jest o wiele mniej skomplikowany w porównaniu z poprzednim :)

Ostatnia instrukcja, getch(); , nie wymaga już nawet żadnego komentarza. Na niej też kończy się wykonywanie naszej aplikacji, a my możemy również zakończyć tutaj jej omawianie. I odetchnąć z ulgą ;)

Uff! To wcale nie było takie łatwe, prawda? Wszystko dlatego, że postawiony problem także nie należał do trywialnych. Analiza algorytmu, służącego do jego rozwiązania, powinna jednak bardziej przybliżyć ci sposób konstruowania kodu, realizującego konkretne zadanie.

Mamy oto przejrzysty i, mam nadzieję, zrozumiały przykład na wykorzystanie tablic w programowaniu. Przyglądając mu się dokładnie, mogłeś dobrze poznać zastosowanie tandemu tablica + pętla for do wykonywania dosyć skomplikowanych czynności na złożonych danych. Jeszcze nie raz użyjemy tego mechanizmu, więc z pewnością będziesz miał szansę na jego doskonałe opanowanie :)

1 Wyliczenie jest bardzo proste. Załóżmy, że losujemy n liczb, z których największa może być równa a . Wtedy pierwsze losowanie nie może rzecz jasna skutkować duplikatem. W drugim jest na to szansa równa 1/ a (gdyż mamy już jedną liczbę), w trzecim - 2/ a (bo mamy już dwie liczby), itd. Dla n liczb całościowe prawdopodobieństwo wynosi zatem (1 + 2 + 3 + ... + n -1)/ a , czyli n ( n - 1)/2 a .
U nas n = 6 , zaś a = 49 , więc mamy 6(6 - 1)/(2*49) ? 30,6% szansy na otrzymanie zestawu liczb, w którym przynajmniej jedna się powtarza. Gdybyśmy nie umieścili kodu sprawdzającego, wtedy przeciętnie co czwarte uruchomienie programu dawałoby nieprawidłowe wyniki. Byłaby to ewidentna niedoróbka.

Więcej wymiarów

Dotychczasowym przedmiotem naszego zainteresowania były tablice jednowymiarowe , czyli takie, których poszczególne elementy są identyfikowane poprzez jeden indeks. Takie struktury nie zawsze są wystarczające. Pomyślmy na przykład o szachownicy, planszy do gry w statki czy mapach w grach strategicznych. Wszystkie te twory wymagają większej liczby wymiarów i nie dają się przedstawić w postaci zwykłej, ponumerowanej listy.

Naturalnie, tablice wielowymiarowe mogłyby być z powodzeniem symulowane poprzez ich jednowymiarowe odpowiedniki oraz formuły służące do przeliczania indeksów. Trudno jednak uznać to za wygodne rozwiązanie. Dlatego też C++ radzi sobie z tablicami wielowymiarowymi w znacznie prostszy i bardziej przyjazny sposób. Warto więc przyjrzeć się temu wielkiemu dobrodziejstwu ;)

Deklaracja i inicjalizacja

Domyślasz się może, iż aby zadeklarować tablicę wielowymiarową, należy podać więcej niż jedną liczbę określającą jej rozmiar. Rzeczywiście tak jest:

int aTablica[ 4 ][ 5 ];

Linijka powyższa tworzy nam dwuwymiarową tablicę o wymiarach 4 na 5 , zawierającą elementy typu int . Możemy ją sobie wyobrazić w sposób podobny do tego:


Schemat 8. Wyobrażenie tablicy dwuwymiarowej 4 X 5

Widać więc, że początkowa analogia do szachownicy była całkiem na miejscu :)

Nasza dziewicza tablica wymaga teraz nadania wstępnych wartości swoim elementom. Jak pamiętamy, przy korzystaniu z jej jednowymiarowych kuzynów intensywnie używaliśmy do tego odpowiednich pętli for . Nic nie stoi na przeszkodzie, aby podobnie postąpić i w tym przypadku:

for ( int i = 0 ; i < 4 ; ++i)
for ( int j = 0 ; j < 5 ; ++j)
aTablica[i][j] = i + j;

Teraz jednak mamy dwa wymiary tablicy, zatem musimy zastosować dwie zagnieżdżone pętle. Ta bardziej zewnętrzna przebiega nam po czterech kolejnych wierszach tablicy, natomiast wewnętrzna zajmuje się każdym z pięciu elementów wybranego wcześniej wiersza. Ostatecznie, przy każdym cyklu zagnieżdżonej pętli liczniki i oraz j mają odpowiednie wartości, abyśmy mogli za ich pomocą uzyskać dostęp do każdego z dwudziestu ( 4 * 5 ) elementów tablicy.

Znamy wszakże jeszcze inny środek, służący do wstępnego ustawiania zmiennych - chodzi oczywiście o inicjalizację. Zobaczyliśmy niedawno, że możliwe jest zaprzęgnięcie jej do pracy także przy tablicach jednowymiarowych. Czy będziemy mogli z niej skorzystać również teraz, gdy dodaliśmy do nich następne wymiary?.

Jak to zwykle w C++ bywa, odpowiedź jest pozytywna :) Inicjalizacja tablicy dwuwymiarowej wygląda bowiem następująco:

int aTablica[ 4 ][ 5 ] = { { 0 , 1 , 2 , 3 , 4 },
{ 1 , 2 , 3 , 4 , 5 },
{ 2 , 3 , 4 , 5 , 6 },
{ 3 , 4 , 5 , 6 , 7 } };

Opiera się ona na tej samej zasadzie, co analogiczna operacja dla tablic jednowymiarowych: kolejne wartości oddzielamy przecinkami i umieszczamy w nawiasach klamrowych. Tutaj są to cztery wiersze naszej tabeli.

Jednak każdy z nich sam jest niejako odrębną tablicą! W taki też sposób go traktujemy: ostateczne, liczbowe wartości elementów podajemy albowiem wewnątrz zagnieżdżonych nawiasów klamrowych. Dla przejrzystości rozmieszczamy je w oddzielnych linijkach kodu, co sprawia, że całość łudząco przypomina wyobrażenie tablicy dwuwymiarowej jako prostokąta podzielonego na pola.


Schemat 9. Inicjalizacja tablicy dwuwymiarowej 4 X 5

Otrzymany efekt jest zresztą taki sam, jak ten osiągnięty przez dwie wcześniejsze, zagnieżdżone pętle.

Warto również wiedzieć, że inicjalizując tablicę wielowymiarową możemy pominąć wielkość pierwszego wymiaru:

int aTablica[][ 5 ] = { { 0 , 1 , 2 , 3 , 4 },
{ 1 , 2 , 3 , 4 , 5 },
{ 2 , 3 , 4 , 5 , 6 },
{ 3 , 4 , 5 , 6 , 7 } };

Zostanie on wtedy wywnioskowany z inicjalizatora.

Tablice w tablicy

Sposób obsługi tablic wielowymiarowych w C++ różni się zasadniczo od podobnych mechanizmów w wielu innych językach. Tutaj bowiem nie są one traktowane wyjątkowo, jako byty odrębne od swoich jednowymiarowych towarzyszy. Powoduje to, że w C++ dozwolone są pewne operacje, na które nie pozwala większość pozostałych języków programowania.

Dzieje się to za przyczyną dość ciekawego pomysłu potraktowania tablic wielowymiarowych jako zwykłych tablic jednowymiarowych , których elementami są. inne tablice ! Brzmi to trochę topornie, ale w istocie nie jest takie trudne, jak być może wygląda :)

Najprostszy przykład tego faktu, z jakim mieliśmy już do czynienia, to konstrukcja dwuwymiarowa. Z punktu widzenia C++ jest ona jednowymiarową tablicą swoich wierszy ; zwróciliśmy zresztą na to uwagę, dokonując jej inicjalizacji. Każdy z owych wierszy jest zaś także jednowymiarową tablicą, tym razem składającą się już ze zwykłych, skalarnych elementów.

Zjawisko to (oraz kilka innych ;D) nieźle obrazuje poniższy diagram:


Schemat 10. Przedstawienie tablicy dwuwymiarowej jako tablicy tablic

Uogólniając, możemy stwierdzić, iż:

Każda tablica n -wymiarowa składa się z odpowiedniej liczby tablic ( n -1)-wymiarowych.

Przykładowo, dla trzech wymiarów będziemy mieli tablicę, składającą się z tablic dwuwymiarowych, które z kolei zbudowane są z jednowymiarowych, a te dopiero z pojedynczych skalarów. Nietrudne, prawda? ;)

Zadajesz sobie pewnie pytanie: cóż z tego? Czy ma to jakieś praktyczne znaczenie i zastosowanie w programowaniu?.

Pospieszam z odpowiedzią, brzmiącą jak zawsze "ależ oczywiście!" :)) Ujęcie tablic w takim stylu pozwala na ciekawą operację wybrania jednego z wymiarów i przypisania go do innej, pasującej tablicy. Wygląda to mniej więcej tak:

// zadeklarowanie tablicy trój- i dwuwymiarowej
int aTablica3D[ 2 ][ 2 ][ 2 ] = { { { 1 , 2 },
{ 2 , 3 } },
{ { 3 , 4 },
{ 4 , 5 } } };
in t aTablica2D[ 2 ][ 2 ];
// przypisanie drugiej "płaszczyzny" tablicy aTablica3D do aTablica2D
aTablica2D = aTablica3D[ 1 ];
// aTablica2D zawiera teraz liczby: { { 3, 4 }, { 4, 5 } }

Przykład ten ma w zasadzie charakter ciekawostki, lecz przyjrzenie mu się z pewnością nikomu nie zaszkodzi :D

Nieco praktyczniejsze byłoby odwołanie do części tablicy - tak, żeby możliwa była jej zmiana niezależnie od całości (np. przekazanie do funkcji). Takie działanie wymaga jednak poznania wskaźników, a to stanie się dopiero w rozdziale 8.

***

Poznaliśmy właśnie tablice jako sposób na tworzenie złożonych struktur, składających się z wielu elementów. Ułatwiają one (lub wręcz umożliwiają) posługiwanie się złożonymi danymi, jakich nie brak we współczesnych aplikacjach. Znajomość zasad wykorzystywania tablic z pewnością zatem zaprocentuje w przyszłości :)

Także w tym przypadku niezawodnym źródłem uzupełniających informacji jest MSDN .

| | | |
Copyright © 2006-2013 egrafik.pl | Kontakt | Reklama | Projekty domów
jocker