Wskazówki dotyczące zmiennych, tablic i procedur 1 Spis treści 1. Tworzenie zmiennych i tablic 1 2. Procedury i zmienne, przekazywanie zmiennych do procedur 5 3. Zakończenie działania procedury 9 1. Tworzenie zmiennych i tablic Projektując algorytm należy pamiętać, że podczas operowania wartością zmiennej procedura musi ją wcześniej znać - tzn. zmienna ta musi mieć przypisaną wartość początkową (proces nadawania wartości początkowej nazywamy inicjalizacją). Inicjalizacja zachodzi zazwyczaj w chwili pierwszego przypisania wartości przy pomocy strzałki:. W przypadku danych wejściowych jest inaczej - zmienne, które za nie odpowiadają, zostają zainicjalizowane wraz z wywołaniem procedury z odpowiednimi argumentami w programie nadrzędnym. (a) W pierwszym przykładzie mamy do czynienia z dwiema inicjalizacjami: w kroku 1. zmiennej a, natomiast w kroku 2. zmiennej b. Zwróćmy uwagę na to, że bez uprzedniego przypisania wartości zmiennej a nie moglibyśmy wykonać kroku 2: nie wiedzielibyśmy, co oznacza 2a, jeżeli nie znalibyśmy a. Przyklad_a() 1. a 3 2. b 2a (b) Jeśli zamierzamy użyć zmiennej w warunku, również musi mieć ona wcześniej zdefiniowaną wartość. W procedurze Przyklad_b błędne byłoby pominięcie 1. linijki: Przyklad_b() 1. ocena 2 2. if ocena < 3 3. then Wypisz Brak zaliczenia... 4. else Wypisz Brawo: zaliczyłeś przedmiot! Wywołanie Przyklad_b() Brak zaliczenia... 1 Ćwiczenia audytoryjne z przedmiotu Podstawy informatyki, Wydział Fizyki i Informatyki Stosowanej AGH, 2017/2018. Elżbieta Wach (Elzbieta.Wach@fis.agh.edu.pl), http://galaxy.agh.edu.pl/~ewach/pi.html 1
(c) Identyczne reguły rządzą zmienną sterującą pętlą - bez inicjalizacji wartości i pętla byłaby błędnie zapisana: Przyklad_c() 1. i 0 2. while i 5 3. do Wypisz i, 4. i i + 1 Wywołanie Przyklad_c() 0 1 2 3 4 5 (d) Tak samo jest w przypadku pętli for, jednak tu wystarczy, że inicjalizacja zmiennej sterującej znajdzie się w definicji pętli: w procedurze Przyklad_d w 1. linii. Pomijamy również i i + 1, ponieważ w pętli for dzieje się to automatycznie. Przyklad_d() 1. for i 0 to 5 2. do Wypisz i, Wywołanie Przyklad_d() 0 1 2 3 4 5 (e) Oczywiście nic nie stoi na przeszkodzie, aby zmienną sterującą zainicjalizować przed pętlą for - aczkolwiek nie zwalnia nas to z obowiązku zdefiniowania jej wartości w chwili pierwszego wejścia do pętli (poprzednia wartość zostanie nadpisana): Przyklad_e() 1. i 8 2. for i 0 to 5 3. do Wypisz i, Wywołanie Przyklad_e() 0 1 2 3 4 5 (f) Naturalnie nazwy nie mają znaczenia - nie musimy się przywiązywać do nazywania zmiennej sterującej literką i : Przyklad_f() 1. for zmienna 0 to 5 2. do Wypisz zmienna, Wywołanie Przyklad_f() 0 1 2 3 4 5 (g) Wróćmy do pętli while. W algorytmie Przyklad_g() w porównaniu z procedurą z podpunktu (c) wprowadzono tylko jedną zmianę: w linii o numerze 4 brakuje wcięcia. W związku z tym zwiększenie i następuje dopiero po zakończeniu pętli, więc w jej wnętrzu i ma stałą wartość 0. Niewielka zmiana w kodzie doprowadziła do olbrzymiej zmiany w wykonaniu algorytmu - pętla stała się nieskończona! Oczywiście jest to błąd (należy uważnie tworzyć pętle, aby każda była skończona). 2
Przyklad_g() 1. i 0 2. while i 5 3. do Wypisz i, 4. i i + 1 Wywołanie Przyklad_g() 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0... (h) Naprawiliśmy pętlę while, już nie jest nieskończona. Istnieje sposób na ominięcie linijki z inicjalizacją zmiennej i - możemy pobierać i na wejściu procedury. Wówczas inicjalizacja następuje wraz z wywołaniem procedury Przyklad_h w procedurze nadrzędnej (np. wywołanie Przyklad_h(-3) powoduje inicjalizację zmiennej i wartością -3): Przyklad_h(i) 1. while i 5 2. do Wypisz i, 3. i i + 1 4. Wypisz STOP Wywołanie Przyklad_h(-3) 3 2 1 0 1 2 3 4 5 STOP Wywołanie Przyklad_h(4) 4 5 STOP Wywołanie Przyklad_h(15) STOP (i) Wszystkie zasady inicjalizacji zmiennych dotyczą także tablic zmiennych - nie wolno nam odwoływać się do wartości elementów tablicy bez wcześniejszej inicjalizacji. W procedurze Przyklad_i pierwsze trzy linijki tworzą tablicę o trzech elementach i nadają im początkowe wartości. Gdyby ich zabrakło, nie moglibyśmy ani wypisać wartości elementów C[k], ani używać ich w obliczeniach czy warunkach. Przyklad_i() 1. C[0] 4 2. C[1] 5 3. C[2] 6 4. for k 0 to length[c] 1 5. do x (C[k]) 2 6. if C[k] > 0 7. then Wypisz C[k], do kwadratu =, x 8. C[k] k 9. Wypisz znak nowej linii Wywołanie Przyklad_i() 4 do kwadratu = 16 5 do kwadratu = 25 6 do kwadratu = 36 3
(j) Usuńmy wspomniane trzy pierwsze linijki procedury. Jak wspomnieliśmy, większości kroków poprzedniego algorytmu nie wolno nam już wykonać. Co nam pozostało? Tylko przypisanie wartości C[k] k, ponieważ tym razem jest ono równoznaczne z inicjalizacją. Ponadto nie możemy użyć length[c], gdyż jest to jeszcze nieznana wartość (przy pierwszym wykonaniu pętli for tablica C jeszcze nie istnieje). Przyklad_j() 1. for k 0 to 2 2. do C[k] k 3. Wypisz znak nowej linii Wywołanie Przyklad_j() (k) Długość tablicy, tj. liczba kryjąca się pod length[c], jest równa liczbie elementów, które zostały zainicjalizowane. Pierwsza pętla for w procedurze Przyklad_k tworzy tablicę o trzech elementach (C[0], C[1], C[2]). W linijce 5. nie inicjalizujemy nowego elementu, tylko edytujemy już istniejący (inicjalizacja danego elementu tablicy - tak jak pojedynczej zmiennej - zachodzi tylko raz, za to możemy wiele razy coś do niego przypisywać, tj. zmieniać jego wartość). Inicjalizacja nowych elementów zachodzi w 7. linijce oraz dalej: w drugiej pętli for. Przyklad_k() 1. for k 0 to 2 Wywołanie Przyklad_k() I. Długość tablicy = 3 2. do C[k] k II. Długość tablicy = 3 3. Wypisz I. Długość tablicy =, length[c] 4. Wypisz znak nowej linii III. Długość tablicy = 4 IV. Długość tablicy = 11 5. C[2] 16 6. Wypisz II. Długość tablicy =, length[c], znak nowej linii 7. C[3] 18 8. Wypisz III. Długość tablicy =, length[c], znak nowej linii 9. for k length[c] to 10 10. do C[k] 2 k 11. Wypisz IV. Długość tablicy =, length[c] (l) Oczywiście nic nie stoi na przeszkodzie, aby procedura dostawała gotową tablicę na wejściu - wówczas jej wartości są już znane i nie trzeba ich ustalać, możemy korzystać z danych elementów. Wywołanie Tworzymy_tab() daje ten sam efekt, co procedura z podpunktu (i). 4
Tworzymy_tab() 1. T ab[0] 4 2. T ab[1] 5 3. T ab[2] 6 4. Przyklad_l(T ab) Wywołanie Tworzymy_tab() 4 do kwadratu = 16 5 do kwadratu = 25 6 do kwadratu = 36 Przyklad_l(C) 1. for k 0 to length[c] 1 2. do x (C[k]) 2 3. if C[k] > 0 4. then Wypisz C[k], do kwadratu =, x 5. C[k] k 6. Wypisz znak nowej linii 2. Procedury i zmienne Przekazywanie zmiennych do procedur Algorytmy, które projektujemy, często przyjmują dane wejściowe - tzw. argumenty, parametry. Pojedyncze zmienne (np. x, i, klucz, itd.) są do nich przekazywane przy pomocy wartości - oznacza to, że procedura dostaje tylko kopię parametru. Min2(a, b) Program() 2. then return a 2. d Min2(2, c) 3. else return b 3. Wypisz Minimalna liczba to:, d Wywołanie Program() Minimalna liczba to: 2 Algorytm Program w 2. linii kodu wywołuje procedurę Min2 z parametrami 2 i c, a wynik przypisuje do zmiennej d. W efekcie rozpoczyna się procedura Min2, która jako drugi argument dostaje kopię zmiennej c, a jej wartość znajduje się w b w czasie działania procedury Min2. Innymi słowy: z punktu widzenia Min2 zmienna a zostaje zainicjalizowana wartością 2, natomiast zmienna b wartością zmiennej c (tj. 8). Obie inicjalizacje odbyły się w momencie wywołania Min2(2, c). Tutaj ewentualna zmiana wartości b (np. przypisanie b 65 w Min2) pozostanie bez wpływu na wartość zmiennej c w procedurze nadrzędnej Program. Zmienna b jest tzw. zmienną lokalną, tzn. jej istnienie rozpoczyna się wraz z początkiem wykonania Min2, a kończy podczas zakończenia wykonywania tej procedury. Później po b nie ma śladu - jedyna możliwość zapamiętania jej wartości to zwrócenie poprzez słowo kluczowe return. Podobnie zmienna c jest widoczna tylko w procedurze Program, ale nie jest dostępna w Min2 - dlatego musieliśmy przekazać jej wartość jako daną wejściową. 5
Dla podsumowania rozpatrzmy kilka przypadków: (a) Wraz z wywołaniem procedury Min2_a powstają jej zmienne lokalne: a i b. Są równe co do wartości przekazanym przez Program_a argumentom: odpowiednio 2 i 8. W 2. linijce procedury Min2_a b zostaje trzykrotnie zwiększone. Pozostaje to bez wpływu na zmienną c w programie nadrzędnym: efektem polecenia Wypisz c w Program_a będzie wypisanie na ekran starej liczby 8. Min2_a(a, b) Program_a() 2. then b 3b 2. d Min2_a(2, c) 3. return a 3. Wypisz Minimalna liczba to:, d 4. else return b 4. Przejdź do nowej linii 5. Wypisz c Wywołanie Program_a() Minimalna liczba to: 2 8 (b) Równie dobrze zmienną lokalną procedury Min2_b możemy nazwać identycznie, jak zmienną lokalną procedury nadrzędnej, nie wywoła to konfliktu. Dla jasności różne zmienne o tej samej nazwie zostały zaznaczone różnymi kolorami: czerwone c jest niewidoczne w procedurze Min2_b, natomiast niebieskie c jest niewidoczne w procedurze Program_b. Krok nr 2 wewnątrz Min2_b przypisze 3 8 do niebieskiej zmiennej c, ale czerwona pozostanie bez zmian. Efekt wykonania tych procedur jest więc identyczny jak w podpunkcie (a). Min2_b(a, c) Program_b() 1. if a < c 1. c 8 2. then c 3c 2. d Min2_b(2, c) 3. return a 3. Wypisz Minimalna liczba to:, d 4. else return c 4. Przejdź do nowej linii 5. Wypisz c Wywołanie Program_b() Minimalna liczba to: 2 8 (c) Procedura Min2_c zwraca wartość zmiennej a lub b. Skoro tak, to tę wartość wolno nam przypisać do zmiennej w algorytmie nadrzędnym, w którym wywołalibyśmy tę procedurę (np. d - jak 6
w poprzednich przykładach). Wolno nam również wywołać ją bez jawnego przypisania jej wyniku do zmiennej. W efekcie wykonania algorytmu Program_c uzyskamy napis: Minimalna liczba to: 2, czyli poprawny wynik - różnica jest taka, że liczba 2 nie będzie zapamiętana, więc w ewentualnych kolejnych krokach algorytmu nie moglibyśmy z niej skorzystać. Min2_c(a, b) Program_c() 2. then return a 2. Wypisz Minimalna liczba to: 3. else return b 3. Wypisz Min2_c(2, c) Wywołanie Program_c() Minimalna liczba to: 2 (d) Tak jak poprzednio, tak i teraz nie przypiszemy wyniku działania procedury Min2_d bezpośrednio do żadnej zmiennej. Tym razem w kroku 2. algorytmu Program_d zostanie ona od razu zwrócona poprzez słowo kluczowe return - tak też nam wolno zrobić. W związku z tym nic nie zostanie wypisane na ekran. Aby uzyskać dostęp do naszej minimalnej wartości, musielibyśmy ją obsłużyć w kolejnej procedurze, w której wywołalibyśmy Program_d. Min2_d(a, b) Program_d() 2. then return a 2. return Min2_d(2, c) 3. else return b Wywołanie Program_d() (e) Dopuszczalne jest również wykonanie procedury Min2_e tylko poprzez samo wywołanie - jak w 2. kroku procedury Program_e. Od poprzedniego przykładu różni się tylko brakiem słowa return. Jaki będzie efekt? Minimum z dwóch liczb zostanie poprawnie znalezione, ale nie będziemy mieli do niego już żadnego dostępu, ponieważ ani nie przypisaliśmy go do żadnej zmiennej (np. d Min2_e(2, c) ), ani nie wypisaliśmy ( Wypisz Min2_e(2, c) ), ani nie zwróciliśmy ( return Min2_e(2, c) ). Min2_e(a, b) Program_e() 2. then return a 2. Min2_e(2, c) 3. else return b Wywołanie Program_e() 7
(f) W poprzednim przykładzie nie mieliśmy żadnego pożytku z procedury znajdującej minimum dwóch liczb. Jak możemy temu zaradzić? Niech procedura Min2_f chociaż wypisuje tę najmniejszą wartość. W tym celu zamienimy return na wypisywanie (oczywiście nie jest to konieczne - można i wypisywać, i zwracać). Min2_f(a, b) Program_f() 2. then Wypisz a 2. Min2_f(2, c) 3. else Wypisz b Wywołanie Program_f() 2 Jaka jest najważniejsza różnica pomiędzy tą wersją procedury Min2_f a poprzednimi? Tym razem procedura nic nie zwraca, więc możemy ją tylko wywołać, ale nie wolno nam efektu tego wywołania przypisać do zmiennej ani wypisywać! W tabeli 1 znajdziemy podsumowanie różnych sposobów wywoływania procedur. Procedura Możliwość wywołania Procedura_zwracajaca() poprzez przypisanie do zmiennej, np.: 1. return 5 d Procedura_zwracajaca() poprzez użycie w operacji, np.: d 4 (Procedura_zwracajaca() 3) 4 poprzez użycie w warunku, np.: if (Procedura_zwracajaca() 5) < 10 5 poprzez wypisanie: Wypisz Procedura_zwracajaca() poprzez zwrócenie: return Procedura_zwracajaca() poprzez samo wywołanie: Procedura_zwracajaca() Procedura_nie_zwracajaca() poprzez samo wywołanie: 1. Wypisz 5 Procedura_nie_zwracajaca() Tabela 1: Różne sposoby wywołania procedur; procedura, która nie zwraca żadnej wartości (ani tablicy czy innej struktury danych) może zostać wywołana tylko w jeden sposób. 8
3. Zakończenie działania procedury Procedura kończy się w momencie, gdy po raz pierwszy napotka słowo ERROR lub return. Jeżeli nie trafi na żadne z nich, zakończy się wraz z ostatnią wykonywaną linią kodu. Odwrotnosc(x) // x R 1. if x < 10 6 2. then ERROR Błąd: nie można dzielić przez 0! 3. else if x > 0 4. then Wypisz Podana liczba jest dodatnia. 5. return 1.0/x 6. else Wypisz Podana liczba jest ujemna. 7. return 1.0/x 8. Wypisz Pozostałe kroki algorytmu 9. y 10x 10. return 0.5y 3 Wykonanie Odwrotnosc z zerowym argumentem wejściowym spowoduje najszybsze zakończenie procedury: stanie się to w 2. linijce razem ze zgłoszeniem błędu. Kolejne warunki nie będą już sprawdzane. Wywołanie Odwrotnosc(0.0) Błąd: nie można dzielić przez 0! Zakładając, że wpisaliśmy dodatnią wartość dla x, spełniony będzie dopiero drugi warunek, a procedura zakończy się w linii nr 5 zwracając 1.0/x. Wywołanie Odwrotnosc(5.6) Podana liczba jest dodatnia. Podobnie dla ujemnego x: procedura kończy się na 7. linii. Wywołanie Odwrotnosc(-5.2) Podana liczba jest ujemna. Warunki obsłużyły wszystkie możliwe wartości zmiennej x - niezależnie od tego, co podamy na wejściu 2, procedura zakończy się na linii 2., 5. lub 7. W związku z tym kroki 8., 9. i 10. nigdy nie zostaną wykonane! 2...zakładając, że procedura byłaby uruchamiana tylko i wyłącznie z danymi liczbowymi. 9