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.
#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
#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
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
}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.
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.
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!"; }
};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_;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();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.
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.
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
}#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>();
}#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>();
}