Funkcje wirtualne i VTable
Część sekcji Programowanie obiektowe ścieżki C++ w Coddy. Lekcja 52 z 104.
Słowo kluczowe virtual rozwiązuje problem, który omówiliśmy w poprzedniej lekcji. Gdy deklarujesz metodę jako wirtualną, C++ określa w czasie wykonywania, którą wersję wywołać, na podstawie rzeczywistego typu obiektu, a nie typu wskaźnika.
class Animal {
public:
virtual void speak() {
std::cout << "Some sound" << std::endl;
}
};
class Dog : public Animal {
public:
void speak() override {
std::cout << "Woof!" << std::endl;
}
};
Dog d;
Animal* ptr = &d;
ptr->speak(); // Wynik: Woof!Teraz wywoływana jest właściwa metoda! Słowo kluczowe override jest opcjonalne, ale zalecane. Informuje kompilator, że zamierzasz przesłonić funkcję wirtualną, dzięki czemu wykrywane są błędy, jeśli sygnatury nie są zgodne.
Jak to działa? Gdy klasa ma funkcje wirtualne, kompilator tworzy tablicę wirtualną (vtable). Jest to ukryta tablica wyszukiwania zawierająca wskaźniki do funkcji wirtualnych klasy. Każdy obiekt z funkcjami wirtualnymi przechowuje ukryty wskaźnik (vptr) do tablicy wirtualnej swojej klasy.
Gdy wywołujesz funkcję wirtualną za pośrednictwem wskaźnika, program wyszukuje prawidłowy adres funkcji w tabeli vtable w czasie działania. Nazywa się to dynamicznym wiązaniem. Powoduje to niewielki narzut w porównaniu ze zwykłymi wywołaniami funkcji, ale umożliwia potężne zachowanie polimorficzne.
Jedna ważna zasada: jeśli klasa ma funkcje wirtualne i będzie używana jako klasa bazowa, jej destruktor również powinien być wirtualny. Zapewnia to prawidłowe sprzątanie podczas usuwania obiektów klas pochodnych za pomocą wskaźników do klasy bazowej:
class Animal {
public:
virtual ~Animal() {} // Wirtualny destruktor
virtual void speak() {}
};Wyzwanie
ŁatwyZbudujmy system odtwarzacza multimediów, który pokazuje możliwości funkcji wirtualnych i polimorfizmu w czasie działania programu. Utworzysz hierarchię typów multimediów, z których każdy odtwarza zawartość w inny sposób, i zobaczysz, jak funkcje wirtualne umożliwiają właściwe działanie nawet wtedy, gdy dostęp do nich uzyskuje się za pośrednictwem wskaźników do klasy bazowej.
Rozmieścisz kod w trzech plikach:
Media.h: Zdefiniuj bazową klasęMedia, która reprezentuje dowolne multimedia, które można odtwarzać:- Chronione pole
std::string title - Konstruktor, który przyjmuje tytuł i go zapisuje
- Metoda wirtualna
play(), która wyświetla:Playing media: <title> - Wirtualny destruktor, który wyświetla:
Media [<title>] destroyed
- Chronione pole
AudioTrack.h: Zdefiniuj klasęAudioTrack, która publicznie dziedziczy poMedia:- Prywatne pole
std::string artist - Konstruktor, który przyjmuje tytuł i wykonawcę, przekazuje tytuł do klasy bazowej i zapisuje wykonawcę
- Nadpisz
play(), używając słowa kluczowegooverride, aby wyświetlać:Playing audio: <title> by <artist> - Destruktor, który wyświetla:
AudioTrack [<title>] destroyed
- Prywatne pole
main.cpp: Wczytaj trzy dane wejściowe (każde w osobnym wierszu):- Tytuł utworu audio (ciąg znaków)
- Nazwę wykonawcy (ciąg znaków)
- Tytuł wideo (ciąg znaków)
Zdefiniuj klasę
VideoClipbezpośrednio w main.cpp. Ma ona publicznie dziedziczyć poMedia:- Prywatne pole
int duration(w sekundach) - Konstruktor, który przyjmuje tytuł i czas trwania (domyślny czas trwania ustaw na 120)
- Nadpisz
play(), aby wyświetlać:Playing video: <title> (<duration>s) - Destruktor, który wyświetla:
VideoClip [<title>] destroyed
Utwórz tablicę trzech wskaźników
Media*wewnątrz bloku. Przydziel dynamicznie pamięć na:- Obiekt bazowej klasy
Mediaz tytułem "Generic Media" - Obiekt
AudioTrackz wczytanym tytułem i wykonawcą - Obiekt
VideoClipz wczytanym tytułem wideo
Przejdź przez tablicę i wywołaj
play()dla każdego wskaźnika. Następnie usuń wszystkie obiekty w odwrotnej kolejności. Po bloku wyświetl:Playback complete!
Na przykład, dla danych wejściowych Bohemian Rhapsody, Queen i Nature Documentary:
Playing media: Generic Media
Playing audio: Bohemian Rhapsody by Queen
Playing video: Nature Documentary (120s)
VideoClip [Nature Documentary] destroyed
Media [Nature Documentary] destroyed
AudioTrack [Bohemian Rhapsody] destroyed
Media [Bohemian Rhapsody] destroyed
Media [Generic Media] destroyed
Playback complete!Zauważ, jak wywołanie play() za pośrednictwem wskaźników Media* uruchamia właściwą metodę klasy pochodnej dzięki funkcjom wirtualnym. Zwróć też uwagę, że wirtualny destruktor zapewnia prawidłowe czyszczenie: podczas usuwania za pośrednictwem wskaźnika do klasy bazowej uruchamiają się destruktory zarówno klasy pochodnej, jak i bazowej.
Spróbuj swoich sił
#include <iostream>
#include <string>
#include "Media.h"
#include "AudioTrack.h"
using namespace std;
// TODO: Zdefiniuj tutaj klasę VideoClip, która publicznie dziedziczy po Media
// - Prywatne pole duration typu int (w sekundach)
// - Konstruktor przyjmuje tytuł i czas trwania (domyślna wartość czasu trwania to 120)
// - Przesłoń play(), aby wypisywała: Playing video: <title> (<duration>s)
// - Destruktor wypisuje: VideoClip [<title>] destroyed
class VideoClip : public Media {
private:
int duration;
public:
// TODO: Zaimplementuj konstruktor
VideoClip(const std::string& t, int d = 120) : Media(t) {
// TODO: Zapisz czas trwania
}
// TODO: Przesłoń metodę play()
void play() override {
// TODO: Zaimplementuj tę metodę
}
// TODO: Zaimplementuj destruktor
~VideoClip() {
// TODO: Zaimplementuj ten destruktor
}
};
int main() {
// Wczytaj dane wejściowe
string audioTitle;
string artist;
string videoTitle;
getline(cin, audioTitle);
getline(cin, artist);
getline(cin, videoTitle);
// TODO: Utwórz zakres blokowy za pomocą nawiasów klamrowych
// Wewnątrz bloku:
// 1. Utwórz tablicę trzech wskaźników Media*
// 2. Przydziel dynamicznie pamięć dla:
// - Obiektu klasy bazowej Media o tytule "Generic Media"
// - Obiektu AudioTrack z tytułem i wykonawcą podanymi na wejściu
// - Obiektu VideoClip z tytułem filmu podanym na wejściu
// 3. Przejdź w pętli przez tablicę i wywołaj play() dla każdego wskaźnika
// 4. Usuń wszystkie obiekty w odwrotnej kolejności
// TODO: Po bloku wypisz: Playback complete!
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