Funcții virtuale. Ce este? Partea 1
Partea 1. Teoria generală a funcțiilor virtuale
Privind la titlul acestui articol, vă puteți gândi: "Hmm, care nu știe ce funcții virtuale sunt!" Este același lucru: "Dacă este așa, puteți să renunți la lectură chiar aici.
Și pentru cei care abia încep să înțeleagă complexitatea C ++, dar deja are, de exemplu, cunoștințe inițiale despre un astfel de lucru ca moștenirea. și am auzit ceva despre polimorfism. are un sentiment direct de a citi acest material. Dacă înțelegeți funcțiile virtuale, veți obține cheia pentru a dezvălui secretele unui design cu succes orientat spre obiect.
Am decis să împart toate materialele în 3 părți.
Să încercăm, în prima parte, să înțelegem teoria generală a funcțiilor virtuale. Să ne uităm la a doua parte a cererii (și puterea și puterea lor!) În unele exemple de viață mai mult sau mai puțin. Ei bine, în a treia parte, să vorbim despre astfel de lucruri ca distrugătorii virtuali.
Deci, ce este?
Să începem prin a reaminti cum în programarea clasică C puteți trece un obiect de date unei funcții. Nu este nimic complicat în acest sens, este necesar doar să setați tipul obiectului transmis în momentul în care scrieți codul funcției. Asta este, pentru a descrie comportamentul obiectelor, este necesar să cunoaștem în prealabil și să descriem tipul lor. Puterea OOP în acest caz se manifestă prin faptul că puteți scrie funcții virtuale astfel încât obiectul însuși să determine ce funcție trebuie să numească în timpul executării programului.
Cu alte cuvinte, cu ajutorul funcțiilor virtuale, obiectul însuși determină comportamentul său (acțiunile proprii). Tehnica utilizării funcțiilor virtuale se numește polimorfism. În mod literar, polimorfismul înseamnă deținerea mai multor forme. Un obiect din programul dvs. nu poate reprezenta o clasă, ci mai multe clase diferite, dacă sunt legate de mecanismul de moștenire cu o clasă de bază comună. Ei bine, comportamentul obiectelor din aceste clase în ierarhie, desigur, va fi diferit.
Ei bine, acum până la punct!
După cum se știe, în conformitate cu regulile C ++, un pointer la o clasă de bază poate face referință unui obiect din această clasă, precum și un obiect al oricărei alte clase derivate din clasa de bază. Înțelegerea acestei reguli este foarte importantă. Să ne uităm la o ierarhie simplă a anumitor clase A, B și C. Și vom avea o clasă de bază, B derivă din clasa A și C derivă din B. Pentru clarificare, a se vedea figura.
În program, obiecte din aceste clase pot fi declarate, de exemplu, în acest fel.
Conform acestei reguli, un pointer de tip A poate referi la oricare dintre aceste trei obiecte. Asta este, va fi adevărat:
Dar nu este corect:
Deși point_to_Object este de tip A *, nu C * (sau B *), el poate referi obiecte de tip C (sau B). Poate regula va fi mai ușor de înțeles dacă te gândești la obiectul C ca pe un obiect special A. De exemplu, un pinguin este un fel de pasăre special, dar rămâne o pasăre, deși nu zboară. Desigur, această relație între obiecte și pointeri funcționează numai î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 se poate spune cu siguranță că dacă toate păsările ar fi un tip special de pinguini - pur și simplu nu ar fi putut zbura!
clasa A
publice:
virtual void v_function (void); // funcția descrie un comportament din clasa A
>;
O funcție virtuală poate fi declarată cu parametrii, aceasta poate returna o valoare, ca orice altă funcție. O clasă poate declara cât mai multe funcții virtuale de care aveți nevoie. Și pot fi în orice parte a clasei - închise, deschise sau protejate.
Dacă în clasa B, generată din clasa A, trebuie să descrieți un alt comportament, atunci puteți declara din nou o funcție virtuală, numită v_function ().
clasa B: publică
publice:
virtual void v_function (void); // funcția de înlocuire descrie a
// comportament nou al clasei B
>;
Atunci când o funcție virtuală este definită într-o clasă ca B, având același nume ca și funcția virtuală a clasei strămoșilor, o astfel de funcție se numește o funcție de înlocuire. Funcția virtuală v_function () din B înlocuiește funcția virtuală cu același nume în clasa A. De fapt, totul este oarecum mai complicat și nu ajunge la un simplu nume de nume. Dar mai multe despre acest lucru mai târziu, în secțiunea "Unele 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 obiectul object_V de tip B *. Să aruncăm o privire mai atentă la declarația care apelează funcția virtuală v_function () pentru obiectul indicat de point_to_Object.
Ei bine, ce ne dă asta?
E timpul să vedem - și ce ne dau funcțiile virtuale? Ne-am uitat la teoria funcțiilor virtuale în termeni generali. Este timpul să luăm în considerare o anumită situație reală, unde se poate înțelege semnificația practică a subiectului în lumea reală a programării.
Un exemplu clasic (din experiența mea - în 90% din toată literatura despre C ++), care conduc la acest scop - scrierea unui program grafic. Se construiește o ierarhie a clasei, cum ar fi "dot -gt; line -gt; figura plată -gt; forma tridimensională". Și o funcție virtuală este luată în considerare, să zicem, Draw (), care atrage totul. Este plictisitor!
Să ne uităm la un exemplu mai puțin academic, dar totuși un exemplu grafic. (Clasic, unde să scapi de ea?). Să încercăm să gândim ipotetic un principiu care poate fi încorporat într-un joc pe calculator. Și nu doar un joc, ci baza oricărui shooter (indiferent de 3D sau 2D, cool sau așa). Fotografiere, cu alte cuvinte. Eu nu sunt însetat de sânge în viață, dar, un păcătos, îmi place uneori să trag!
Deci, am decis să facem un shooter răcoros. Ce este necesar în primul rând? Desigur, arme! (Ei bine, nu în primul.) Nu contează. În funcție de tema pe care o compunem, vor fi necesare astfel de arme. Poate că va fi un set de la un simplu club la un arbaletă. Poate de la arquebus la lansatorul de grenade. Sau poate de la un blaster la un dezintegrator. Curând vom vedea că acest lucru nu este important.
Ei bine, deoarece există atât de multe posibilități, este necesar să avem o clasă de bază.
clasa de armament
publice:
. // vor exista membri de date, care pot fi descriși, de exemplu, ca
// grosimea unui club și numărul de grenade dintr-un lansator de grenade
// această parte nu este importantă pentru noi
virtual void Use1 (void); // de obicei - butonul din stânga al mouse-ului
virtual void Use2 (void); // de obicei - butonul drept al mouse-ului
// vor exista alte date și metode ale membrilor
>;
Fără a intra în detaliile acestei clase, putem spune că cele mai importante, probabil, sunt funcțiile Use1 () și Use2 (), care descriu comportamentul (sau utilizarea) acestei arme. Din această clasă, puteți genera orice fel de arme. Vor fi adăugați noi membri de date (cum ar fi numărul de cartușe, rata focului, nivelul de energie, lungimea lamei etc.) și noile funcții. Și înlăturând funcțiile Use1 () și Use2 (), vom descrie diferența de utilizare a armelor (pentru un cuțit acesta poate fi o lovitură și aruncare, pentru mașină - împușcare unică și exploziile).
Colecția de arme trebuie păstrată undeva. Aparent, cel mai simplu mod de a organiza o serie de pointeri, cum ar fi Weapon *. Pentru simplitate, să presupunem că aceasta este o gamă globală de arme, pentru 10 arme și toate indicii sunt inițializați la zero pentru început.
Arme * Arme [10]; // array of pointers la obiecte de tip Weapon
Crearea la începutul programului a obiectelor dinamice dinamice - tipuri de arme, le vom adăuga indicii în matrice.
Pentru a specifica ce armă este utilizată, vom seta indexul variabil al matricei, valoarea căreia va fi modificată în funcție de tipul de armă ales.
Ca rezultat al acestor eforturi, codul care descrie utilizarea armelor în joc poate să arate, de exemplu, astfel:
dacă (LeftMouseClick) arme [TypeOfWeapon] -gt; Use1 ();
altceva Arms [TypeOfWeapon] -> Use2 ();
Asta e tot! Am creat un cod care descrie războiul de tragere-război chiar înainte de a decide ce tipuri de arme vor fi folosite. Mai mult decât atât. Nici măcar nu avem nici un tip real de arme! Un avantaj suplimentar (uneori foarte important) - acest cod poate fi compilat separat și stocat în bibliotecă. În viitor, tu (sau un alt programator) poți scoate noi clase din armă, salvează-le în arme # 91;] și folosește. Nu aveți nevoie să recompilați codul.
De notat în mod special că acest cod nu necesită să specificați cu precizie tipurile de obiecte de date la care se referă Arms # 91;], numai că acestea sunt derivate din Weapon. Obiectele sunt definite la timpul de execuție. ce funcție Utilizați () ar trebui să fie numiți.
Unele subtilități ale aplicației
Să ne petrecem ceva timp în problema înlocuirii funcțiilor virtuale.
Să ne întoarcem la început - la clasele plictisitoare A, B și C. Clasa C se află acum în partea de jos a ierarhiei, la sfârșitul liniei de moștenire. În clasa C, puteți defini exact o funcție virtuală de înlocuire. Și pentru a utiliza cuvântul cheie virtual nu este necesar, deoarece este clasa finală în linia de moștenire. Funcție și așa va funcționa și va fi selectat ca virtual. Dar! Dar dacă doriți să deduceți o clasă D din clasa C și chiar să schimbați comportamentul funcției v_function (), atunci nu va veni nimic din ea. Pentru aceasta, în clasa C, funcția v_function () trebuie declarată ca virtuală. De aici regula, care poate fi formulată după cum urmează: "odată virtual - întotdeauna virtuală!". Aceasta este, cuvântul virtual este mai bine să nu se arunce - brusc vine la îndemână?
O altă subtilitate. Într-o clasă derivată, nu puteți defini o funcție cu același nume și cu același set de parametri, dar cu un alt tip de valoare de retur decât funcția virtuală a clasei de bază. În acest caz, compilatorul bate la etapa de compilare a programului.
Mai departe. Dacă introduceți o funcție în clasa derivată cu același nume și cu același tip de returnare ca și funcția virtuală a clasei de bază, dar cu un set diferit de parametri, atunci această funcție de clasă derivată nu va mai fi virtuală. Chiar dacă îl însoțiți cu cuvântul cheie virtual, nu va fi ceea ce vă așteptați. În acest caz, folosind un indicator pentru clasa de bază, pentru orice valoare a acestui pointer, se va efectua un apel la funcția de clasă de bază. Amintiți-vă de regula despre funcțiile de supraîncărcare! Acestea sunt doar funcții diferite. Veți obține o funcție virtuală complet diferită. În general, astfel de erori sunt foarte subtile, deoarece ambele forme de scriere sunt complet acceptabile și speranța pentru diagnosticarea compilatorului în acest caz nu este necesară.
Prin urmare, încă o regulă. Atunci când se înlocuiesc funcțiile virtuale, este necesară o potrivire completă a tipurilor de parametri, a numelor funcțiilor și a tipurilor valorilor returnate în clasele de bază și derivate.
Și mai mult. O funcție virtuală poate fi doar o funcție componentă non-statică a clasei. Virtualul nu poate fi o funcție globală. O funcție virtuală poate fi declarată prietenoasă (
) într-o altă clasă. Dar vom vorbi despre funcții prietenoase, indiferent cât de diferit este articolul.
Asta, de fapt, tot timpul.
În partea următoare, veți vedea un exemplu complet funcțional al celui mai simplu program care demonstrează toate momentele despre care am vorbit.
La scrierea acestui material au fost folosite următoarele cărți:
Dacă aveți întrebări - scrieți, vom înțelege.
Serghei Malyshev (alias Mihalych).