C++/Funkcje wirtualne: Różnice pomiędzy wersjami

Usunięta treść Dodana treść
Piotr (dyskusja | edycje)
Derbeth (dyskusja | edycje)
usunięcie strasznych ogólników, konkrety, dodanie części "konsekwencje", kolorowanie składni w pierwszym przykładzie
Linia 1:
{{
==Wstęp==
Funkcje wirtualne to specjalne funkcje składowe, które przydają się szczególnie, gdy używamy obiektów posługując się wskaźnikami lub referencjami do nich. Dla zwykłych funkcji to, czy zostanie wywołana funkcja z klasy podstawowej, czy pochodnej, zależy od typu wskaźnika, a nie tego, na co faktycznie on wskazuje. Dysponując funkcjami wirtualnymi będziemy mogli użyć prawdziwego '''polimorfizmu''' - używać klasy pochodnej wszędzie tam, gdzie spodziewana jest klasa podstawowa. W ten sposób będziemy mogli
Funkcje wirtualne są jedną z tych rzeczy, dzięki którym C++ jest tak ceniony i powszechnie używany.
 
==Opis==
Na początek rozpatrzymy przykład, który pokarze, dlaczego zwykłe, niewirtualne funkcje składowe nie zdają egzaminu gdy posługujemy się wskaźnikiem, który może wskazywać i na obiekt klasy podstawowej i na obiekt dowolnej z jej klas pochodnych.
 
Mając klasę bazową wyprowadzamy od niej klasę pochodną:
<pre '''class''' Baza="lang-cpp">
class Baza
{
{
'''public''':
'''void''' pisz()
void {pisz()
cout<<"Tu funkcja pisz z klasy baza";
}
};
'''class''' Baza2 : public Baza
{
cout<<"Tu funkcja pisz z klasy baza";
'''public''':
{ }
'''void''' pisz()
};
{
'''class''' Baza2 : public Baza
cout<<"Tu funkcja pisz z klasy Baza2";
{
}
};public:
'''void''' pisz()
}; {
cout<<"Tu funkcja pisz z klasy Baza2";
}
};
</pre>
Jeżeli teraz w funkcji main stworzymy wskaźnik do obiektu typu Baza, to możemy ten wskaźnik ustawiać na dowolne obiekty tego typu.
Można też ustawić go na obiekt typu pochodnego, czyli Baza2:
Linia 33 ⟶ 39:
}
Po skompilowaniu na ekranie zobaczymy dwa wypisy: "Tu funkcja pisz z klasy Baza".
Stało się tak dlatego, że wskaźnik jest do typu Baza. Gdy ustawiliśmy wskaźnik na objekt typu pochodnego (wolno nam), a nastepnie wywołaliśmy funkcję składową, to kompilator "na ślepo" sięgnął po funkcję pisz z klasy bazowej (bo wskaźnik wskazuje na klasę bazową).
 
Można jednak określić żeby kompilator nie sięgał po funkcję z klasy bazowej, ale sam się zorientował na co wskaźnik pokazuje. Do tego służy przydomek ''virtual'', a funkcja składowa nim oznaczona nazywa się wirtualną.
Różnica polega tylko na dodaniu słowa kluczowego ''virtual'', co wygląda tak:
Linia 52 ⟶ 59:
}
};
 
== Konsekwencje ==
 
Gdy funkcja jest oznaczona jako wirtualna, kompilator nie przypisuje na stałe wywołania funkcji z tej klasy, na którą pokazuje wskaźnik, już podczas kompilacji. Pozostawia decyzję co do wyboru właściwej wersji funkcji aż do momentu wykonania programu - jest to tzw. ''późne wiązanie''. Wtedy program skorzysta z krótkiej informacji zapisanej w obiekcie a określającej klasę, do jakiej należy dany obiekt. Dopiero po odczytaniu informacji o klasie danego obiektu wybierana jest właściwa metoda.
 
Jeśli klasa ma choć jedną funkcję wirtualną, to do każdego jej obiektu dopisywany jest identyfikator tej klasy a do wywołania funkcji dopisywany jest kod, który ten identyfikator czyta i odnajduje odpowiednią funkcję. Gdy klasa funkcji wirtualnych nie posiada, takie informacje nie są dodawane, bo nie są potrzebne.
 
Zauważmy też, że nie zawsze decyzja o wyborze funkcji jest dokonywana dopiero na etapie wykonania. Gdy do obiektów odnosimy się przez zmienną a nie przez wskaźnik lub referencję to kompilator już na etapie kompilacji wie, jaki jest typ (klasa) danej zmiennej (bo do zmiennej w przeciwieństwie do wskaźnika lub referencji nie można przypisać klasy pochodnej). Tak więc wirtualność nie gra roli gdy nie używamy wskaźników; kompilator generuje wtedy taki sam kod, jakby wszystkie funkcje były niewirtualne. Przy wskaźnikach musi orientować się czytając informację o klasie obiektu, na który wskazuje wskaźnik, bo moglibyśmy np. losować, czy do wskaźnika przypiszemy klasę bazową czy jej pochodną - wtedy przy każdym uruchomieniu programu byłaby wywoływana inna funkcja.
 
Jak widać, za wirtualność się płaci - zarówno drobnym narzutem pamięciowym na każdy obiekt (identyfikator klasy), jak i drobnym narzutem czasowym (odnajdywanie przy każdym wywołaniu odpowiedniej klasy i jej funkcji składowej). Jednak zyskujemy możliwośc płynnego rozwoju naszego programu przez zastępowanie klas ich podklasami, co bez witualności jest niewykonalne. Przy możliwościach obecnych komputerów koszt wirtualności jest zaniedbywalny, ale wciąż warto przemyśleć, czy potrzebujemy wirtualności dla wszystkich funkcji.