Paradygmaty programowania/Wykład 4: Podprogramy: Różnice pomiędzy wersjami

Z Studia Informatyczne
Przejdź do nawigacjiPrzejdź do wyszukiwania
Pitab (dyskusja | edycje)
Wkm (dyskusja | edycje)
 
(Nie pokazano 14 wersji utworzonych przez 4 użytkowników)
Linia 1: Linia 1:
===Wprowadzenie=== W module czwartym omówimy kwestie związane z podprogramami — ich semantyką i sposobami implementacji w językach programowania. Tak jak dotychczas, podprogramy będziemy rozumieli szeroko, obejmując tym pojęciem wszelkie procedury, funkcje, metody itp. Omówimy różnice występujące w naszych ulubionych językach programowania, czyli głównie w Adzie, Javie, C++ i C#.
==Wstęp==
 
===Wstęp===


Podprogramy realizują jedną z dwóch podstawowych abstrakcji programowania, mianowicie abstrakcję procesu. O ile abstrakcja danych rozpowszechniła się dopiero w latach 80-tych XX wieku, abstrakcja procesu wydawała się czymś oczywistym od zarania dziejów algorytmiki i programowania. Któż nie chciałby budować programów z gotowych fragmentów, realizujących dobrze określone zadania w sposób szybki i niezawodny? By uświadomić sobie, jak daleka droga dzieli tworzenie wielkich systemów informatycznych od pisania drobnych programów w asemblerze, przytoczmy prosty przykład programu asemblerowego i jego odpowiednik w języku C (a przecież od owego programu w C do wielkiego systemu jest wciąż ogromnie daleko...).
Podprogramy realizują jedną z dwóch podstawowych abstrakcji programowania, mianowicie abstrakcję procesu. O ile abstrakcja danych rozpowszechniła się dopiero w latach 80-tych XX wieku, abstrakcja procesu wydawała się czymś oczywistym od zarania dziejów algorytmiki i programowania. Któż nie chciałby budować programów z gotowych fragmentów, realizujących dobrze określone zadania w sposób szybki i niezawodny? By uświadomić sobie, jak daleka droga dzieli tworzenie wielkich systemów informatycznych od pisania drobnych programów w asemblerze, przytoczmy prosty przykład programu asemblerowego i jego odpowiednik w języku C (a przecież od owego programu w C do wielkiego systemu jest wciąż ogromnie daleko...).


       ''petla:
       petla:
         pushl %ebp
         pushl %ebp
         movl %esp,%ebp
         movl %esp,%ebp
Linia 23: Linia 21:
         movl %ebp,%esp
         movl %ebp,%esp
         popl %ebp
         popl %ebp
         ret''
         ret


Odpowiednik w języku C:
Odpowiednik w języku C:


       ''int petla(int x, int y)
       int petla(int x, int y)
       {
       {
         int wyn;
         int wyn;
 
       
         '''for''' (wyn = 0; x < y; wyn++) {
         for (wyn = 0; x < y; wyn++) {
           x++;
           x++;
           y--;
           y--;
         }
         }
         '''return''' wyn + 1;
         return wyn + 1;
       }''
       }




Linia 53: Linia 51:
*Podprogramy mogą mieć definicje i oddzielne deklaracje. Te drugie są używane w niektórych językach programowania (np. w C i C++, gdzie nazywane są prototypami i umieszczane są zwykle w plikach .h).
*Podprogramy mogą mieć definicje i oddzielne deklaracje. Te drugie są używane w niektórych językach programowania (np. w C i C++, gdzie nazywane są prototypami i umieszczane są zwykle w plikach .h).


===Przekazywanie parametrów===
==Przekazywanie parametrów==


Podprogram nie będący metodą ma dostęp do danych na dwa sposoby: przez zmienne nielokalne (zgodnie z zasadami zakresu widoczności) oraz przez przekazane parametry. Oczywiście drugi sposób jest znacznie lepszy, gdyż pozwala na jasne zdefiniowanie sprzężenia podprogramu z otoczeniem, bez polegania na trudnych często do uchwycenia efektach ubocznych. Metoda ma dodatkowo dostęp do danych obiektu, z którego jest wołana.
Podprogram nie będący metodą ma dostęp do danych na dwa sposoby: przez zmienne nielokalne (zgodnie z zasadami zakresu widoczności) oraz przez przekazane parametry. Oczywiście drugi sposób jest znacznie lepszy, gdyż pozwala na jasne zdefiniowanie sprzężenia podprogramu z otoczeniem, bez polegania na trudnych często do uchwycenia efektach ubocznych. Metoda ma dodatkowo dostęp do danych obiektu, z którego jest wołana.
W pewnych sytuacjach chcielibyśmy przekazać do podprogramu nie tyle dane, co sposób obliczeń.
Pewne języki na to pozwalają. Szerzej zajmiemy się tą sprawą w przedstawionych poniżej


przykładach.
W pewnych sytuacjach chcielibyśmy przekazać do podprogramu nie tyle dane, co sposób obliczeń. Pewne języki na to pozwalają. Szerzej zajmiemy się tą sprawą w przedstawionych poniżej przykładach.


Wypunktujmy podstawowe definicje dotyczące przekazywania parametrów:
Wypunktujmy podstawowe definicje dotyczące przekazywania parametrów:
*Parametry w nagłówku nazywane są ''parametrami formalnymi''.
*Parametry w nagłówku nazywane są ''parametrami formalnymi''.
*Parametry w instrukcji wywołania podprogramu, które zostaną przypisane parametrom formalnym,  
*Parametry w instrukcji wywołania podprogramu, które zostaną przypisane parametrom formalnym, nazywane są ''parametrami aktualnymi''.
 
*W większości języków, odpowiedniość pomiędzy parametrami formalnymi a aktualnymi ustalana jest poprzez zestawienie ich położenia w liście. Mówi się, że są to ''parametry pozycyjne''.
nazywane są ''parametrami aktualnymi''.
*Stosuje się też wiązanie parametrów formalnych z aktualnymi na podstawie nazw podanych wraz z parametrami aktualnymi. Są to ''parametry z kluczem''.
*W większości języków, odpowiedniość pomiędzy parametrami formalnymi a aktualnymi ustalana  
*Niektóre języki pozwalają na użycie parametrów z wartością domyślną, np. Ada, C++, PHP. Wówczas liczba parametrów aktualnych może być mniejsza niż liczba parametrów formalnych.
*Niektóre języki programowania posiadają mechanizmy przekazywania zmiennej liczby parametrów, np. params w C++. Wróćmy teraz do przekazywania przez parametr sposobu obliczeń. Załóżmy, że piszemy funkcję, która służy do całkowania funkcji na liczbach rzeczywistych. Chcielibyśmy, by jej parametrami były: funkcja do scałkowania oraz granice przedziału całkowania. Innymi słowy, chcielibyśmy móc używać naszej funkcji np. tak:


jest poprzez zestawienie ich położenia w liście. Mówi się, że są to ''parametry pozycyjne''.
      wynik = całkuj(sin, 0, 0.123)
*Stosuje się też wiązanie parametrów formalnych z aktualnymi na podstawie nazw podanych wraz


z parametrami aktualnymi. Są to ''parametry z kluczem''.
Pierwszym parametrem jest tu funkcja sin. Typem takiego parametru jest cały protokół przekazywanej funkcji(tu np.: jeden parametr double i wynik double). Nie każdy język na to pozwala. W Pascalu można przekazywać funkcje i procedury bezpośrednio. W C i C++ można przekazywać wskaźniki do funkcji, co daje podobny efekt. W C# można używać delegatów instancjonowanych dla żądanej metody.
*Niektóre języki pozwalają na użycie parametrów z wartością domyślną, np. Ada, C++, PHP.
 
Wówczas liczba parametrów aktualnych może być mniejsza niż liczba parametrów formalnych.
*Niektóre języki programowania posiadają mechanizmy przekazywania zmiennej liczby parametrów,
 
np. params w C++.
 
Wróćmy teraz do przekazywania przez parametr sposobu obliczeń. Załóżmy, że piszemy funkcję,
 
która służy do całkowania funkcji na liczbach rzeczywistych. Chcielibyśmy, by jej parametrami
 
były: funkcja do scałkowania oraz granice przedziału całkowania. Innymi słowy, chcielibyśmy
 
móc używać naszej funkcji np. tak:
 
      ''wynik = całkuj(sin, 0, 0.123)''
 
Pierwszym parametrem jest tu funkcja sin. Typem takiego parametru jest cały protokół  
 
przekazywanej funkcji(tu np.: jeden parametr double i wynik double). Nie każdy język na to  
 
pozwala. W Pascalu można przekazywać funkcje i procedury bezpośrednio. W C i C++ można  
 
przekazywać wskaźniki do funkcji, co daje podobny efekt. W C# można używać delegatów  
 
instancjonowanych dla żądanej metody.


Przykład przekazywania wskaźnika do funkcji w języku C
Przykład przekazywania wskaźnika do funkcji w języku C
*Rozważmy następującą funkcję:
*Rozważmy następującą funkcję:


       ''double calkuj(double f(double), double a, double b)
       double calkuj(double f(double), double a, double b)
       {
       {
         ...
         ...
         x = f(...);
         x = f(...);
         ...
         ...
       }''
       }


*Wywołanie może wyglądać tak, jak opisano powyżej.
*Wywołanie może wyglądać tak, jak opisano powyżej.
*W „klasycznej” składni języka C nagłówek wyglądałby tak:
*W „klasycznej” składni języka C nagłówek wyglądałby tak:


       ''double calkuj(double (*f)(double), double a, double b)''
       double calkuj(double (*f)(double), double a, double b)


Przykład użycia delegatów w C#
Przykład użycia delegatów w C#
*Deklarujemy odpowiedni typ delegata:
*Deklarujemy odpowiedni typ delegata:


       ''delegate double dtype(double, double);''
       delegate double dtype(double, double);


*Metoda służąca do całkowania może mieć postać:
*Metoda służąca do całkowania może mieć postać:


       ''double calkuj(dtype f, double a, double b)
       double calkuj(dtype f, double a, double b)
       {
       {
         ...
         ...
         x = f(...);
         x = f(...);
         ...
         ...
       }''
       }


*Załóżmy, że jako parametr chcemy przekazać metodę f z klasy C. Tworzymy zatem delegata:
*Załóżmy, że jako parametr chcemy przekazać metodę f z klasy C. Tworzymy zatem delegata:


       ''dtype dfun = new dtype(C.f);''
       dtype dfun = new dtype(C.f);


*Możemy teraz wykorzystać delegata w wywołaniu, np.
*Możemy teraz wykorzystać delegata w wywołaniu, np.


       ''wynik = A.calkuj(dfun, 1.2, 3.4);''
       wynik = A.calkuj(dfun, 1.2, 3.4);
 
Rozważmy teraz rozróżnienie pomiędzy procedurami a funkcjami. Pod względem technicznym,


procedury i funkcje różnią się jedynie zwracaniem wartości, tzn. procedury jej nie zwracają.  
Rozważmy teraz rozróżnienie pomiędzy procedurami a funkcjami. Pod względem technicznym, procedury i funkcje różnią się jedynie zwracaniem wartości, tzn. procedury jej nie zwracają. Semantycznie jednak różnica jest istotna. Procedury są zestawem instrukcji, które definiują sparametryzowane obliczenia. Są one uruchamiane poprzez pojedyncze wywołanie. Można zatem powiedzieć, że procedury definiują nowe instrukcje. Funkcje natomiast przypominają funkcje matematyczne. Są wywoływane poprzez użycie ich nazwy w wyrażeniu. Nie powinny dawać żadnych efektów ubocznych (typowych w przypadku procedur). Funkcje definiują zatem nowe operatory. Języki programowania oparte na języku C formalnie nie posiadają procedur, ale funkcje typu void zachowują się tak jak procedury.
 
Semantycznie jednak różnica jest istotna. Procedury są zestawem instrukcji, które definiują  
 
sparametryzowane obliczenia. Są one uruchamiane poprzez pojedyncze wywołanie. Można zatem  
 
powiedzieć, że procedury definiują nowe instrukcje. Funkcje natomiast przypominają funkcje  
 
matematyczne. Są wywoływane poprzez użycie ich nazwy w wyrażeniu. Nie powinny dawać żadnych  
 
efektów ubocznych (typowych w przypadku procedur). Funkcje definiują zatem nowe operatory.  
 
Języki programowania oparte na języku C formalnie nie posiadają procedur, ale funkcje typu  
 
void zachowują się tak jak procedury.


Kolejna sprawa: sprawdzanie zgodności typów parametrów
Kolejna sprawa: sprawdzanie zgodności typów parametrów
Linia 161: Linia 114:
*Są jednak wyjątki: JavaScript, Perl, PHP.
*Są jednak wyjątki: JavaScript, Perl, PHP.
*Język C w wersji z końca lat 80-tych dawał wybór.
*Język C w wersji z końca lat 80-tych dawał wybór.
*Jeśli typy parametrów były zadeklarowane poza listą parametrów, to zgodność typów nie była  
*Jeśli typy parametrów były zadeklarowane poza listą parametrów, to zgodność typów nie była sprawdzana:


sprawdzana:
       int f(x)
 
       ''int f(x)
         double x;
         double x;
         {...}''
         {...}


*Jeśli typy były wymienione w owej liście, to ich zgodność była sprawdzana:
*Jeśli typy były wymienione w owej liście, to ich zgodność była sprawdzana:


       ''int f(double x)
       int f(double x)
         {...}''
         {...}


*W C i C++ można uniknąć sprawdzania typów za pomocą nagłówka z wielokropkiem, np.
*W C i C++ można uniknąć sprawdzania typów za pomocą nagłówka z wielokropkiem, np.


       ''int printf(const char *format, ...)''
       int printf(const char *format, ...)
 
===Problemy implementacyjne===
 
Twórca języka programowania musi podjąć wiele decyzji, których skutki sięgają niejednokrotnie


znacznie dalej niż mogłoby się wydawać. Przykładowo, stworzenie kompilatora dla zbyt
==Problemy implementacyjne==


wyrafinowanego języka może być trudne. Ta przypadłość spotkała niektóre języki, np. Adę. A to  
Twórca języka programowania musi podjąć wiele decyzji, których skutki sięgają niejednokrotnie znacznie dalej niż mogłoby się wydawać. Przykładowo, stworzenie kompilatora dla zbyt wyrafinowanego języka może być trudne. Ta przypadłość spotkała niektóre języki, np. Adę. A to — być może — dopiero początek kłopotów, jakie będą spotykały programistów, zmuszonych do uczenia się zawiłych konstrukcji. Tak było z... Adą. Decyzje odnoszące się do implementacji podprogramów są o tyle ważne, że wchodzą tu w grę bardzo złożone mechanizmy.
 
— być może — dopiero początek kłopotów, jakie będą spotykały programistów, zmuszonych do  
 
uczenia się zawiłych konstrukcji. Tak było z... Adą. Decyzje odnoszące się do implementacji  
 
podprogramów są o tyle ważne, że wchodzą tu w grę bardzo złożone mechanizmy.


Wymieńmy zatem niektóre kwestie związane z podprogramami:
Wymieńmy zatem niektóre kwestie związane z podprogramami:
*Wybór sposobów przekazywania parametrów.
*Wybór sposobów przekazywania parametrów.
*Czy typ parametrów aktualnych będzie sprawdzany z typem parametrów formalnych?
*Czy typ parametrów aktualnych będzie sprawdzany z typem parametrów formalnych?
*Czy lokalne zmienne będą alokowane statycznie czy dynamicznie?
*Czy lokalne zmienne będą alokowane statycznie, czy dynamicznie?
*Czy definicje podprogramów mogą być zagnieżdżone?
*Czy definicje podprogramów mogą być zagnieżdżone?
*Jeżeli podprogramy mogą być zagnieżdżane i mogą być przekazywane jako parametry, to co jest  
*Jeżeli podprogramy mogą być zagnieżdżane i mogą być przekazywane jako parametry, to co jest środowiskiem przekazywanego podprogramu?
 
środowiskiem przekazywanego podprogramu?
*Czy podprogramy mogą być przeciążane?
*Czy podprogramy mogą być przeciążane?
*Czy podprogramy mogą być uniwersalne (rodzajowe)?
*Czy podprogramy mogą być uniwersalne (rodzajowe)?


Konsekwencją istnienia podprogramów jest m.in. powstanie lokalnego środowiska odwołań.  
Konsekwencją istnienia podprogramów jest m.in. powstanie lokalnego środowiska odwołań. Powiedzmy zatem parę słów o zmiennych lokalnych. Lokalne zmienne w podprogramach mogą być statyczne bądź dynamiczne alokowane na stosie. Te pierwsze są szybkie i niezależne od historii wywołań (co może być użyteczne na przykład w generowaniu liczb pseudolosowych). Te drugie umożliwiają wywołania rekurencyjne. W języku C oraz C++, lokalne zmienne są dynamiczne, chyba że jawnie zadeklarowano, że mają być statyczne. Języki C++, C# i Java nie zezwalają na użycie zmiennych statycznych w metodach.
 
Powiedzmy zatem parę słów o zmiennych lokalnych. Lokalne zmienne w podprogramach mogą być  
 
statyczne bądź dynamiczne alokowane na stosie. Te pierwsze są szybkie i niezależne od  
 
historii wywołań (co może być użyteczne na przykład w generowaniu liczb pseudolosowych). Te  
 
drugie umożliwiają wywołania rekurencyjne. W języku C oraz C++, lokalne zmienne są  
 
dynamiczne, chyba że jawnie zadeklarowano, że mają być statyczne. Języki C++, C# i Java nie  
 
zezwalają na użycie zmiennych statycznych w metodach.


'''3.1 Sposoby przekazywania parametrów'''
===Sposoby przekazywania parametrów===


Na początku zajmiemy się różnymi modelami semantycznymi, a następnie rozważymy modele i  
Na początku zajmiemy się różnymi modelami semantycznymi, a następnie rozważymy modele i decyzje implementacyjne.
 
decyzje implementacyjne.
*Parametr formalny charakteryzuje jeden z trzech modeli semantycznych:
*Parametr formalny charakteryzuje jeden z trzech modeli semantycznych:
**Może otrzymać dane poprzez odpowiadający mu parametr aktualny (tryb wejściowy, in).
**Może otrzymać dane poprzez odpowiadający mu parametr aktualny (tryb wejściowy, in).
Linia 227: Linia 152:
**Może też używać obu trybów (tryb wejściowo-wyjściowy, in-out).
**Może też używać obu trybów (tryb wejściowo-wyjściowy, in-out).
*Przekazanie danych może następować na dwa sposoby:
*Przekazanie danych może następować na dwa sposoby:
**Kopiowana jest faktyczna wartość (do wywoływanego podprogramu, do programu wołającego lub w  
**Kopiowana jest faktyczna wartość (do wywoływanego podprogramu, do programu wołającego lub w obie strony).
 
obie strony).
**Dostęp do danych przekazywany jest pośrednio (np. przez wskaźnik).
**Dostęp do danych przekazywany jest pośrednio (np. przez wskaźnik).


Linia 240: Linia 163:


Przekazywanie przez wartość
Przekazywanie przez wartość
*Wartość parametru aktualnego jest używana do zainicjowania odpowiadającego mu parametru  
*Wartość parametru aktualnego jest używana do zainicjowania odpowiadającego mu parametru formalnego.
 
formalnego.
*Parametr formalny funkcjonuje następnie w podprogramie jako zmienna lokalna.
*Parametr formalny funkcjonuje następnie w podprogramie jako zmienna lokalna.
*Tak implementujemy semantykę trybu wejściowego.
*Tak implementujemy semantykę trybu wejściowego.
*Dane przekazuje się zwykle przez kopiowanie wartości, ale można też użyć dostępu pośredniego  
*Dane przekazuje się zwykle przez kopiowanie wartości, ale można też użyć dostępu pośredniego (z dodatkowym zabezpieczeniem przed zmianą wartości parametru aktualnego).
 
(z dodatkowym zabezpieczeniem przed zmianą wartości parametru aktualnego).
*Przekazywanie przez wartość jest kosztowne, gdy trzeba przekazać duże struktury danych.
*Przekazywanie przez wartość jest kosztowne, gdy trzeba przekazać duże struktury danych.


Linia 253: Linia 172:
*Nie przekazujemy wartości do podprogramu.
*Nie przekazujemy wartości do podprogramu.
*Parametr formalny działa jak zmienna lokalna.
*Parametr formalny działa jak zmienna lokalna.
*Tuż przez przekazaniem sterowania z powrotem do wywołującego, wartość parametru formalnego  
*Tuż przez przekazaniem sterowania z powrotem do wywołującego, wartość parametru formalnego jest przesyłana do parametru aktualnego w programie wywołującym.
 
jest przesyłana do parametru aktualnego w programie wywołującym.
*Parametr aktualny musi zatem być zmienną (a nie np. wyrażeniem arytmetycznym).
*Parametr aktualny musi zatem być zmienną (a nie np. wyrażeniem arytmetycznym).
*Tak implementujemy semantykę trybu wyjściowego.
*Tak implementujemy semantykę trybu wyjściowego.
*Dane przekazuje się zwykle przez kopiowanie.
*Dane przekazuje się zwykle przez kopiowanie.
*Przy przekazywaniu przez wynik może dojść do kolizji parametrów aktualnych, np. przy  
*Przy przekazywaniu przez wynik może dojść do kolizji parametrów aktualnych, np. przy wywołaniu subp(x, x). Załóżmy, że formalne parametry podprogramu subp to a i b. Jaka powinna być wartość x, jeśli subp zawiera podstawienia a := 1; b := 2?
 
*Inny problem: Załóżmy, że parametr aktualny to T[i]. Jeśli podprogram zmieni wartość zmiennej i, gdzie powinna znaleźć się zwrócona wartość, tzn. czy powinniśmy użyć starej czy nowej wartości indeksu?
wywołaniu subp(x, x). Załóżmy, że formalne parametry podprogramu subp to a i b. Jaka powinna  
 
być wartość x, jeśli subp zawiera podstawienia a := 1; b := 2?
*Inny problem: Załóżmy, że parametr aktualny to T[i]. Jeśli podprogram zmieni wartość  
 
zmiennej i, gdzie powinna znaleźć się zwrócona wartość, tzn. czy powinniśmy użyć starej czy  
 
nowej wartości indeksu?


Przekazywanie przez wartość i wynik
Przekazywanie przez wartość i wynik
*Jest to kombinacja dwóch wcześniejszych sposobów implementacji, dająca semantykę trybu  
*Jest to kombinacja dwóch wcześniejszych sposobów implementacji, dająca semantykę trybu wejściowo-wyjściowego.
 
*Zwane niekiedy przekazywaniem przez kopiowanie, jako że parametr aktualny jest kopiowany do parametru formalnego, a następnie kopiowany z powrotem przy zakończeniu podprogramu.
wejściowo-wyjściowego.
*Zwane niekiedy przekazywaniem przez kopiowanie, jako że parametr aktualny jest kopiowany do  
 
parametru formalnego, a następnie kopiowany z powrotem przy zakończeniu podprogramu.


Przekazywanie przez referencję
Przekazywanie przez referencję
*Jest to alternatywna implementacja semantyki trybu wejściowo-wyjściowego.
*Jest to alternatywna implementacja semantyki trybu wejściowo-wyjściowego.
*Dostęp do danych przekazywany jest pośrednio, czyli przekazywany jest w istocie wskaźnik do  
*Dostęp do danych przekazywany jest pośrednio, czyli przekazywany jest w istocie wskaźnik do wartości a nie sama wartość.
 
wartości a nie sama wartość.
*Podprogram zyskuje więc faktyczny dostęp do parametru aktualnego.
*Podprogram zyskuje więc faktyczny dostęp do parametru aktualnego.
*Taki sposób przekazania jest efektywny.
*Taki sposób przekazania jest efektywny.
*Przekazywanie przez referencję może powodować aliasowanie, np. w przypadku wywołań subp(x,  
*Przekazywanie przez referencję może powodować aliasowanie, np. w przypadku wywołań subp(x, x) lub subp(T[i], T[j]) przy i = j.
 
*Aliasowanie może również się pojawić na skutek kolizji między parametrami formalnymi a zmiennymi nielokalnymi.
x) lub subp(T[i], T[j]) przy i = j.
*Zauważmy, że rzecz jest podobna (choć nie identyczna...) do kolizji parametrów aktualnych przy przekazywaniu przez wynik.
*Aliasowanie może również się pojawić na skutek kolizji między parametrami formalnymi a  
 
zmiennymi nielokalnymi.
*Zauważmy, że rzecz jest podobna (choć nie identyczna...) do kolizji parametrów aktualnych  
 
przy przekazywaniu przez wynik.


Przekazywanie przez nazwę
Przekazywanie przez nazwę
*Ten sposób implementuje semantykę trybu wejściowo-wyjściowego.
*Ten sposób implementuje semantykę trybu wejściowo-wyjściowego.
*Parametr aktualny jest wstawiany (tekstowo) w miejsce odpowiadającego mu parametru  
*Parametr aktualny jest wstawiany (tekstowo) w miejsce odpowiadającego mu parametru formalnego, we wszystkich wystąpieniach w podprogramie.
 
*W poprzednio omówionych sposobach było inaczej — parametry formalne były wiązane z aktualnymi wartościami (lub adresami) w chwili wywołania podprogramu.
formalnego, we wszystkich wystąpieniach w podprogramie.
*Przy przekazywaniu przez nazwę parametr formalny jest wiązany z metodą dostępu do danych w chwili wywołania podprogramu, ale właściwe wiązanie z wartością lub adresem następuje dopiero w momencie odwołania lub przypisania do owego parametru formalnego.
*W poprzednio omówionych sposobach było inaczej — parametry formalne były wiązane z  
 
aktualnymi wartościami (lub adresami) w chwili wywołania podprogramu.
*Przy przekazywaniu przez nazwę parametr formalny jest wiązany z metodą dostępu do danych w  
 
chwili wywołania podprogramu, ale właściwe wiązanie z wartością lub adresem następuje dopiero  
 
w momencie odwołania lub przypisania do owego parametru formalnego.
*Jest to „późne wiązanie” — kosztowne...
*Jest to „późne wiązanie” — kosztowne...


'''3.2 Sposoby przekazywania parametrów w różnych językach'''
===Sposoby przekazywania parametrów w różnych językach===


Omówimy teraz pokrótce przekazywanie parametrów w kilku popularnych językach.
Omówimy teraz pokrótce przekazywanie parametrów w kilku popularnych językach.
Linia 321: Linia 210:
Język C
Język C
*Stosuje zawsze przekazywanie przez wartość.
*Stosuje zawsze przekazywanie przez wartość.
*Można jednak przekazywać (przez wartość) wskaźniki, co daje efekt taki jak tryb  
*Można jednak przekazywać (przez wartość) wskaźniki, co daje efekt taki jak tryb wejściowo-wyjściowy z przekazywaniem przez referencję.
 
*W szczególności, wskaźniki do stałych pozwalają uzyskać tryb wejściowy (niejawne zabezpieczenie przed zmianą wartości parametru w wywoływanej funkcji) z efektywnością właściwą dla przekazywania przez referencję.
wejściowo-wyjściowy z przekazywaniem przez referencję.
*Nie ma bezpośredniego sposobu przekazania tablicy inaczej niż przez przekazanie wskaźnika do niej, gdyż nazwa tablicy jest traktowana jako wskaźnik. Stąd tablice są w istocie przekazywane przez referencję. Mechanizm ten można ominąć, umieszczając tablicę jako fragment rekordu (struct) i przekazując ów rekord.
*W szczególności, wskaźniki do stałych pozwalają uzyskać tryb wejściowy (niejawne  
*Zwróćmy uwagę na różnicę między „przekazywaniem wskaźnika do ''x'' przez wartość” a „przekazywaniem ''x'' przez referencję”. W pierwszym przypadku programista musi (jawnie) pobrać i przekazać wskaźnik; w drugim — programista nie musi pobierać wskaźnika.
 
zabezpieczenie przed zmianą wartości parametru w wywoływanej funkcji) z efektywnością  
 
właściwą dla przekazywania przez referencję.
*Nie ma bezpośredniego sposobu przekazania tablicy inaczej niż przez przekazanie wskaźnika do  
 
niej, gdyż nazwa tablicy jest traktowana jako wskaźnik. Stąd tablice są w istocie  
 
przekazywane przez referencję. Mechanizm ten można ominąć, umieszczając tablicę jako fragment  
 
rekordu (struct) i przekazując ów rekord.
*Zwróćmy uwagę na różnicę między „przekazywaniem wskaźnika do x przez wartość” a  
 
„przekazywaniem x przez referencję”. W pierwszym przypadku programista musi (jawnie) pobrać i  
 
przekazać wskaźnik; w drugim — programista nie musi pobierać wskaźnika.


Język C++
Język C++
*Oprócz tego, co w języku C, dostępne są parametry referencyjne, dające przekazywanie przez  
*Oprócz tego, co w języku C, dostępne są parametry referencyjne, dające przekazywanie przez referencję.
 
*Zauważmy różnicę między parametrami stałymi (deklarowanymi z użyciem const) a parametrami w trybie wejściowym  w ogóle. Te pierwsze realizują oczywiście semantykę trybu wejściowego, z dodatkową cechą: nie mogą być zmieniane nawet w obrębie podprogramu. W większości języków natomiast parametry wejściowe funkcjonują jako zmienne lokalne — ich wartość można zmieniać, choć nie ma ona wpływu na parametry aktualne.
referencję.
*Zauważmy różnicę między parametrami stałymi (deklarowanymi z użyciem const) a parametrami w  
 
trybie wejściowym  w ogóle. Te pierwsze realizują oczywiście semantykę trybu wejściowego, z  
 
dodatkową cechą: nie mogą być zmieniane nawet w obrębie podprogramu. W większości języków  
 
natomiast parametry wejściowe funkcjonują jako zmienne lokalne — ich wartość można zmieniać,  
 
choć nie ma ona wpływu na parametry aktualne.


Java
Java
*Stosuje zawsze przekazywanie przez wartość.
*Stosuje zawsze przekazywanie przez wartość.
*Ponieważ jednak obiekty są dostępne tylko przez zmienne referencyjne, w istocie obiekty są  
*Ponieważ jednak obiekty są dostępne tylko przez zmienne referencyjne, w istocie obiekty są przekazywane przez referencję.
 
przekazywane przez referencję.


Ada
Ada
*Używa trzech trybów: in, out, in out.
*Używa trzech trybów: in, out, in out.
*W ten sposób jawnie stosujemy trzy modele semantyczne.
*W ten sposób jawnie stosujemy trzy modele semantyczne.
*Dodatkowo, pod parametry in nie można podstawiać, a do parametrów out nie można się odwołać  
*Dodatkowo, pod parametry in nie można podstawiać, a do parametrów out nie można się odwołać inaczej niż po lewej stronie podstawienia.
 
inaczej niż po lewej stronie podstawienia.


C#
C#
*Podobnie do C++, z dodatkowymi cechami opisanymi poniżej.
*Podobnie do C++, z dodatkowymi cechami opisanymi poniżej.
*Jawne przekazywanie przez referencję jest możliwe przy użyciu słowa kluczowego ref (trzeba  
*Jawne przekazywanie przez referencję jest możliwe przy użyciu słowa kluczowego ref (trzeba je umieścić i w nagłówku podprogramu, i w wywołaniu, tzn. przy parametrze formalnym i przy aktualnym).
 
*Użycie czystego trybu wyjściowego jest możliwe przy użyciu słowa kluczowego out. Tryb ten jest implementowany za pomocą przekazywania przez referencję.
je umieścić i w nagłówku podprogramu, i w wywołaniu, tzn. przy parametrze formalnym i przy  
 
aktualnym).
*Użycie czystego trybu wyjściowego jest możliwe przy użyciu słowa kluczowego out. Tryb ten  
 
jest implementowany za pomocą przekazywania przez referencję.


PHP
PHP
*Podobnie do C#, z tym że przekazanie przez referencję wystarczy wskazać w jednym miejscu (za  
*Podobnie do C#, z tym że przekazanie przez referencję wystarczy wskazać w jednym miejscu (za pomocą znaku &) — przy parametrze formalnym lub przy aktualnym.
 
pomocą znaku &) — przy parametrze formalnym lub przy aktualnym.


Per1
Per1
*Wszystkie parametry aktualne są umieszczane w predefiniowanej tablicy @_.
*Wszystkie parametry aktualne są umieszczane w predefiniowanej tablicy @_.
*Jej elementy są aliasami dla parametrów aktualnych.
*Jej elementy są aliasami dla parametrów aktualnych.
*Jeśli element @_ zostanie zmieniony, zmiana ta jest odzwierciedlana w odpowiadającym mu  
*Jeśli element @_ zostanie zmieniony, zmiana ta jest odzwierciedlana w odpowiadającym mu parametrze aktualnym (o ile jest to zmienna).
 
parametrze aktualnym (o ile jest to zmienna).
 


===Implementacja przekazywania parametrów===
==Implementacja przekazywania parametrów==


Zakładamy, że przekazywanie parametrów odbywa się za pośrednictwem stosu — tak jest w  
Zakładamy, że przekazywanie parametrów odbywa się za pośrednictwem stosu — tak jest w zdecydowanej większości języków programowania.


zdecydowanej większości języków programowania.
Przekazywanie przez wartość i/lub wynik jest realizowane poprzez kopiowanie wartości na stos/ze stosu. Odpowiednia komórka pamięci na stosie jest alokowana w chwili wywołania podprogramu. W trakcie działania podprogramu funkcjonuje ona jako zmienna lokalna.


Przekazywanie przez wartość i/lub wynik jest realizowane poprzez kopiowanie wartości na  
Przekazywanie przez referencję jest realizowane poprzez umieszczenie odpowiedniego adresu na stosie. Jeśli parametr aktualny jest stałą (a w szczególności literałem, np. „abc”, 12.34), to na stosie trzeba umieścić jej adres. Kompilator nie może pozwolić, by parametr taki był zmieniany. Jeśli parametr aktualny jest wyrażeniem, na stosie trzeba umieścić adres komórki pamięci z wynikiem wyrażenia.


stos/ze stosu. Odpowiednia komórka pamięci na stosie jest alokowana w chwili wywołania
===Przekazywanie wielowymiarowych tablic===


podprogramu. W trakcie działania podprogramu funkcjonuje ona jako zmienna lokalna.
To jest trudne, jeśli kompilator ma generować kod podprogramu oddzielnie, tzn. nie znając wymiarów tablicy
 
*Przykładowo, znajomość liczby kolumn jest konieczna, by prawidłowo odwoływać się do elementów tablicy dwuwymiarowej, gdyż adres(A[''i'', ''j'']) = adres(A[0, 0]) + ''i'' * (liczba kolumn) + ''j''.
Przekazywanie przez referencję jest realizowane poprzez umieszczenie odpowiedniego adresu na
 
stosie. Jeśli parametr aktualny jest stałą (a w szczególności literałem, np. „abc”, 12.34),
 
to na stosie trzeba umieścić jej adres. Kompilator nie może pozwolić, by parametr taki był
 
zmieniany. Jeśli parametr aktualny jest wyrażeniem, na stosie trzeba umieścić adres komórki
 
pamięci z wynikiem wyrażenia.
 
'''4.1 Przekazywanie wielowymiarowych tablic'''
 
To jest trudne, jeśli kompilator ma generować kod podprogramu oddzielnie, tzn. nie znając  
 
wymiarów tablicy
*Przykładowo, znajomość liczby kolumn jest konieczna, by prawidłowo odwoływać się do  
 
elementów tablicy dwuwymiarowej, gdyż adres(A[i, j]) = adres(A[0, 0]) + i * (liczba kolumn) +  
 
j.
*Zatem np. w C/C++ liczba kolumn musi być jawnie podana, int f(int matrix[][88]).
*Zatem np. w C/C++ liczba kolumn musi być jawnie podana, int f(int matrix[][88]).
*Inny sposób to przekazanie wskaźnika i rozmiaru tablicy, np.
*Inny sposób to przekazanie wskaźnika i rozmiaru tablicy, np.


       ''int f(int *wsk, int wiersze, int kolumny)
       int f(int *wsk, int wiersze, int kolumny)
       {...
       {...
       x = *(wsk + i*kolumny + j);
       x = *(wsk + i*kolumny + j);
       ...}''
       ...}
 
*W innych językach rozmiar przekazywanej do podprogramu tablicy jest ustalany w czasie
 
kompilacji (np. w Adzie).
*Co więcej, Ada pozwala na używanie jako parametrów formalnych tablic nieoznaczonych, bez
 
podanego rozmiaru. Parametry aktualne mogą wówczas mieć różne rozmiary, a kod podprogramu
 
określa ów rozmiar za pomocą atrybutu range.
*Podobnie jest w C# i Javie. Parametrem formalnym może być tablica bez określenia rozmiaru.
 
Rozmiar określa się sprawdzając własność Length obiektu stanowiącego tablicę (tablice są
 
obiektami; tablice wielowymiarowe są tablicami tablic).
 
'''4.2 Nazwy podprogramów w roli parametrów'''
 
Materia jest delikatna... Dobrze, by sprawdzany był cały protokół przekazywanej funkcji. W
 
jęzuku C/C++ osiąga się to przekazując wskaźniki do funkcji, jako że typ takiego wskaźnika to
 
właśnie protokół funkcji. Ada nie pozwala na przekazywanie podprogramów jako parametrów. W
 
zamian za to można używać podprogramów rodzajowych (o czym będzie później). W C# mamy do
 
dyspozycji delegatów. Mechanizm ten pozwala nie tylko na przekazywanie metod statycznych, ale
 
również metod z konkretnych instancji.
 
Jakie środowisko powinno być użyte do wykonywania przekazanego przez parametr podprogramu? Są
 
trzy możliwości:
*Środowisko instrukcji (w podprogramie), wywołującej przekazany podprogram. Jest to
 
''wiązanie płytkie''.
*Środowisko definicji przekazanego podprogramu. W tym przypadku mówimy o ''wiązaniu
 
głębokim''.
*Środowisko instrukcji, która przekazała podprogram jako parametr. Jest to ''wiązanie ad
 
hoc''.
*Wydaje się, że w językach z zakresem wiązanym statycznie najbardziej naturalne jest wiązanie
 
głębokie.
 
'''4.3 Podprogramy przeciążone i rodzajowe'''
 
Operator przeciążony to operator, który ma więcej niż jedno znaczenie. Wybór konkretnego
 
znaczenia dokonywany jest na podstawie typów operandów. Przykładowo, „x + y” oznacza
 
dodawanie całkowite, jeśli i x, i y są typu całkowitego. W przeciwnym razie jest to dodawanie
 
zmiennopozycyjne. Tak jest w wielu popularnych językach.
 
Przeciążony podprogram to taki, który ma więcej niż jedną definicję w tym samym środowisku
 
odwołań. By dało się je odróżnić, każda wersja musi mieć inny protokół. Niektóre języki
 
wymagają, by wersje przeciążonego podprogramu różniły się sygnaturą, tzn. nie mogą różnić się
 
jedynie typem wyniku. Jest to m.in. konsekwencja stosowania niejawnych konwersji typów,
 
sprawiających że np. w podstawieniu double x = f(...) nie da się powiedzieć, czy f daje wynik
 
typ double, float czy int. Tak jest w C++, C# i w Javie. W Adzie inny typ wyniku wystarcza do
 
rozróżnienia.
 
Podprogram rodzajowy (zwany też polimorficznym) to podprogram, któremu w różnych wywołaniach
 
można przekazać parametry różnych typów. Podprogramy przeciążone są szczególnym rodzajem
 
podprogramów polimorficznych. Mówimy tu o ''polimorfizmie ad hoc''. Mówimy o polimorfizmie


parametrycznym, jeśli podprogram ma parametr rodzajowy, którego używa się do opisu typu
*W innych językach rozmiar przekazywanej do podprogramu tablicy jest ustalany w czasie kompilacji (np. w Adzie).
*Co więcej, Ada pozwala na używanie jako parametrów formalnych tablic nieoznaczonych, bez podanego rozmiaru. Parametry aktualne mogą wówczas mieć różne rozmiary, a kod podprogramu określa ów rozmiar za pomocą atrybutu range.
*Podobnie jest w C# i Javie. Parametrem formalnym może być tablica bez określenia rozmiaru. Rozmiar określa się sprawdzając własność Length obiektu stanowiącego tablicę (tablice są obiektami; tablice wielowymiarowe są tablicami tablic).


parametrów. Chodzi tu o mechanizm taki jak np. szablony definiowane za pomocą template w C++.
===Nazwy podprogramów w roli parametrów===


Ada oferuje jednostki rodzajowe (o których będzie mowa za chwilę). Wszystkie wspomniane
Materia jest delikatna... Dobrze, by sprawdzany był cały protokół przekazywanej funkcji. W jęzuku C/C++ osiąga się to przekazując wskaźniki do funkcji, jako że typ takiego wskaźnika to właśnie protokół funkcji. Ada nie pozwala na przekazywanie podprogramów jako parametrów. W zamian za to można używać podprogramów rodzajowych (o czym będzie później). W C# mamy do dyspozycji delegatów. Mechanizm ten pozwala nie tylko na przekazywanie metod statycznych, ale również metod z konkretnych instancji.


powyżej rodzaje polimorfizmu można określić mianem ''polimorfizmu statycznego'' — decyzja o  
Jakie środowisko powinno być użyte do wykonywania przekazanego przez parametr podprogramu? Są trzy możliwości:
*Środowisko instrukcji (w podprogramie), wywołującej przekazany podprogram. Jest to ''wiązanie płytkie''.
*Środowisko definicji przekazanego podprogramu. W tym przypadku mówimy o ''wiązaniu głębokim''.
*Środowisko instrukcji, która przekazała podprogram jako parametr. Jest to ''wiązanie ad hoc''.
*Wydaje się, że w językach z zakresem wiązanym statycznie najbardziej naturalne jest wiązanie głębokie.


użyciu konkretnej wersji podprogramu zapada w czasie kompilacji. Z polimorfizmem dynamicznym
===Podprogramy przeciążone i rodzajowe===


mamy do czynienia w niektórych językach, które dopuszczają sparametryzowane abstrakcyjne typy
Operator przeciążony to operator, który ma więcej niż jedno znaczenie. Wybór konkretnego znaczenia dokonywany jest na podstawie typów operandów. Przykładowo, ''x + y'' oznacza dodawanie całkowite, jeśli i ''x'', i ''y'' są typu całkowitego. W przeciwnym razie jest to dodawanie zmiennopozycyjne. Tak jest w wielu popularnych językach.


danych (o czym mówiliśmy w poprzednim wykładzie), oraz przy dynamicznym wiązaniu wywołań
Przeciążony podprogram to taki, który ma więcej niż jedną definicję w tym samym środowisku odwołań. By dało się je odróżnić, każda wersja musi mieć inny protokół. Niektóre języki wymagają, by wersje przeciążonego podprogramu różniły się sygnaturą, tzn. nie mogą różnić się jedynie typem wyniku. Jest to m.in. konsekwencja stosowania niejawnych konwersji typów, sprawiających, że np. w podstawieniu double x = f(...) nie da się powiedzieć, czy f daje wynik
typ double, float, czy int. Tak jest w C++, C# i w Javie. W Adzie inny typ wyniku wystarcza do rozróżnienia.


metod (o czym powiemy w wykładzie poświęconym programowaniu obiektowemu).
Podprogram rodzajowy (zwany też polimorficznym) to podprogram, któremu w różnych wywołaniach można przekazać parametry różnych typów. Podprogramy przeciążone są szczególnym rodzajem podprogramów polimorficznych. Mówimy tu o ''polimorfizmie ad hoc''. Mówimy o polimorfizmie parametrycznym, jeśli podprogram ma parametr rodzajowy, którego używa się do opisu typu parametrów. Chodzi tu o mechanizm taki jak np. szablony definiowane za pomocą template w C++. Ada oferuje jednostki rodzajowe (o których będzie mowa za chwilę). Wszystkie wspomniane powyżej rodzaje polimorfizmu można określić mianem ''polimorfizmu statycznego'' — decyzja o użyciu konkretnej wersji podprogramu zapada w czasie kompilacji. Z polimorfizmem dynamicznym mamy do czynienia w niektórych językach, które dopuszczają sparametryzowane abstrakcyjne typy danych (o czym mówiliśmy w poprzednim wykładzie), oraz przy dynamicznym wiązaniu wywołań metod (o czym powiemy w wykładzie poświęconym programowaniu obiektowemu).


Omówimy teraz kilka kwestii związanych z polimorfizmem.
Omówimy teraz kilka kwestii związanych z polimorfizmem.


Polimorfizm statyczny i dynamiczny
Polimorfizm statyczny i dynamiczny
*Jednostki rodzajowe w Adzie i funkcje zdefiniowane w C++ za pomocą template wymagają  
*Jednostki rodzajowe w Adzie i funkcje zdefiniowane w C++ za pomocą template wymagają oddzielnej wersji kodu podprogramu dla każdego użytego typu parametrów. Wiązanie wywołań podprogramu z poszczególnymi wersjami kodu jest statyczne.
 
*Jeśli wiązanie typu parametrów formalnych z typem parametrów aktualnych jest dynamiczne (lub jeśli język nie ma typów...), wystarcza jedna wersja kodu.
oddzielnej wersji kodu podprogramu dla każdego użytego typu parametrów. Wiązanie wywołań  
 
podprogramu z poszczególnymi wersjami kodu jest statyczne.
*Jeśli wiązanie typu parametrów formalnych z typem parametrów aktualnych jest dynamiczne (lub  
 
jeśli język nie ma typów...), wystarcza jedna wersja kodu.
*Instancjonowanie dynamiczne w C# można uznać za rozwiązanie pośrednie.
*Instancjonowanie dynamiczne w C# można uznać za rozwiązanie pośrednie.


Jednostki rodzajowe w Adzie
Jednostki rodzajowe w Adzie
*Są to wzorce procedur.
*Są to wzorce procedur.
*Kod jest generowany dopiero w chwili instancjonowania podprogramu rodzajowego konkretnym  
*Kod jest generowany dopiero w chwili instancjonowania podprogramu rodzajowego konkretnym typem.
 
typem.
*Przykład:
*Przykład:


       ''procedure SortowanieCałk is new
       procedure SortowanieCałk is new
         SortowanieRodz(Indeksy => Integer; Elementy => Integer);''
         SortowanieRodz(Indeksy => Integer; Elementy => Integer);


*W definicji SortowanieRodz pojawiają się „niedodefiniowane” typy Indeksy i Elementy.
*W definicji SortowanieRodz pojawiają się „niedodefiniowane” typy Indeksy i Elementy.
*Ten mechanizm pozwala uzyskać efekt zbliżony do przekazywania podprogramów jako parametrów.
*Ten mechanizm pozwala uzyskać efekt zbliżony do przekazywania podprogramów jako parametrów.
*Należy w tym celu instancjonować niedodefiniowany podprogram (w jednostce rodzajowej) za  
*Należy w tym celu instancjonować niedodefiniowany podprogram (w jednostce rodzajowej) za pomocą konkretnego podprogramu.
 
pomocą konkretnego podprogramu.


Przypomnijmy, jak działają funkcje rodzajowe w C++
Przypomnijmy, jak działają funkcje rodzajowe w C++
Linia 556: Linia 307:
*Przykładowo:
*Przykładowo:


       ''template <class T>
       template <class T>
         T max(T x, T y)
         T max(T x, T y)
         { '''return''' (x > y) ? x : y; }''
         { return (x > y) ? x : y; }
 
*Taka funkcja jest niejawnie instancjonowana, gdy odwołanie do niej pojawi się w programie,


np.
*Taka funkcja jest niejawnie instancjonowana, gdy odwołanie do niej pojawi się w programie, np.


       ''int a, b, c;
       int a, b, c;
       float d, e, f;
       float d, e, f;
       a = max(b, c);
       a = max(b, c);
       d = max(e, f);''
       d = max(e, f);


*W powyższym przykładzie kompilator wygeneruje dwie wersje kodu, jedną dla typu int i drugą  
*W powyższym przykładzie kompilator wygeneruje dwie wersje kodu, jedną dla typu int i drugą dla typu float.
*Zauważmy, że do poprawnego działania funkcji potrzebne jest, by dla użytych typów określony był operator porównania >.


dla typu float.
==Implementacja podprogramów==
*Zauważmy, że do poprawnego działania funkcji potrzebne jest, by dla użytych typów określony


był operator porównania >.
===Semantyka wywołań podprogramów i powrotów z podprogramów===
 
===Implementacja podprogramów===
 
'''5.1 Semantyka wywołań podprogramów i powrotów z podprogramów'''


Z wywołaniem podprogramu wiążą się następujące czynności:
Z wywołaniem podprogramu wiążą się następujące czynności:
Linia 585: Linia 330:
*Przechowanie stanu programu wywołującego.
*Przechowanie stanu programu wywołującego.
*Przekazanie sterowania do podprogramu.
*Przekazanie sterowania do podprogramu.
*Jeśli dopuszczamy zagnieżdżone podprogramy, trzeba zapewnić (i odpowiednio ustawić)  
*Jeśli dopuszczamy zagnieżdżone podprogramy, trzeba zapewnić (i odpowiednio ustawić) mechanizm dostępu do zmiennych nielokalnych.
 
mechanizm dostępu do zmiennych nielokalnych.


Z powrotem z podprogramu wiążą się następujące czynności:
Z powrotem z podprogramu wiążą się następujące czynności:
Linia 596: Linia 339:
*Przekazanie sterowania z powrotem do programu wywołującego.
*Przekazanie sterowania z powrotem do programu wywołującego.


'''5.2 Implementacja podprogramów bez zagnieżdżeń, bez rekurencji i ze statycznymi zmiennymi  
===Implementacja podprogramów bez zagnieżdżeń, bez rekurencji i ze statycznymi zmiennymi lokalnymi===
 
lokalnymi'''


W tej sytuacji nie wszystkie czynności wspomniane powyżej są konieczne
W tej sytuacji nie wszystkie czynności wspomniane powyżej są konieczne
*Nie potrzebujemy mechanizmu dostępu do zmiennych nielokalnych, gdyż wszystkie zmienne są  
*Nie potrzebujemy mechanizmu dostępu do zmiennych nielokalnych, gdyż wszystkie zmienne są statyczne.
 
statyczne.
*Nie dopuszczamy wywołań rekurencyjnych.
*Nie dopuszczamy wywołań rekurencyjnych.
*Taki podprogram składa się z właściwego kodu, zmiennych lokalnych oraz ''rekordu  
*Taki podprogram składa się z właściwego kodu, zmiennych lokalnych oraz ''rekordu aktywacyjnego''.
 
aktywacyjnego''.
*Rekord aktywacyjny zawiera:
*Rekord aktywacyjny zawiera:
**Informację o stanie podprogramu wywołującego.
**Informację o stanie podprogramu wywołującego.
Linia 614: Linia 351:
**Wynik, o ile jest potrzebny.
**Wynik, o ile jest potrzebny.


Kod podprogramu jest niezmienny, natomiast zmienne lokalne i rekord aktywacyjny mogą zmieniać  
Kod podprogramu jest niezmienny, natomiast zmienne lokalne i rekord aktywacyjny mogą zmieniać się w trakcie wykonania podprogramu
 
się w trakcie wykonania podprogramu
*Struktura rekordu aktywacyjnego jest jednak statyczna.
*Struktura rekordu aktywacyjnego jest jednak statyczna.
*Jako że nie dopuszczamy rekurencji, w dowolnej chwili aktywna może być tylko jedna wersja  
*Jako że nie dopuszczamy rekurencji, w dowolnej chwili aktywna może być tylko jedna wersja danego podprogramu.
 
danego podprogramu.
*Tylko jedna może być zatem instancja jego rekordu aktywacyjnego.
*Tylko jedna może być zatem instancja jego rekordu aktywacyjnego.
*W takim przypadku rekord aktywacyjny może być przechowywany razem z kodem podprogramu.
*W takim przypadku rekord aktywacyjny może być przechowywany razem z kodem podprogramu.


'''5.3 Implementacja podprogramów bez zagnieżdżeń, ale z rekurencją i z dynamicznymi  
===Implementacja podprogramów bez zagnieżdżeń, ale z rekurencją i z dynamicznymi zmiennymi lokalnymi na stosie===
 
zmiennymi lokalnymi na stosie'''


Sprawa jest nieco bardziej skomplikowana
Sprawa jest nieco bardziej skomplikowana
*Kompilator musi wygenerować kod obsługujący niejawną alokację i dealokację zmiennych  
*Kompilator musi wygenerować kod obsługujący niejawną alokację i dealokację zmiennych lokalnych.
 
lokalnych.
*W danej chwili może istnieć więcej niż jedna instancja danego podprogramu.
*W danej chwili może istnieć więcej niż jedna instancja danego podprogramu.
*Może zatem istnieć więcej instancji zmiennych lokalnych i rekordu aktywacyjnego.
*Może zatem istnieć więcej instancji zmiennych lokalnych i rekordu aktywacyjnego.
*Rozsądne jest przechowywanie rekordu aktywacyjnego na stosie, razem ze zmiennymi lokalnymi.
*Rozsądne jest przechowywanie rekordu aktywacyjnego na stosie, razem ze zmiennymi lokalnymi.


Zauważmy, że nawet jeśli w danej chwili istnieje więcej niż jedna instancja danego  
Zauważmy, że nawet jeśli w danej chwili istnieje więcej niż jedna instancja danego podprogramu (jesteśmy „w trakcie” rekurencji), to odwołania do zmiennych lokalnych dotyczą tych z ostatniej instancji
 
*Dostęp do wszelkich zmiennych obejmuje zatem dwie możliwości: zmienne globalne (alokowane statycznie) lub zmienne lokalne (w rekordzie aktywacyjnym na szczycie stosu).
podprogramu (jesteśmy „w trakcie” rekurencji), to odwołania do zmiennych lokalnych dotyczą  
 
tych z ostatniej instancji
*Dostęp do wszelkich zmiennych obejmuje zatem dwie możliwości: zmienne globalne (alokowane  
 
statycznie) lub zmienne lokalne (w rekordzie aktywacyjnym na szczycie stosu).
*Do zmiennej lokalnej sięgamy znając jej położenie względne.
*Do zmiennej lokalnej sięgamy znając jej położenie względne.
*Położenie względne to położenie zmiennej względem początku rekordu aktywacyjnego. Jest ono  
*Położenie względne to położenie zmiennej względem początku rekordu aktywacyjnego. Jest ono statycznie ustalane przez kompilator w trakcie przetwarzania deklaracji zmiennych w podprogramie.
 
statycznie ustalane przez kompilator w trakcie przetwarzania deklaracji zmiennych w  
 
podprogramie.


By ułatwić dealokację pamięci na stosie, rekord aktywacyjny podprogramu zawiera ''łącze  
By ułatwić dealokację pamięci na stosie, rekord aktywacyjny podprogramu zawiera ''łącze dynamiczne''
 
*Jest to adres ostatniej komórki pamięci wykorzystywanej przez rekord aktywacyjny podprogramu wywołującego (czyli poprzednika dynamicznego).
dynamiczne''
*Jest to adres ostatniej komórki pamięci wykorzystywanej przez rekord aktywacyjny podprogramu  
 
wywołującego (czyli poprzednika dynamicznego).
*Gdy podprogram kończy działanie, wierzchołek stosu ustawia się na tę właśnie wartość.
*Gdy podprogram kończy działanie, wierzchołek stosu ustawia się na tę właśnie wartość.
*W ten sposób przywracamy sytuację sprzed wywołania podprogramu.
*W ten sposób przywracamy sytuację sprzed wywołania podprogramu.
*Ciąg łączy dynamicznych od rekordu aktywacyjnego danego podprogramu, poprzez kolejne  
*Ciąg łączy dynamicznych od rekordu aktywacyjnego danego podprogramu, poprzez kolejne poprzedniki dynamiczne, aż do rekordu aktywacyjnego podprogramu wywołanego najwcześniej (main) to ''łańcuch dynamiczny''.
 
poprzedniki dynamiczne, aż do rekordu aktywacyjnego podprogramu wywołanego najwcześniej  
 
(main) to ''łańcuch dynamiczny''.
 
'''5.4 Implementacja podprogramów z zagnieżdżeniami, z rekurencją i z dynamicznymi zmiennymi


lokalnymi na stosie, w językach ze statycznym wiązaniem zakresu'''
===Implementacja podprogramów z zagnieżdżeniami, z rekurencją i z dynamicznymi zmiennymi lokalnymi na stosie, w językach ze statycznym wiązaniem zakresu===


Oprócz dotychczas omówionych czynności, potrzebny jest mechanizm dostępu do zmiennych  
Oprócz dotychczas omówionych czynności, potrzebny jest mechanizm dostępu do zmiennych nielokalnych
 
*Chodzi o zmienne nielokalne, ale też nieglobalne (te ostatnie są statyczne, więc dostęp jest prosty).
nielokalnych
*Chodzi o zmienne nielokalne, ale też nieglobalne (te ostatnie są statyczne, więc dostęp jest  
 
prosty).
*Dostęp do zmiennych nielokalnych jest dwuetapowy.
*Dostęp do zmiennych nielokalnych jest dwuetapowy.
*Zmienne te znajdują się gdzieś na stosie...
*Zmienne te znajdują się gdzieś na stosie...
*W pierwszym etapie trzeba znaleźć instancję rekordu aktywacyjnego, gdzie zmienna została  
*W pierwszym etapie trzeba znaleźć instancję rekordu aktywacyjnego, gdzie zmienna została zaalokowana.
 
*W drugim etapie należy sięgnąć do zmiennej, znając jej położenie względne.
zaalokowana.
*Podobnie jak poprzednio jest ono znane w czasie kompilacji.
*W drugim etapie należy sięgnąć do zmiennej znając jej położenie względne.
*Podobnie jak poprzednio, jest ono znane w czasie kompilacji.


Łącza statyczne
Łącza statyczne
*W rekordzie aktywacyjnym umieszcza się ''łącze statyczne''.
*W rekordzie aktywacyjnym umieszcza się ''łącze statyczne''.
*Jest to adres początku rekordu aktywacyjnego (najpóźniejszej instancji) '''statycznego'''  
*Jest to adres początku rekordu aktywacyjnego (najpóźniejszej instancji) '''statycznego''' poprzednika podprogramu.
 
*Ciąg łączy statycznych od rekordu aktywacyjnego danego podprogramu do rekordu aktywacyjnego podprogramu najbardziej zewnętrznego (main) to ''łańcuch statyczny''.
poprzednika podprogramu.
*Zagnieżdżenie zakresów widoczności jest znane w czasie kompilacji, zatem dla każdej zmiennej kompilator może statycznie określić długość fragmentu łańcucha statycznego, który należy przejść, by dotrzeć do owej zmiennej.
*Ciąg łączy statycznych od rekordu aktywacyjnego danego podprogramu do rekordu aktywacyjnego  
 
podprogramu najbardziej zewnętrznego (main) to ''łańcuch statyczny''.
*Zagnieżdżenie zakresów widoczności jest znane w czasie kompilacji, zatem dla każdej zmiennej  


kompilator może statycznie określić długość fragmentu łańcucha statycznego, który należy
===Implementacja podprogramów z zagnieżdżeniami, z rekurencją i z dynamicznymi zmiennymi lokalnymi na stosie, w językach z dynamicznym wiązaniem zakresu===
 
przejść, by dotrzeć do owej zmiennej.
 
'''5.5 Implementacja podprogramów z zagnieżdżeniami, z rekurencją i z dynamicznymi zmiennymi  
 
lokalnymi na stosie, w językach z dynamicznym wiązaniem zakresu'''


Mamy do dyspozycji dwie metody
Mamy do dyspozycji dwie metody
*Dostęp głęboki: Zamiast podążać wzdłuż łańcucha statycznego, idziemy wzdłuż łańcucha  
*Dostęp głęboki: Zamiast podążać wzdłuż łańcucha statycznego, idziemy wzdłuż łańcucha dynamicznego.
 
*Dostęp płytki: Jest to alternatywna metoda implementacji (opisana poniżej), dająca taką samą semantykę.
dynamicznego.
*Dostęp płytki: Jest to alternatywna metoda implementacji (opisana poniżej), dająca taką samą  
 
semantykę.


Dostęp płytki
Dostęp płytki
*Zmienne nie są przechowywane razem z rekordami aktywacyjnymi.
*Zmienne nie są przechowywane razem z rekordami aktywacyjnymi.
*Dla każdej zmiennej tworzy się osobny stos.
*Dla każdej zmiennej tworzy się osobny stos.
*Za każdym razem, gdy tworzona jest zmienna, alokowana jest dla niej komórka na odpowiednim  
*Za każdym razem, gdy tworzona jest zmienna, alokowana jest dla niej komórka na odpowiednim stosie.
 
*Przy zakończeniu podprogramu, ze stosów przechowujących zmienne tego podprogramu zdejmuje się ostatnią komórkę.
stosie.
*Przy odwołaniu do zmiennej odczytuje się wierzchołek odpowiedniego stosu, jako że tam jest „najświeższa” wersja.
*Przy zakończeniu podprogramu, ze stosów przechowujących zmienne tego podprogramu zdejmuje  
*Alternatywnie, można przechowywać globalną tablicę z odsyłaczami do zmiennych, aktualizowaną przy każdym wywołaniu i powrocie z podprogramu.
 
się ostatnią komórkę.
*Przy odwołaniu do zmiennej odczytuje się wierzchołek odpowiedniego stosu, jako że tam jest  
 
„najświeższa” wersja.
*Alternatywnie, można przechowywać globalną tablicę z odsyłaczami do zmiennych, aktualizowaną  
 
przy każdym wywołaniu i powrocie z podprogramu.

Aktualna wersja na dzień 19:49, 22 wrz 2006

Wstęp

Podprogramy realizują jedną z dwóch podstawowych abstrakcji programowania, mianowicie abstrakcję procesu. O ile abstrakcja danych rozpowszechniła się dopiero w latach 80-tych XX wieku, abstrakcja procesu wydawała się czymś oczywistym od zarania dziejów algorytmiki i programowania. Któż nie chciałby budować programów z gotowych fragmentów, realizujących dobrze określone zadania w sposób szybki i niezawodny? By uświadomić sobie, jak daleka droga dzieli tworzenie wielkich systemów informatycznych od pisania drobnych programów w asemblerze, przytoczmy prosty przykład programu asemblerowego i jego odpowiednik w języku C (a przecież od owego programu w C do wielkiego systemu jest wciąż ogromnie daleko...).

     petla:
       pushl %ebp
       movl %esp,%ebp
       movl 8(%ebp),%ecx
       movl 12(%ebp),%edx
       xorl %eax,%eax
       cmpl %edx,%ecx
       jge .L1
     .L2:
       incl %ecx
       decl %edx
       incl %eax
       cmpl %edx,%ecx
       jl .L2
     .L1:
       incl %eax
       movl %ebp,%esp
       popl %ebp
       ret

Odpowiednik w języku C:

     int petla(int x, int y)
     {
       int wyn;
       
       for (wyn = 0; x < y; wyn++) {
         x++;
         y--;
       }
       return wyn + 1;
     }


Pamiętajmy, że abstrakcja danych i związana z nią metodologia tworzenia programów, rozpowszechniona wraz z językami obiektowymi, nie zastępuje abstrakcji procesu, lecz ją uzupełnia. Przyjrzenie się podprogramom i sposobom ich implementacji odsłania wiele dalece nieoczywistych szczegółów (by nie rzec — tajemnic) języków programowania. Tak jak dotychczas, podprogramy będziemy rozumieli szeroko, obejmując tym pojęciem wszelkie procedury, funkcje, metody itp.

Mówiąc o podprogramach będziemy zakładali, że:

  • Każdy podprogram posiada jeden punkt wejścia.
  • Program wywołujący podprogram zostaje zawieszony na czas działania podprogramu.
  • Sterowanie zawsze powraca do programu wywołującego w momencie zakończenia działania podprogramu.

Sprawy składniowe

  • Definicja podprogramu opisuje sposób komunikowania się podprogramu z otoczeniem oraz sposób działania podprogramu.
  • Nagłówek podprogramu mówi o tym, że następujący po nim fragment kodu jest definicją podprogramu, podaje jego nazwę oraz (niekoniecznie) wyszczególnia parametry.
  • Sygnatura podprogramu to liczba, porządek i typ jego parametrów formalnych.
  • Protokół podprogramu to opis parametrów formalnych (czyli sygnatura) oraz typ wyniku (o ile taki istnieje).
  • Podprogramy mogą mieć definicje i oddzielne deklaracje. Te drugie są używane w niektórych językach programowania (np. w C i C++, gdzie nazywane są prototypami i umieszczane są zwykle w plikach .h).

Przekazywanie parametrów

Podprogram nie będący metodą ma dostęp do danych na dwa sposoby: przez zmienne nielokalne (zgodnie z zasadami zakresu widoczności) oraz przez przekazane parametry. Oczywiście drugi sposób jest znacznie lepszy, gdyż pozwala na jasne zdefiniowanie sprzężenia podprogramu z otoczeniem, bez polegania na trudnych często do uchwycenia efektach ubocznych. Metoda ma dodatkowo dostęp do danych obiektu, z którego jest wołana.

W pewnych sytuacjach chcielibyśmy przekazać do podprogramu nie tyle dane, co sposób obliczeń. Pewne języki na to pozwalają. Szerzej zajmiemy się tą sprawą w przedstawionych poniżej przykładach.

Wypunktujmy podstawowe definicje dotyczące przekazywania parametrów:

  • Parametry w nagłówku nazywane są parametrami formalnymi.
  • Parametry w instrukcji wywołania podprogramu, które zostaną przypisane parametrom formalnym, nazywane są parametrami aktualnymi.
  • W większości języków, odpowiedniość pomiędzy parametrami formalnymi a aktualnymi ustalana jest poprzez zestawienie ich położenia w liście. Mówi się, że są to parametry pozycyjne.
  • Stosuje się też wiązanie parametrów formalnych z aktualnymi na podstawie nazw podanych wraz z parametrami aktualnymi. Są to parametry z kluczem.
  • Niektóre języki pozwalają na użycie parametrów z wartością domyślną, np. Ada, C++, PHP. Wówczas liczba parametrów aktualnych może być mniejsza niż liczba parametrów formalnych.
  • Niektóre języki programowania posiadają mechanizmy przekazywania zmiennej liczby parametrów, np. params w C++. Wróćmy teraz do przekazywania przez parametr sposobu obliczeń. Załóżmy, że piszemy funkcję, która służy do całkowania funkcji na liczbach rzeczywistych. Chcielibyśmy, by jej parametrami były: funkcja do scałkowania oraz granice przedziału całkowania. Innymi słowy, chcielibyśmy móc używać naszej funkcji np. tak:
     wynik = całkuj(sin, 0, 0.123)

Pierwszym parametrem jest tu funkcja sin. Typem takiego parametru jest cały protokół przekazywanej funkcji(tu np.: jeden parametr double i wynik double). Nie każdy język na to pozwala. W Pascalu można przekazywać funkcje i procedury bezpośrednio. W C i C++ można przekazywać wskaźniki do funkcji, co daje podobny efekt. W C# można używać delegatów instancjonowanych dla żądanej metody.

Przykład przekazywania wskaźnika do funkcji w języku C

  • Rozważmy następującą funkcję:
     double calkuj(double f(double), double a, double b)
     {
       ...
       x = f(...);
       ...
     }
  • Wywołanie może wyglądać tak, jak opisano powyżej.
  • W „klasycznej” składni języka C nagłówek wyglądałby tak:
     double calkuj(double (*f)(double), double a, double b)

Przykład użycia delegatów w C#

  • Deklarujemy odpowiedni typ delegata:
     delegate double dtype(double, double);
  • Metoda służąca do całkowania może mieć postać:
     double calkuj(dtype f, double a, double b)
     {
       ...
       x = f(...);
       ...
     }
  • Załóżmy, że jako parametr chcemy przekazać metodę f z klasy C. Tworzymy zatem delegata:
     dtype dfun = new dtype(C.f);
  • Możemy teraz wykorzystać delegata w wywołaniu, np.
     wynik = A.calkuj(dfun, 1.2, 3.4);

Rozważmy teraz rozróżnienie pomiędzy procedurami a funkcjami. Pod względem technicznym, procedury i funkcje różnią się jedynie zwracaniem wartości, tzn. procedury jej nie zwracają. Semantycznie jednak różnica jest istotna. Procedury są zestawem instrukcji, które definiują sparametryzowane obliczenia. Są one uruchamiane poprzez pojedyncze wywołanie. Można zatem powiedzieć, że procedury definiują nowe instrukcje. Funkcje natomiast przypominają funkcje matematyczne. Są wywoływane poprzez użycie ich nazwy w wyrażeniu. Nie powinny dawać żadnych efektów ubocznych (typowych w przypadku procedur). Funkcje definiują zatem nowe operatory. Języki programowania oparte na języku C formalnie nie posiadają procedur, ale funkcje typu void zachowują się tak jak procedury.

Kolejna sprawa: sprawdzanie zgodności typów parametrów

  • Niewątpliwie jest korzystne...
  • Wczesne wersje Fortranu i C nie sprawdzały.
  • Większość współczesnych języków to czyni.
  • Są jednak wyjątki: JavaScript, Perl, PHP.
  • Język C w wersji z końca lat 80-tych dawał wybór.
  • Jeśli typy parametrów były zadeklarowane poza listą parametrów, to zgodność typów nie była sprawdzana:
     int f(x)
       double x;
       {...}
  • Jeśli typy były wymienione w owej liście, to ich zgodność była sprawdzana:
     int f(double x)
       {...}
  • W C i C++ można uniknąć sprawdzania typów za pomocą nagłówka z wielokropkiem, np.
     int printf(const char *format, ...)

Problemy implementacyjne

Twórca języka programowania musi podjąć wiele decyzji, których skutki sięgają niejednokrotnie znacznie dalej niż mogłoby się wydawać. Przykładowo, stworzenie kompilatora dla zbyt wyrafinowanego języka może być trudne. Ta przypadłość spotkała niektóre języki, np. Adę. A to — być może — dopiero początek kłopotów, jakie będą spotykały programistów, zmuszonych do uczenia się zawiłych konstrukcji. Tak było z... Adą. Decyzje odnoszące się do implementacji podprogramów są o tyle ważne, że wchodzą tu w grę bardzo złożone mechanizmy.

Wymieńmy zatem niektóre kwestie związane z podprogramami:

  • Wybór sposobów przekazywania parametrów.
  • Czy typ parametrów aktualnych będzie sprawdzany z typem parametrów formalnych?
  • Czy lokalne zmienne będą alokowane statycznie, czy dynamicznie?
  • Czy definicje podprogramów mogą być zagnieżdżone?
  • Jeżeli podprogramy mogą być zagnieżdżane i mogą być przekazywane jako parametry, to co jest środowiskiem przekazywanego podprogramu?
  • Czy podprogramy mogą być przeciążane?
  • Czy podprogramy mogą być uniwersalne (rodzajowe)?

Konsekwencją istnienia podprogramów jest m.in. powstanie lokalnego środowiska odwołań. Powiedzmy zatem parę słów o zmiennych lokalnych. Lokalne zmienne w podprogramach mogą być statyczne bądź dynamiczne alokowane na stosie. Te pierwsze są szybkie i niezależne od historii wywołań (co może być użyteczne na przykład w generowaniu liczb pseudolosowych). Te drugie umożliwiają wywołania rekurencyjne. W języku C oraz C++, lokalne zmienne są dynamiczne, chyba że jawnie zadeklarowano, że mają być statyczne. Języki C++, C# i Java nie zezwalają na użycie zmiennych statycznych w metodach.

Sposoby przekazywania parametrów

Na początku zajmiemy się różnymi modelami semantycznymi, a następnie rozważymy modele i decyzje implementacyjne.

  • Parametr formalny charakteryzuje jeden z trzech modeli semantycznych:
    • Może otrzymać dane poprzez odpowiadający mu parametr aktualny (tryb wejściowy, in).
    • Może przekazywać dane do parametru aktualnego (tryb wyjściowy, out).
    • Może też używać obu trybów (tryb wejściowo-wyjściowy, in-out).
  • Przekazanie danych może następować na dwa sposoby:
    • Kopiowana jest faktyczna wartość (do wywoływanego podprogramu, do programu wołającego lub w obie strony).
    • Dostęp do danych przekazywany jest pośrednio (np. przez wskaźnik).

Modele przekazywania parametrów można implementować jako:

  • Przekazywanie przez wartość.
  • Przekazywanie przez wynik.
  • Przekazywanie przez wartość i wynik.
  • Przekazywanie przez referencję.
  • Przekazywanie przez nazwę.

Przekazywanie przez wartość

  • Wartość parametru aktualnego jest używana do zainicjowania odpowiadającego mu parametru formalnego.
  • Parametr formalny funkcjonuje następnie w podprogramie jako zmienna lokalna.
  • Tak implementujemy semantykę trybu wejściowego.
  • Dane przekazuje się zwykle przez kopiowanie wartości, ale można też użyć dostępu pośredniego (z dodatkowym zabezpieczeniem przed zmianą wartości parametru aktualnego).
  • Przekazywanie przez wartość jest kosztowne, gdy trzeba przekazać duże struktury danych.

Przekazywanie przez wynik

  • Nie przekazujemy wartości do podprogramu.
  • Parametr formalny działa jak zmienna lokalna.
  • Tuż przez przekazaniem sterowania z powrotem do wywołującego, wartość parametru formalnego jest przesyłana do parametru aktualnego w programie wywołującym.
  • Parametr aktualny musi zatem być zmienną (a nie np. wyrażeniem arytmetycznym).
  • Tak implementujemy semantykę trybu wyjściowego.
  • Dane przekazuje się zwykle przez kopiowanie.
  • Przy przekazywaniu przez wynik może dojść do kolizji parametrów aktualnych, np. przy wywołaniu subp(x, x). Załóżmy, że formalne parametry podprogramu subp to a i b. Jaka powinna być wartość x, jeśli subp zawiera podstawienia a := 1; b := 2?
  • Inny problem: Załóżmy, że parametr aktualny to T[i]. Jeśli podprogram zmieni wartość zmiennej i, gdzie powinna znaleźć się zwrócona wartość, tzn. czy powinniśmy użyć starej czy nowej wartości indeksu?

Przekazywanie przez wartość i wynik

  • Jest to kombinacja dwóch wcześniejszych sposobów implementacji, dająca semantykę trybu wejściowo-wyjściowego.
  • Zwane niekiedy przekazywaniem przez kopiowanie, jako że parametr aktualny jest kopiowany do parametru formalnego, a następnie kopiowany z powrotem przy zakończeniu podprogramu.

Przekazywanie przez referencję

  • Jest to alternatywna implementacja semantyki trybu wejściowo-wyjściowego.
  • Dostęp do danych przekazywany jest pośrednio, czyli przekazywany jest w istocie wskaźnik do wartości a nie sama wartość.
  • Podprogram zyskuje więc faktyczny dostęp do parametru aktualnego.
  • Taki sposób przekazania jest efektywny.
  • Przekazywanie przez referencję może powodować aliasowanie, np. w przypadku wywołań subp(x, x) lub subp(T[i], T[j]) przy i = j.
  • Aliasowanie może również się pojawić na skutek kolizji między parametrami formalnymi a zmiennymi nielokalnymi.
  • Zauważmy, że rzecz jest podobna (choć nie identyczna...) do kolizji parametrów aktualnych przy przekazywaniu przez wynik.

Przekazywanie przez nazwę

  • Ten sposób implementuje semantykę trybu wejściowo-wyjściowego.
  • Parametr aktualny jest wstawiany (tekstowo) w miejsce odpowiadającego mu parametru formalnego, we wszystkich wystąpieniach w podprogramie.
  • W poprzednio omówionych sposobach było inaczej — parametry formalne były wiązane z aktualnymi wartościami (lub adresami) w chwili wywołania podprogramu.
  • Przy przekazywaniu przez nazwę parametr formalny jest wiązany z metodą dostępu do danych w chwili wywołania podprogramu, ale właściwe wiązanie z wartością lub adresem następuje dopiero w momencie odwołania lub przypisania do owego parametru formalnego.
  • Jest to „późne wiązanie” — kosztowne...

Sposoby przekazywania parametrów w różnych językach

Omówimy teraz pokrótce przekazywanie parametrów w kilku popularnych językach.

Fortran

  • Używał trybu wejściowo-wyjściowego od zarania dziejów.
  • Implementacja jako przekazywanie przez referencję lub wartość i wynik.
  • Nowsze wersje pozwalają na wybranie trybu (in, out, inout), podobnie jak w Adzie.

Język C

  • Stosuje zawsze przekazywanie przez wartość.
  • Można jednak przekazywać (przez wartość) wskaźniki, co daje efekt taki jak tryb wejściowo-wyjściowy z przekazywaniem przez referencję.
  • W szczególności, wskaźniki do stałych pozwalają uzyskać tryb wejściowy (niejawne zabezpieczenie przed zmianą wartości parametru w wywoływanej funkcji) z efektywnością właściwą dla przekazywania przez referencję.
  • Nie ma bezpośredniego sposobu przekazania tablicy inaczej niż przez przekazanie wskaźnika do niej, gdyż nazwa tablicy jest traktowana jako wskaźnik. Stąd tablice są w istocie przekazywane przez referencję. Mechanizm ten można ominąć, umieszczając tablicę jako fragment rekordu (struct) i przekazując ów rekord.
  • Zwróćmy uwagę na różnicę między „przekazywaniem wskaźnika do x przez wartość” a „przekazywaniem x przez referencję”. W pierwszym przypadku programista musi (jawnie) pobrać i przekazać wskaźnik; w drugim — programista nie musi pobierać wskaźnika.

Język C++

  • Oprócz tego, co w języku C, dostępne są parametry referencyjne, dające przekazywanie przez referencję.
  • Zauważmy różnicę między parametrami stałymi (deklarowanymi z użyciem const) a parametrami w trybie wejściowym w ogóle. Te pierwsze realizują oczywiście semantykę trybu wejściowego, z dodatkową cechą: nie mogą być zmieniane nawet w obrębie podprogramu. W większości języków natomiast parametry wejściowe funkcjonują jako zmienne lokalne — ich wartość można zmieniać, choć nie ma ona wpływu na parametry aktualne.

Java

  • Stosuje zawsze przekazywanie przez wartość.
  • Ponieważ jednak obiekty są dostępne tylko przez zmienne referencyjne, w istocie obiekty są przekazywane przez referencję.

Ada

  • Używa trzech trybów: in, out, in out.
  • W ten sposób jawnie stosujemy trzy modele semantyczne.
  • Dodatkowo, pod parametry in nie można podstawiać, a do parametrów out nie można się odwołać inaczej niż po lewej stronie podstawienia.

C#

  • Podobnie do C++, z dodatkowymi cechami opisanymi poniżej.
  • Jawne przekazywanie przez referencję jest możliwe przy użyciu słowa kluczowego ref (trzeba je umieścić i w nagłówku podprogramu, i w wywołaniu, tzn. przy parametrze formalnym i przy aktualnym).
  • Użycie czystego trybu wyjściowego jest możliwe przy użyciu słowa kluczowego out. Tryb ten jest implementowany za pomocą przekazywania przez referencję.

PHP

  • Podobnie do C#, z tym że przekazanie przez referencję wystarczy wskazać w jednym miejscu (za pomocą znaku &) — przy parametrze formalnym lub przy aktualnym.

Per1

  • Wszystkie parametry aktualne są umieszczane w predefiniowanej tablicy @_.
  • Jej elementy są aliasami dla parametrów aktualnych.
  • Jeśli element @_ zostanie zmieniony, zmiana ta jest odzwierciedlana w odpowiadającym mu parametrze aktualnym (o ile jest to zmienna).

Implementacja przekazywania parametrów

Zakładamy, że przekazywanie parametrów odbywa się za pośrednictwem stosu — tak jest w zdecydowanej większości języków programowania.

Przekazywanie przez wartość i/lub wynik jest realizowane poprzez kopiowanie wartości na stos/ze stosu. Odpowiednia komórka pamięci na stosie jest alokowana w chwili wywołania podprogramu. W trakcie działania podprogramu funkcjonuje ona jako zmienna lokalna.

Przekazywanie przez referencję jest realizowane poprzez umieszczenie odpowiedniego adresu na stosie. Jeśli parametr aktualny jest stałą (a w szczególności literałem, np. „abc”, 12.34), to na stosie trzeba umieścić jej adres. Kompilator nie może pozwolić, by parametr taki był zmieniany. Jeśli parametr aktualny jest wyrażeniem, na stosie trzeba umieścić adres komórki pamięci z wynikiem wyrażenia.

Przekazywanie wielowymiarowych tablic

To jest trudne, jeśli kompilator ma generować kod podprogramu oddzielnie, tzn. nie znając wymiarów tablicy

  • Przykładowo, znajomość liczby kolumn jest konieczna, by prawidłowo odwoływać się do elementów tablicy dwuwymiarowej, gdyż adres(A[i, j]) = adres(A[0, 0]) + i * (liczba kolumn) + j.
  • Zatem np. w C/C++ liczba kolumn musi być jawnie podana, int f(int matrix[][88]).
  • Inny sposób to przekazanie wskaźnika i rozmiaru tablicy, np.
     int f(int *wsk, int wiersze, int kolumny)
     {...
      x = *(wsk + i*kolumny + j);
     ...}
  • W innych językach rozmiar przekazywanej do podprogramu tablicy jest ustalany w czasie kompilacji (np. w Adzie).
  • Co więcej, Ada pozwala na używanie jako parametrów formalnych tablic nieoznaczonych, bez podanego rozmiaru. Parametry aktualne mogą wówczas mieć różne rozmiary, a kod podprogramu określa ów rozmiar za pomocą atrybutu range.
  • Podobnie jest w C# i Javie. Parametrem formalnym może być tablica bez określenia rozmiaru. Rozmiar określa się sprawdzając własność Length obiektu stanowiącego tablicę (tablice są obiektami; tablice wielowymiarowe są tablicami tablic).

Nazwy podprogramów w roli parametrów

Materia jest delikatna... Dobrze, by sprawdzany był cały protokół przekazywanej funkcji. W jęzuku C/C++ osiąga się to przekazując wskaźniki do funkcji, jako że typ takiego wskaźnika to właśnie protokół funkcji. Ada nie pozwala na przekazywanie podprogramów jako parametrów. W zamian za to można używać podprogramów rodzajowych (o czym będzie później). W C# mamy do dyspozycji delegatów. Mechanizm ten pozwala nie tylko na przekazywanie metod statycznych, ale również metod z konkretnych instancji.

Jakie środowisko powinno być użyte do wykonywania przekazanego przez parametr podprogramu? Są trzy możliwości:

  • Środowisko instrukcji (w podprogramie), wywołującej przekazany podprogram. Jest to wiązanie płytkie.
  • Środowisko definicji przekazanego podprogramu. W tym przypadku mówimy o wiązaniu głębokim.
  • Środowisko instrukcji, która przekazała podprogram jako parametr. Jest to wiązanie ad hoc.
  • Wydaje się, że w językach z zakresem wiązanym statycznie najbardziej naturalne jest wiązanie głębokie.

Podprogramy przeciążone i rodzajowe

Operator przeciążony to operator, który ma więcej niż jedno znaczenie. Wybór konkretnego znaczenia dokonywany jest na podstawie typów operandów. Przykładowo, x + y oznacza dodawanie całkowite, jeśli i x, i y są typu całkowitego. W przeciwnym razie jest to dodawanie zmiennopozycyjne. Tak jest w wielu popularnych językach.

Przeciążony podprogram to taki, który ma więcej niż jedną definicję w tym samym środowisku odwołań. By dało się je odróżnić, każda wersja musi mieć inny protokół. Niektóre języki wymagają, by wersje przeciążonego podprogramu różniły się sygnaturą, tzn. nie mogą różnić się jedynie typem wyniku. Jest to m.in. konsekwencja stosowania niejawnych konwersji typów, sprawiających, że np. w podstawieniu double x = f(...) nie da się powiedzieć, czy f daje wynik typ double, float, czy int. Tak jest w C++, C# i w Javie. W Adzie inny typ wyniku wystarcza do rozróżnienia.

Podprogram rodzajowy (zwany też polimorficznym) to podprogram, któremu w różnych wywołaniach można przekazać parametry różnych typów. Podprogramy przeciążone są szczególnym rodzajem podprogramów polimorficznych. Mówimy tu o polimorfizmie ad hoc. Mówimy o polimorfizmie parametrycznym, jeśli podprogram ma parametr rodzajowy, którego używa się do opisu typu parametrów. Chodzi tu o mechanizm taki jak np. szablony definiowane za pomocą template w C++. Ada oferuje jednostki rodzajowe (o których będzie mowa za chwilę). Wszystkie wspomniane powyżej rodzaje polimorfizmu można określić mianem polimorfizmu statycznego — decyzja o użyciu konkretnej wersji podprogramu zapada w czasie kompilacji. Z polimorfizmem dynamicznym mamy do czynienia w niektórych językach, które dopuszczają sparametryzowane abstrakcyjne typy danych (o czym mówiliśmy w poprzednim wykładzie), oraz przy dynamicznym wiązaniu wywołań metod (o czym powiemy w wykładzie poświęconym programowaniu obiektowemu).

Omówimy teraz kilka kwestii związanych z polimorfizmem.

Polimorfizm statyczny i dynamiczny

  • Jednostki rodzajowe w Adzie i funkcje zdefiniowane w C++ za pomocą template wymagają oddzielnej wersji kodu podprogramu dla każdego użytego typu parametrów. Wiązanie wywołań podprogramu z poszczególnymi wersjami kodu jest statyczne.
  • Jeśli wiązanie typu parametrów formalnych z typem parametrów aktualnych jest dynamiczne (lub jeśli język nie ma typów...), wystarcza jedna wersja kodu.
  • Instancjonowanie dynamiczne w C# można uznać za rozwiązanie pośrednie.

Jednostki rodzajowe w Adzie

  • Są to wzorce procedur.
  • Kod jest generowany dopiero w chwili instancjonowania podprogramu rodzajowego konkretnym typem.
  • Przykład:
     procedure SortowanieCałk is new
       SortowanieRodz(Indeksy => Integer; Elementy => Integer);
  • W definicji SortowanieRodz pojawiają się „niedodefiniowane” typy Indeksy i Elementy.
  • Ten mechanizm pozwala uzyskać efekt zbliżony do przekazywania podprogramów jako parametrów.
  • Należy w tym celu instancjonować niedodefiniowany podprogram (w jednostce rodzajowej) za pomocą konkretnego podprogramu.

Przypomnijmy, jak działają funkcje rodzajowe w C++

  • Używa się parametrów „typowych” w nawiasach ostrych < >.
  • Przykładowo:
     template <class T>
       T max(T x, T y)
       { return (x > y) ? x : y; }
  • Taka funkcja jest niejawnie instancjonowana, gdy odwołanie do niej pojawi się w programie, np.
     int a, b, c;
     float d, e, f;
     a = max(b, c);
     d = max(e, f);
  • W powyższym przykładzie kompilator wygeneruje dwie wersje kodu, jedną dla typu int i drugą dla typu float.
  • Zauważmy, że do poprawnego działania funkcji potrzebne jest, by dla użytych typów określony był operator porównania >.

Implementacja podprogramów

Semantyka wywołań podprogramów i powrotów z podprogramów

Z wywołaniem podprogramu wiążą się następujące czynności:

  • Przekazanie parametrów.
  • Alokacja i wiązanie pamięci dla niestatycznych zmiennych lokalnych.
  • Przechowanie stanu programu wywołującego.
  • Przekazanie sterowania do podprogramu.
  • Jeśli dopuszczamy zagnieżdżone podprogramy, trzeba zapewnić (i odpowiednio ustawić) mechanizm dostępu do zmiennych nielokalnych.

Z powrotem z podprogramu wiążą się następujące czynności:

  • Skopiowanie parametrów w trybie wyjściowym (o ile są).
  • Dealokacja pamięci na zmienne lokalne.
  • Przywrócenie stanu programu wywołującego.
  • Przywrócenie stanu mechanizmu dostępu do zmiennych nielokalnych.
  • Przekazanie sterowania z powrotem do programu wywołującego.

Implementacja podprogramów bez zagnieżdżeń, bez rekurencji i ze statycznymi zmiennymi lokalnymi

W tej sytuacji nie wszystkie czynności wspomniane powyżej są konieczne

  • Nie potrzebujemy mechanizmu dostępu do zmiennych nielokalnych, gdyż wszystkie zmienne są statyczne.
  • Nie dopuszczamy wywołań rekurencyjnych.
  • Taki podprogram składa się z właściwego kodu, zmiennych lokalnych oraz rekordu aktywacyjnego.
  • Rekord aktywacyjny zawiera:
    • Informację o stanie podprogramu wywołującego.
    • Parametry.
    • Adres powrotu (dokąd należy wrócić po wykonaniu podprogramu).
    • Wynik, o ile jest potrzebny.

Kod podprogramu jest niezmienny, natomiast zmienne lokalne i rekord aktywacyjny mogą zmieniać się w trakcie wykonania podprogramu

  • Struktura rekordu aktywacyjnego jest jednak statyczna.
  • Jako że nie dopuszczamy rekurencji, w dowolnej chwili aktywna może być tylko jedna wersja danego podprogramu.
  • Tylko jedna może być zatem instancja jego rekordu aktywacyjnego.
  • W takim przypadku rekord aktywacyjny może być przechowywany razem z kodem podprogramu.

Implementacja podprogramów bez zagnieżdżeń, ale z rekurencją i z dynamicznymi zmiennymi lokalnymi na stosie

Sprawa jest nieco bardziej skomplikowana

  • Kompilator musi wygenerować kod obsługujący niejawną alokację i dealokację zmiennych lokalnych.
  • W danej chwili może istnieć więcej niż jedna instancja danego podprogramu.
  • Może zatem istnieć więcej instancji zmiennych lokalnych i rekordu aktywacyjnego.
  • Rozsądne jest przechowywanie rekordu aktywacyjnego na stosie, razem ze zmiennymi lokalnymi.

Zauważmy, że nawet jeśli w danej chwili istnieje więcej niż jedna instancja danego podprogramu (jesteśmy „w trakcie” rekurencji), to odwołania do zmiennych lokalnych dotyczą tych z ostatniej instancji

  • Dostęp do wszelkich zmiennych obejmuje zatem dwie możliwości: zmienne globalne (alokowane statycznie) lub zmienne lokalne (w rekordzie aktywacyjnym na szczycie stosu).
  • Do zmiennej lokalnej sięgamy znając jej położenie względne.
  • Położenie względne to położenie zmiennej względem początku rekordu aktywacyjnego. Jest ono statycznie ustalane przez kompilator w trakcie przetwarzania deklaracji zmiennych w podprogramie.

By ułatwić dealokację pamięci na stosie, rekord aktywacyjny podprogramu zawiera łącze dynamiczne

  • Jest to adres ostatniej komórki pamięci wykorzystywanej przez rekord aktywacyjny podprogramu wywołującego (czyli poprzednika dynamicznego).
  • Gdy podprogram kończy działanie, wierzchołek stosu ustawia się na tę właśnie wartość.
  • W ten sposób przywracamy sytuację sprzed wywołania podprogramu.
  • Ciąg łączy dynamicznych od rekordu aktywacyjnego danego podprogramu, poprzez kolejne poprzedniki dynamiczne, aż do rekordu aktywacyjnego podprogramu wywołanego najwcześniej (main) to łańcuch dynamiczny.

Implementacja podprogramów z zagnieżdżeniami, z rekurencją i z dynamicznymi zmiennymi lokalnymi na stosie, w językach ze statycznym wiązaniem zakresu

Oprócz dotychczas omówionych czynności, potrzebny jest mechanizm dostępu do zmiennych nielokalnych

  • Chodzi o zmienne nielokalne, ale też nieglobalne (te ostatnie są statyczne, więc dostęp jest prosty).
  • Dostęp do zmiennych nielokalnych jest dwuetapowy.
  • Zmienne te znajdują się gdzieś na stosie...
  • W pierwszym etapie trzeba znaleźć instancję rekordu aktywacyjnego, gdzie zmienna została zaalokowana.
  • W drugim etapie należy sięgnąć do zmiennej, znając jej położenie względne.
  • Podobnie jak poprzednio jest ono znane w czasie kompilacji.

Łącza statyczne

  • W rekordzie aktywacyjnym umieszcza się łącze statyczne.
  • Jest to adres początku rekordu aktywacyjnego (najpóźniejszej instancji) statycznego poprzednika podprogramu.
  • Ciąg łączy statycznych od rekordu aktywacyjnego danego podprogramu do rekordu aktywacyjnego podprogramu najbardziej zewnętrznego (main) to łańcuch statyczny.
  • Zagnieżdżenie zakresów widoczności jest znane w czasie kompilacji, zatem dla każdej zmiennej kompilator może statycznie określić długość fragmentu łańcucha statycznego, który należy przejść, by dotrzeć do owej zmiennej.

Implementacja podprogramów z zagnieżdżeniami, z rekurencją i z dynamicznymi zmiennymi lokalnymi na stosie, w językach z dynamicznym wiązaniem zakresu

Mamy do dyspozycji dwie metody

  • Dostęp głęboki: Zamiast podążać wzdłuż łańcucha statycznego, idziemy wzdłuż łańcucha dynamicznego.
  • Dostęp płytki: Jest to alternatywna metoda implementacji (opisana poniżej), dająca taką samą semantykę.

Dostęp płytki

  • Zmienne nie są przechowywane razem z rekordami aktywacyjnymi.
  • Dla każdej zmiennej tworzy się osobny stos.
  • Za każdym razem, gdy tworzona jest zmienna, alokowana jest dla niej komórka na odpowiednim stosie.
  • Przy zakończeniu podprogramu, ze stosów przechowujących zmienne tego podprogramu zdejmuje się ostatnią komórkę.
  • Przy odwołaniu do zmiennej odczytuje się wierzchołek odpowiedniego stosu, jako że tam jest „najświeższa” wersja.
  • Alternatywnie, można przechowywać globalną tablicę z odsyłaczami do zmiennych, aktualizowaną przy każdym wywołaniu i powrocie z podprogramu.