Skip to content

Latest commit

 

History

History
349 lines (254 loc) · 8.94 KB

File metadata and controls

349 lines (254 loc) · 8.94 KB

Programowanie obiektowe

Polimorfizm

Coders School

Słowo kluczowe virtual

Jeżeli chcemy, aby przy używaniu wskaźników lub referencji na klasę bazową, jakaś metoda zachowywała się inaczej w zależności od prawdziwego typu obiektu, to należy ją oznaczyć słowem kluczowym virtual. Jest to tzw. funkcja wirtualna.


Funkcja nie-wirtualna

#include <iostream>

struct Bird {
    void sing() { std::cout << "tweet, tweet\n"; }
};

struct Sparrow : Bird {
    void sing() { std::cout << "chirp, chirp\n"; }
};

int main() {
    Sparrow sparrow;
    Bird& bird = sparrow;
    bird.sing();
    return 0;
}

Co pojawi się na ekranie?

tweet, tweet


Funkcja wirtualna

#include <iostream>

struct Bird {
    virtual void sing() { std::cout << "tweet, tweet\n"; }
};

struct Sparrow : Bird {
    void sing() { std::cout << "chirp, chirp\n"; }
};

int main() {
    Sparrow sparrow;
    Bird& bird = sparrow;
    bird.sing();
    return 0;
}

Co pojawi się na ekranie?

chirp, chirp

Sprawdź na ideone.com


Słowo kluczowe override

Jeżeli w klasie pochodnej nadpisujemy metodę wirtualną, czyli zmieniamy jej zachowanie, to należy dodać słowo override.

class Interface {
public:
    virtual void doSth() = 0;
};

class SomeClass : public Interface {
public:
    void doSth() override;   // there should be an implementation in the cpp file
};

int main() {
    // Interface interface;    // Compilation error, Interface is pure virtual
    SomeClass someClass;    // OK
    Interface* interface = &someClass;  // OK, we hold a pointer
}

Mała uwaga

override jest opcjonalne. Jeśli go nie podamy za sygnaturą funkcji klasy pochodnej to metoda z klasy bazowej i tak zostanie nadpisana.

Jego użycie jest jednak dobrą praktyką, bo dzięki niemu kompilator sprawdzi, czy faktycznie nadpisujemy metodę z klasy bazowej i jeśli nie, to program się nie skompiluje.

Bez override mogłaby zostać utworzona nowa metoda w klasie pochodnej, która nie nadpisuje niczego z klasy bazowej.

Metody wirtualne nadpisujemy, nie przeciążamy.


Nadpisywanie metod - override

Wracając do przykładu o ptakach, klasy Penguin, Hummingbird oraz Goose to klasy pochodne, które dziedziczą po pewnych klasach bazowych jak Bird, Flyable, Soundable, Swimmable oraz nadpisują kilka ich metod jak:

  • void eat() override
  • void sleep() override
  • void makeSound() override
  • void fly() override
  • void swim() override

Nadpisanie takich metod powoduje, że możemy zmienić ich implementacje.


override

class Soundable {
public:
    virtual void makeSound() = 0;
};
class Goose : public Soundable {
public:
    void makeSound() override { std::cout << "Honk! Honk!"; }
};
class Hen : public Soundable {
public:
    void makeSound() override { std::cout << "Cluck! Cluck!"; }
};
class Duck : public Soundable {
public:
    void makeSound() override { std::cout << "Quack! Quack!"; }
};

Wspólna klasa bazowa

Ponieważ wspólnym rodzicem wszystkich klas jest klasa Soundable możemy przechowywać w kontenerze wskaźniki typu Soundable.

std::vector<std::shared_ptr<Soundable>> birds_;

Jakie dane otrzymamy na wyjściu?

std::vector<std::shared_ptr<Soundable>> birds_;
birds_.push_back(std::make_shared<Goose>());
birds_.push_back(std::make_shared<Hen>());
birds_.push_back(std::make_shared<Duck>());

birds_[0]->makeSound();
birds_[1]->makeSound();
birds_[2]->makeSound();

Polimorfizm

Zjawisko, które właśnie zaobserwowaliśmy, nazywa się polimorfizmem.

Polimorfizm pozwala funkcji przybrać różne formy (implementacje), tak jak na przykładzie.

Dlatego, jeżeli utworzymy kolejno obiekty Goose, Hen i Duck w zależności od obiektu zostanie wywołana jego wersja metody makeSound.

Polimorfizm włącza się, gdy mamy funkcje wirtualne i używamy wskaźników lub referencji na typ bazowy.

Kto grał lub czytał Wiedźmina?


Doppler :)

W uniwersum wykreowanym przez naszego rodzimego pisarza Andrzeja Sapkowskiego, występuje pewna intrygująca i ciekawa rasa zwana Dopplerami.

Rasa ta potrafi przyjąć, postać różnych form życia, może stać się człowiekiem, elfem, krasnoludem. Zmienia w ten sposób swoje cechy jak głos, kolor włosów, a nawet ubranie!

Pomimo że rasa ta jest typu Doppler, potrafi w różnych okolicznościach podszywać się pod inne rasy jak elf, krasnolud czy człowiek.

Z punktu widzenia C++ nasz Doppler podlega zjawisku polimorfizmu.


class Doppler {
public:
    virtual void sayHello() { std::cout << "I'm Doppler!"; }
};

class Dwarf : public Doppler {
public:
    virtual void sayHello() { std::cout << "I'm Dwarf!"; }
};

class Elf : public Doppler {
public:
    virtual void sayHello() { std::cout << "I'm Elf!"; }
};

class Human : public Doppler {
public:
    virtual void sayHello() { std::cout << "I'm Human!"; }
};

int main() {
    std::shared_ptr<Doppler> doppler1 = std::make_shared<Dwarf>();
    std::shared_ptr<Doppler> doppler2 = std::make_shared<Elf>();
    std::shared_ptr<Doppler> doppler3 = std::make_shared<Human>();

    std::cout << doppler1->sayHello() << '\n';
    std::cout << doppler2->sayHello() << '\n';
    std::cout << doppler3->sayHello() << '\n';
}

Jak widzimy, nasz Doppler może przyjąć różne formy i zachowywać się tak jak one. Wskaźnik jest typu Doppler, ale program dobrze wie, kiedy Doppler podszywa się pod człowieka, kiedy pod krasnoluda, a kiedy pod elfa.


Nie-wirtualne destruktory

Bardzo ważne w przypadku tworzenia metod wirtualnych i dziedziczenia jest tworzenie wirtualnych destruktorów. Jeżeli korzystamy z dobroci polimorfizmu i nie oznaczymy destruktor jako virtual to destruktor ten nie zostanie wywołany.

#include <iostream>
#include <string>

class Parent {
public:
    Parent() { std::cout << "PARENT C'tor called\n"; }
    ~Parent() { std::cout << "PARENT D'tor caller\n"; }
};

class Child : public Parent {
public:
    Child() { std::cout << "CHILD C'tor called\n"; }
    ~Child() { std::cout << "CHILD D'tor caller\n"; }
};

int main() {
    Child child;    // ok, object on stack, not a pointer
}

Nie-wirtualne destruktory - problem

#include <iostream>
#include <memory>
#include <string>

class Parent {
public:
    Parent() { std::cout << "PARENT C'tor called\n"; }
    ~Parent() { std::cout << "PARENT D'tor caller\n"; }
};

class Child : public Parent {
public:
    Child() { std::cout << "CHILD C'tor called\n"; }
    ~Child() { std::cout << "CHILD D'tor caller\n"; }
};

int main() {
    // Problem
    std::unique_ptr<Parent> child = std::make_unique<Child>();

    // But shared_ptr will cleanup properly
    std::shared_ptr<Parent> child2 = std::make_shared<Child>();
}

Wirtualny destruktor

#include <iostream>
#include <memory>
#include <string>

class Parent {
public:
    Parent() { std::cout << "PARENT C'tor called\n"; }
    virtual ~Parent() { std::cout << "PARENT D'tor caller\n"; }
};

class Child : public Parent {
public:
    Child() { std::cout << "CHILD C'tor called\n"; }
    ~Child() override { std::cout << "CHILD D'tor caller\n"; }
};

int main() {
    std::unique_ptr<Parent> child2 = std::make_unique<Child>();
}