Artmetic Qt/C++ Dziedziczenie, polimorfizm

Dziedziczenie, polimorfizm

polimorfizm w C++

Definicja polimorfizmu

Polimorfizm (wielopostaciowość) jest to cecha programowania obiektowego, umożliwiająca wywołanie różnych funkcji o tej samej nazwie i w zależności od kontekstu.

w odniesieniu do świata realnego można powiedzieć że polimorfizm polega na opieraniu się o cechy wspólne. Jeżeli mamy różne rasy ludzi i każda ma z nich inną skórę to można powiedzieć że jest to wielopostaciowość, ponieważ każdy z nich jest człowiekiem, ale mają różne rodzaje skór (wielopostaciowe). Jeżeli zaś definiowalibyśmy (bez uprzedzeń rasistowskich) ludzi jako reprezentacje klas moglibyśmy zrobić w ten sposób, że mamy ogólną wspólną klasę człowiek oraz bardziej konkretne jak Europejczyk, Afroamerykanin i Azjata:

class Czlowiek{ 
public:
virtual QString getSkora(){ return "nieokreślona";}
/* dalsze ciało */ 
} 

class Afroamerykanin : public Czlowiek{
public:
QString getSkora(){ return "czarna";}
 /* dalsze ciało */ 
}

class Azjata : public Czlowiek{ 
public:
QString getSkora() { return "żółta";}
/* dalsze ciało */ 
}

class Europejczyk : public Czlowiek{ 
public:
QString getSkora() { return "biała";}
/* ciało */ 
}

To w takiej sytuacji każda z klas pochodnych od Człowieka czyli Azjata, Europejczyk i Afroamerykanin dziedziczy w sposób publiczny klasę człowiek i możliwość odpytania konkretnego obiektu stworzonego na tej podstawie odpytania go o kolor skory za pomocą getSkora() i na tym polega dziedziczenie. Polimorfizm zaś ujawni się jeżeli chcielibyśmy wszystkich zapisać w jednym wektorze jako wskaźniki lub referencje do konkretnych obiektów i odpytać o kolor skóry:

main.cpp

void getKolorySkory(Czlowiek *czlowiek)
{
qDebug() << czlowiek->getSkora();
}


int main()
{
// ...

Afroamerykanin afroamerykanin;
Azjata azjata;
Europejczyk europejczyk;

getKolorSkory(&afroamerykanin);
getKolorSkory(&azjata);
getKolorSkory(&europejczyk);

//..
}

Tu ujawnia ujawniają się dwie cechy pierwsza z nich to taka że klasa pochodna np. Europejczyk z urzędu jest też klasą podstawową człowiek, widać to podczas wysyłania do funkcji gdzie następuje niejawna konwersja z Europejczyka na człowieka. Druga zaś cecha to wywołanie funkcji getKolorSkory(Clowiek *czlowiek) w zależności od tego co wysłaliśmy raz zostanie wywołana funkcja dla Afroamerykanina raz dla Azjat a raz dla Europejczyka i tą cechę właśnie nazywamy polimorfizmem ponieważ klasa Człowiek ma wiele postaci. Mówimy podczas wywołania funkcji czlowiek->getSkora() o wiązaniu dynamicznym ponieważ program dopiero w trakcie wykonywania wybierze właściwą metodę. W innych przypadkach byłoby to wiązanie statyczne ponieważ program na etapie kompilacji wie która konkretnie metoda będzie wywołana.

Warto zwrócić uwagę na słowo kluczowe dla polimorfizmu czyli virtual gdyby nie występowało ono nie moglibyśmy skorzystać z możliwości jakie daje polimorfizm. Czyli zamiast zwrócenia koloru skóry dla poszczególnych konkretnych klas np: „żółta”, „czarna”, „biała” zostanie zwrócona „nieokreślona”.

Warto zwrócić uwagę również na słowo public przed klasami pochodnymi, ponieważ domyślnie klasy są dziedziczone w sposób prywatny, więc gdybyśmy napisali słowa public klasy pochodne byłyby dziedziczone w sposób prywatny. W przypadku struktur dostęp domyślny jest public.

Klasa abstrakcyjna i metoda czysto wirtualna

Dzięki temu że po klasie Człowiek dziedziczone są inne klasy mówimy że jest ona klasą podstawową dla każdej z klasy Europejczyk, Afroamerykanin i Azjata. Jeżeli w klasie Człowiek zdefiniowalibyśmy metodę wirtualną w taki sposób:

 virtual QString getSkora() = 0;

to mówilibyśmy o tym że jest to klasa abstrakcyjna ponieważ posiada zadeklarowaną co najmniej jedną metodę czysto wirtualną. W związku z tym nie moglibyśmy zdefiniować obiektu Człowiek człowiek; byłoby to spowodowane tym że dopisek =0 mówi o tym że nie ma zadeklarowanego ciała funkcji, a w klasach pochodnych wymusza to na nas zdefiniowanie konkretnego ciała tej metody.

W C++ klasą abstrakcyjną jest klasa, która posiada zadeklarowaną co najmniej jedną metodę czysto wirtualną. Każda klasa, która dziedziczy po klasie abstrakcyjnej i sama nie chce być abstrakcyjną, musi implementować wszystkie odziedziczone metody czysto wirtualne.

Dobre praktyki dziedziczenia

A teraz poniżej dłuższa implementacja więc proponuję pobrać z git-a ten przykład po poniższym linkiem:

Pliki źródłowe można pobrać z gita:

git clone https://github.com/artmetic/Dziedziczenie.git
// main.cpp
#include <QCoreApplication>
#include <QDebug>

#include "jamnik.h"
#include "pies.h"
#include "kot.h"

void sprawdzZwierzeta(Zwierze *zwierze)
{
    zwierze->jedz();
    zwierze->wydajDzwiek();
    qDebug() << "Waga zwierzaka: " << zwierze->getWaga() << "kg";
}

int main(int argc, char *argv[])
{
    QCoreApplication a(argc, argv);

    Kot kot("Bonifacy");
    Pies pies("Reksio");
    Pies piesPluto("Pluto");
    Jamnik jamnik("Jamulek");

    piesPluto.zwarz(35.75);

    QVector<Zwierze *> zwierzyniec{&kot, &pies, &piesPluto, &jamnik};

    for(auto &a: zwierzyniec)
        sprawdzZwierzeta(a);

    qDebug() << "Waga zwierzaka klasy podstawowej:" << jamnik.Zwierze::getWaga() << "kg";

    return EXIT_SUCCESS;
}
//zwierze.h
#ifndef ZWIERZE_H
#define ZWIERZE_H

#include <QString>
#include <QDebug>

class Zwierze
{
public:
    Zwierze(QString &nazwa, const double &waga) : waga(waga), nazwa(nazwa){
        qDebug() << "Konstrukcja zwierzaka: " << getNazwa();
    }
    virtual void jedz() = 0;
    virtual void wydajDzwiek() = 0;
    virtual void zwarz(double waga) = 0;
    virtual double getWaga(){ return 0.; }
    virtual ~Zwierze(){qDebug() << "Destrukcja zwierzaka: " << getNazwa();}

protected:
    double waga;
    QString getNazwa(){return nazwa;}

private:
    const QString nazwa;

};

#endif // ZWIERZE_H

// kot.h
#ifndef KOT_H
#define KOT_H

#include "QDebug"

#include "zwierze.h"

class Kot : public Zwierze
{
public:
    Kot(QString nazwa);
    ~Kot();
    void jedz() override;
    void wydajDzwiek()  override;
    double getWaga()  override;
    void zwarz(double waga) override;
};

#endif // KOT_H
//kot.cpp
#include "kot.h"

Kot::Kot(QString nazwa) : Zwierze(nazwa,5.)
{
    qDebug() << "Konstrukcja kota: " << getNazwa();
}

Kot::~Kot()
{
    qDebug() << "Destrukcja kota: " << getNazwa();
}

void Kot::jedz()
{
    qDebug() << QString("Kotek %1 mlaska z miski mleko").arg(getNazwa()) ;
}

void Kot::wydajDzwiek()
{
    qDebug() << QString("Kotek %1 mruczy").arg(getNazwa()) ;
}

double Kot::getWaga()
{
    qDebug() << QString("Kotka %1 waga to %2").arg(getNazwa()).arg(waga);
    return waga;
}

void Kot::zwarz(double waga)
{
    Zwierze::waga = waga;
    qDebug() << QString("Kotek %1 zważony").arg(getNazwa())  ;
}
// pies.h
#ifndef PIES_H
#define PIES_H

#include <QDebug>
#include "zwierze.h"

class Pies : public Zwierze
{
public:
    Pies(QString nazwa);
    Pies(QString nazwa, const double &waga);


    ~Pies() override;
    void jedz() override;
    void wydajDzwiek()  override;
    double getWaga()  override;
    void zwarz(double waga) final override;

private:
    QList<int *> iloscLatek;
};

#endif // PIES_H
//pies.cpp
#include "pies.h"

Pies::Pies(QString nazwa,const double &waga) : Zwierze(nazwa, waga)
{
qDebug() << "Konstrukcja pieska: " << getNazwa();

iloscLatek.append(new int(1));
iloscLatek.append(new int(3));
iloscLatek.append(new int(4));
}

Pies::~Pies()
{
    qDebug() << "Destrukcja pieska: " << getNazwa();
    qDeleteAll(iloscLatek);
}


Pies::Pies(QString nazwa) : Zwierze(nazwa, 20.)
{
    qDebug() << "Konstrukcja domyślna pieska: " << getNazwa();
}

void Pies::jedz()
{
    qDebug() << QString("Pies %1 dostał karmę").arg(getNazwa()) ;
}

void Pies::wydajDzwiek()
{
    qDebug() << QString("Pies %1 szczeka").arg(getNazwa()) ;
}

double Pies::getWaga()
{
    qDebug() << QString("Kotka %1 waga to %2").arg(getNazwa()).arg(waga);
    return waga;
}

void Pies::zwarz(double waga)
{
    Zwierze::waga = waga;
    qDebug() << QString("Kotek %1 zważony").arg(getNazwa())  ;

}

//jamnik.h
#ifndef JAMNIK_H
#define JAMNIK_H

#include "pies.h"

class Jamnik final : public Pies
{
public:
    Jamnik(QString nazwa): Pies(nazwa, 10.) {
        qDebug() << "Konstrukcja jamnika: " << getNazwa();
    }
    ~Jamnik() override{
        qDebug() << "Destrukcja jamnika: " << getNazwa();
    }

    void jedz() override{
        qDebug() << QString("Jamnik %1 dostał karmę").arg(getNazwa()) ;
    }
};

#endif // JAMNIK_H
Konstrukcja zwierzaka:  "Bonifacy"
Konstrukcja kota:  "Bonifacy"
Konstrukcja zwierzaka:  "Reksio"
Konstrukcja domyślna pieska:  "Reksio"
Konstrukcja zwierzaka:  "Pluto"
Konstrukcja domyślna pieska:  "Pluto"
Konstrukcja zwierzaka:  "Jamulek"
Konstrukcja pieska:  "Jamulek"
Konstrukcja jamnika:  "Jamulek"
"Kotek Pluto zważony"
"Kotek Bonifacy mlaska z miski mleko"
"Kotek Bonifacy mruczy"
"Kotka Bonifacy waga to 5"
Waga zwierzaka:  5 kg
"Pies Reksio dostał karmę"
"Pies Reksio szczeka"
"Kotka Reksio waga to 20"
Waga zwierzaka:  20 kg
"Pies Pluto dostał karmę"
"Pies Pluto szczeka"
"Kotka Pluto waga to 35.75"
Waga zwierzaka:  35.75 kg
"Jamnik Jamulek dostał karmę"
"Pies Jamulek szczeka"
"Kotka Jamulek waga to 10"
Waga zwierzaka:  10 kg
Waga zwierzaka klasy podstawowej: 0 kg
Destrukcja jamnika:  "Jamulek"
Destrukcja pieska:  "Jamulek"
Destrukcja zwierzaka:  "Jamulek"
Destrukcja pieska:  "Pluto"
Destrukcja zwierzaka:  "Pluto"
Destrukcja pieska:  "Reksio"
Destrukcja zwierzaka:  "Reksio"
Destrukcja kota:  "Bonifacy"
Destrukcja zwierzaka:  "Bonifacy"
Naciśnij <RETURN> aby zamknąć to okno...

Słowo kluczowe final – czyli zabraniam dziedziczenia

Jeżeli przyjrzymy się deklaracji klasy Jamnik możemy zauważyć słowo final, służy ono zabronienu dalszego dziedziczenia, czyli nie możemy zadaklarować pochodnej klasy

class JamnikDlugowlosy : public Jamnik { // błąd kompilacji

Pytanie po co tak robić? Po to aby zabronić np. dziedziczenia naszej biblioteki lub zwiększyć prędkość kompilacji i działania programu. A w niektórych przypadkach ominąć problem kompatybilności pomiędzy platformami (zabraniamy używania innych typów kompilatorowi).

Jak sprawdzić czy klasa jest ostateczna – is_final<> ?

Aby sprawdzić w kodzie czy klasa jest ostateczną i ma final możemy posłużyć się szablonem biblioteki standardowej std is_final.

    if(std::is_final<Jamnik>::value)
        qDebug() << "Klasa ostateczna";
    else
        qDebug() << "Klasa z możliwością dziedzicznia:";
Należy pamiętać o zadeklarowaniu w Qt w pliku .pro standardu C++14 inaczej nie będzie można skompilować kodu.


CONFIG += c++14

Słowo kluczowe override – czyli pamiętaj za mnie kompilatorze

Słowo override służy temu, abyśmy się nie pomylili w deklaracji funkcji. Jeżeli zdefiniowalibyślmy funkcję z innymi argumentami od metody wirtualnej lub o innej sygnaturze kompilator wyświetli błąd.

Warto zwrócić uwagę że w powyższym kodzie użyłem również słówka override do destruktora, zrobiłem tak pomimo iż destruktora nie możemy w inny sposób od domyślnego ~NazwaKlasy() zdefiniować. Więc po co słówko override zostało użyte? A po to aby kompilator sprawdził czy dodaliśmy słówko virtual do klasy podstawowej. A co jeżelibyśmy tego nie uczynili? Wówczas moglibyśmy doprowadzić do wycieku pamięci:

Zwierze *zwierze = new Pies("Reksio", 24);
delete zwierze;

W takiej sytuacji jeżeli klasa zwierze nie miałaby wirtualnego destruktora, doprowadziłaby do wycieku pamięci zmiennej ilosciLatek alokowanej w konstruktorze Pies(QString nazwa, double waga) w sposób dynamiczny ponieważ zostanie wykonany tylko destruktor ~Zwierze().

Pamiętaj że dobrą praktyką jest stawianie override wszędzie tam gdzie wykorzystujemy polimorfizm łącznie z destruktorem

Metody wirtualne zabronione static i opcjonalne inline

W przypadku metod wirtualnych nie możemy użyć słówka static, jednak słówka inline jak najbardziej. Tak właśnie może zachować się funkcja jedz() w klasie Jamnik została ona zdefiniowana w pliku nagłówkowym, a więc jest inline z urzędu nawet bez tego słówka poprzedzającego klasę. Mamy tu jednak pewne zastrzeżenie ponieważ zachowa się ona inline, ale tylko wtedy gdy dojdzie do wiązania statycznego czyli na etapie kompilacji, a nie podczas wiązania dynamicznego wtedy gdy program jest wykonywany, a więc polimorfizmu.

Wymuszanie wyłuskania zmiennej lub funkcji klasy bazowej

Warto zwrócić uwagę na poniższy fragment kodu w klasie Pies:

Zwierze::waga = waga;

Wymusza on odwołanie się do zmiennej dostępnej dla klasy pochodnej po znajdującej się w klasie bazowej w obszarze protected waga. Gdybyśmy zastosowali zwykły zapis waga = waga; otrzymalibyśmy bezsensowne przypisanie wartości do samej swojej, jednak poprzedzając zmienną klasyfikatorem obiektu Zwierze:: sięgamy do zmiennej z klasy bazowej.

Analogicznie możemy wywołać funkcję kot.Zwierze::getWaga(); w funkcji głównej main.

Dziedziczenie a konstrukcja i destrukcja obiektów

Konstrukcja oraz destrukcja obiektów jest przeprowadzana w odwrotny sposób czyli obiekty są konstruowane od klasy bazowej przez wszystkie pochodne do klasy którą konstrujemy czyli:

~Zwierze() -> ~Pies() -> ~Jamnik()

a destrukcja odbywa się w odwrotnej kolejności czyli:

~Jamnik() -> ~Pies() -> ~Zwierze()

Konstrukcja zwierzaka:  "Jamulek"
Konstrukcja pieska:  "Jamulek"
Konstrukcja jamnika:  "Jamulek"

Destrukcja jamnika:  "Jamulek"
Destrukcja pieska:  "Jamulek"
Destrukcja zwierzaka:  "Jamulek"

Pamiętaj że konstruktory nie mogą być wirtualne zaś destruktory jak najbardziej, w szczególności zwróć uwagę na kwestię ewentualnych wycieków pamięci przy braku wirtualnych destruktorów.

Zmiana obiektu z bazowej na pochodną

czyli jak pies stał się konkretny. O ile konwersja z klasy konkretnej na bazową/podstawową nie jest problemem bo dokonywaliśmy to w funkcji void sprawdzZwierzeta(Zwierze *zwierze), gdzie psy i koty stawały się niejawnie zwierzętami to konwersja z nieokreślonego zwierzęcia na konkretną klasę Pies stanowi już problem. Jest jednak na to rowiązanie w postaci rzutowania za pomocą dynamic_cast<NazwaKonkretnejKlasy *> dzięki temu zwierze może się stać Psem, Kotem lub Jamnikiem.

    Pies piesBary("Bary");
    Pies *piesek;
    Zwierze * p;

    p = &piesBary; // konwersja na klasę podstawową

    piesek = dynamic_cast<Pies *>(p);

    if(piesek)
        qDebug() << "Rzutowanie ok";
    else
        qDebug() << "Błąd rzutowania:";
Należy jednak uważać żeby z Kota nie zrobić Psa bo może to doprowadzić do katastrofy, która ukarze się gdy wywołamy funkcję ze składnikiem którego dana klasa nie ma. Gdyby rozszerzyć klasę Kot o zmienną lapka.
  
    int lapka = 0;
    void podrap(){qDebug() << "Drapie" << lapka;}

i wywołać ją

piesek = dynamic_cast<Kot *>(p);
piesek->podrap();

program wywali błąd w postaci błąd rzutowania.

Zachęcam do pozostawienia komentarzy szczególnie tych krtycznych, które w uzasadniony i w miarę kulturalny sposób zmotywują mnie do pisania dalszych artykułów z innymi zagadnieniami.

Leave a Reply

Twój adres email nie zostanie opublikowany. Pola, których wypełnienie jest wymagane, są oznaczone symbolem *

Witryna wykorzystuje Akismet, aby ograniczyć spam. Dowiedz się więcej jak przetwarzane są dane komentarzy.

Related Post

wzorzec adapter

Wzorzec Adapter C++/QtWzorzec Adapter C++/Qt

Zastosowanie Adapter inaczej nazywany Nakładką (ang. wrapper) to strukturalny wzorzec projektowy, którego zadaniem jest stworzenie spójnego interfejsu dla dwóch niekompatybilnych klas. Adapter przekształca interfejs jednej z klas na interfejs drugiej.