Funkcje wirtualne — powtórka
Część sekcji Programowanie obiektowe ścieżki C++ w Coddy. Lekcja 58 z 104.
Skoro rozumiemy już różnicę między polimorfizmem w czasie kompilacji a polimorfizmem w czasie wykonywania, przyjrzyjmy się bliżej funkcjom wirtualnym i słowu kluczowemu override, które sprawia, że można ich używać bezpieczniej.
Gdy oznaczysz funkcję jako virtual w klasie bazowej, klasy pochodne mogą dostarczyć własną implementację. Specyfikator override jawnie informuje kompilator, że zamierzasz zastąpić funkcję wirtualną:
class Animal {
public:
virtual void speak() {
std::cout << "Some sound" << std::endl;
}
virtual ~Animal() = default;
};
class Dog : public Animal {
public:
void speak() override {
std::cout << "Woof!" << std::endl;
}
};Używanie override jest kluczowe, ponieważ pozwala wykrywać błędy podczas kompilacji. Jeśli przypadkowo błędnie zapiszesz nazwę funkcji lub użyjesz niewłaściwych parametrów, kompilator Cię o tym powiadomi, zamiast po cichu tworzyć nową funkcję:
class Cat : public Animal {
public:
void speek() override { // Błąd kompilatora: brak funkcji do nadpisania
std::cout << "Meow!" << std::endl;
}
};Specyfikator final zapobiega dalszemu przesłanianiu. Użyj go, gdy klasa pochodna powinna być ostatnią, która przesłania daną funkcję:
class Bulldog : public Dog {
public:
void speak() override final {
std::cout << "Gruff woof!" << std::endl;
}
};
class TinyBulldog : public Bulldog {
void speak() override {} // Błąd: nie można przesłonić funkcji final
};Zawsze używaj override przy przesłanianiu funkcji wirtualnych. Dokumentuje to Twój zamiar i pozwala kompilatorowi sprawdzić, czy rzeczywiście przesłaniasz istniejącą funkcję wirtualną.
Wyzwanie
ŁatwyZbudujmy system powiadomień, który pokazuje możliwości słów kluczowych override i final w przypadku funkcji wirtualnych. Utworzysz hierarchię programów obsługi powiadomień, w której niektóre metody można dalej dostosowywać, a inne są zablokowane, aby zapobiec ich modyfikacji.
Podzielisz kod na trzy pliki:
Notifier.h: Zdefiniuj bazową klasęNotifier, która reprezentuje dowolnego nadawcę powiadomień:- Chronione pole
std::string recipient - Konstruktor, który przyjmuje i przechowuje nazwę odbiorcy
- Wirtualna metoda
send(const std::string& message), która wyświetla:Notifying <recipient>: <message> - Wirtualna metoda
getType(), która zwraca ciąg znaków"Generic" - Wirtualny destruktor
- Chronione pole
EmailNotifier.h: Zdefiniuj klasęEmailNotifier, która dziedziczy poNotifier:- Prywatne pole
std::string domain - Konstruktor, który przyjmuje odbiorcę i domenę oraz przekazuje odbiorcę do klasy bazowej
- Nadpisz
send(), aby wyświetlała:Emailing <recipient>@<domain>: <message> - Nadpisz
getType()i oznacz ją jakofinal. Powinna zwracać"Email"
Następnie zdefiniuj klasę
UrgentEmailNotifier, która dziedziczy poEmailNotifier:- Konstruktor, który przyjmuje odbiorcę i domenę oraz przekazuje obie wartości do
EmailNotifier - Nadpisz
send(), aby wyświetlała:[URGENT] Emailing <recipient>@<domain>: <message> - Uwaga: Nie możesz tutaj nadpisać
getType(), ponieważ wEmailNotifieroznaczono ją jakofinal
- Prywatne pole
main.cpp: Odczytaj trzy dane wejściowe (każdą w osobnym wierszu):- Nazwę odbiorcy
- Domenę e-mail
- Treść wiadomości
Utwórz dynamicznie trzy obiekty powiadamiające: bazowy
Notifier,EmailNotifierorazUrgentEmailNotifier, wszystkie z tym samym odbiorcą (i domeną, jeśli dotyczy). Zapisz je w tablicy wskaźnikówNotifier*.Przejdź przez tablicę i dla każdego obiektu powiadamiającego wyświetl jego typ za pomocą
getType(), a następnie wywołajsend(), przekazując swoją wiadomość. Sformatuj każdy wpis w następujący sposób:Type: <type> <send output>Wyświetl pusty wiersz między poszczególnymi obiektami powiadamiającymi. Po zakończeniu zwolnij dynamicznie przydzielone obiekty.
Na przykład dla danych wejściowych Alice, company.com i Meeting at 3pm:
Type: Generic
Notifying Alice: Meeting at 3pm
Type: Email
Emailing Alice@company.com: Meeting at 3pm
Type: Email
[URGENT] Emailing Alice@company.com: Meeting at 3pmZwróć uwagę, że UrgentEmailNotifier może nadpisać send(), aby dostosować format wiadomości, ale dziedziczy typ "Email" z EmailNotifier, ponieważ getType() oznaczono jako final. Używaj słowa kluczowego override we wszystkich nadpisanych metodach, aby wykrywać niezgodności sygnatur podczas kompilacji.
Spróbuj swoich sił
#include <iostream>
#include <string>
#include "Notifier.h"
#include "EmailNotifier.h"
using namespace std;
int main() {
// Odczytaj dane wejściowe
string recipient;
string domain;
string message;
getline(cin, recipient);
getline(cin, domain);
getline(cin, message);
// TODO: Utwórz tablicę 3 wskaźników typu Notifier*
// TODO: Utwórz dynamicznie:
// - Obiekt klasy bazowej Notifier z recipient
// - Obiekt EmailNotifier z recipient i domain
// - Obiekt UrgentEmailNotifier z recipient i domain
// TODO: Przejdź przez tablicę i dla każdego obiektu powiadamiającego:
// - Wypisz "Type: ", a następnie wynik getType()
// - Wywołaj send() z message
// - Wypisz pusty wiersz między obiektami powiadamiającymi (ale nie po ostatnim)
// TODO: Zwolnij pamięć przydzieloną dynamicznie obiektom
return 0;
}
Ta lekcja zawiera krótki quiz. Zacznij lekcję, żeby na niego odpowiedzieć i śledzić swoje postępy.
Wszystkie lekcje w sekcji Programowanie obiektowe
1Podstawy programowania obiektowego
Pliki zewnętrzneBudowanie i kompilacja C++Pliki nagłówkowe i pliki źródłowePrzestrzenie nazw i zakresWprowadzenie do programowania obiektowego w C++Klasy a obiektyWskaźnik „this”Metody (funkcje składowe)Atrybuty (składowe danych)Podstawy konstruktorów i destruktorówPowtórzenie – prosty kalkulator4Właściwości klas
Elementy instancji a elementy statyczneGettery i setteryStałe funkcje składoweSłowo kluczowe mutableMetody i zmienne statyczneFunkcje i klasy zaprzyjaźnionePodsumowanie – menedżer konta bankowego7Dziedziczenie
Podstawy dziedziczeniaPoziomy dostępu w dziedziczeniuKolejność wywołań konstruktora i destruktoraNadpisywanie metodFunkcje wirtualne i VTableWielokrotne dziedziczenieDziedziczenie wirtualnePowtórzenie — hierarchia pracowników2Zarządzanie pamięcią
Pamięć stosu a stertyWskaźniki i referencjePamięć dynamiczna (new/delete)Inteligentne wskaźniki w C++RAII w C++Podsumowanie — menedżer tablicy dynamicznej5Hermetyzacja
Specyfikatory dostępu w C++Specyfikatory dostępu — szczegółowoUkrywanie informacjiStruktura a klasaKlasy zagnieżdżone i wewnętrznePodsumowanie — system ewidencji studentów8Polimorfizm
Polimorfizm czasu kompilacji i wykonaniaPrzeciążanie funkcjiFunkcje wirtualne — powtórkaCzysto wirtualne funkcjeKlasy abstrakcyjneProjektowanie interfejsów w C++Rzutowanie dynamiczne i RTTIPodsumowanie — kalkulator kształtów3Konstruktory i destruktory
Konstruktor domyślnyKonstruktor z parametramiKonstruktor kopiującyKonstruktor przenoszącyListy inicjalizacyjne konstruktoraKonstruktory delegująceDestruktor — szczegółowe omówienieZasada trzech / pięciu / zeraPodsumowanie — klasa String6Przeciążanie operatorów
Wprowadzenie do przeciążania operatorówPrzeciążanie operatorów arytmetycznychPrzeciążanie operatorów porównaniaOperatory strumieniowePrzeciążanie operatora przypisaniaPrzeciążanie operatorów [] i ()Operatory konwersji typówPodsumowanie — klasa Matrix9Szablony
Szablony funkcjiSzablony klasSpecjalizacja szablonówSzablony wariadycznePodstawy SFINAE i cech typówPodsumowanie — kontener generycznyPoćwicz samodzielnie: Kompilator C++ online