Programowanie obiektowe
Wykład 2. Dziedziczenie, polimorfizm
Operator zakresu
Operator zakresu ma postać ’::’. Przed nim stawia się nazwę
klasy, przestrzeni nazw bądź nic. Służy do wskazania zakresu, z
którego pochodzi nazwa.
std::cout
Stos::n
::n
zmienna this
czyli wskaźnik do się
this
to specjalna zmienna dostępna w ciele definicji metody
niestatycznej, której wartością jest wskaźnik do obiektu na rzecz
którego metoda została wywołana.
zmienna this
przykłady użycia
class Licznik
{
public:
Licznik() : n(0) {};
Licznik& zwieksz() { ++n; return *this; }
// (1)
Licznik& ustaw(int n) { this->n = n; return *this; }
// (1) (3)
int pokaz() { return n; }
private:
int n;
};
main()
{
Licznik l;
cout << l.ustaw(5).zwieksz().pokaz();
// (2)
}
(1)
metoda zwraca referencję do obiektu na rzecz którego została wywołana
(2)
dzięki temu można dla jednego obiektu wywołać sekwencję metod, jak w
przykładzie (praktyka często stosowana w przypadku metod, które
zmieniają stan obiektu, nie mając przy tym naturalnej wartości)
(3)
zmienna this użyta w celu odwołania się do przysłoniętej nazwy
Składowe statyczne
Składowe statyczne to składowe ”wspólne” dla całej klasy. Są to
składowe związane z klasą, nie konkretnym obiektem.
◮
atrybut statyczny — istnieje tylko jeden egzemplarz takiego
atrybutu dla całej klasy (w przypadku atrybutów
niestatycznych, każdy obiekt ma swój egzemplarz atrybutu)
◮
metoda statyczna — nie jest wywoływana na rzecz
konkretnego obiektu
Na marginesie: W niektórych językach obiektowych (Smalltalk, Ruby) klasa też
jest obiektem, składowe statyczne z C++ odpowiadają tam składowym obiektu
klasy.
Składowe statyczne
definicja i deklaracja atrybutu statycznego
deklaracja
class Stos
{
public:
static const int rozmiar; // to jest tylko deklaracja
private:
static Stos* ostatnio_uzywany_stos;
...
}
definicja:
int Stos::rozmiar=100; // definicję trzeba umieścić poza definicją klasy
Stos* Stos::ostatnio_uzywany_stos = NULL;
◮
w definicji można, a w przypadku składowych stałych — trzeba
zainicjalizować atrybut statyczny
Składowe statyczne
odwołanie do atrybutu statycznego
◮
wewnątrz klasy — zwyczajnie, przez nazwę
cout << rozmiar;
ostatnio_uzywany_stos=this;
// w metodzie niestatycznej
◮
poza zakresem klasy
Stos::rozmiar; // z użyciem operatora zakresu
s.rozmiar;
// tak jak w przypadku składowej niestatycznej
sp->rozmiar;
Składowe statyczne
metody statyczne
Metody statyczne definiuje się tak jak niestatyczne (zwykłe), z tym
że:
◮
definicję/deklarację w ciele klasy poprzedza się słowem static
◮
nie można odwoływać się w nich do składowych niestatycznych
◮
nie można używać zmiennej this
Do metod statycznych odwołyjemy się podobnie jak do atrybutów
statycznych: w ciele definicji metody - przez nazwę, poza klasą z
użyciem operatora zakresu lub wywołując ją ’dla obiektu’, jak
metodę niestatyczną.
Dziedziczenie
◮
Dziedziczenie (ang. inheritance) to technika polegająca na
definiowaniu nowej klasy (klasy pochodnej, podklasy) na
podstawie klasy już istniejącej (klasy bazowej, nadklasy).
◮
składnia:
class A
{
...
}
class B : A
// B jest podklasą klasy A
{
...
}
Klas bazowych może być więcej niż jedna (wtedy oddziela się je
przecinkiem).
Dziedziczenie
klasa pochodna
Klasa pochodna dziedziczy:
◮
wszystkie atrybuty klasy bazowej
◮
metody klasy bazowej, za wyjątkiem
◮
konstruktorów
◮
destruktora
◮
operatora przypisania (będzie omówiony później)
W klasie pochodnej można:
◮
zdefiniować dodatkowe atrybuty
◮
zdefiniować dodatkowe metody
◮
zredefiniować metody zdefiniowane w nadklasie
Dziedziczenie
dostęp do składowych dziedziczonych w klasie pochodnej
Klasa pochodna ma dostęp do składowych publicznych (public) i
chronionych (protected) kasy bazowej. Nie ma dostępu do
składowych prywatnych.
Atrybuty chronione (protected) są dostępne w klasach
pochodnych, poza nimi zachowują się jak prywatne.
Dziedziczenie
zakres widoczności składowych dziedziczonych
Zakres widoczności składowych dziedziczonych w klasie pochodnej
określa się poprzedzając w definicji klasy klasę bazową słowem
public
, protected, lub private (domyślne).
class A : public B
{
// składowe publiczne w B są publiczne w A
// składowe chronione w B są chronione w A
}
class A : protected B
{
// składowe publiczne w B są chronione w A
// składowe chronione w B są chronione w A
}
class A : private B lub class A : B
{
// składowe publiczne w B są prywatne w A
// składowe chronione w B są prywatne w A
}
Dziedziczenie
zakres widoczności składowych dziedziczonych: wybiórcze udostępnianie składowych
dziedziczonych
Możliwe jest też precyzyjniejsze określanie widoczności składowych
dziedziczonych, na poziomie pojedynczych składowych (Symfonia
C++, t. III, pkt. 19.2.4, Po znajomości, czyli udostępnianie
wybiórcze
)
Dziedziczenie
dziedziczenie wielokrotne
Klasa może mieć więcej niż jedną klasę bazową. Dziedziczy wtedy
wszystkie składowe (oprócz konstruktorów, destruktora i op. =) ze
wszystkich klas bazowych.
Dziedziczenie wielokrotne
konflikt nazw
class A
{
public:
int a,i;
}
class B
{
public:
int b,i;
}
class C : public A, public B
{
void f()
{
a = b;
// poprawne
i = 0;
// błąd (niejednoznaczne)
A::i = 0;
// poprawne
}
}
Klasa C dziedziczy obie składowe i, są to różne składowe w klasie C.
Dopiero odwołanie się do składowej i jest błędem (niejednoznaczność).
Niejednoznaczność można usunąć stosując operator zakresu.
Dziedziczenie
a wskaźniki i referencje
Wskaźnik do danej klasy może wskazywać na obiekt klasy
potomnej. Podobnie z referencjami.
Dziedziczenie
wywołanie metod z użyciem wskaźnika i referencji
class A {
public:
void f() { cout << "Metoda z klasy A"; }
};
class B : public A {
public:
void f() { cout << "Metoda z klasy B"; } // metoda zredefiniowana
};
int main() {
A a;
B b;
A *p1 = &a, *p2 = &b;
A &r1 = a, &r2 = b;
a.f();
b.f();
p1->f();
p2->f();
r1.f();
r2.f();
};
Dziedziczenie
wywołanie metod z użyciem wskaźnika i referencji
class A {
public:
void f() { cout << "Metoda z klasy A"; }
};
class B : public A {
public:
void f() { cout << "Metoda z klasy B"; }
};
int main() {
A a;
B b;
A *p1 = &a, *p2 = &b;
A &r1 = a, &r2 = b;
a.f();
// Metoda z klasy A
b.f();
// Metoda z klasy B
p1->f();
// Metoda z klasy A
p2->f();
// Metoda z klasy A
r1.f();
// Metoda z klasy A
r2.f();
// Metoda z klasy A
};
Kiedy znienna typu
wskaźnik do obiektu
klasy A, wskazuje na
obiekt klasy potomnej
B, o wyborze metody
decyduje typ
wskaźnika, nie typ
obiektu (dotyczy
metod
niewirtualnych).
Polimorfizm
Polimorfizm to cecha języka programowania umożliwiająca
operowanie na danych różnego typu za pomocą jednolitego
interfejsu.
Polimorfizm
w języku C?
C nie jest językiem polimorficznym. Polimorficzne zachowanie da
się wyprogramować w dość skomplikowany niskopoziomowy sposób.
Przykład: funkcja qsort
void qsort(void *base, size_t n, size_t size,
int (*cmp) (const void *, const void *) )
Polimorfizm
w języku C++
Polimorfizm w C++ realizowany jest w oparciu hierarchię klas oraz
metody wirtualne. Jest to typowy sposób uzyskiwania polimorfizmu
w językach obiektowych.
W C++ istnieje jeszcze drugi sposób na realizację polimorfizmu: szablony.
Polimorfizm
metody wirtualne
◮
definicję metody wirtualnej poprzedza się słowem virtual
◮
metody wirtualne są normalnie dziedziczone, mogą być w
klasach pochodnych redefiniowane, jak inne, przy czym we
wszystkich klasach pochodnych nadal są metodami
wirtualnymi (redefiniując taką metodę w klasie pochodnej nie
trzeba jej już poprzedzać słowem virtual)
Metody wirtualne
różnica
Wirtualność metody ujawnia się w momencie jej wywołania: jeśli
operujemy wskaźnikiem do obiektu (może on wskazywać na obiekt
dowolnej klasy potomnej) i wywołujemy dla wskazywanego obiektu
metodę wirtualną to o wyborze metody decyduje typ obiektu, nie
wskaźnika. Decyzja co do wyboru metody podejmowana jest
dopiero w trakcie działania programu.
Podobnie z referencjami.
Metody wirtualne
wywołanie metody wirtualnej z użyciem wskaźnika i referencji
class A {
public:
virtual void f() { cout << "Metoda z klasy A"; }
};
class B : public A {
public:
void f() { cout << "Metoda z klasy B"; } // metoda zredefiniowana
};
int main() {
A a;
B b;
A *p1 = &a, *p2 = &b;
A &r1 = a, &r2 = b;
a.f();
b.f();
p1->f();
p2->f();
r1.f();
r2.f();
}
Metody wirtualne
wywołanie metody wirtualnej z użyciem wskaźnika i referencji
class A {
public:
virtual void f() { cout << "Metoda z klasy A"; }
};
class B : public A {
public:
void f() { cout << "Metoda z klasy B"; }
};
int main() {
A a;
B b;
A *p1 = &a, *p2 = &b;
A &r1 = a, &r2 = b;
a.f();
// Metoda z klasy A
b.f();
// Metoda z klasy B
p1->f();
// Metoda z klasy A
p2->f();
// Metoda z klasy B
r1.f();
// Metoda z klasy A
r2.f();
// Metoda z klasy B
}
Kiedy znienna typu
wskaźnik do obiektu
klasy A, wskazuje na
obiekt klasy potomnej
B, o wyborze metody
wirtualnej decyduje
typ obiektu, nie typ
wskaźnika.
Wczesne i późne wiązanie
◮
wczesne wiązanie (early binding)
podczas kompilacji wywołanie metody (funkcji) jest wiązane z
adresem kodu metody
◮
późne wiązanie (late binding)
powiazanie z konkretnym adresem kodu następuje podczas
wykonywania programu: na podstawie dodatkowej informacji
w obiekcie ustalana jest klasa obiektu; sprawdzane jest, czy
wywoływana metoda jest zdefiniowana w tej klasie; jeśli nie,
przeszukiwane są kolejne nadklasy
Wczesne i późne wiązanie
Kompilacja kodu C++
Kompilator C++ stosuje wczesne wiązanie w sytuacjach kiedy
może to zrobić. Również w przypadku metod wirtualnych, jeśli:
◮
są wywoływane na rzecz ’prawdziwego’ obiektu, nie referencji
ani wskaźnika; z poprzedniego przykładu:
A a;
B b;
a.f();
b.f();
◮
operator zakresu rozstrzyga
A a;
B b;
A *p1 = &a, *p2 = &b;
A &r1 = a, &r2 = b;
p1->A::f();
p2->B::f();
r1.A::f();
r2.B::f();
Metody wirtualne
koszty
Metody wirtualne generują koszty.
◮
obiekty klas zawierających choć jedną metodę wirtualną są
większe; obiekt taki zawiera dodatkowo informację pozwalającą
zidentyfikować jego klasę; informacja ta jest wykorzystywana w
trakcie wywołania metody wirtualnej
◮
wywołanie metody wirtualne jest bardziej czasochłonne;
dochodzi czas potrzebny na wybór właściwej wersji metody
Metody abstrakcyjne
Metoda abstrakcyjna to metoda wirtualna, którą wprowadza się bez
podania definicji, określając jedynie sygnaturę (nazwa, argumenty,
typ wartości).
Metodę abstrakcyjną oznacza się, dodając do deklaracji napis ’= 0’
Klasa abstrakcyjna
◮
Klasa zawierająca choćby jedną metodę abstrakcyjną jest klasą
abstrakcyjną. Nie ma możliwości tworzenia obiektów takiej
klasy.
◮
Klasa abstrakcyjna określa jedynie pewien zestaw własności,
które wszystkie klasy potomne będą posiadać.
◮
Klasy abstrakcyjne są narzędziem ułatwiającym modularyzację
kodu.
Klasa abstrakcyjna
przykład
class Figura
{
public:
virtual void narysuj() = 0;
}
Tyle wiedząc, możemy już np. pisać kod wyświetlający figury na
ekranie. Kod ten nie zależy od tego jakie konkretnie figury
wprowadzimy. Dla każdej z figur zdefiniowany będzie sposób jej
rysowania.
class Trojkat
: public Figura
{
public:
void narysuj()
... ;
}
class Prostakat
: public Figura
{
public:
void narysuj()
... ;
}
class Okrag
: public Figura
{
public:
void narysuj()
... ;
}