Paradygmaty programowania/Ćwiczenia 5: Programowanie obiektowe — przegląd

From Studia Informatyczne

Spis treści

Zadanie 1

Co wypisuje poniższy program w języku C++? Odpowiedz, a potem skompiluj program i sprawdź... Uwaga: do poprawnego skompilowania programu potrzebne będzie dołączenie pliku nagłówkowego (#include <iostream>) i deklaracja przestrzeni nazw (using namespace std;).

     class A
     {
       public:
       virtual void f() { cout << ”A.f”; g(); }
       void g() { cout << ”A.g”; h(); }
       virtual void h() { cout << ”A.h”; }
     };
     
     class B: public A
     {
       public:
       virtual void f() { cout << ”B.f”; g(); }
       void g() { cout << ”B.g”; h(); }
       virtual void h() { cout << ”B.h”; }
     };
     
     void main()
     {
       A x = B();
       A* y = new B();
       x.f();
       y–>f();
     }

Wskazówka:

Pamiętaj, że dynamiczne wiązanie wywołań z metodami następuje tylko wtedy, gdy metoda jest zadeklarowana jako virtual, a instancja obiektu użyta do wywołania jest zaalokowana na stercie.

Zadanie 2

Jak zmieni się wynik programu z zadania 1, jeśli usuniemy deklarację virtual przy funkcji f w klasie A? Które deklaracje virtual można usunąć bez żadnej zmiany zachowania programu?

Wskazówka:

Ważna jest deklaracja virtual przy metodzie w klasie bazowej. W klasie pochodnej nie jest konieczna, chyba że wiązanie dynamiczne ma prowadzić jeszcze dalej w dół hierarchii klas. Zwyczajowo jednak umieszcza się virtual i tu, i tu — dla zwiększenia czytelności kodu. Warto też pamiętać, że użycie virtual nie pociąga „obowiązku” zredefiniowania danej metody w klasie pochodnej.

Zadanie 3

Wykorzystanie dynamicznego wiązania wywołań z metodami oznacza, że często będziemy odwoływali się do obiektów z klas pochodnych za pomocą zmiennej (wskaźnika) z klasy bazowej. Jakie są tego konsekwencje dla dealokacji pamięci w językach, w których jest ona jawna? Czy z alokacją jest podobnie?

Wskazówka:

Dealokacja obiektu za pomocą wskaźnika klasy bazowej spowoduje wykonanie destruktora z klasy bazowej, który zapewne nie zajmie się pamięcią zaalokowaną przez obiekt z klasy pochodnej. Rozwiązaniem jest zadeklarowanie w klasie bazowej destruktora wirtualnego (być może pustego). Dzięki temu wywołanie destruktora za pomocą wskaźnika klasy bazowej spowoduje wykonanie destruktora z klasy pochodnej. Ten problem nie występuje przy alokacji, gdyż przy tworzeniu obiektu zawsze podajemy jego typ — a zatem wywoływany jest właściwy konstruktor.

Zadanie 4

Napisz program w Javie, dzięki któremu będzie można zobaczyć, czy i kiedy następuje wywołanie metody finalize. Powinien on dać się uruchomić w różnych wariantach, w których będzie potrzebował mniej lub więcej pamięci.

Zadanie 5

Przyjrzyj się dwom poniższym klasom. Czy taki układ to dobry pomysł?

     class Silnik {...};
     class Samochód : Silnik {...};

Wskazówka:

Dziedziczenie powinno pojawiać się tam, gdzie chcemy zdefiniować coś bardziej wyspecjalizowanego. Klasyczne dziedziczenie odpowiada przecież relacji bycia podtypem, czyli relacji „obiekt z podklasy jest obiektem z klasy bazowej”. W przykładzie z silnikiem i samochodem ta relacja nie zachodzi: obiekt z klasy Samochód ewidentnie nie jest szczególnym przypadkiem obiektu z klasy Silnik. Powiedzielibyśmy natomiast, że (obiekt z klasy) Samochód ma (obiekt z klasy) Silnik, a w takim razie napisalibyśmy zapewne tak:

     class Samochód {
       Silnik s;
       ...
     };

Zadanie 6

Popatrz teraz na takie dwie klasy i porównaj ten przykład z sytuacją z poprzedniego zadania.

     class Samochód {...};
     class Ciężarówka : Samochód {...};

Wskazówka:

Te dwie sytuacje stanowią przejrzystą ilustrację różnicy między dziedziczeniem a enkapsulacją...