Z tymi odpowiedziami wchodzisz w hipotetyczne, więc dla jasności spróbuję przedstawić prostsze, bardziej przyziemne wyjaśnienie.
Podstawowe zależności w projektowaniu obiektowym są dwie: IS-A i HAS-A. Ja tego nie wymyśliłem. Tak się nazywają.
IS-A wskazuje, że określony obiekt identyfikuje się jako należący do klasy znajdującej się nad nim w hierarchii klas. Obiekt banana jest obiektem owocu, jeśli jest podklasą klasy owoców. Oznacza to, że wszędzie tam, gdzie można użyć klasy owoców, można użyć banana. Nie jest to jednak odruchowe. Nie można podstawić klasy bazowej dla określonej klasy, jeśli ta konkretna klasa jest wywoływana.
Has-a wskazał, że obiekt jest częścią klasy złożonej i że istnieje relacja własności. W C ++ oznacza to, że jest to obiekt składowy i jako taki na klasie będącej właścicielem spoczywa obowiązek pozbycia się go lub przekazania własności przed zniszczeniem samego siebie.
Te dwie koncepcje są łatwiejsze do zrealizowania w językach z pojedynczym dziedziczeniem niż w modelu wielokrotnego dziedziczenia, takim jak c ++, ale zasady są zasadniczo takie same. Komplikacja pojawia się, gdy tożsamość klasy jest niejednoznaczna, na przykład przekazanie wskaźnika klasy Banana do funkcji, która pobiera wskaźnik klasy Fruit.
Funkcje wirtualne są, po pierwsze, kwestią czasu wykonywania. Jest częścią polimorfizmu, ponieważ służy do decydowania, która funkcja ma być uruchomiona w momencie wywołania w uruchomionym programie.
Słowo kluczowe virtual jest dyrektywą kompilatora, która wiąże funkcje w określonej kolejności, jeśli istnieje niejasność dotycząca tożsamości klasy. Funkcje wirtualne są zawsze w klasach nadrzędnych (o ile wiem) i wskazują kompilatorowi, że powiązanie funkcji składowych z ich nazwami powinno odbywać się najpierw z funkcją podklasy, a następnie z funkcją klasy nadrzędnej.
Klasa Fruit może mieć wirtualną funkcję color (), która domyślnie zwraca „NONE”. Funkcja Color () klasy Banana zwraca „ŻÓŁTY” lub „BRĄZOWY”.
Ale jeśli funkcja pobierająca wskaźnik Fruit wywoła color () w wysłanej do niej klasie Banana - która funkcja color () zostanie wywołana? Funkcja normalnie wywołałaby Fruit :: color () dla obiektu Fruit.
W 99% przypadków nie byłoby to zamierzone. Ale jeśli Fruit :: color () zostałaby zadeklarowana jako wirtualna, wówczas Banana: color () zostałby wywołany dla obiektu, ponieważ prawidłowa funkcja color () byłaby powiązana ze wskaźnikiem Fruit w momencie wywołania. Środowisko uruchomieniowe sprawdzi, na który obiekt wskazuje wskaźnik, ponieważ został on oznaczony jako wirtualny w definicji klasy Fruit.
Różni się to od zastępowania funkcji w podklasie. W takim przypadku wskaźnik Fruit wywoła Fruit :: color (), jeśli jedyne, co wie, to to, że jest wskaźnikiem IS-A do Fruit.
Więc teraz pojawia się idea „czystej funkcji wirtualnej”. Jest to raczej niefortunne zdanie, ponieważ czystość nie ma z tym nic wspólnego. Oznacza to, że jest zamierzone, aby metoda klasy bazowej nigdy nie była wywoływana. Rzeczywiście, nie można wywołać czystej funkcji wirtualnej. Jednak nadal należy go zdefiniować. Podpis funkcji musi istnieć. Wielu programistów tworzy pustą implementację {} dla kompletności, ale kompilator wygeneruje ją wewnętrznie, jeśli nie. W takim przypadku, gdy funkcja jest wywoływana, nawet jeśli wskaźnik wskazuje na Fruit, zostanie wywołana Banana :: color (), ponieważ jest to jedyna dostępna implementacja color ().
Teraz ostatni element układanki: konstruktorzy i destruktory.
Czyste wirtualne konstruktory są całkowicie nielegalne. To właśnie się skończyło.
Ale czyste wirtualne destruktory działają w przypadku, gdy chcesz zabronić tworzenia instancji klasy bazowej. Tylko podklasy mogą być tworzone, jeśli destruktor klasy bazowej jest czysto wirtualny. Konwencja polega na przypisaniu go do 0.
virtual ~Fruit() = 0; // pure virtual
Fruit::~Fruit(){} // destructor implementation
W takim przypadku musisz utworzyć implementację. Kompilator wie, że to właśnie robisz i upewnia się, że robisz to dobrze, lub bardzo narzeka, że nie może połączyć się ze wszystkimi funkcjami, których potrzebuje do skompilowania. Błędy mogą być mylące, jeśli nie jesteś na dobrej drodze, jeśli chodzi o modelowanie hierarchii klas.
Więc w tym przypadku nie możesz tworzyć instancji Fruit, ale możesz tworzyć instancje Banana.
Wywołanie usunięcia wskaźnika Fruit, który wskazuje na instancję klasy Banana, najpierw wywoła Banana :: ~ Banana (), a następnie zawsze Fuit :: ~ Fruit (). Ponieważ bez względu na wszystko, kiedy wywołujesz destruktor podklasy, musi być zgodny z destruktorem klasy bazowej.
Czy to zły model? Jest to bardziej skomplikowane na etapie projektowania, tak, ale może zapewnić prawidłowe łączenie w czasie wykonywania oraz wykonywanie funkcji podklasy w przypadku niejasności co do tego, do której podklasy uzyskuje się dostęp.
Jeśli piszesz w C ++ tak, że przekazujesz tylko dokładne wskaźniki do klas bez wskaźników ogólnych ani niejednoznacznych, wtedy funkcje wirtualne nie są tak naprawdę potrzebne. Ale jeśli potrzebujesz elastyczności typów w czasie wykonywania (jak w Apple Banana Orange ==> Fruit), funkcje stają się łatwiejsze i bardziej wszechstronne z mniej redundantnym kodem. Nie musisz już pisać funkcji dla każdego rodzaju owocu i wiesz, że każdy owoc zareaguje na color () swoją własną, poprawną funkcją.
Mam nadzieję, że to rozwlekłe wyjaśnienie raczej utrwali koncepcję, a nie wprowadzi w błąd. Istnieje wiele dobrych przykładów, na które można spojrzeć, przyjrzeć się im wystarczająco dużo, uruchomić je i zadzierać z nimi, a otrzymasz to.