Metode virtuale, proprietăți și indexatori. Funcții virtuale, funcții virtuale pure

O altă modificare a clasei de bază duce la consecințe neașteptate. Această modificare constă în modificarea specificatorului funcției membru al clasei de bază. Noi (pentru prima dată!) folosim specificatorul virtual într-o declarație de funcție. Funcțiile declarate cu specificatorul virtual sunt numite funcții virtuale. Introducerea funcțiilor virtuale în declarația clasei de bază (doar un singur specificator) are implicații atât de semnificative pentru metodologia de programare orientată pe obiecte încât vom prezenta din nou o declarație modificată a clasei A:

Clasa A ( public: virtual int Fun1(int); );

Un specificator suplimentar în declarația funcției și nu mai multe modificări (deocamdată) în declarațiile claselor derivate. Ca întotdeauna, o funcție main() foarte simplă. În el definim un pointer către un obiect din clasa de bază, îl setăm la un obiect de tip derivat, după care apelăm funcția Fun1() folosind pointerul:

Void main () ( A *pObj; A MyA; AB MyAB; pObj = pObj->Fun1(1); AC MyAC; pObj = pObj->Fun1(1); )

Dacă nu este pentru specificatorul virtual, rezultatul executării expresiei de apel

PObj->Fun1(1);

ar fi evident: după cum știți, alegerea funcției este determinată de tipul indicatorului.

Cu toate acestea, specificatorul virtual schimbă totul. Alegerea funcției este acum determinată de tipul de obiect la care este setat indicatorul clasei de bază. Dacă o clasă derivată declară o funcție nestatică al cărei nume, tip returnat și lista de parametri sunt aceleași cu cele ale unei funcții virtuale din clasa de bază, atunci expresia de apel rezultată invocă o funcție membru a clasei derivate.

Trebuie remarcat imediat că abilitatea de a apela o funcție membru a unei clase derivate folosind un pointer către clasa de bază nu înseamnă că este posibil să se observe un obiect „de sus în jos” de la un pointer la un obiect al bazei. clasă. Funcțiile și datele non-virtuale ale membrilor nu sunt încă disponibile. Și acest lucru poate fi verificat foarte ușor. Pentru a face acest lucru, încercați să faceți ceea ce am făcut deja o dată - apelați o funcție membru a unei clase derivate care este necunoscută în clasa de bază:

//pObj->Fun2(2); //pObj->AC::Fun1(2);

Rezultatul este negativ. Pointerul, ca și înainte, este configurat numai pentru fragmentul de bază al obiectului de clasă derivată. Totuși, este posibilă apelarea funcțiilor unei clase derivate. Pe vremuri, în secțiunile dedicate descrierii constructorilor, am trecut în revistă lista acțiunilor de reglementare care sunt efectuate de un constructor în timpul conversiei unui fragment de memorie alocat într-un obiect de clasă. Printre aceste activități a fost menționată inițializarea tabelelor de funcții virtuale.

Puteți încerca să detectați prezența acestor tabele de funcții virtuale folosind dimensiunea operației. Desigur, toate acestea sunt specifice implementării, dar cel puțin în Borland C++, un obiect reprezentativ al unei clase care conține declarații de funcții virtuale ocupă mai multă memorie decât un obiect dintr-o clasă similară în care aceleași funcții sunt declarate fără specificatorul virtual.

Cout<< "Размеры объекта: " << sizeof(MyAC) << "…" << endl;

Deci, obiectul clasei derivate dobândește un element suplimentar - un pointer către tabelul de funcții virtuale. Diagrama unui astfel de obiect poate fi reprezentată astfel (notăm pointerul către tabel cu identificatorul vptr, tabelul funcțiilor virtuale cu identificatorul vtbl):

MyAC::= vptr A AC vtbl::= &AC::Fun1

În noua noastră diagramă de obiecte, nu este o coincidență faptul că indicatorul către tabel (o matrice de un element) de funcții virtuale este separat de fragmentul obiectului care reprezintă clasa de bază doar printr-o linie punctată. Este în câmpul vizual al acestui fragment al obiectului. Datorită disponibilității acestui pointer, funcția virtuală apelează operatorul Fun1

PObj->Fun1(1);

poate fi reprezentat astfel:

(*(pObj->vptr)) (pObj,1);

Aici, doar la prima vedere, totul este confuz și de neînțeles. De fapt, nu există o singură expresie în acest operator care să ne fie necunoscută.

Literal spune asta:

Apelați FUNCȚIA SITUATĂ LA INDEXUL 0 AL TABELULUI DE FUNCȚII VIRTUALE vtbl (avem un singur element în acest tabel), A CĂRĂ ADRĂ DE START POATE FI GĂSIT DE INDEX vptr.

LA RÂNDUL SĂU, ACEST POINTER ESTE ACCESIBIL PRIN POINTERUL pObj, CONFIGURAT LA OBIECTUL MYAC. FUNCȚIA ÎNTRECĂ DOI (!) PARAMETRI, PRIMUL ESTE ADRESA OBIECTULUI MyAC (valoarea acestui pointer!), AL DOILEA ESTE O VALOARE INTEGRALĂ EGALĂ CU 1.

Un apel către o funcție de membru al clasei de bază este furnizat de un nume calificat.

PObj->A::Fun1(1);

În această declarație, renunțăm la serviciile virtuale de tabel de funcții. În același timp, informăm traducătorul despre intenția noastră de a apela o funcție membru a clasei de bază. Mecanismul de susținere a funcțiilor virtuale este strict și foarte strict reglementat. Un pointer către tabelul de funcții virtuale este inclus în mod necesar în fragmentul de bază „sus” al obiectului de clasă derivată. Tabelul pointer include adresele funcțiilor membre ale fragmentului de cel mai jos nivel care conține declarații ale acestei funcții.

Modificăm încă o dată declarația claselor A, AB și declarăm o nouă clasă ABC.

Modificarea claselor A și AB se rezumă la declararea de noi funcții membre în ele:

Clasa A ( public: virtual int Fun1(int key); virtual int Fun2(int key); ); ::::: int A::Fun2(int key) ( cout<< " Fun2(" << key << ") from A " << endl; return 0; } class AB: public A { public: int Fun1(int key); int Fun2(int key); }; ::::: int AB::Fun2(int key) { cout << " Fun2(" << key << ") from AB " << endl; return 0; } Класс ABC является производным от класса AB: class ABC: public AB { public: int Fun1(int key); }; int ABC::Fun1(int key) { cout << " Fun1(" << key << ") from ABC " << endl; return 0; }

Această clasă include o declarație a funcției membru Fun1, care este declarată în clasa de bază indirectă A ca funcție virtuală. În plus, această clasă moștenește din baza imediată funcția membru Fun2. Această funcție este, de asemenea, declarată în clasa de bază A ca virtuală. Declarăm un obiect reprezentativ al clasei ABC:

ABC MyABC;

Diagrama acestuia poate fi reprezentată după cum urmează:

MyABC::= vptr A AB ABC vtbl::= &AB::Fun2 &ABC::Fun1

Tabelul de funcții virtuale conține acum două elemente. Setăm indicatorul obiectului clasei de bază la obiectul MyABC, apoi apelăm funcțiile membre:

PObj = pObj->Fun1(1); pObj->Fun2(2);

În acest caz, este imposibil să apelați funcția membru AB::Fun1(), deoarece adresa sa nu este conținută în lista de funcții virtuale și pur și simplu nu este vizibilă de la nivelul superior al obiectului MyABC la care pObj indicatorul este setat. Tabelul de funcții virtuale este construit de constructor în momentul în care obiectul obiectului corespunzător este creat. Desigur, traducătorul se asigură că constructorul este codificat corespunzător. Dar traducătorul nu este capabil să determine conținutul tabelului de funcții virtuale pentru un anumit obiect. Aceasta este o sarcină de rulare. Până când tabelul de funcții virtuale este construit pentru un anumit obiect, funcția membru corespunzătoare a clasei derivate nu poate fi apelată. Acest lucru este ușor de verificat după o altă modificare a declarației de clasă.

Programul este mic, așa că are sens să-i oferi textul în întregime. Nu trebuie să vă lăsați păcăliți de operația de acces la componentele clasei::. Discuția despre problemele asociate cu această operațiune este încă de urmat.

#include clasa A ( public: virtual int Fun1(int key); ); int A::Fun1 (tasta int) ( cout<< " Fun1(" << key << ") from A." << endl; return 0; } class AB: public A { public: AB() {Fun1(125);}; int Fun2(int key); }; int AB::Fun2(int key) { Fun1(key * 5); cout << " Fun2(" << key << ") from AB." << endl; return 0; } class ABC: public AB { public: int Fun1(int key); }; int ABC::Fun1(int key) { cout << " Fun1(" << key << ") from ABC." << endl; return 0; } void main () { ABC MyABC; // Вызывается A::Fun1(). MyABC.Fun1(1); // Вызывается ABC::Fun1(). MyABC.Fun2(1); // Вызываются AB::Fun2() и ABC::Fun1(). MyABC.A::Fun1(1); // Вызывается A::Fun1(). A *pObj = &MyABC; // Определяем и настраиваем указатель. cout << "==========" << endl; pObj->Fun1(2); // Apelați ABC::Fun1(). //pObj->Fun2(2); // Această funcție nu este accesibilă printr-un pointer!!! pObj->A::Fun1(2); // Denumit A::Fun1(). )

Acum, în momentul creării obiectului MyABC

ABC MyABC;

din constructorul clasei AB (și este numit înaintea constructorului clasei ABC), se va apela funcția A::Fun1(). Această funcție este membră a clasei A. Obiectul MyABC nu a fost încă complet format, tabelul de funcții virtuale nu a fost încă completat și nu se știe încă nimic despre existența funcției ABC::Fun1(). După ce obiectul MyABC este în sfârșit format, tabelul de funcții virtuale este completat, iar pointerul pObj este setat la obiectul MyABC, apelarea funcției A::Fun1() prin pointerul pObj va fi posibilă numai folosind numele complet calificat al acestui funcţie:

PObj->Fun1(1); // Acesta este un apel de funcție ABC::Fun1()! pObj->A::Fun1(1); // Evident, acesta este un apel de funcție A::Fun1()!

Rețineți că apelarea funcției membru Fun1 direct din obiectul MyABC produce un rezultat similar:

MyABC.Fun1(1); // Apelați funcția ABC::Fun1().

Și o încercare de a apela funcția non-virtuală AB::Fun2() printr-un pointer către un obiect din clasa de bază eșuează. Nu există nicio adresă pentru această funcție în tabelul de funcții virtuale și este imposibil să „priviți în jos” de la nivelul superior al obiectului.

//pObj->Fun2(2); // Nu poți face așa!

Rezultatul executării acestui program demonstrează clar specificul utilizării funcțiilor virtuale. Doar câteva rânduri...

Fun1(125) din A. Fun1(1) din ABC. Fun1(5) de la ABC. Fun2(1) din AB. Fun1(1) din A. ========== Fun1(2) din ABC. Fun1(2) de la A.

Același indicator în timpul execuției programului poate fi ajustat la obiecte reprezentative ale diferitelor clase derivate. Ca rezultat, literalmente aceeași expresie de apel al funcției membre îndeplinește funcții complet diferite. Pentru prima dată ne confruntăm cu așa-numita LEGARE ÎNTÂRZIată.

Rețineți că specificația virtuală se aplică numai funcțiilor. Nu există membri virtuali de date. Aceasta înseamnă că nu există nicio modalitate de a accesa membrii de date ai unui obiect de clasă derivată printr-un pointer către un obiect de clasă de bază care este setat la un obiect de clasă derivată.

Pe de altă parte, este evident că, dacă puteți apela o funcție de înlocuire, atunci direct „prin” această funcție aveți acces la toate funcțiile și membrii de date ai clasei derivate și apoi „de jos în sus” la toate funcțiile non-private și membri de date ai claselor de bază directe și indirecte. În acest caz, toate datele și funcțiile non-private ale claselor de bază devin disponibile din funcție.

Și încă un mic exemplu care demonstrează schimbarea comportamentului unui obiect reprezentativ al unei clase derivate după ce una dintre funcțiile clasei de bază devine virtuală.

#include clasa A ( public: void funA () (xFun();); /*virtual*/void xFun () (cout<<"this is void A::xFun();"<< endl;}; }; class B: public A { public: void xFun () {cout <<"this is void B::xFun ();"<

La început, specificatorul virtual din definiția funcției A::xFun() este comentat. Procesul de executare a unui program constă în definirea unui obiect reprezentativ objB al unei clase derivate B și apelarea funcției membru funA() pe acest obiect. Această funcție este moștenită din clasa de bază, este una și este evident că identificarea ei nu pune probleme traducătorului. Această funcție aparține clasei de bază, ceea ce înseamnă că în momentul în care este apelată, controlul este transferat „la nivelul superior” al obiectului objB. La același nivel există una dintre funcțiile numite xFun(), iar această funcție este transferată controlul în timpul execuției expresiei de apel în corpul funcției funA(). Mai mult decât atât, este pur și simplu imposibil să apelezi o altă funcție cu același nume din funcția funA(). În momentul analizării structurii clasei A, traducătorul nu are nicio idee despre structura clasei B. Funcția xFun() , un membru al clasei B, se dovedește a fi inaccesabilă din funcția funA().

Dar dacă decomentați specificatorul virtual din definiția funcției A::xFun(), se va stabili o relație de substituție între două funcții cu același nume, iar generarea obiectului objB va fi însoțită de crearea unui tabel. de funcții virtuale, conform cărora va fi apelată funcția de înlocuire, membru al clasei B. Acum, pentru a o apela pe cea înlocuită, o funcție trebuie să folosească numele ei calificat:

Void A::funA () ( xFun(); A::xFun(); )

Sergey Malyshev (alias Mikhalych)

Partea 1. Teoria generală a funcţiilor virtuale

Privind titlul acestui articol, s-ar putea să vă gândiți: "Hmm! Cine nu știe ce sunt funcțiile virtuale! Aceasta este..." Dacă da, puteți opri în siguranță să citiți chiar aici.

Și pentru cei care abia încep să înțeleagă complexitățile C++, dar au deja, să zicem, cunoștințe de bază despre un astfel de lucru precum moștenirea și au auzit ceva despre polimorfism, are sens direct să citească acest material. Dacă înțelegeți funcțiile virtuale, veți avea cheia pentru a debloca secretele unui design de succes orientat pe obiecte.

În general, materialul nu este foarte dificil. Și tot ce va fi discutat aici poate fi găsit fără îndoială în cărți. Singura problemă este că probabil nu veți găsi o prezentare completă a întregii probleme într-una sau două cărți. Pentru a scrie despre funcțiile virtuale, a trebuit să „studiez” 6 publicații diferite. Și nici în acest caz, nu mă prefac deloc a fi complet. În lista de referințe, le indic doar pe cele principale, cele care m-au inspirat în stilul de prezentare și conținut.

Am decis să împart tot materialul în 3 părți.
Să încercăm să înțelegem teoria generală a funcțiilor virtuale din prima parte. În a doua parte ne vom uita la aplicarea lor (și la puterea și puterea lor!) folosind un exemplu mai mult sau mai puțin din viața reală. Ei bine, în a treia parte vom vorbi despre un astfel de lucru ca destructori virtuali.

Deci ce este?

Să ne amintim mai întâi cum, în programarea clasică C, puteți trece un obiect de date unei funcții. Nu este nimic complicat în acest sens, trebuie doar să setați tipul obiectului care este transmis în momentul în care scrieți codul funcției. Adică, pentru a descrie comportamentul obiectelor, este necesar să cunoaștem și să descriem în prealabil tipul acestora. Puterea OOP în acest caz este că puteți scrie funcții virtuale, astfel încât obiectul însuși determină ce funcție trebuie să o apeleze în timp ce programul rulează.

Cu alte cuvinte, cu ajutorul funcțiilor virtuale, obiectul însuși își determină comportamentul (propriile acțiuni). Tehnica utilizării funcțiilor virtuale se numește polimorfism. Literal, polimorfismul înseamnă a avea mai multe forme. Un obiect din programul dvs. poate reprezenta de fapt nu doar o clasă, ci multe clase diferite dacă sunt legate prin moștenire de o clasă de bază comună. Ei bine, comportamentul obiectelor acestor clase din ierarhie va fi, desigur, diferit.

Ei bine, acum la obiect!

După cum știți, conform regulilor C++, un pointer către o clasă de bază se poate referi la un obiect din această clasă, precum și la un obiect din orice altă clasă derivată din cea de bază. Înțelegerea acestei reguli este foarte importantă. Să ne uităm la o ierarhie simplă a anumitor clase A, B și C. A va fi clasa noastră de bază, B va fi derivat (generat) din clasa A și C va fi derivat din B. Vezi figură pentru explicații.

Într-un program, obiectele acestor clase pot fi declarate, de exemplu, în acest fel.

Un obiect_A; //declararea unui obiect de tip A
B obiect_B; //declararea unui obiect de tip B
C obiect_C; //declararea unui obiect de tip C

Conform acestei reguli, un pointer de tip A se poate referi la oricare dintre aceste trei obiecte. Adică, acesta va fi adevărat:


point_to_Object=&object_C; //atribuiți adresa obiectului C pointerului

Dar asta nu mai este corect:

În *point_to_Object; // declară un pointer către o clasă derivată
point_to_Object=&object_A; //nu puteți atribui un pointer la adresa obiectului de bază

Chiar dacă pointerul point_to_Object este de tip A* și nu C* (sau B*), se poate referi la obiecte de tip C (sau B). Poate că regula va fi mai clară dacă te gândești la obiectul C ca la un tip special de obiect A. Ei bine, de exemplu, un pinguin este un fel special de pasăre, dar este totuși o pasăre, deși nu zboară. Desigur, această relație dintre obiecte și pointeri funcționează doar într-o singură direcție. Un obiect de tip C este un tip special de obiect A, dar obiectul A nu este un tip special de obiect C. Revenind la pinguini, putem spune cu siguranță că dacă toate păsările ar fi un tip special de pinguin, pur și simplu nu ar putea a zbura!

Acest principiu devine deosebit de important atunci când funcțiile virtuale sunt definite în clase legate de moștenire. Funcțiile virtuale au exact același aspect și sunt programate în același mod ca majoritatea funcțiilor obișnuite. Doar anunțul lor este făcut cu un cuvânt cheie virtual. De exemplu, clasa noastră de bază A poate declara o funcție virtuală v_function().

clasa a
{
public:
virtual void v_function(void);//funcția descrie un comportament al clasei A
};

O funcție virtuală poate fi declarată cu parametri și poate returna o valoare, ca orice altă funcție. O clasă poate declara câte funcții virtuale aveți nevoie. Și pot fi în orice parte a clasei - închise, deschise sau protejate.

Dacă în clasa B, derivată din clasa A, trebuie să descrii un alt comportament, atunci poți declara o funcție virtuală, numită din nou v_function().

clasa B: public A
{
public:
virtual void v_function(void);//funcția de înlocuire descrie ceva
//comportament nou al clasei B
};

Când o clasă precum B definește o funcție virtuală care are același nume ca o funcție virtuală a clasei sale strămoși, funcția se numește o funcție de suprascriere. Funcția virtuală v_function() din B înlocuiește funcția virtuală cu același nume din clasa A. De fapt, totul este ceva mai complicat și nu se reduce la o simplă coincidență de nume. Dar mai multe despre asta puțin mai târziu, în secțiunea „Câteva subtilități de aplicare”.
Ei bine, acum cel mai important lucru!

Să revenim la pointerul point_to_Object de tip A*, care se referă la obiect object_B de tip B*. Să aruncăm o privire mai atentă la instrucțiunea care apelează funcția virtuală v_function() pe obiectul indicat de punct_la_obiect.

Un *point_to_Object; // declară un pointer către clasa de bază
point_to_Object=&object_B; //atribuiți adresa obiectului B pointerului
point_to_Object->;v_function(); //apelați funcția

Pointerul point_to_Object poate stoca adresa unui obiect de tip A sau B. Aceasta înseamnă că în timpul execuției acest operator point_to_Object-gt;v_function(); apelează o funcție virtuală a clasei al cărei obiect se referă în prezent. Dacă point_to_Object se referă la un obiect de tip A, este apelată o funcție aparținând clasei A. Dacă point_to_Object se referă la un obiect de tip B, este apelată o funcție aparținând clasei B. Deci, aceeași instrucțiune apelează o funcție din clasa lui obiectul care se adresează. Aceasta este acțiunea determinată în timpul execuției programului.

Deci, ce ne oferă asta?

Este timpul să ne uităm – ce ne oferă funcțiile virtuale? Am aruncat o privire generală asupra teoriei funcțiilor virtuale. Este timpul să luați în considerare o situație din viața reală în care puteți înțelege semnificația practică a subiectului în cauză în lumea reală a programării.

Un exemplu clasic (din experiența mea - în 90% din toată literatura despre C++) care este dat în acest scop este scrierea unui program grafic. Se construiește o ierarhie de clase, ceva de genul „punct -gt; linie -gt; figură plată -gt; cifră volumetrică”. Și considerăm o funcție virtuală, să zicem, Draw(), care atrage toate acestea... Plictisitoare!

Să ne uităm la un exemplu mai puțin academic, dar totuși grafic. (Clasic! Unde pot scăpa de el?). Să încercăm să luăm în considerare un principiu ipotetic care poate fi încorporat într-un joc pe calculator. Și nu doar un joc, ci baza oricărui shooter (indiferent 3D sau 2D, cool sau așa așa). Shooters, pentru a spune simplu. Nu sunt însetat de sânge în viață, dar, păcătos, îmi place să trag uneori!

Deci, am decis să facem un shooter cool. De ce ai nevoie mai întâi? Desigur arme! (Ei bine, poate nu este prima dată. Nu contează.) În funcție de subiectul despre care vom scrie, vor fi necesare astfel de arme. Poate va fi un set de la un simplu club la o arbaletă. Poate de la o archebuză la un lansator de grenade. Sau poate chiar de la un blaster la un dezintegrator. Vom vedea în curând că tocmai acesta este ceea ce nu este important.

Ei bine, deoarece există atât de multe posibilități, trebuie să creăm o clasă de bază.

clasă Armă
{
public:
... //vor exista membri de date care pot fi descriși, de exemplu, ca
//grosimea bâtului și numărul de grenade din lansatorul de grenade
//această parte nu este importantă pentru noi

virtual void Utilizare1(void);//de obicei - butonul stâng al mouse-ului
virtual void Use2(void);//de obicei - butonul dreapta al mouse-ului

... //vor fi mai mulți membri de date și metode
};

Fără a intra în detalii despre această clasă, putem spune că cele mai importante, poate, vor fi funcțiile Use1() și Use2(), care descriu comportamentul (sau utilizarea) acestei arme. Această clasă poate genera orice tip de armă. Vor fi adăugați noi membri de date (cum ar fi numărul de runde, cadența de foc, nivelul de energie, lungimea lamei etc.) și noi funcții. Și prin redefinirea funcțiilor Use1() și Use2(), vom descrie diferența de utilizare a armelor (pentru un cuțit aceasta poate fi lovirea și aruncarea, pentru o mitralieră aceasta poate fi împușcare simplă și explozie).

Colecția de arme trebuie depozitată undeva. Aparent, cel mai simplu mod de a face acest lucru este de a organiza o serie de indicatori de tip Weapon*. Pentru simplitate, să presupunem că acesta este o matrice globală Arms, cu 10 tipuri de arme și, pentru început, toți pointerii sunt inițializați la zero.

Armă *Arme; //matrice de pointeri către obiecte de tip Weapon

Prin crearea de obiecte dinamice - tipuri de arme - la începutul programului, vom adăuga pointeri la ele în matrice.

Pentru a indica ce armă este utilizată, vom crea o variabilă de index matrice, a cărei valoare se va modifica în funcție de tipul de armă selectat.

int TypeOfWeapon;

Ca rezultat al acestor eforturi, codul care descrie utilizarea armelor în joc ar putea arăta astfel:

if(LeftMouseClick) Arms-gt;Use1();
else Arms->Use2();

Toate! Am creat un cod care descrie împușcare-împușcare-război chiar înainte de a decide ce tipuri de arme vor fi folosite. În plus. Nu avem încă un singur tip real de armă! Un avantaj suplimentar (uneori foarte important) este că acest cod poate fi compilat separat și stocat într-o bibliotecă. Mai târziu, tu (sau un alt programator) poți obține noi clase din Weapon, le poți stoca în matricea Arms și le poți folosi. Acest lucru nu necesită recompilare a codului dvs.

În special, rețineți că acest cod nu vă cere să specificați tipurile de date exacte ale obiectelor la care se face referire de către pointerii Arms, doar că acestea sunt derivate din Weapon. Obiectele determină în timpul execuției ce funcție Use() ar trebui să apeleze.

Câteva subtilități de aplicare

Să petrecem puțin timp problemei înlocuirii funcțiilor virtuale.

Să ne întoarcem la început - la clasele plictisitoare A, B și C. Clasa C se află în prezent în partea de jos a ierarhiei, la sfârșitul liniei de moștenire. În clasa C, puteți defini o funcție virtuală de înlocuire exact în același mod. Mai mult, nu este deloc necesar să folosiți cuvântul cheie virtual, deoarece aceasta este clasa finală din linia de moștenire. Funcția va funcționa deja și va fi selectată ca virtuală. Dar! Dar dacă doriți să eliminați o anumită clasă D din clasa C și chiar să schimbați comportamentul funcției v_function(), atunci nu va rezulta nimic. Pentru a face acest lucru, în clasa C, funcția v_function() trebuie declarată ca virtuală. De aici și regula, care poate fi formulată astfel: „o dată virtual, întotdeauna virtual!” Adică, este mai bine să nu aruncați cuvântul cheie virtual - ce se întâmplă dacă vă este la îndemână?

Încă o subtilitate. O clasă derivată nu poate defini o funcție cu același nume și același set de parametri, dar cu un tip de returnare diferit față de funcția virtuală din clasa de bază. În acest caz, compilatorul va blestema în etapa de compilare a programului.

Mai departe. Dacă introduceți o funcție într-o clasă derivată cu același nume și tip returnat ca o funcție virtuală a clasei de bază, dar cu un set diferit de parametri, atunci această funcție a clasei derivate nu va mai fi virtuală. Chiar dacă îl etichetați cu cuvântul cheie virtual, nu va fi ceea ce vă așteptați. În acest caz, folosind un pointer către clasa de bază, orice valoare a acestui pointer va apela funcția clasei de bază. Amintiți-vă regula despre supraîncărcarea funcției! Sunt doar funcții diferite. Veți ajunge cu o funcție virtuală complet diferită. În general, astfel de erori sunt foarte evazive, deoarece ambele forme de notație sunt destul de acceptabile și nu există nicio speranță pentru diagnosticarea compilatorului în acest caz.

Prin urmare, încă o regulă. La înlocuirea funcțiilor virtuale, este necesară o potrivire completă a tipurilor de parametri, a numelor de funcții și a tipurilor de valori returnate în clasele de bază și derivate.

Și mai departe. O funcție virtuală poate fi doar o funcție de clasă componente non-statică. O funcție globală nu poate fi virtuală. O funcție virtuală poate fi declarată prietenă într-o altă clasă. Dar despre funcțiile prietenoase vom vorbi într-un alt articol.

Asta e tot pentru data asta.

În partea următoare veți vedea un exemplu complet funcțional de program simplu care demonstrează toate punctele despre care am vorbit.

Dacă aveți întrebări, scrieți, vom rezolva.

Funcții virtuale- un tip special de funcție de membru al clasei. O funcție virtuală diferă de o funcție obișnuită prin aceea că, pentru o funcție obișnuită, legarea unui apel de funcție la definiția sa se face în etapa de compilare. Pentru funcțiile virtuale, acest lucru are loc în timpul execuției programului.

Pentru a declara o funcție virtuală, utilizați cuvântul cheie virtual. O funcție de membru al clasei poate fi declarată virtuală dacă

  • o clasă care conține o funcție virtuală, bazată în ierarhia generației;
  • implementarea funcției este specifică clasei și va fi diferită în fiecare clasă derivată.

Este o funcție care este definită în clasa de bază și orice clasă derivată o poate suprascrie. O funcție virtuală este apelată numai printr-un pointer sau referință la clasa de bază.

Determinarea carei instanțe a unei funcții virtuale este apelată de o expresie de apel de funcție depinde de clasa obiectului adresat de pointer sau referință și este determinată în momentul execuției programului. Acest mecanism se numește legare dinamică (târzie). sau tip rezoluție în timpul execuției.

Un pointer de clasă de bază poate indica fie un obiect de clasă de bază, fie un obiect de clasă derivată. Alegerea funcției membru depinde de obiectul către care clasă indică pointerul în timpul execuției programului, dar nu de tipul pointerului. Dacă nu există niciun membru al clasei derivate, este utilizată funcția virtuală implicită a clasei de bază.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

#include
folosind namespace std;
clasa X
{
protejat:
int i;
public:
void seti(int c) ( i = c; )
virtual void print() (cout<< endl << "clasa X: " << i; }
};
clasa Y: public X // mostenire
{
public:
void print() (cout<< endl << "clasa Y: " << i; } // suprascriind funcția de bază
};
int main()
{
X x;
X *px = // Pointer către clasa de bază
Y y;
x.seti(10);
y.seti(15);
px->print(); // clasa X: 10
px =
px->print(); // clasa Y: 15
cin.get();
returnează 0;
}

Rezultatul executiei

În fiecare caz, este executată o versiune diferită a funcției print(). Selecția este dinamică în funcție de obiectul la care se referă indicatorul.

Dacă eliminați cuvântul cheie virtual din rândul 9 (vezi codul de mai sus), atunci rezultatul execuției va fi diferit, deoarece Legarea funcției va avea loc în etapa de compilare:

În terminologia OOP, „un obiect trimite un mesaj tipărit și își selectează propria versiune a metodei corespunzătoare”. Numai o funcție membru nestatică a unei clase poate fi virtuală. Pentru clasa derivată, funcția devine automat virtuală, astfel încât cuvântul cheie virtual poate fi omis.

Exemplu: selectarea funcției virtuale

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37

#include
folosind namespace std;
figura de clasă
{
protejat:
dublu x, y;
public:
figura (dublu a = 0, dublu b = 0) ( x = a; y = b; )
virtual double area() ( return (0); ) // implicit
};
dreptunghi de clasă: persoană publică
{
public:
dreptunghi(dublu a = 0, dublu b = 0) : figura(a, b) ();
zonă dublă() ( return (x*y); )
};
cerc de clasă: persoană publică
{
public:
cerc(dublu a = 0): figura(a, 0) ();
zonă dublă() ( return (3.1415*x*x); )
};
int main()
{
figura *f;
dreptunghi rect(3, 4);
cerc cir(2);
dublu total = 0;
f =
f =
total = f->area();
cout<< total << endl;
total += f->area();
cout<< total << endl;
cin.get();
returnează 0;
}

Rezultatul executiei


Funcție virtuală pură

Clasa de bază a unei ierarhii de tip conține de obicei un număr de funcții virtuale care oferă tastare dinamică. Adesea, în clasa de bază în sine, funcțiile virtuale în sine sunt false și au un corp gol. Li se dă un anumit sens numai în clasele generate. Astfel de funcții sunt numite funcții virtuale pure.

Funcție virtuală pură este o metodă de clasă al cărei corp este nedefinit.

În clasa de bază, o astfel de funcție este scrisă după cum urmează.

Funcții virtuale pure

Mecanismul funcției virtuale este utilizat în cazurile în care este necesară plasarea unei funcții în clasa de bază care trebuie executată diferit în clasele derivate. Mai precis, nu numai singura funcție din clasa de bază trebuie executată diferit, dar fiecare clasă de producție necesită propria versiune a acestei funcții.

Înainte de a explica capacitățile funcțiilor virtuale, rețineți că clasele care includ astfel de funcții joacă un rol special în programarea orientată pe obiecte. De aceea au un nume special - polimorf. .

Nu orice funcție poate fi virtuală, ci numai funcțiile componente non-statice ale unei clase. Odată ce o funcție este definită ca virtuală, redefinirea acesteia într-o clasă derivată (cu același prototip) creează o nouă funcție virtuală în acea clasă, fără a fi nevoie să folosiți specificatorul virtual.

O clasă derivată nu poate defini o funcție cu același nume și același set de parametri, dar cu un tip de returnare diferit față de funcția virtuală din clasa de bază. Aceasta are ca rezultat o eroare la momentul compilării.

Dacă introduceți o funcție într-o clasă derivată cu același nume și tip returnat ca o funcție virtuală în clasa de bază, dar cu un set diferit de parametri, atunci această funcție de clasă derivată nu va fi virtuală. În acest caz, folosind un pointer către clasa de bază, pentru orice valoare a acestui pointer, se efectuează un apel la funcția clasei de bază (în ciuda specificatorului virtual și a prezenței unei funcții similare în clasa derivată).

Metode (funcții)

Metodele virtuale sunt declarate în clasa de bază cu cuvântul cheie virtual și pot fi suprascrise în clasa derivată. Prototipurile metodelor virtuale atât în ​​clasele de bază, cât și în cele derivate trebuie să fie aceleași.

Utilizarea metodelor virtuale vă permite să implementați un mecanism de legare tardivă, în care definirea metodei apelate are loc în timpul execuției, și nu în etapa de compilare. În acest caz, metoda virtuală numită depinde de tipul de obiect pentru care este apelată. Cu legarea timpurie, utilizată pentru metode non-virtuale, determinarea metodei de apelat are loc în timpul compilării.

În etapa de compilare, se construiește un tabel de metode virtuale, iar adresa specifică este introdusă în etapa de execuție.

Când apelați o metodă folosind un pointer de clasă, se aplică următoarele reguli:

  • pentru o metoda virtuala se apeleaza metoda corespunzatoare tipului de obiect indicat de pointer.
  • pentru o metodă non-virtuală se numește metoda corespunzătoare tipului de indicator în sine.

Următorul exemplu ilustrează apelarea metodelor virtuale:

Clasa A // Declarație de clasă de bază( public: virtual void VirtMetod1(); // Metoda virtuală void Metod2(); // Metodă non-virtuală); void A::VirtMetod() ( cout<< "Вызван A::VirtMetod1\n";} void A::Metod2() { cout << "Вызван A::Metod2\n"; } class B: public A // Объявление производного класса{public: void VirtMetod1(); // Виртуальный метод void Metod2(); // Не виртуальный метод};void B::VirtMetod1() { cout << "B::VirtMetod1\n";}void B::Metod2() { cout << "B::Metod2\n"; }void main() { B aB; // Объект класса B B *pB = &aB; // Указатель на объект класса B A *pA = &aB; // Указатель на объект класса A pA->VirtMetod1(); // Apelarea metodei VirtMetod din clasa B pB->VirtMetod1(); // Apelați metoda VirtMetod din clasa B pA->Metod2(); // Apelarea metodei Method2 din clasa A pB->Metod2(); // Apelați metoda Method2 din clasa B)

Rezultatul acestui program va fi următoarele linii:

Numit B::VirtMetod1Called B::VirtMetod1Called A::Metod2Called B::Metod2

O funcție virtuală pură este o funcție virtuală specificată cu un inițializator

De exemplu:

Vidul virtual F1(int) =0;

O declarație de clasă poate conține un destructor virtual care este folosit pentru a șterge un obiect de un anumit tip. Cu toate acestea, nu există un constructor virtual în C++. O alternativă care vă permite să creați obiecte de un anumit tip sunt metodele virtuale, în care un constructor este chemat pentru a crea un obiect dintr-o anumită clasă.

Polimorfismul runtime se realizează prin utilizarea claselor derivate și a funcțiilor virtuale. O funcție virtuală este o funcție declarată cu cuvântul cheie virtual într-o clasă de bază și suprascrisă în una sau mai multe clase derivate. Funcțiile virtuale sunt funcții speciale deoarece atunci când apelați un obiect dintr-o clasă derivată folosind un pointer sau o referință la acesta, C++ determină în timpul execuției ce funcție să apeleze în funcție de tipul obiectului. Pentru obiecte diferite sunt apelate versiuni diferite ale aceleiași funcții virtuale. O clasă care conține una sau mai multe funcții virtuale se numește clasă polimorfă.

O funcție virtuală este declarată în clasa de bază folosind cuvântul cheie virtual. Când este suprascris într-o clasă derivată, nu este nevoie să repetați cuvântul cheie virtual, deși nu va apărea nicio eroare dacă este utilizat din nou.

Ca prim exemplu de funcție virtuală, luați în considerare următorul program scurt:

// un mic exemplu de utilizare a funcțiilor virtuale
#include
clasa de baza(
public:

cout<< *Base\n";
}
};

public:
void who() ( // definiția cine() în raport cu first_d
cout<< "First derivation\n";
}
};
clasa detasata: baza publica (
public:

cout<< "Second derivation\n*";
}
};
int main()
{
Baza baza_obj;
Baza *p;
first_d first_obj;
second_d second_obj;
p = &base_obj;
p->
p = &first_obj;
p->
p = &second_ob;
p-> cine(); // acces la cine din clasa second_d
returnează 0;
}

Programul va produce următorul rezultat:

Baza
Prima derivație
A doua derivație

Să analizăm acest program în detaliu pentru a înțelege cum funcționează.

După cum puteți vedea, în obiectul de bază funcția who() este declarată virtuală. Aceasta înseamnă că această funcție poate fi suprascrisă în clasele derivate. În fiecare dintre clasele first_d și second_d, funcția who() este suprascrisă. Funcția main() definește trei variabile. Primul este un obiect base_obj de tip Base. După aceasta, este declarat un pointer p către clasa Base, apoi obiectele first_obj și second_obj, care aparțin a două clase derivate. Apoi, pointerului p i se atribuie adresa obiectului base_obj și este apelată funcția who(). Deoarece această funcție este declarată ca virtuală, C++ determină în timpul execuției ce versiune a funcției who() să folosească, în funcție de obiectul către care indică p. În acest caz, este un obiect de tip Base, deci versiunea funcției who() declarată în clasa Base este executată. Pointerului p i se atribuie apoi adresa obiectului first_obj. (După cum știți, un pointer către o clasă de bază poate fi folosit pentru orice clasă derivată.) După ce who() a fost apelat, C++ examinează din nou tipul obiectului indicat de p pentru a determina versiunea who( ), care trebuie chemat. Deoarece p indică un obiect de tip first_d, este utilizată versiunea corespunzătoare a funcției who(). La fel, când lui p i se atribuie adresa secund_obj, se folosește versiunea funcției who() declarată în second_d.

Cea mai comună modalitate de a apela o funcție virtuală este utilizarea unui parametru de funcție. De exemplu, luați în considerare următoarea modificare a programului anterior:

/* Aici referința clasei de bază este folosită pentru a accesa funcția virtuală */
#include
clasa de baza(
public:
virtual void who() ( // definiția unei funcții virtuale
cout<< "Base\n";
}
};
clasa first_d: public Base (
public:
void who () ( // definiția who() în raport cu first_d
cout<< "First derivation\n";
}
};

public:
void who() ( // definiția cine() în raport cu second_d
cout<< "Second derivation\n*";
}
};
// folosește o referință la clasa de bază ca parametru
void show_who (Baza &r) (
r.cine();
}
int main()
{
Baza baza_obj;
first_d first_obj;
second_d second_obj;
show_who (base_ob j) ; // acces la cine din clasa Base
arată_cine(primul_obj); // acces la cine din clasa first_d
arată_cine(al doilea_obj); // acces la cine din clasa second_d
returnează 0;
}

Acest program afișează aceleași date ca și versiunea anterioară. În acest exemplu, funcția show_who() are un parametru de tip referință la clasa de bază. În funcția main(), funcția virtuală este apelată folosind obiecte de tip Base, first_d și second_d. Versiunea funcției who() numită în funcția show_who() este determinată de tipul obiectului la care se referă parametrul atunci când funcția este apelată.

Cheia pentru utilizarea unei funcții virtuale pentru a oferi polimorfism în timpul rulării este că este utilizat un pointer către clasa de bază. Polimorfismul de rulare este realizat numai atunci când se apelează o funcție virtuală folosind un pointer sau o referință la o clasă de bază. Cu toate acestea, nimic nu vă împiedică să apelați funcții virtuale ca orice alte funcții „normale”, dar nu este posibil să obțineți polimorfismul de rulare în acest fel.

La prima vedere, suprascrierea unei funcții virtuale într-o clasă derivată arată ca o formă specială de supraîncărcare a funcției. Dar acest lucru nu este adevărat, iar termenul de supraîncărcare a funcției nu se aplică suprascrierii funcțiilor virtuale, deoarece există diferențe semnificative între cele două. În primul rând, funcția trebuie să se potrivească cu prototipul. După cum știți, atunci când supraîncărcați o funcție obișnuită, numărul și tipul parametrilor trebuie să fie diferit. Cu toate acestea, atunci când suprascrieți o funcție virtuală, interfața funcției trebuie să se potrivească exact cu prototipul. Dacă nu există o astfel de corespondență, atunci o astfel de funcție este pur și simplu considerată supraîncărcată și își pierde proprietățile virtuale. În plus, dacă numai tipul de returnare este diferit, este emis un mesaj de eroare. (Funcțiile care diferă doar prin tipul lor de returnare introduc ambiguitate.) O altă limitare este aceea că o funcție virtuală trebuie să fie un membru, nu un prieten, al clasei pentru care este definită. Cu toate acestea, o funcție virtuală poate fi un prieten al unei alte clase. Deși un destructor poate fi virtual, un constructor nu poate fi virtual.

Datorită diferențelor dintre supraîncărcarea funcțiilor obișnuite și suprascrierea funcțiilor virtuale, vom folosi termenul de suprasolicitare pentru acestea din urmă.

Dacă o funcție a fost declarată ca virtuală, atunci rămâne așa, indiferent de numărul de niveluri din ierarhia de clase prin care a trecut. De exemplu, dacă clasa secunda_d este derivată din clasa first_d și nu din clasa de bază, atunci funcția who() va rămâne virtuală și va fi apelată versiunea corectă a acesteia, așa cum se arată în exemplul următor:

// generat din first_d, nu din Base
clasa secunda_d: public first_d (
public:
void who() ( // definiția cine() în raport cu second_d
cout<< "Second derivation\n*";
}
};

Dacă o funcție virtuală nu este suprascrisă într-o clasă derivată, atunci este utilizată versiunea sa din clasa de bază. De exemplu, să rulăm următoarea versiune a programului anterior:

#include
clasa de baza(
public:
gol virtual cine() (
cout<< "Base\n";
}
};
clasa first_d: public Base (
public:
nul cine() (
cout<< "First derivation\n";
}
};
clasa secunda_d: bază publică (
// cine() nu este definit
};
int main()
{
Baza baza_obj;
Baza *p;
first_d first_obj; ,
second_d second_obj;
p = &base_obj;
p-> cine(); // acces la cine din clasa Base
p = &primul obj;
p-> cine(); // acces la cine din clasa first_d
p = &sepond_ob;
p-> cine(); /* acces la baza who() deoarece second_d nu suprascrie */
returnează 0;
}

Acest program va produce următoarele rezultate:

Baza
Prima derivație
Baza

Trebuie avut în vedere faptul că caracteristicile moștenirii sunt ierarhice. Pentru a ilustra acest lucru, să presupunem că în exemplul anterior, clasa second_d este derivată din clasa first_d în loc de clasa Base. Când who() este apelat folosind un pointer către un obiect de tip second_d (în care who() nu a fost definit), versiunea who() declarată în first_d va fi apelată, deoarece acea clasă este cea mai apropiată clasă de second_d. În general, atunci când o clasă nu suprascrie o funcție virtuală, C++ folosește prima definiție pe care o găsește, mergând de la descendenți la strămoși.