to get the file

Transkrypt

to get the file
Instrukcja
Podstawy programowania 2
laboratoryjna
7
Temat: Obiektowość
Przygotował: mgr inż. Tomasz Michno
1 Wstęp teoretyczny
1.1 Obiektowość w Turbo Pascalu
Wszystkie programy pisane na laboratoriach były tworzone strukturalnie, tzn. program
używał wielu zmiennych, funkcji i procedur, które tak naprawdę nie były ze sobą połączone.
Dopiero przekazywanie zmiennych jako parametry oraz wywoływanie procedur i funkcji
w odpowiedniej kolejności powodowało, że program stawał się „jedną całością”. Wprowadzenie
rekordów pozwoliło na zebranie powiązanych ze sobą zmiennych w jednym miejscu, jednak mimo
to kod nadal był oddzielony od danych, na których operował.
Mimo prostoty programowanie strukturalne posiada wiele wad, m. in. w przypadku
większych programów trudno jest zapanować nad zmiennymi, jak również programiści korzystając
z funkcji i procedur mogą używać ich w niepoprawny sposób. Dodatkowo dochodzi jeszcze
problem w przypadku gdy udostępniamy nasz kod innym osobom – programista posiada przez cały
czas pełen dostęp do wszystkich zmiennych, nawet do tych których nie powinien zmieniać. Często
trudno jest również zrozumieć kolejność oraz sposób korzystania ze zmiennych i podprogramów.
Rozwiązaniem tych problemów jest programowanie obiektowe, które pozwala m.in. na
powiązanie ze sobą zmiennych i procedur/funkcji oraz określenie do których z nich programiści
będą posiadali pełny dostęp. Programowanie obiektowe w dużym stopniu jest zbliżone do
tworzenia własnych typów danych za pomocą rekordów. Odpowiednikiem typu rekordowego
(rekord zadeklarowany w sekcji type) jest klasa, która w przeciwieństwie do rekordu pozwala na
deklarowanie procedur i funkcji w jej wnętrzu oraz określanie dostępu do poszczególnych jej
elementów. Odpowiednikiem zmiennej o typie rekordu zadeklarowanego w sekcji type jest obiekt.
Dzięki takiemu rozwiązaniu możliwe jest tworzenie komputerowych odpowiedników obiektów ze
świata rzeczywistego (np. drzew, samochodów itp. ).
Przykład 1.
Rozpatrzmy przykład czajnika o maksymalnej pojemności 3 litrów, dla którego
zadeklarujemy jedynie 2 procedury – wyświetlenia ilości wody w czajniku (ileWody) oraz dolania
do niego wody (dolejWody, w tej procedurze powinno być sprawdzenie, czy nie przekroczono
pojemności czajnika).
Na początku napiszemy kod za pomocą programowania strukturalnego. Dla ułatwienia późniejszej
rozbudowy programu czajnik zdefiniujemy jako rekord.
Listing 1. Program w wersji strukturalnej
program czajnikStrukturalnie;
uses CRT;
type
TCzajnik = record
iloscWody : real;
end;
var
czajnik : Tczajnik;
procedure dolejWody(var cz : TCzajnik; dolanoLitrow : real);
begin
cz.iloscWody := cz.iloscWody + dolanoLitrow;
if( cz.iloscWody > 3) then
begin
cz.iloscWody:=3;
writeln('Wlano za duzo wody. Wszystko powyzej 3 litrow sie wylalo!');
end;
end;
procedure ileWody(var cz : TCzajnik);
begin
writeln('W czajniku jest ', cz.iloscWody:2:2,' l');
end;
BEGIN
clrscr;
czajnik.iloscWody:=0;
ileWody(czajnik);
dolejWody(czajnik, 5);
ileWody(czajnik);
readln;
END.
Poniżej znajduje się wynik działania powyższego programu. Jak widać realizuje on poprawnie
wszystkie założenia.
Wyobraźmy sobie teraz sytuację, w której udostępniamy komuś nasz kod. Osoba ta stwierdziła, że
zamiast wywoływać procedurę dolania wody, sama zmieni wartość zmiennej czajnik.iloscWody.
W takim przypadku możliwe jest dolanie dowolnej ilości wody, bez żadnego błędu podczas
kompilacji, ani komunikatu o przepełnieniu czajnika:
{jeśli dostaniemy się do zmiennej nic nas
nie powstrzyma przed dolaniem nawet 1000 litrów wody}
czajnik.iloscWody:=1000;
ileWody(czajnik);
Wynik działania powyższego kodu:
O ile w tym przykładzie takie zachowanie jedynie spowoduje niespełnienie założeń, o tyle w
rzeczywistych programach jest niedopuszczalne. Jeżeli założymy że pewne zmienne będą miały
wartości tylko z określonego przedziału, może nastąpić sytuacja w której program będzie działał
nieprawidłowo (np. przy obliczeniach matematycznych będzie generował nieprawidłowe wyniki)
lub nawet zakończy swoje działanie błędem. Warto byłoby w jakiś sposób ograniczyć dostęp do
zmiennej iloscWody, jednak w programowaniu strukturalnym nie jest to możliwe w prosty
i czytelny sposób. Z pomocą w takim wypadku przychodzi programowanie obiektowe.
Listing 2. Program w wersji obiektowej
program czajnikObiektowo;
uses CRT;
type
TCzajnik = object
private
iloscWody : real;
public
procedure dolejWody(dolanoLitrow : real);
procedure ileWody;
end;
procedure TCzajnik.dolejWody(dolanoLitrow : real);
begin
iloscWody := iloscWody + dolanoLitrow;
if( iloscWody > 3) then
begin
iloscWody:=3;
writeln('Wlano za duzo wody. Wszystko powyzej 3 litrow sie wylalo!');
end;
end;
procedure TCzajnik.ileWody;
begin
writeln('W czajniku jest ', iloscWody:2:2,' l');
end;
var
czajnik : TCzajnik;
BEGIN
clrscr;
czajnik.ileWody;
czajnik.dolejWody(5);
czajnik.ileWody;
readln;
END.
Listing 2 zawiera wiele nowych elementów.
Pierwszym z nich jest definicja klasy:
TCzajnik = object
private
iloscWody : real; { Zmienna ­ atrybut}
public
procedure dolejWody(dolanoLitrow : real);
procedure ileWody;
end;
Jak już wspomniano, klasa może być traktowana w dużym uproszczeniu jako odpowiednik rekordu.
Jej deklaracja wygląda bardzo podobnie do deklaracji rekordu – znajdują się w niej zmienne
(nazywane w programowaniu obiektowym atrybutami klasy) oraz procedury/funkcje (nazywane
metodami klasy). Słowo private oznacza, że wszystkie atrybuty i metody znajdujące się po nim
będą niewidoczne poza klasą. Słowo public jest jego przeciwieństwem – pozwala na pełny dostęp
z dowolnego miejsca. Jak można zauważyć, deklaracje procedur są pozbawione parametru, w
którym przekazywana była zmienna przechowująca czajnik – nie jest to potrzebne w tym
przypadku, ponieważ gdy procedury/funkcje znajdują się wewnątrz klasy, posiadają pełny dostęp
do wszystkich jej atrybutów (z których można korzystać, tak jakby były zmiennymi globalnymi).
Oprócz samej deklaracji procedur w ciele klasy, należy napisać ich implementację. Tworzy się ją w
identyczny sposób, co dla zwykłych procedur/funkcji, z jedną różnicą – przed nazwą podporgramu
należy podać nazwę klasy oraz kropkę:
procedure TCzajnik.ileWody;
begin
writeln('W czajniku jest ', iloscWody:2:2,' l');
end;
Po stworzeniu kodu klasy i jej metod, możemy przejść do deklaracji w sekcji var zmiennej, która
będzie obiektem. W tym wypadku tworzy się ją identycznie jak inne zmienne własnych typów (np.
rekordów). Należy również zwrócić uwagę, że obiektów można używać również jako atrybuty
innych klas, rekordów, parametry funkcji itp.
Po zadeklarowaniu obiektu, możemy już z niego korzystać w zwykły sposób – dostęp do atrybutów
wygląda identycznie jak w rekordach, metody również wywołuje się w analogiczny sposób:
obiekt.metoda(parametry), np.:
czajnik.ileWody;
czajnik.dolejWody(5);
czajnik.ileWody;
1.2 Konstruktory i destruktory
Konstruktor i destruktor jest czymś, co jeszcze bardziej odróżnia klasy od rekordów.
Konstruktor to specjalna procedura wywoływana w celu zainicjalizowania obiektu, tzn.
przypisania prawidłowych wartości początkowych atrybutów i wykonanie czynności, które muszą
zostać zrobione dla każdego obiektu przed pierwszym użyciem. Podobnie jak zwykła procedura,
konstruktor może być bezparametrowy, jak i może posiadać dowolną liczbę parametrów.
Destruktor jest procedurą wywoływaną pod koniec życia obiektu – zaraz przed jego całkowitym
usunięciem z pamięci. Jest przydatny np. w celu zwalniania pamięci utworzonej w konstruktorze
lub innych metodach.
W języku TurboPascal w celu odróżnienia konstruktorów i destruktorów od zwykłych
procedur podczas ich deklaracji należy używać zamiast słowa kluczowego procedure słowa
construktor (dla konstruktora) i destructor (dla destruktora).
Przykład 2
program p;
uses
CRT;
type
Tpies = object
private
imie : string;
public
constructor init;
constructor initParam(noweImie : string);
destructor
dest;
procedure dajGlos;
end;
var
pies, pies2 : Tpies;
constructor TPies.init;
begin
writeln('Konstruktor bezparametrowy');
imie:='Szarik';
end;
constructor TPies.initParam(noweImie : string);
begin
writeln('Konstruktor z parametrem');
imie:=noweImie;
end;
destructor TPies.dest;
begin
writeln('Destruktor');
end;
procedure Tpies.dajGlos;
begin
writeln(imie,': Hau! Hau!');
end;
begin
clrscr;
pies.init;
{wywołanie konstruktora bezparametrowego}
pies2.initParam('Azor');
{wywołanie konstruktora parametrowego}
pies.dajGlos;
pies2.dajGlos;
pies.dest;
pies2.dest;
readln;
end.
Obiekty można również tworzyć dynamicznie, tak jak zwykłe typy danych. W takim
wypadku konstruktory i destruktory wywołuje się w funkcjach new i dispose, np.:
pies2:=new(WSKTpies, initParam('Azor'));
dispose(pies2, dest);
Przykład 3
program p;
uses
CRT;
type
Tpies = object
private
imie : string;
public
constructor init;
constructor initParam(noweImie : string);
destructor
dest;
procedure dajGlos;
end;
WSKTPies = ^TPies;
var
pies, pies2 : WSKTpies;
constructor TPies.init;
begin
writeln('Konstruktor bezparametrowy');
imie:='Szarik';
end;
constructor TPies.initParam(noweImie : string);
begin
writeln('Konstruktor z parametrem');
imie:=noweImie;
end;
destructor TPies.dest;
begin
writeln('Destruktor');
end;
procedure Tpies.dajGlos;
begin
writeln(imie,': Hau! Hau!');
end;
begin
clrscr;
pies:=new(WSKTpies, init);
pies2:=new(WSKTpies, initParam('Azor'));
pies^.dajGlos;
pies2^.dajGlos;
dispose(pies, dest);
dispose(pies2, dest);
readln;
end.
Konstruktory i destruktory pełnią bardzo ważną rolę przy dziedziczeniu i polimorfizmie, o
którym będzie następna instrukcja.
2 Zadania
1. Napisz program, w którym stworzysz klasę reprezentującą konto bankowe. Obiekt powinien
posiadać atrybuty: imię i nazwisko właściciela, numer konta i ilość pieniędzy na koncie oraz
metody wpłać i wypłać. Przy wypłacaniu należy sprawdzać, czy nie został przekroczony
limit debetowy (np. saldo konta może mieć minimum -1000 zł). Stwórz jeden obiekt konta
bankowego i przetestuj kod, wykonując kilka operacji wpłat i wypłat.
2. Do programu dodaj konstruktor, w którym zainicjujesz obiekt – imię, nazwisko oraz ilość
pieniędzy na koncie powinna być podawana przez parametr, natomiast numer konta
powinien być wartością losową.
3. Do klasy konta bankowego dodaj metodę, która będzie służyła do przelewania pieniędzy
między kontami (jako parametr podaj obiekt klasy konto bankowe oraz sumę do przelania).
Stwórz 3 obiekty konta bankowego i przetestuj jej działanie.