Idea
„Zmienne skalarne” służą do przechowywania jednej wartości określonego typu. Czasami jednak, w praktyce, mamy do czynienia z obiektami, które są bardziej złożone. Na przykład, żeby opisać położenie punktu X w przestrzeni trójwymiarowej potrzebujemy trzech jego współrzędnych. Teoretycznie, można by użyć do zapisu trzech zmiennych zwykłych: X1, X2, X3. Jeżeli teraz Y1, Y2, Y3 będzie opisywał drugi punkt (Y) w przestrzeni i jeżeli (traktując te punkty jako końce wektorów zaczepionych w punkcie (0,0,0) zapragniemy wyliczyć ich iloczyn skalarny obliczenia będą wyglądały jakoś tak:
1 2 3 4 |
1 A = 0. 2 A = A + X1 * Y1 3 A = A + X2 * Y2 4 A = A + X3 * Y3 |
(oczywiście mógłbym zapisać to w postaci „prostszej”: A = X1 * Y1 + X2 * Y2 + X3 * Y3 ale nie zmienia to istoty sprawy; komputer ze względu na swoją konstrukcję bardzo przypominającą konstrukcję najprostszego kalkulatora czterodziałniowego i tak wszystkie obliczenia będzie wykonywał w czterech etapach. Pomyślmy teraz o zmianie liczby wymiarów: istota obliczeń ie zmieni się gdy rozmiar przestrzeni zwiększy się do pięciu, czy zmniejszy do 2. W każdym przypadku będziemy musieli zmodyfikować wyrażenie (dodając lub odejmując człony). Zapiszmy teraz pierwszy blok kodu inaczej. Niech N oznacza rozmiar przestrzeni:
1 2 3 4 5 6 |
1 N = 3 2 A = 0. 3 I = 1 4 A = A + XI * YI 5 I = I + 1 6 IF (I .LE. N) THEN GOTO 4 END IF |
Kod wydłużył się nieznacznie (6 zamiast 4 linijek) ale nabrał cech uniwersalności — może być stosowany dla dowolnego rozmiaru przestrzeni N. W linii 4 użyłem symbolicznego zapisu kombinując nazwę zmiennej z dwu elementów: bazy (X albo Y) i numerka (I). Nie jest to zapis formalny. Aby go sformalizować trzeba wprowadzić pojęcie zmiennej złożonej, mogącej przechować więcej niż jedną wartość tego samego typu). Zmienną taką konstruuje się rezerwując w pamięci operacyjnej pewien obszar pamięci i używając „indeksu” (w naszym wypadku I) do wskazywania kolejnych wartości w tym obszarze.
Polecenie GOTO w powyższym przykładzie nie jest do końca formalnie poprawne, ale należy je rozumieć w sposób następujący: jeżeli I <= N należy powtórzyć wykonywanie kodu od linii oznaczonej numerem 4. Polecenia te realizują ideę „pętli”, czyli fragmentu kodu, który powtarzany jest tak długo, jak długo spełniony jest pewien warunek.
Deklaracje tablic
Aby powyższy kod sformalizować musimy, po pierwsze, zadeklarować tablicę czyli taką zmienną złożoną, w której realizuje się dostęp do poszczególnych elementów korzystając z „pomocniczego” indeksu tablicy. Niestety, poprawnie zadeklarować tablice X i Y można na kilka sposobów.
Najprostsza metoda będzie taka:
1 |
1 REAL X(3), Y(3) |
Kolejny sposób będzie następujący: ponieważ nazwy zmiennych zaczynają się na litery X i Y — są to (domyślnie) zmienne typu REAL. Wystarczy zatem podkreślić jedynie, że są to tablice i określić ich rozmiar:
1 |
1 DIMENSION X(3), Y(3) |
Jeszcze inny sposób jest następujący: najpierw podajemy atrybuty zmiennej, później deklarujemy zmienne zgodne z tymi atrybutami:
1 |
1 REAL, DIMENSION (3) :: X, Y |
Jeżeli chcemy napisać program, który będzie dosyć uniwersalny (to znaczy stosunkowo niezależny od rozmiaru tablicy przestrzeni N) można to zrobić tak: najpierw definiujemy parametr N nadając mu oczekiwaną wartość, a następnie deklarujemy w sposób parametryczny tablice i wszędzie w programie używamy N gdy tylko odnosimy się do wymiaru przestrzeni.
1 2 |
1 PARAMETER (N = 3) 2 REAL, DIMENSION (N) :: X, Y |
Teraz (o ile program napisany jest konsekwentnie) łatwo przystosowywać go do naszych potrzeb.
Istnieje też możliwość (choć to już wyższa szkołą jazdy — umiejętność konstruowania tablic „dynamicznych” to u mnie jest wymaganiem na ocenę bardzo dobrą) napisania programu, który będzie całkowicie niezależny od wymiaru przestrzeni. To znaczy będziemy wymiar podawali w chwili uruchomienia programu.
1 2 3 4 5 6 7 8 |
1 REAL, ALLOCATABLE, DIMENSION (:) :: X, Y 2 INTEGER N 3 … 4 READ(*,*) N 5 … 6 ALLOCATE( X( N ) ) 7 ALLOCATE( Y( N ) ) 8 … |
W programie tym pierwsza instrukcja informuje, że obiekty X i Y są typu rzeczywistego (REAL), są tablicami o nieokreślonym rozmiarze (DIMENSION (:)) oraz, że przydzielimy im rozmiar później (ALLOCATABLE).
Najpierw musimy zdefiniować wartość N (można ją przeczytać z zewnątrz albo wyliczyć). Funkcja ALLOCATE służy do przydzielenia obiektom o nazwach X i Y odpowiedniej ilości pamięci operacyjnej.
Deklaracja tablicy dwuwymiarowej będzie bardzo podobna. Najprostszy wariant jest taki:
1 |
1 REAL V(5,3) |
i deklaruje dwuwymiarową macierz prostokątną o 5 wierszach i 3 kolumnach. Można również stworzyć macierz dynamiczną:
1 2 3 4 5 6 |
1 REAL, ALLOCATABLE, DIMENSION(:,:) :: V 2 INTEGER M, N 3 … 4 READ(*,*) M, N 5 ALLOCATE( V( M, N ) ) 6 … |
Indeks w tablicy zadeklarowanej jako:
1 |
1 REAL X(3) |
musi być liczbą całkowitą z zakresu od 1 do 3 włącznie. Istnieje jednak możliwość zdefiniowania dolnej i górnej granicy indeksu dowolnie:
1 |
1 REAL X(0:2) |
deklarujemy tablicę X o trzech składowych: X(0), X(1), X(2). (NB w języku C wszystkie tablice standardowo mają dolny indeks równy 0; nie można deklarować tablic o innej wartości dolnego indeksu). Dolna granica może również być ujemna.
Do tablic jeszcze wrócimy, bo to i trudny i ważny temat.
Dodatkowe polecenia ułatwiające obsługę tablic
Realizacja idei tablic wymaga wprowadzenia (i opanowania) pewnych poleceń nastawionych na efektywną ich „obsługę” (na przykład „wyzerowanie” tablicy). Tradycyjnie, w języku Fortran, poleceniem takim była zawsze instrukcja DO. Kiedyś wyglądała ona tak:
1 2 |
1 DO 10 I = 1, N 2 10 X(I) = 0. |
Numerek występujący po poleceniu DO oznacza „zakres” kodu który ma być wykonywany. (Ten zakres wskazany jest za pomocą etykiety, czyli „numerycznej” przywieszki dodanej do linii kodu.) Natomiast konstrukcja I = 1, N mówi, że zmienna I wewnątrz pętli DO zmieniać się będzie od wartości 1 do N z (domyślnym) krokiem 1. Gdy chcemy zmienić krok, używamy konstrukcji: DO 10 I = 1, N, 2 (teraz będzie to krok 2).
Zmiany i unowocześnienia języka Fortran dokonane na przestrzeni wielu lat upodobniły język do innych języków „strukturalnych” i współczesna instrukcja DO (robiąca to samo wygląda inaczej:
1 2 3 |
1 DO I = 1, N 2 X(I) = 0. 3 END DO |
Instrukcje DO można „zanurzać” jedną w drugiej. Żeby wyzerować tablicę dwuwymiarową użyjemy konstrukcji:
1 2 3 4 5 |
1 DO I = 1, M 2 DO J = 1, N 3 V(I, J) = 0. 4 END DO 5 END DO |
„Wcięcia” kodu (realizowane za pomocą odstępów) są nieobowiązkowe, ale zwiększają jego czytelność.
Instrukcja DO nie jest jedyną, która ułatwia organizowanie pętli. Inne pojawią się wkrótce.
Na koniec, wreszcie, przykład, od którego zaczęliśmy:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
1 REAL, ALLOCATABLE, DIMENSION (:) :: X, Y 2 INTEGER N, I 3 REAL A 4 … 5 READ(*,*) N 6 … 7 ALLOCATE( X( N ) ) 8 ALLOCATE( Y( N ) ) 9 … 10 ! Tu gdzieś nadajemy/wczytujemy wartości elementom tablic X i Y 11 … 12 A = 0. 13 DO I = 1, N 14 A = A + X(I) * Y(I) 15 END DO |
Użycie tablic
Praktycznie żaden operator arytmetyczny nie pozwala wykonywać operacji na całej tablicy. W operacjach arytmetycznych mogą brać udział jedynie elementy tablicy. Do elementu tablicy odwołujemy się podając nazwę tablicy, a w nawiasach okrągłych indeks/indeksy wskazujące jednoznacznie na wybrany element:
1 |
X(I) * Y(I) |
Aby nadać wartość elementowi tablicy musi znaleźć się on po lewej stronie znaku równości:
1 |
X(I) = 5. |
Na programiście leży obowiązek dbania, żeby odwoływał się do elementów „istniejących” w tablicy. Niektóre kompilatory można „poprosić” aby dodawały kod sprawdzający czy granice tablic nie są przekraczane — zwiększa to długość kodu i spowalnia pracę programu.