Wstęp do Programowania Obiektowego
Wykład 12
ZAKRES WIDOCZNOŚCI
Zakres widoczności zmiennej to zbiór tych instrukcji programu, w
których zmienna ta jest widoczna, tzn.
można się do niej odwołać.
zmienna jest lokalna (w jednostce programu lub w bloku), jeśli jest
zadeklarowana w tej jednostce.
zmienna jest nielokalna , jeśli jest
widoczna, ale zadeklarowana gdzie
Zakres statyczny
Zakres statyczny opiera się na kodzie programu (w sensie
tekstowym). Jest powszechnie stosowany.
Istnieją dwie kategorie języków z zakresem statycznym:
◦ dopuszczające zagnieżdżanie
podprogramów (np. Ada, Pascal)
◦ nie dopuszczające takiego zagnieżdżania
(np. C), za wyjątkiem definicji klas
Odwoływanie się do zmiennych w językach z zakresem statycznym
Napotkawszy odwołanie do zmiennej, kompilator musi odnaleźć jej deklarację i określić atrybuty.
Deklaracji szuka się najpierw w bieżącej jednostce programu.
Jeśli tu jej nie ma, trzeba szukać w jednostce
„okalającej”, zwanej poprzednikiem statycznym.
Jeśli i tu jej nie ma, trzeba szukać w poprzedniku poprzednika itd. (czyli w przodkach statycznych), być może docierając aż do zakresu globalnego.
Zmienne mogą się przesłaniać. Jeśli w
bliższym przodku statycznym jest zadeklarowana
Dostęp do przesłoniętych zmiennych
Język może oferować mechanizmy pozwalające na dostęp do
przesłoniętych zmiennych.
W języku C++ jest to operator ::
W Adzie podobną rolę spełnia kropka,
np. nazwa_jednostki.nazwa_zmiennej.
Problemy z zakresem statycznym
Zbyt swobodny dostęp do zmiennych lub podprogramów.
Wszystkie zmienne w głównym programie (tzn. w zakresie globalnym) są widoczne dla wszystkich procedur
(podprogramów).
Nie ma sposobu, by narzucić szczegółowe ograniczenia w dostępie do
podprogramów.
To prowadzi do nadużywania globalnych
Przykład zakresu statycznego
P0 {
P1 { P3 { }
P4 { }
}
P2 { P5 { }
}
}
Zakres dynamiczny
Zakres dynamiczny opiera się na
kolejności wywołań podprogramów, a nie na ich rozmieszczeniu
„przestrzennym”.
Odwoływanie się do zmiennych w
językach z zakresem dynamicznym
Napotkawszy odwołanie do zmiennej, kompilator musi odnaleźć jej deklarację i określić jej atrybuty.
Deklaracji szuka się najpierw w bieżącym
podprogramie (podobnie jak dla zakresu statycznego).
Jeśli tu jej nie ma, trzeba szukać w podprogramie, który wywołał bieżący podprogram, zwanym
poprzednikiem dynamicznym.
Jeśli i tu jej nie ma, trzeba szukać w poprzedniku poprzednika itd. (czyli w przodkach dynamicznych), być może docierając aż do zakresu globalnego, czyli do programu głównego.
Zmienne mogą się przesłaniać. Jeśli w bliższym
przodku dynamicznym jest zadeklarowana zmienna o takiej samej nazwie, jak w dalszym przodku, to
przesłania ona tę dalszą.
Przykład zakresu dynamicznego
P0() {
int x;
P1() { int x;
P2();
}
P2() {
Put(x);
}
P1();
Cechy zakresu dynamicznego
Zalety:
w wielu sytuacjach wygoda programisty
prosta implementacja.
Wady:
Gorsza efektywność, gdyż rozstrzyganie zakresu musi być robione dynamicznie.
Nie da się statycznie sprawdzić zgodności typów dla zmiennych nielokalnych.
Słaba czytelność odwołań.
Podprogramy są wykonywane w środowisku
wcześniej wywołanych podprogramów, które
jeszcze nie zakończyły działania.
Środowisko odwołań
Zbiór wszystkich nazw widocznych w danym punkcie programu nazywamy środowiskiem odwołań tego punktu.
W języku z zakresami statycznymi środowisko odwołań tworzą wszystkie zmienne zadeklarowane lokalnie wraz ze zmiennymi z przodków statycznych, z wyjątkiem zmiennych przesłoniętych.
Mówimy, że podprogram jest aktywny, jeśli jego wykonanie się rozpoczęło i jeszcze nie zakończyło.
W języku z zakresami dynamicznymi środowisko odwołań tworzą wszystkie zmienne zadeklarowane lokalnie wraz ze zmiennymi z aktywnych
podprogramów, z wyjątkiem zmiennych przesłoniętych.
Stałe nazwane
Stała nazwana to zmienna, która jest wiązana z wartością tylko raz — w
chwili wiązania z pamięcią.
Przykład: Można by używać stałej o nazwie pi zamiast 3.1415926...
Stałe poprawiają czytelność i ułatwiają modyfikowanie
programu.
TYPY
Definicja typu
Typ to pewien ustalony zbiór
wartości oraz związany z nim zbiór operacji , które można wykonywać na wartościach z tego typu.
Dozwolone operacje to wszystkie operatory (w szerokim rozumieniu, czyli również podprogramy,
podstawienia itp.), których dziedziną
jest ów typ lub typ z nim zgodny,
Dozwolone operacje w szerszym ujęciu to m.in.
podstawienie pod zmienną,
przekazanie jako parametr,
sprawdzanie równości (czy x = y?),
sprawdzanie relacji porządku (czy x <
y?),
wybór elementu składowego tablicy
Typ pierwotny to taki, którego w
danym języku nie da się zdefiniować za pomocą innych typów.
Większość języków posiada pewien zestaw typów pierwotnych, np. char, int, float.
Z typów pierwotnych można tworzyć
typy złożone, np. rekordy, tablice.
Cele istnienia typów
Na poziomie maszynowym wszelkie dane
zapisane są jako układy bitów, niezależnie od tego, co reprezentują. Typy są sposobem na nadanie znaczenia tym „anonimowym”
układom bitów.
Dzięki temu zyskujemy możliwość
wykrywania wielu powszechnych błędów (sprawdzanie zgodności typów).
Optymalizacja kodu przez kompilator, który np. zna zakresy liczb i może wybrać bardziej efektywną reprezentację.
Typy abstrakcyjne są sposobem na
Typ abstrakcyjny to konstrukcja języka programowania, w której definiujemy typ (w dotychczasowym rozumieniu) oraz
operacje na nim w taki sposób, że inne byty w programie nie mogą manipulować danymi inaczej niż za pomocą
zdefiniowanych przez nas operacji.
Istotą rzeczy jest tu oddzielenie części
„prywatnej” typu (czyli szczegółów reprezentacji danych i implementacji poszczególnych operacji) od części
„publicznej”
Pierwotne typy danych (tradycyjnie)
Typy całkowite
Typy zmiennopozycyjne
Typ znakowy
Typ logiczny
Pierwotne typy danych
Na ogół typy odzwierciedlające cechy sprzętu.
Podstawowy pierwotny typ całkowity (int, integer)
odpowiada zazwyczaj takiemu zakresowi liczb, jaki mieści się w jednym słowie maszyny (obecnie zwykle 32 lub 64 bity).
Miewa warianty różniące się rozmiarem (byte, short, long) i dopuszczaniem znaku, tzn. liczb ujemnych
(signed/unsigned).
Pierwotne typy zmiennopozycyjne (float, double) to
obecnie prawie zawsze typy obsługiwane sprzętowo, zgodne ze standardem IEEE 754.
Pierwotne typy znakowe (char, character) tradycyjnie wykorzystywały kodowanie ASCII; obecnie coraz częściej używany jest Unicode.
Pierwotny typ logiczny (boolean) jest kodowany za pomocą pojedynczych bitów (co jest oszczędne pamięciowo, ale
wolniejsze w dostępie) lub całych bajtów.
Typ napisowy
Typ napisowy może być typem
pierwotnym, jak np. w Javie (klasa String).
W wielu językach, np. w C/C++, napisy są jednak szczególnym
rodzajem tablic (a więc nie są typem
pierwotnym).
Obsługa napisów o zmiennej długości
Napisy statyczne, czyli po
zadeklarowaniu nie można zmienić długości napisu, np. obiekty z klasy String w Javie.
Napisy dynamiczne o długości
ograniczonej statycznie. Deklarujemy napis z górnym ograniczeniem na
długość, np. tablica znakowa w C.
Napisy w pełni dynamiczne, czyli długość może zmieniać się bez
ograniczeń, np. w Perlu.
Tablica
Tablica to zestaw elementów takiego samego typu, gdzie dostęp do
poszczególnych elementów jest poprzez indeksowanie.
Wymaga to dynamicznego wyliczania
adresu elementu
Dostęp do elementów tablicy
Tłumacząc instrukcje zawierające odwołania do
elementów tablicy, kompilator generuje kod wyliczający adres elementów.
Dla tablic jednowymiarowych (indeksowanych od 0 jak w C/C++ i pochodnych) adres elementu T[i] to:
(adres elementu T[0]) + (i – indeks pierwszego elementu)*(rozmiar elementu).
Tablice wielowymiarowe przechowywane są tak, jakby to były tablice tablic jednowymiarowych, w dwóch
możliwych wariantach: wierszami lub kolumnami.
Przy ułożeniu tablicy dwuwymiarowej wierszami daje to adres elementu T[i, j] równy
(adres elementu T[0, 0]) + (i*n + j)*(rozmiar elementu)
Tablica asocjacyjna
Tablica asocjacyjna to
nieuporządkowany zestaw elementów identyfikowanych za pomocą kluczy.
Klucze mogą być bardzo różne (zwykle są to dowolne napisy). Są użyteczne
tam, gdzie potrzebny jest swobodny (nieuporządkowany) dostęp do
elementów.
Przykład tablic asocjacyjnych (Perl)
%wzrost = (”Jacek” => 177, ”Joanna” => 166, ”Jerzy” => 199);
$wzrost {”Józefina”} = 188;
delete $wzrost{”Jerzy”};
if (exists $wzrost{”Joanna”}) ...
Dostęp do elementu za pomocą klucza.
Dwie możliwe implementacje:
użycie funkcji haszującej do odwzorowania kluczy na adresy.
binarne drzewo poszukiwań (np. czerwono-czarne)
Rekord
Rekord to zestaw elementów
dowolnych typów. Elementy rekordu zwane są polami. Większość języków stosuje zapis „z kropką” na
oznaczenie dostępu do pól rekordu
(niekiedy „strzałka” czyli „->”)
Cechy typu rekordowego
Pola rekordu są zwykle stałej
wielkości, więc da się statycznie przewidzieć rozmiar rekordu
Równość rekordów można rozumieć jako równość wszystkich pól rekordu
W językach obiektowych zwykle nie ma rekordów, ponieważ ich
uogólnieniem są klasy
Unia
Unia to zestaw elementów dowolnych typów, z których w dowolnym momencie przechowywany jest tylko jeden.
Celem takiej implementacji jest oszczędność pamięci kosztem
bezpieczeństwa (trudno jest sprawdzać zgodność typów, gdy nie jest oczywiste jaki typ jest aktualnie przechowywany).
Unie są współcześnie mało przydatne,
ponieważ nie ma kłopotów z
Przykład definicji oraz użycia unii
union unia {
float u_zm1;
short int u_zm2;
};
unia uu;
uu.u_zm2 = 456;
uu.u_zm1 = 123.45; // „nadpisanie” wartości w unii
Typ wskaźnikowy
Typ wskaźnikowy obejmuje wartości, które mogą wskazywać inne wartości w pamięci,
Dodatkowa wartość to wartość „pusta”, oznaczana jako null lub nil.
Technicznie wskaźnik to po prostu adres komórki pamięci.
Różnica między wskaźnikiem a adresem to
dodatkowa informacja o typie wskazywanych
obiektów, (którą kompilator wykorzystuje do
sprawdzania zgodności typu).
Zalety wskaźników
możliwość dynamicznego zarządzania pamięcią
elastyczność, jaką daje adresowanie pośrednie.
wskaźniki są jedynym sposobem korzystania ze zmiennych
alokowanych dynamicznie na stercie.
Operacje na wskaźnikach
Przypisanie wartości
Dereferencja wskaźnika, czyli dostęp do elementu wskazywanego przez wskaźnik.
Arytmetyka wskaźników, czyli możliwość swobodnego przesuwania wskaźnika, daje programiście duże
możliwości, ale jest niebezpieczna: łatwo sięgnąć do
„nieswojej” pamięci (np. w C i C++).
◦ Bez możliwości przesuwania wskaźnika mechanizm staje się bezpieczniejszy (Java, C#).
Do zarządzania pamięcią potrzebny jest mechanizm alokacji, np. new, malloc.
Adresowanie pośrednie wymaga operatora pobrania w języku C).
Przykłady użycia wskaźników (C/C++)
int a;
int* wsk;
wsk= &a;
*wsk = 25;
int* wsk2;
wsk2 = new int;
*wsk2 = 15;
Problemy ze wskaźnikami
Wiszący wskaźnik to wskaźnik odnoszący się do zmiennej, która została już zdealokowana. Ten
problem znika, gdy w języku nie ma jawnej dealokacji (np. Java, C#).
Zgubiona zmienna to zmienna na stercie, do której nie mamy żadnego wskaźnika.
Aliasowanie może prowadzić do
Typy referencyjne
Referencje to typy wskaźnikowe o ograniczonych (dla bezpieczeństwa) możliwościach.
W C++ typ referencyjny obejmuje stałe wskaźniki z niejawną dereferencją, na których nie są dozwolone operacje
arytmetyczne.
W Javie nie ma wskaźników tylko same referencje. Referencje odnoszą się do obiektów. Można je kopiować przez
przypisanie; arytmetyka nie jest
dozwolona.
Niejawna dealokacja
Niejawna dealokacja to zautomatyzowane
zarządzanie pamięcią zmiennych dynamicznych (programista nie musi o to dbać)
Pierwsza metoda — liczniki odwołań. Dla każdego przydzielonego bloku pamięci utrzymujemy licznik odwołań do tego bloku. Licznik aktualizujemy przy kopiowaniu wskaźników, wyjściu wskaźnika poza zakres widoczności itp. Blok, do którego nie ma odwołań, jest dealokowany.
Druga metoda — zbieranie śmieci. Gdy brakuje miejsca na stercie, rozpoczyna się zbieranie śmieci.
Przeglądamy stertę i wszystkie wskaźniki, zaznaczając te bloki, do których nie ma odwołań. Bloki te są
następnie dealokowane.
ABSTRAKCYJNE TYPY
DANYCH
Dwie podstawowe abstrakcje w językach programowania to:
Abstrakcja procesu: Abstrakcjami procesów są podprogramy. Pozwalają wskazać (przez ich wywołanie), że pewna czynność ma być wykonana, bez wskazywania jak ma być
wykonana. Szczegóły znajdują się w treści podprogramu, której wywołujący nie musi znać.
Abstrakcja danych : Zamknięta całość
obejmująca reprezentację pewnego typu
danych wraz z podprogramami,
Przykłady
Wbudowane w języki programowania typy pierwotne można uznać za abstrakcyjne: nie mamy dostępu do reprezentacji wewnętrznej, więc możemy posługiwać się jedynie tym, co dostarcza język.
Zdefiniowany w Pascalu typ rekordowy i kilka procedur na nim działających nie stanowi
abstrakcyjnego typu danych. Ktokolwiek zadeklaruje rekord tego typu, będzie mógł działać bezpośrednio na tym rekordzie z pominięciem „oficjalnych” procedur.
Sztandarowy przykład to klasa np. z Javy lub C++.
Dane schowane w części prywatnej klasy nie są dostępne na zewnątrz, stąd nie da się wykonywać żadnych operacji bezpośrednio.
Co to jest obiekt?
Obiekt to instancja abstrakcyjnego typu danych, czyli pojedyncza
zmienna (instancja) tego typu
zaalokowany na stercie (najczęściej), na stosie lub statycznie (najrzadziej).
Sam abstrakcyjny typ danych w
większości języków zwany jest klasą.
Klasy w C++ (1/2)
Język oferuje dwie konstrukcje: class i struct, różniące się domyślnymi regułami dostępu.
Klasy języka C++ są typami.
Jednostka programu, która zadeklarowała instancję klasy (obiekt), ma dostęp do publicznych bytów tej klasy, ale tylko poprzez tę instancję.
Każda instancja klasy ma własny zestaw danych, natomiast funkcje (metody) nie są powielane lecz przechowywane wspólnie dla całej klasy.
Obiekty mogą być statyczne oraz dynamiczne,
alokowane na stosie (dostęp przez „zwykłe” zmienne) lub na stercie (dostęp przez wskaźniki).
Obiekty mogą zawierać zmienne dynamiczne alokowane na stercie, czyli poza obiektem.
Alokacja i dealokacja na stercie są jawne; służą do tego
Klasy w C++ (2/2)
Funkcje z klasy mogą być kompilowane jako inline. Kod
funkcji jest wówczas kopiowany do wywołującego, eliminując czasochłonną obsługę wywołania. Mechanizm ten jest
przydatny dla funkcji niewielkich objętościowo.
Elementy klasy mogą być deklarowane jako prywatne (dostęp tylko wewnątrz obiektu), publiczne (dostęp bez ograniczeń) lub chronione (dostęp dla potomków). Osiąga się to przez umieszczenie deklaracji w częściach, odpowiednio, private, public i protected.
Deklaracja friend pozwala dać „obcym” klasom dostęp do elementów prywatnych.
Definicja klasy może zawierać konstruktor, który będzie niejawnie wywoływany przy tworzeniu obiektu z klasy.
Konstruktor ma taką samą nazwę jak klasa. Konstruktory mogą mieć parametry i mogą być przeciążane.