Witam. W tym artykule przedstawiam używanie tablic jedno-, dwu- i wielowymiarowych, statycznych jak i dynamicznych, przesyłanie ich jako argumenty do procedur/funkcji itd.

Po co nam tablice?

Załóżmy, że w programie będziesz musiał operować na dziesiątkach zmiennych np. typu Integer.
Co wtedy? Czy trzeba definiować każdą zmienną po kolei? Nie, jeśli posłużymy się tablicami, która pozwalają na grupowanie obiektów danego typu. Dzięki nim praca na dziesiątkach/setkach/tysiącach zmiennych jest banalnie prosta, definiując tablicę dostajemy jakby paczkę uporządkowanych elementów danego typu, których używamy jak zwykłych zmiennych.

Trochę teorii.

Zacznijmy od wyjaśnienia: czym są tablice?

Tablice reprezentują uporządkowane, ponumerowane elementy tego samego typu:

Spójrzmy powyżej jeszcze raz. Co tam widzimy? Definicję tablicy, którą można uogólniając zapisać w następujący sposób:

Po kolei. Na początku, nazwa tablicy(niesłychane!), następnie znany nam dwukropek, po którym widnieje słowo kluczowe array, które informuje kompilator, że ma do czynienia z definicją tablicy. Dalej, w nawiasach klamrowych, pomiędzy dwoma kropkami umieszczamy dolny oraz górny zakres tablicy. Potem słowo kluczowe of a po nim, typ, którego mają być elementy tej tablicy. OK, wiemy jak definiować tablicę.

Wiemy już, że tablica zawiera elementy, na których operuje się tak jak na zwykłych zmiennych. Znowu spójrzmy na znaną nam już definicję:

Ile elementów zawiera powyższa tablica? No właśnie, ile? Koderzy znający C++ zapewne od razu odpowiedzieliby: 10! Ale to nie C++ 😉 W tym przypadku tablica ta ma 11 elementów: 0, 1, 2,…,10. Czyli mamy 11 elementów typu Integer. Ilość elementów można policzyć odejmując dolny zakres od górnego i dodając 1.
No tak, tak, ale jak operuje na poszczególnych elementach? Jak się odwołać do np. 5 elementu? Używając operatora []:

Tablica[4] := 12345 ;

Pewnie pomyślałeś, że popełniłem błąd, bo przecież miał być 5 element, a ja podałem jako indeks 4! Błędu nie ma, gdyż pierwszy elementem ma indeks 0, a nie 1-więc wszystko jasne.

Cztery sprawy o których powinieneś wiedzieć.

Po pierwsze, czy zakres tablicy([0..10]) może być dowolnie duży? Dowolnie duży nie-zasięg każdego zakresu(w naszym przypadku jest tylko jeden zakres-0..10, ale wkrótce poznamy tablice wielowymiarowe, a w nich będzie dowolna wręcz ilość zakresów) nie może przekraczać dwóch gigabajtów.
Po drugie: czy zakres zawsze musi zaczynać się od 0(tak jak w C++) ? Nie, nie musi, może to być dowolna liczba całkowita, np:

Po trzecie, jak widzimy powyżej, tablica może być zbiorem elementów dowolnego typu(typ elementów danej tablicy nazywamy typem bazowym)-nawet typu zdefiniowanego przez użytkownika.
Po czwarte, zakresami tablic wcale nie muszą być liczby całkowite, mogą to być na przykład…litery czy identyfikatory prawdy(true) i fałszu(false), czy też nazwy typów. Bo przecież, tak naprawdę, pod wszystkimi znakami kryją się liczby całkowite, a pod prawdą-1 a fałszem-0, a co do nazw typów-to tak, jakbyśmy zapisali dolny zakres jako najmniejszą wartość jaką może przyjąć zmienna tego typu i analogicznie, górny zakres jako wartość największą, więc nie ma w tym dziwnego:

Tylko w takim przypadku, nie odnosimy się do poszczególnych elementów poprzez zwykłe liczby całkowite, ale identyfikatory odpowiednie dla danego ‘typu’ zakresu tablicy:

Poza tym, zakresami mogą być też typy wyliczeniowe oraz typy okrojone:

Nieco więcej o używania i definiowaniu tablic.

Elementy tablicy mogą być wykorzystywane jak normalne zmienne typu np. Integer, to znaczy, że można je mnożyć, dodawać itp. Mogą też być argumentami funkcji i procedur(o przesyłaniu całych tablic powiemy sobie później):

Żadnych rewelacji, na elementach tablicy operujemy tak samo jak na zwykłych zmiennych. Mamy do dyspozycji 3 funkcje, które ułatwiają pracę z tablicami: LowHigh oraz Length.
Low zwraca indeks pierwszego elementu, High-ostatniego a Length zwraca ilość elementów w tablicy:

W Label1 pojawi się tekst: ‘Indkes pierwszego elementu to: 4, indeks ostatniego to 17, ilość elementów w tablicy = 14

Funkcje te przydają się podczas np. pracy z tablicą w pętli:

Dzięki nim nie musimy pamiętać ile elementów ma dana tablica.

Jeżeli definiujemy tablice jako lokalne, to nie są one wstępnie niczym inicjalizowane i zawierają śmieci-całkowicie losowe wartości. Jeżeli zdefiniujemy je jako globalne-to zostaną wstępnie zainicjalizowane.
Jeżeli wiemy, że będziemy w programie korzystać częściej z tablicy o określonym typie i zakresie, to możemy utworzyć nowy typ tablicy i potem definiować nowe tablice jako tablice tego naszego nowo stworzonego typu:

Teraz możemy w programie definiować nasze tablice w ten sposób:

Co zaoszczędza nam pisania, poza tym, może się też przydać, gdy na przykład chcemy wszystkie tablice naszego typu TablicaInteger powiększyć o 5-wystarczy zmienić zakres w naszym typie i wszystkie tablice zdefiniowane jako TablicaInteger będą większe o 5 elementów! To nie wszystkie przypadki, gdy typ tablicowy jest przydatny, o następnych niebawem.

Jeżeli chcemy, aby jedna tablica była identyczna jak druga, to znaczy zawierała takie same elementy, to po prostu przypisujemy jedną do drugiej:

Wszystkie elementy z Tab1 zostają skopiowane do Tab2. Oczywiście, muszą one mieć jednakowe rozmiary oraz ich elementy muszą być tego samego typu, ale, poza tym, muszą spełniać jeszcze jeden warunek. Spójrz:

Te tablice są takie same, prawda? A jednak, próba kompilacji tego kodu:

zakończy się błędem ‘Incompatible types’. Jak to?! Przecież one są identyczne-no tak, spełniają dwa z wyżej wymienionych warunków, ale jest jeszcze jeden, otóż, Delphi używa nazwy podczas sprawdzania poprawności typów. Jeżeli chcemy móc przypisać jedną tablicę do drugiej, musimy je to zrobić w ten sposób:

lub

Tablice dwuwymiarowe.

Znamy już obsługę tablic jednowymiarowych, często jednak przydałoby się mieć tablicę dwuwymiarową, np. gdy tworzymy grę i mapy chcemy przechowywać w tablicach-używanie takiej mapy w jednowymiarowej tablicy byłoby kompletną porażką. W tym przypadku możemy użyć tablicy dwuwymiarowej:

Jak widzisz, tablicę dwuwymiarową definiuje się poprzez podanie następnego zakresu. Pierwszy zakres to ilość kolumn, a drugi to ilość wierszy. Można by to zapisać również w krótszy sposób:

Ułożenie elementów wygląda następująco(założyłem, że tablicę zdefiniowaliśmy jako globalną, czyli została ona wypełniona zerami):

Pięć kolumn i pięć rzędów. Jeżeli chcemy odwołać się do danego elementu, np. 2 w 3 kolumnie, to postępujemy następująco:

lub

Praca z dwuwymiarowymi tablicami jest tak samo łatwa jak z jednowymiarowymi. Na razie nie zamieszczam żadnych przykładowych procedur/funkcji operujących na tablicach-znajdziecie je w podpunkcie o przesyłaniu tablic do proc/fun.

Tablice wielowymiarowe.

Tablice wymiarowe definiuje się poprzez dopisywanie kolejnych zakresów w definicji:

lub

To, co napisałem odnośnie tablic jednowymiarowych jak najbardziej odnosi się do tablic dwu- i wielowymiarowych, więc można np. napisać taką definicje:

Jeżeli chcemy dowiedzieć się, tak jak w przypadku tablic jednowymiarowych, jaki jest górny i dolny zakres danej tablicy lub ile posiada elementów to postępuje w ten sposób:

Mnożąc dwa ostatnie wyniki przez siebie otrzymamy liczbę wszystkich elementów w tablicy.
Tak samo postępujemy z tablicami o większej ilości wymiarów.

Stałe tablice.

Czasami zachodzi potrzeba użycia tablicy, której wartości elementów są już przez nas znane na etapie pisania aplikacji. Możemy wtedy zdefiniować tablicę jako stałą:

Oczywiście, należy od razu zainicjalizować tablicę.

Tablice dynamiczne.

Bardzo często zdarza się w naszym programie nie wiemy dokładnie, ile elementów powinna zawierać nasza tablica np. tworzymy program-książkę adresową i za każdym dodaniem nowego wpisu tworzymy nowy element tablicy. Gdybyś używał tablicy statycznej, to ile elementów powinna zawierać tablica przechowująca wpisy? 100? 1000? 10000? Może na zapas 1000000, bo chcemy mieć pewność, że dojdzie do przekroczenia zakresu tablicy. Takie rozwiązanie jest jednak bardzo nieeleganckie, przez cały czas trwania programu w pamięci trzymana jest ogromna tablica. Dzięki tablicom dynamicznym, możemy za zawołanie zmieniać jej wielkość wedle naszych potrzeb. Popatrz:

To właśnie definicja tablicy dynamicznej-nie podajemy zakresu. Na razie nie możemy korzystać z naszej tablicy, gdyż nie ma ona żadnych elementów. Powiększanie tablicy dynamicznej odbywa się za pomocą procedury SetLength:

Teraz tablica ta ma 10 elementów ponumerowanych od 0 do 9-zakres tablic dynamicznych zawsze zaczyna się od zera, poza tym, indeksami są zawsze tylko liczby całkowite (Integer):

Nowe elementy tablicy dynamicznej zawsze inicjalizowane są zerami(o ile jej elementy są typu Integer), znakiem #0 w przypadku Charów czy ‘’(pustym ciągiem) w przypadku Stringów podczas używania procedury SetLength:

Aby zwolnić pamięć zajmowaną przez tablicę dynamiczną, można albo przypisać jej wartość nil lub ustalić jej zakres jako 0:

Tablicy TabDyn nadal można używać, nadając jej nowy rozmiar:

Podczas omawiania tablic statycznych, przedstawiłem problem przypisywania do siebie dwóch tablic. Obowiązuje on także w przypadku tablic dynamicznych, jednak, przypisywanie do siebie tablic dynamicznych to nie to samo, co tablic statycznych.
W tym przypadku, wartości z tablicy a zostaną skopiowane do tablicy b:

W przypadku tablic dynamicznych jest inaczej:

Zagadka: co zostanie pokazane w wiadomości? 10? Nie!-20. Dzieje się tak dlatego, że przy przypisywaniu do siebie dwóch tablic dynamicznych, nie występuje kopiowanie. Tablice są wskaźnikami to uporządkowanych w pamięci elementów i w tym przypadku, poprzez d := c każemy wskaźnikowi d pokazywać na to samo, na co pokazuje wskaźnik c. Dlatego nie ważne od tej pory którego wskaźnika będziemy używać-oba pokazują na to samo miejsce w pamięci.
Jeżeli chcemy przekopiować wartości tablicy, tak jak w wypadku tablic statycznych, musi posłużyć się funkcją Copy:

Jak widać nie trzeba używać procedury SetLength w stosunku do tablicy D.

Jeżeli porównujemy dwie tablice dynamiczne:

To nie są porównywane wartości elementów, lecz to, czy oba wsakźniki pokazują na ten sam blok pamięci, więc w powyższym przykładzie nie pojawi się komunikat Takie same.

Pracując z tablicami dynamicznymi, którym ustalono już jakąś ilość elementów, mamy do dyspozycji funkcje Length, która zwraca ilość elementów w tablicy, Low, która zawsze zwraca 0 oraz funkcję High, która zwraca indeks ostatniego elementu(w przypadku tablic dynamicznych-High = Length -1), jeżeli tablica nie ma żadnego elementu-High zwróci -1. Jeżeli chcemy zmienić rozmiar tablicy, użyamy funkcji Copy lub SetLength (ta jest szybsza).

Wielowymiarowe tablice dynamiczne.

Oczywiście, tablice dynamiczne również mogą mieć wiele wymiarów.

Jak widać, w procedurze SetLength podajemy kolejną ilość elementów (gdyby tablica miała więcej wymiarów, to po kolei dodalibyśmy ilość elementów).
Jednakże, co odróżnia tablice dynamiczne od statycznych, nie muszą być one prostokątne:

To tyle, jeśli chodzi o dynamiczne i statyczne. Zostało jeszcze do omówienia przesyłania i odbieranie tablic statycznych/dynamicznych w funkcjach i procedurach.

Przesyłanie tablic.

Jeżeli chcemy przesłać do procedury/funkcji tablicę pięcioelementową, to możemy to zrobić na dwa sposoby:

 

Niemożliwe jest napisanie takiej deklaracji:

Do Proc1 możemy przesłać jedynie tablice o ilości elementów równej pięć typu Integer, co więcej, muszą one być typu TTab5El, inaczej nie będziemy mogli jej przesłać.

W drugim przypadku mamy do czynienia z tak zwanymi otwartymi tablicami, możemy do Proc2 przesłać każdą tablicę typu Integer, nie ważne czy jest ona dynamiczna lub statyczna, ma 5 czy 100 elementów, jej typem indeksu jest Char czy Boolean:

W obu przypadkach(Proc1, Proc2), przesyłając tablicę, kompilator tworzy kopię naszej tablicy i to na niej operujemy w procedurze/funkcji. Co za tym idzie, zmieniając w procedurze tablicę nie zmieniamy jej oryginału lecz kopię, więc po wykonaniu procedury nasza tablica jest nietknięta. Poza tym, tworzenie kopii tablicy jest czasochłonne i w przypadku dużych tablic jest to bardzo nie korzystne:

Jeżeli jednak chcemy, aby nie kopiowano naszej tablicy lecz pracowano na oryginalne, co spowoduje, że wszelkie zmiany będą widoczne w przesyłanej tablicy oraz całość będzie się wykonywać szybciej, musimy posłużyć się słowem kluczowym var:

O ile posługiwanie się tablicą przesłaną do 1. procedury(Proc1) jest intuicyjne i nie potrzeba żadnych wyjaśnień, o tyle w Proc2, z racji tego że parametr to array of Typ, jest trochę inaczej.

1. Indeksowanie przesyłanych tablic zawsze zaczyna się od 0, a typ indeksu to zawsze Integer.

2. Nie wolno nam działać na przesłanej tablicy jako całości-nie można zmieniać jej rozmiaru procedurą SetLength, przypisywać danych z innej tablicy itp.

3. Możemy ją przesyłać dalej, do innych procedur/funkcji o ile ich parametrem jest (w naszym przypadku) Nazwa : array of Integer lub var Nazwa : array of Integer ;

4. Zamiast przesyłać do naszej procedury/funkcji tablicę, możemy przesłać zmienną typu takiego jak tablica-wtedy będzie to traktowane jak przesłanie tablicy o jednym elemencie o wartości przesłanej zmiennej:

W przypadku, gdy parametrem jest array of Typ tak jak w Proc2, możemy posłużyć się konstruktorem, który tworzy tablicę podczas uruchomiania proc/fun:

co jest równoważne do:

Konstruktorów tablic otwartych używać można jedynie w parametrach przesyłanych przez wartość lub jako stałe parametry (const).

Pozostaje jedno pytanie: co zrobić, jeżeli chcemy mieć możliwość zmiany rozmiaru przesłanej do proc/fun tablicy dynamicznej? Należy utworzyć nowy typ tablicowy:

Mamy możliwość zmieniać rozmiar kopii przesłanej tablicy. Jeżeli chcielibyśmy, aby to ‘oryginalna’ była zmieniana, musimy dodać słowo kluczowe var:

W przypadku zwracania tablicy przez funkcję, nie może się posłużyć zapisami:

Musimy utworzyć nowy typ tablicowy i obiekt tego typu zwrócić:

To, można by pomyśleć, nareszcie koniec 😉 Starałem się wyczerpać temat, jednak i tak zostaje jeszcze to i owo, na przykład praca na tablicach używając wskaźników, ale na to zostawiam miejsce w artykule o wskaźnikach.

W razie jakichkolwiek uwag/sugestii proszę o maile lub PW, a w razie pytań-na forum.

Autor: Iskar