C miks on vaja virtuaalseid meetodeid. virtuaalne funktsioon. Puhas virtuaalne funktsioon

Sergei Malõšev (teise nimega Mihhalych)

Osa 1. Virtuaalsete funktsioonide üldteooria

Selle artikli pealkirja vaadates võite mõelda: "Hmm! Kes ei tea, mis on virtuaalsed funktsioonid! See on..." Kui jah, siis lõpetage lugemine kohe.

Ja neil, kes alles hakkavad mõistma C ++ keerukust, kuid neil on juba näiteks esialgsed teadmised sellisest asjast nagu pärilikkus ja on kuulnud midagi polümorfismist, on mõttekas seda materjali lugeda. Kui mõistate virtuaalseid funktsioone, on teil võti eduka objektorienteeritud disaini saladuste avamiseks.

Üldiselt ei ole materjal väga raske. Ja kõike, millest siin juttu tuleb, leiab muidugi raamatutest. Ainus probleem on selles, et tõenäoliselt ei leia ühest või kahest raamatust kogu probleemi täielikku kirjeldust. Virtuaalsetest funktsioonidest kirjutamiseks pidin "läbi lugema" 6 erinevat väljaannet. Ja isegi sel juhul ei pretendeeri ma täielikkusele. Viidete loendis toon välja ainult peamised, need, mis ajendasid mind esituslaadi ja sisu juurde.

Otsustasin kogu materjali jagada 3 osaks.
Proovime esimeses osas aru saada üldine teooria virtuaalsed funktsioonid. Vaatame nende rakenduse teist osa (ja nende jõudu ja jõudu!) mõnel enam-vähem elulisel näitel. Noh, kolmandas osas räägime sellisest asjast nagu virtuaalsed hävitajad.

Mis see siis on?

Alustuseks tuletame meelde, kuidas klassikalises C-programmeerimises saate andmeobjekti funktsioonile edastada. Selles pole midagi keerulist, peate lihtsalt funktsiooni koodi kirjutamise ajal määrama läbitud objekti tüübi. See tähendab, et objektide käitumise kirjeldamiseks on vaja eelnevalt teada ja kirjeldada nende tüüpi. OOP-i võimsus seisneb sel juhul selles, et saate kirjutada virtuaalseid funktsioone, nii et objekt määrab ise, millist funktsiooni tal on vaja käivitamise ajal kutsuda.

Ehk siis virtuaalsete funktsioonide abil määrab objekt ise oma käitumise (oma tegevused). Virtuaalsete funktsioonide kasutamise tehnikat nimetatakse polümorfismiks. Sõna otseses mõttes tähendab polümorfism paljude vormide olemasolu. Teie programmi objekt võib tegelikult esindada mitte ainult ühte klassi, vaid paljusid erinevaid klasse, kui need on päritud ühisest baasklassist. Muidugi on nende klasside objektide käitumine hierarhias erinev.

Noh, nüüd asja juurde!

Nagu teate, võib C ++ reeglite kohaselt baasklassi osuti viidata nii selle klassi objektile kui ka mis tahes muu baasklassist tuletatud objektile. Selle reegli mõistmine on väga oluline. Vaatleme teatud klasside A, B ja C lihtsat hierarhiat. A on meie baasklass, B – tuletatakse (genereeritakse) klassist A ja C – tuletatakse klassist B. Vt selgitusi jooniselt.

Programmis saab nende klasside objekte deklareerida näiteks nii.

objekt_A; //A tüüpi objekti deklaratsioon
B objekt_B; //B-tüüpi objekti deklaratsioon
C objekt_C; //C tüüpi objekti deklaratsioon

Vastavalt see reegel A-tüüpi osuti võib viidata ükskõik millisele neist kolmest objektist. Nii et see oleks õige:


point_to_Object=&object_C; //seadke kursor objekti C aadressile

Kuid see pole enam õige:

At *punkt_objektile; // deklareerige kursor tuletatud klassile
point_to_Object=&object_A; //ei saa baasobjekti aadressile kursorit määrata

Kuigi point_to_Object on tüüpi A* ja mitte C* (või B*), võib see viidata C (või B) tüüpi objektidele. Võib-olla on reegel selgem, kui mõelda objektile C kui eriliigile objektile A. Näiteks pingviin on eriline lind, kuid ta on siiski lind, kuigi ta ei lenda. Muidugi toimib selline objektide ja osutite suhe ainult ühes suunas. C-tüüpi objekt on A-objekti eriliik, kuid objekt A ei ole eritüüpi objekt C. Pingviinide juurde tagasi tulles võib julgelt öelda, et kui kõik linnud oleksid eriliiki pingviinid, siis nad lihtsalt ei saaks lennata!

See põhimõte muutub eriti oluliseks, kui virtuaalsed funktsioonid on defineeritud pärimisklassides. Virtuaalsed funktsioonid on täpselt samasuguse kujuga ja programmeeritud samamoodi nagu enamik tavalised funktsioonid. Ainult nende teadaanne tehakse koos märksõna virtuaalne. Näiteks võib meie baasklass A deklareerida virtuaalse funktsiooni v_function().

klass A
{
avalik:
virtual void v_function(void);//funktsioon kirjeldab mõnda klassi A käitumist
};

Virtuaalset funktsiooni saab deklareerida parameetritega, see võib tagastada väärtuse nagu iga teinegi funktsioon. Klass saab deklareerida nii palju virtuaalseid funktsioone, kui vaja. Ja need võivad asuda mis tahes klassi osas - suletud, avatud või kaitstud.

Kui klassist A tuletatud klassis B peate kirjeldama mõnda muud käitumist, saate deklareerida virtuaalse funktsiooni, mille nimi on jälle v_function().

B-klass: avalik A
{
avalik:
virtual void v_function(void);//asendusfunktsioon kirjeldab mõnda
//klassi B uus käitumine
};

Kui klass, näiteks B, määratleb virtuaalse funktsiooni, millel on sama nimi kui esivanemaklassi virtuaalsel funktsioonil, nimetatakse seda funktsiooni asendusfunktsiooniks. Virtuaalne funktsioon v_function() B-s asendab samanimelist virtuaalset funktsiooni klassis A. Tegelikult on kõik mõnevõrra keerulisem ega taandu lihtsale nimede kokkulangemisele. Kuid sellest lähemalt hiljem, jaotises "Mõned rakenduse peensused".
Noh, nüüd kõige tähtsam!

Läheme tagasi A* tüüpi objektile point_to_Object, mis viitab B* tüüpi objektile_B. Vaatame lähemalt lauset, mis kutsub välja virtuaalse funktsiooni v_function() objektil, millele osutab punkt_objektile.

*punkt_objektile; // deklareerib kursori baasklassile
point_to_Object=&object_B; //seadke kursor objekti B aadressile
punkt_objektile->;v_funktsioon(); // funktsiooni kutsumine

Point_to_Object osuti võib salvestada A- või B-tüüpi objekti aadressi. Seega käitusajal see lause point_to_Object-gt;v_function(); kutsub välja selle klassi virtuaalse funktsiooni, mille objektil see asub Sel hetkel viitab. Kui punkt_objektile viitab A-tüüpi objektile, kutsutakse välja klassi A kuuluv funktsioon. Kui punkt_objektile viitab B-tüüpi objektile, kutsutakse välja klassi B funktsioon. Seega kutsub sama operaator välja klassi B funktsiooni. adresseeritud objekt. See on toiming, mis määratakse käitusajal.

Noh, mida see meile annab?

On aeg vaadata – mida virtuaalfunktsioonid meile annavad? Virtuaalsete funktsioonide teooriast aastal üldiselt vaatasime. On aeg mõelda mõnele tegelikule olukorrale, millest saate aru praktiline väärtus kõnealune teema reaalses programmeerimismaailmas.

Klassikaline näide (minu kogemuse kohaselt - 90% kogu C++ kirjandusest), mis selleks on toodud, on kirjutamine graafika programm. Ehitatakse klassihierarhia, umbes nagu "punkt -gt; joon -gt; lame kujund -gt; kolmemõõtmeline kujund". Ja kaaluge virtuaalset funktsiooni, näiteks Draw(), mis joonistab kõik selle ... Igav!

Vaatame vähem akadeemilist, aga siiski graafiline näide. (Klassika! Kust sellest eemale saada?). Proovime hüpoteetiliselt kaaluda põhimõtet, mille saab sisse seada arvutimäng. Ja mitte ainult mängus, vaid iga (ükskõik, kas 3D või 2D, lahe või nii-nii) laskuri põhjal. Laskurid, teisisõnu. Ma ei ole elus verejanuline, aga, patune, mulle meeldib vahel tulistada!

Niisiis, otsustasime teha laheda laskuri. Mida sa kõigepealt vajad? Muidugi relv! (No võib-olla mitte esiteks. Vahet pole.) Olenevalt sellest, mis teemal kirjutame, läheb sellist relva vaja. Võib-olla tuleb see komplekt lihtsast nuiast kuni ambni. Võib-olla arquebusist granaadiheitjani. Või ehk isegi lõhkajast lagundajaks. Peagi näeme, et see pole lihtsalt oluline.

Noh, kuna võimalusi on nii palju, peame looma baasklassi.

klassi relv
{
avalik:
... //tulevad liikmeandmed, mida saab kirjeldada näiteks kui
// nuia paksus ja granaatide arv granaadiheitjas
//see osa pole meie jaoks oluline

virtual void Use1(void);//tavaliselt - vasak nupp hiired
virtual void Use2(void);//tavaliselt - parem nupp hiired

... //on veel mõned andmeliikmed ja meetodid
};

Selle klassi üksikasjadesse laskumata on ehk kõige olulisemad funktsioonid Use1() ja Use2(), mis kirjeldavad selle relva käitumist (või kasutamist). Sellest klassist saate genereerida mis tahes tüüpi relvi. Lisatakse uute liikmete andmed (nagu laskemoona arv, tulekiirus, energiatase, tera pikkus jne) ja uusi funktsioone. Ja funktsioonide Use1() ja Use2() ümberdefineerimisega kirjeldame relvade kasutamise erinevust (noa puhul võib selleks olla löök ja viskamine, ründerelvi puhul üksik- ja sarilaskmine).

Relvade kogu tuleb kuskil hoida. Ilmselt on lihtsaim viis selleks korraldada Relv* tüüpi osutite massiiv. Lihtsuse huvides oletame, et see on nii globaalne massiiv Relvad, 10 relva jaoks, ja kõik osutid lähtestatakse alguses nulliks.

Relv *Relvad; //viite massiiv relva tüüpi objektidele

Programmi alguses dünaamiliste objektide-tüüpi relvade loomisel lisame neile massiivi viidad.

Kasutatava relva näitamiseks loome massiiviindeksi muutuja, mille väärtus muutub sõltuvalt valitud relvatüübist.

int TypeOfWeapon;

Nende jõupingutuste tulemusena võib kood, mis kirjeldab relvade kasutamist mängus, välja näha järgmine:

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

Kõik! Oleme loonud koodi, mis kirjeldab tulistamist, enne kui otsustame, milliseid relvatüüpe kasutada. Enamgi veel. Meil ei ole veel ühtegi tõelist relvaliiki! Täiendav (mõnikord väga oluline) eelis on see, et seda koodi saab eraldi koostada ja raamatukogus salvestada. Hiljem saate sina (või mõni muu programmeerija) tuletada Weaponist uusi klasse, salvestada need relvade massiivi ja kasutada. See ei nõua teie koodi uuesti kompileerimist.

Pange tähele, et see kood ei nõua relvade osutitega viidatud objektide täpsete andmetüüpide määramist, vaid ainult seda, et need pärinevad relvast. Objektid määravad käitamise ajal kindlaks, millist funktsiooni Use() nad peaksid kutsuma.

Mõned rakenduse peensused

Pöörame natuke aega virtuaalsete funktsioonide asendamise probleemile.

Lähme tagasi algusesse – igavate klasside A, B ja C juurde. C-klass asub hetkel hierarhia kõige lõpus, pärimisrea lõpus. C-klassis saate sama hästi määratleda asendus virtuaalse funktsiooni. Pealegi pole vaja kasutada virtuaalset märksõna, kuna see on pärimisrea viimane klass. Funktsioon ja nii töötavad ja valitakse virtuaalseks. Aga! Aga kui tunned, et tahad klassist C teatud klassi D tuletada ja isegi funktsiooni v_function () käitumist muuta, siis ei tule sellest midagi välja. Selleks tuleb klassis C funktsioon v_function () deklareerida virtuaalseks. Siit ka reegel, mille võib sõnastada järgmiselt: "üks kord virtuaalne – alati virtuaalne!". Ehk siis märksõna virtuaalne on paremära viska ära - äkki tuleb kasuks?

Veel üks peensus. Tuletatud klassis ei saa määratleda sama nime ja sama parameetrite komplektiga funktsiooni, kuid erineva tagastustüübiga kui põhiklassi virtuaalne funktsioon. Sel juhul kompilaator kirub programmi koostamise etapis.

Edasi. Kui sisestate tuletatud klassi funktsiooni, millel on sama nimi ja tagastustüüp kui põhiklassi virtuaalne funktsioon, kuid millel on erinev parameetrite komplekt, siis see tuletatud klassi funktsioon ei ole enam virtuaalne. Isegi kui lisate sellele virtuaalse märksõna, pole see see, mida ootasite. Sel juhul, kasutades kursorit põhiklassile, kutsub iga kursori väärtus baasklassi funktsiooni. Pidage meeles funktsiooni ülekoormuse reeglit! See on lihtne erinevaid funktsioone. Saate lõpuks täiesti erineva virtuaalse funktsiooni. Üldiselt on selliseid vigu väga raske tabada, kuna mõlemad tähistusvormid on täiesti vastuvõetavad ja kompilaatori diagnostikale pole sel juhul vaja loota.

Sellest ka teine ​​reegel. Virtuaalsete funktsioonide asendamisel on nõutav parameetritüüpide, funktsioonide nimede ja tagastustüüpide täielik vaste põhi- ja tuletatud klassides.

Ja edasi. Virtuaalne funktsioon saab olla ainult klassi mittestaatiline liigefunktsioon. Ei saa olla virtuaalne globaalne funktsioon. Virtuaalse funktsiooni saab kuulutada sõbraks teises klassis. Sõbrafunktsioonidest räägime aga millalgi teises artiklis.

See on tegelikult kogu see aeg.

Järgmises osas näete täismahus funktsionaalne näide kõige lihtsam programm, mis näitab kõiki punkte, millest me rääkisime.

Kui teil on küsimusi - kirjutage, saame aru.

Puhtad virtuaalsed funktsioonid

Virtuaalsete funktsioonide mehhanismi kasutatakse nendel juhtudel, kui on vaja baasklassi paigutada funktsioon, mida tuletatud klassides tuleb täita erinevalt. Täpsemalt, mitte ühtegi põhiklassi funktsiooni ei tohiks erinevalt täita, vaid iga tootmisklass nõuab selle funktsiooni oma versiooni.

Enne virtuaalsete funktsioonide võimaluste selgitamist märgime, et selliseid funktsioone sisaldavad klassid mängivad eriline roll objektorienteeritud programmeerimises. Sellepärast nad kannavad eriline nimi- polümorfne. .

Kõik funktsioonid ei saa olla virtuaalsed, vaid ainult mõne klassi mittestaatilised komponentfunktsioonid. Kui funktsioon on määratletud virtuaalsena, loob selle uuesti määratlemine tuletatud klassis (sama prototüübiga) sellesse klassi uue virtuaalse funktsiooni, ilma virtuaalset spetsifikaatorit kasutamata.

Tuletatud klassis ei saa määratleda sama nime ja sama parameetrite komplektiga funktsiooni, kuid erineva tagastustüübiga kui põhiklassi virtuaalne funktsioon. Selle tulemuseks on kompileerimisaja viga.

Kui sisestate tuletatud klassi funktsiooni, millel on sama nimi ja tagastustüüp kui põhiklassi virtuaalne funktsioon, kuid millel on erinev parameetrite komplekt, siis see tuletatud klassi funktsioon ei ole virtuaalne. Sel juhul kutsutakse baasklassi kursorit kasutades selle osuti mis tahes väärtusega välja põhiklassi funktsioon (hoolimata virtuaalsest spetsifikaatorist ja sarnase funktsiooni olemasolust tuletatud klassis).

Meetodid (funktsioonid)

Virtuaalsed meetodid on deklareeritud baasklass virtuaalse märksõnaga, kuid selle saab tuletatud klassis alistada. Virtuaalsete meetodite prototüübid nii baasklassis kui ka tuletatud klassis peavad olema samad.

Virtuaalsete meetodite kasutamine võimaldab teil mehhanismi rakendada hiline köitmine, milles kutsutava meetodi määratlus toimub käitamisajal, mitte kompileerimise ajal. Sel juhul sõltub kutsutav virtuaalne meetod objekti tüübist, mille jaoks seda kutsutakse. Mittevirtuaalsete meetodite puhul kasutatava varajase sidumise korral tehakse kindlaks, millist meetodit kutsuda, kompileerimise ajal.

Koostamisetapis koostatakse virtuaalsete meetodite tabel ja juba täitmisetapis kinnitatakse konkreetne aadress.

Meetodi kutsumisel, kasutades klassi kursorit, järgides reegleid:

  • virtuaalse meetodi puhul kutsutakse välja meetod, mis vastab osuti poolt osutatava objekti tüübile.
  • mittevirtuaalse meetodi puhul kutsutakse välja meetod, mis vastab osuti enda tüübile.

Järgmine näide illustreerib virtuaalsete meetodite kutsumist:

Klass A // Põhiklassi deklaratsioon( public: virtual void VirtMetod1(); // Virtuaalne meetod void Metod2(); // Pole virtuaalne meetod); 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->VirtMeetod1(); // Klassi B VirtMetod meetodi kutsumine pB->VirtMetod1(); // Klassi B VirtMetod meetodi kutsumine pA->Metod2(); // A-klassi meetod Metod2 pB->Metod2(); // Väljakutse meetod 2. meetodi klass B)

Selle programmi väljund on järgmised read:

B::VirtMetod1 kutsus B::VirtMetod1 kutsus A::Metod2 kutsus B::Metod2 kutsus

Puhas virtuaalne funktsioon on initsialiseerijaga määratud virtuaalne funktsioon

Näiteks:

Virtuaalne tühimik F1(int) =0;

Klassideklaratsioon võib sisaldada virtuaalset hävitajat, mida kasutatakse objekti kustutamiseks teatud tüüpi. Kuid C++ keeles pole virtuaalset konstruktorit. Mõni alternatiiv, mis võimaldab luua antud tüüpi objekte, võib olla virtuaalsed meetodid, milles tehakse konstruktori kutse, et luua antud klassi objekt.

Baasklassi järgmine modifikatsioon toob kaasa ootamatud tagajärjed. See muudatus seisneb põhiklassi liikme funktsiooni spetsifikaatori muutmises. Me (esimest korda!) kasutame funktsioonideklaratsioonis virtuaalset spetsifikaatorit. Virtuaalse spetsifikaatoriga deklareeritud funktsioone nimetatakse virtuaalfunktsioonideks. Virtuaalsete funktsioonide sisestamine baasklassi deklaratsiooni (ainult üks spetsifikaator) mõjutab objektorienteeritud programmeerimise metoodikat nii palju, et anname taas klassi A muudetud deklaratsiooni:

A klass ( avalik: virtuaalne int Fun1(int); );

Üks täiendav täpsustaja funktsiooni deklaratsioonis ja tuletatud klassideklaratsioonides pole (veel) muudatusi. Nagu alati, väga lihtne. põhifunktsioon(). Selles määratleme kursori põhiklassi objektile, määrame selle tuletatud tüüpi objektiks, mille järel kutsume kursori abil funktsiooni Fun1 ():

Peamine tühine () ( A *pObj; A MyA; AB MyAB; pObj = pObj->Fun1(1); AC MyAC; pObj = pObj->Fun1 (1); )

Kui mitte virtuaalse määraja jaoks, siis kõneavaldise täitmise tulemus

PObj->Lõbus1(1);

oleks ilmne: nagu teate, määrab funktsiooni valiku kursori tüüp.

Virtuaalne spetsifikaator muudab aga kõike. Funktsiooni valiku määrab nüüd objekti tüüp, millele baasklassi osuti on seatud. Kui tuletatud klass deklareerib mittestaatilise funktsiooni, mille nimi, tagastustüüp ja parameetrite loend ühtivad põhiklassi virtuaalfunktsiooni omadega, siis kutsutakse kutsuavaldise tulemusel välja tuletatud klassi liigefunktsioon.

Kohe tuleb märkida, et võimalus kutsuda tuletatud klassi liigefunktsiooni baasklassi kursori abil ei tähenda, et objekti on võimalik jälgida "ülevalt alla" kursorist põhiklassi objektini. . Mittevirtuaalsete liikmete funktsioonid ja andmed pole endiselt saadaval. Ja seda on väga lihtne näha. Selleks piisab, kui proovime teha seda, mida oleme juba korra teinud - kutsuda välja baasklassis tundmatu tuletatud klassi liigefunktsioon:

//pObj->Lõbus2(2); //pObj->AC::Fun1(2);

Tulemus on negatiivne. Kursor, nagu varemgi, on seatud ainult väärtusele alusjupp tuletatud klassiobjekt. Siiski on tuletatud klassi funktsioonide kutsumine võimalik. Kunagi käsitlesime konstruktorite kirjeldusele pühendatud jaotistes loendit rutiinsetest toimingutest, mida konstruktor teostab eraldatud mälufragmendi klassiobjektiks teisendamisel. Nende tegevuste hulgas oli ka virtuaalsete funktsioonitabelite initsialiseerimine.

Saate proovida tuvastada nende virtuaalsete funktsioonitabelite olemasolu toimingu suuruse abil. Muidugi oleneb kõik konkreetsest teostusest, kuid vastavalt vähemalt, Borland C++ versioonis võtab virtuaalfunktsioonide deklaratsioone sisaldava klassi esindusobjekt rohkem mälu kui sarnase klassi objekt, milles samad funktsioonid deklareeritakse ilma virtuaalse spetsifikaatorita.

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

Seega omandab tuletatud klassi objekt täiendava elemendi - osuti virtuaalsete funktsioonide tabelile. Sellise objekti skeemi saab kujutada järgmiselt (tabeli osutit tähistame identifikaatoriga vptr, virtuaalsete funktsioonide tabelit identifikaatoriga vtbl):

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

Meie uues objektidiagrammis ei eralda kursorit virtuaalfunktsioonide tabelile (ühe elemendi massiivile) kogemata baasklassi esindavast objekti fragmendist ainult punktiirjoonega. See on selle objekti fragmendi vaateväljas. Selle osuti olemasolu tõttu helistab virtuaalne funktsioon operaatorile Fun1

PObj->Lõbus1(1);

saab esitada järgmiselt:

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

Siin on ainult esmapilgul kõik segane ja arusaamatu. Tegelikult pole selles operaatoris ühtegi tundmatut väljendit.

See ütleb sõna otseses mõttes järgmist:

KUTSAGE VIRTUAALFUNKTSIOONIDE TABELI NULLINDEKSI JÄRJEL ASUVAT FUNKTSIOONI vtbl (meil on selles tabelis ainult üks element), MILLE ALGUSAADRESSI LEIAB INDEKSI JÄRELE vptr.

SELLELE OSUTTILE ON VEEL JUURDEPÄÄS MYAC OBJEKTILE SEADETUD POBJ OSUTJAGA. FUNKTSIOONIL ON EDASTATUD KAKS (!) PARAMEETRIT, SELLEST ESIMENE ON OBJEKTI AADRESS MyAC (selle osuti väärtus!), TEINE ON INTEGRAATVÄÄRTUS VÕRDNE 1-GA.

Põhiklassi liikme funktsiooni kutsumine toimub kvalifitseeritud nime kaudu.

PObj->A::Lõbus1(1);

Selles avalduses keeldume virtuaalse funktsioonitabeli teenustest. Seda tehes ütleme tõlkijale, et kavatseme kutsuda baasklassi liikme funktsiooni. Virtuaalsete funktsioonide toetamise mehhanism on range ja väga rangelt reguleeritud. Virtuaalsete funktsioonide tabeli osuti peab sisalduma tuletatud klassiobjekti "ülemises" baasfragmendis. Osutitabel sisaldab selle funktsiooni deklaratsioone sisaldava "madalaima" taseme fragmendi liikmefunktsioonide aadresse.

Muudame veel kord klasside A, AB deklaratsiooni ja kuulutame välja uue klassi ABC.

Klasside A ja AB muutmine taandub nendes uute liikmefunktsioonide deklareerimisele:

Klass A ( avalik: virtuaalne int Fun1 (int võti); virtuaalne int Fun2 (int võti); ); ::::: int A::Fun2(int klahv) ( 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; }

See klass sisaldab liikmefunktsiooni Fun1 deklaratsiooni, mis deklareeritakse kaudses baasklassis A virtuaalse funktsioonina. Lisaks pärib see klass fun2 liikmefunktsiooni oma vahetust baasist. See funktsioon on deklareeritud ka põhiklassis A virtuaalseks. Kuulutame ABC-klassi esindusobjektiks:

ABCMyABC;

Selle skeemi saab esitada järgmiselt:

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

Virtuaalsete funktsioonide tabel sisaldab nüüd kahte kirjet. Seadsime baasklassi objektikursori MyABC objektile ja kutsume seejärel liikmefunktsioone:

PObj = pObj->Lõbus1(1); pObj->Lõbus2(2);

Sel juhul ei saa liikmefunktsiooni AB::Fun1() kutsuda, kuna selle aadressi pole virtuaalsete funktsioonide loendis ja see pole lihtsalt nähtav selle MyABC objekti ülemiselt tasemelt, kuhu kursor liigub. pObj on terav. Virtuaalsete funktsioonide tabeli ehitab konstruktor vastava objekti objekti loomise hetkel. Loomulikult annab tõlkija konstruktorile sobiva kodeeringu. Kuid kompilaator ei suuda konkreetse objekti jaoks määrata virtuaalsete funktsioonide tabeli sisu. See on käitusaegne ülesanne. Kuni konkreetse objekti jaoks pole koostatud virtuaalsete funktsioonide tabelit, ei saa tuletatud klassi vastavat liigefunktsiooni kutsuda. Selles on lihtne veenduda pärast klasside deklaratsiooni järgmist muudatust.

Programm on väike, seega on mõttekas selle tekst täielikult tsiteerida. Te ei tohiks end petta klassi komponentidele juurdepääsu toiminguga::. Selle operatsiooniga seotud probleemide arutelu on alles ees.

#kaasa klass A ( public: virtual int Fun1(int key); ); int A::Fun1(int klahv) ( 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); // Kutsutakse välja ABC::Fun1(). //pObj->Lõbus2(2); // See funktsioon pole kursori kaudu saadaval!!! pObj->A::Lõbus1(2); // A::Fun1() kutsutakse. )

Nüüd MyABC objekti loomise hetkel

ABCMyABC;

AB-klassi konstruktorist (ja seda kutsutakse enne ABC-klassi konstruktorit) kutsutakse välja funktsioon A::Fun1(). See funktsioon on klassi A liige. MyABC objekt pole veel täielikult moodustatud, virtuaalsete funktsioonide tabel pole veel täidetud ja funktsiooni ABC::Fun1() olemasolust pole midagi teada. Pärast MyABC-objekti lõplikku moodustamist täidetakse virtuaalfunktsioonide tabel ja pObj-kursor seatakse MyABC-objektile, funktsiooni A::Fun1() kutsumine pObj-kursori kaudu on võimalik ainult selle täiskvalifitseeritud nime kasutades. funktsioon:

PObj->Lõbus1(1); // See on funktsiooni ABC::Fun1() kutse! pObj->A::Lõbus1(1); // Ilmselgelt on see funktsiooni A::Fun1() kutse!

Pange tähele, et Fun1 liikme funktsiooni kutsumine otse MyABC objektist annab sama tulemuse:

MyABC Fun1(1); // Funktsiooni ABC::Fun1() kutsumine.

Ja katse kutsuda mittevirtuaalset funktsiooni AB::Fun2() baasklassi objektile viiva osuti kaudu lõppeb ebaõnnestumisega. Virtuaalsete funktsioonide tabelis selle funktsiooni aadress puudub ja objekti ülemiselt tasemelt "alla vaadata" on võimatu.

//pObj->Lõbus2(2); // Sa ei saa seda nii teha!

Selle programmi täitmise tulemus näitab selgelt virtuaalsete funktsioonide kasutamise eripära. Vaid paar rida...

Fun1(125) firmalt A. Fun1(1) ABC-st. Fun1(5) ABC-st. Fun2(1) AB-lt. Fun1(1) ABC-st. ========== Fun1(2) ABC-st. Lõbu1(2) A-lt.

Ühte ja sama osutit saab programmi täitmise ajal kohandada erinevate tuletatud klasside esindusobjektidele. Selle tulemusena täidab sõna otseses mõttes sama liikmefunktsiooni kõneavaldis täiesti erinevaid funktsioone. Esmakordselt seisame silmitsi nn HILINE ehk HILINE KÖITMISega.

Pange tähele, et virtuaalne spetsifikatsioon kehtib ainult funktsioonide kohta. Virtuaalseid andmeliikmeid pole. See tähendab, et tuletatud klassiobjekti andmeliikmetele ei ole võimalik juurde pääseda, osutades põhiklassi objektile, mis on seatud tuletatud klassiobjektile.

Teisest küljest on ilmne, et kui on võimalik kutsuda asendusfunktsiooni, siis otse "läbi" see funktsioon avab juurdepääsu kõikidele tuletatud klassi funktsioonidele ja andmeliikmetele ning seejärel "alt-üles" kõigile mitteprivaatsetele. otseste ja kaudsete baasklasside funktsioonid ja andmeliikmed. Sel juhul muutuvad funktsioonist kättesaadavaks kõik mitteprivaatsed andmed ja põhiklasside funktsioonid.

Ja veel üks väike näide, mis demonstreerib tuletatud klassi esindusobjekti käitumise muutumist pärast seda, kui üks põhiklassi funktsioonidest muutub virtuaalseks.

#kaasa klass A ( avalik: void funA () (xFun();); /*virtuaalne*/kehtetu xFun () (cout<<"this is void A::xFun();"<< endl;}; }; class B: public A { public: void xFun () {cout <<"this is void B::xFun ();"<

Alguses kommenteeritakse funktsiooni A::xFun() definitsiooni virtuaalne spetsifikaator. Programmi täitmisprotsess seisneb tuletatud klassi B tüüpilise objekti objB määramises ja selle objekti liikmefunktsiooni funA() kutsumises. See funktsioon on päritud baasklassist, see on üks ja on ilmne, et selle tuvastamine ei tekita tõlkijale probleeme. See funktsioon kuulub baasklassi, mis tähendab, et selle väljakutsumise korral viiakse juhtimine üle objekti objB "ülemisele tasemele". Samal tasemel asub üks funktsioonidest nimega xFun() ja sellele funktsioonile antakse juhtimine üle funktsiooni funA() kehas oleva kõneavaldise täitmise ajal. Veelgi enam, funktsioonist funA() on lihtsalt võimatu kutsuda välja teist samanimelist funktsiooni. Klassi A struktuuri parsimise hetkel ei ole tõlkijal klassi B struktuurist aimugi.. Funktsioon xFun(), mis on klassi B liige, on funA() funktsioonist kättesaamatu.

Kui aga tühistate funktsiooni A::xFun() definitsioonis virtuaalse spetsifikaatori kommentaarid, luuakse asendussuhe kahe samanimelise funktsiooni vahel ja objekti objB genereerimisega kaasneb virtuaalsete funktsioonide tabel, mille järgi kutsutakse klassi B asendusfunktsiooni liiget. Nüüd peate asendatud funktsiooni kutsumiseks kasutama selle kvalifitseeritud nime:

Kehtetu A::funA() ( xFun(); A::xFun(); )

virtuaalne funktsioon

virtuaalne meetod (virtuaalne funktsioon) - objektorienteeritud programmeerimisel klassi meetod (funktsioon), mida saab järeltulijates klassides alistada nii, et käivitamise ajal määratakse välja kutsutava meetodi konkreetne teostus. Seega ei pea programmeerija teadma objekti täpset tüüpi, et sellega virtuaalmeetodite kaudu töötada: piisab teadmisest, et objekt kuulub klassi või selle klassi järeltulijasse, milles meetod on deklareeritud.

Virtuaalsed meetodid on polümorfismi rakendamise üks olulisemaid tehnikaid. Need võimaldavad teil luua ühist koodi, mis võib töötada nii põhiklassi objektidega kui ka selle järglaste klasside objektidega. Samal ajal määrab baasklass objektidega töötamise viisi ja iga selle pärija võib pakkuda selle viisi konkreetse teostuse. Mõnes programmeerimiskeeles, näiteks inglise keeles. puhas virtuaalne) või abstraktne. Klass, mis sisaldab vähemalt ühte sellist meetodit, on samuti abstraktne. Sellise klassi objekti ei saa luua (mõnes keeles on see lubatud, kuid abstraktse meetodi kutsumine toob kaasa vea). Abstraktse klassi pärijad peavad tagama kõigi selle abstraktsete meetodite teostuse, vastasel juhul on need omakorda abstraktsed klassid.

Iga klassi jaoks, millel on vähemalt üks virtuaalne meetod, a virtuaalse meetodi tabel. Iga objekt salvestab osuti oma klassi tabelile. Virtuaalse meetodi kutsumiseks kasutatakse järgmist mehhanismi: objektilt võetakse osuti vastavale virtuaalsete meetodite tabelile ja sellelt fikseeritud nihkega osuti selle klassi jaoks kasutatava meetodi realiseerimisele. Mitme pärimise või liidese kasutamisel muutub olukord mõnevõrra keerulisemaks, kuna virtuaalne meetoditabel muutub mittelineaarseks.

Näide

Virtuaalse funktsiooni näide Delphis

Üsna sageli unustatakse virtuaalsed meetodid märksõnaga alistada alistama. See põhjustab meetodi sulgemise. Sel juhul ei toimu VMT-s meetodi asendamist ja vajalikku funktsionaalsust ei saada.

Seda viga jälgib kompilaator, mis annab asjakohase hoiatuse.

Esivanema meetodi kutsumine tühistatud meetodist

Alistatud meetodis võib osutuda vajalikuks kutsuda esivanema meetod.

Deklareerime kaks klassi. Esivanem:

tantsija = klass privaatne kaitstud avalik(Virtuaalne protseduur.) menetlust Virtuaalne protseduur ; virtuaalne; lõpp;

ja selle järglane (Descendant):

TDescendant = klass(TAestor) privaatne kaitstud avalik(Kattuv virtuaalne protseduur.) menetlust Virtuaalne protseduur; alistama; lõpp;

Esivanema meetodi kutse rakendatakse märksõnaga ""päritud""

menetlust TDescendant.VirtualProcedure; alustada päritud; lõpp;

Tasub meeles pidada, et Delfis tuleb hävitajast üle sõita. ""alista""; ja sisaldavad kõnet esivanema hävitajale

TDescendant = klass(TAestor) privaatne kaitstud avalik hävitaja Hävitada; alistama; lõpp; hävitaja Tjärglane. Hävitada; alustada päritud; lõpp;

C++ keeles ei pea kutsuma esivanema konstruktorit ja hävitajat, hävitaja peab olema virtuaalne. Esivanemate hävitajad kutsutakse automaatselt välja. Esivanema meetodi kutsumiseks peate meetodi selgesõnaliselt kutsuma:

Klass Ancestor ( public : virtual void function1 () ( printf ("Ancestor::function1" ) ; ) ); klass Järeltulija: public Ancestor ( public : virtual void function1 () ( printf ("Descendant::function1" ) ; Ancestor::function1 () ; // see prindib teksti "Ancestor::function1" } } ;

Esivanema konstruktori kutsumiseks peate määrama konstruktori:

Klass Descendant: avalik Esivanem ( avalik : Järeltulija() : Esivanem() ; );

Vaata ka

Lingid

  • C++ KKK Lite: C++ virtuaalsed funktsioonid

Wikimedia sihtasutus. 2010 .

Alustuseks kordan: te ei tohiks kutsuda virtuaalseid funktsioone, kui konstruktorid või destruktorid töötavad, sest need väljakutsed ei tee seda, mida arvate, ja te ei ole nende töö tulemustega rahul. Kui oled Java või C# programmeerija, siis pööra sellele reeglile erilist tähelepanu, sest C++ käitub selles osas erinevalt.

Oletame, et börsitehingute modelleerimiseks on olemas klasside hierarhia, st ostukorraldused, müügikorraldused jne. On oluline, et neid tehinguid oleks lihtne kontrollida, nii et iga kord, kui luuakse uus tehinguobjekt, tuleks teha asjakohane kirje auditi logi . Selle probleemi lahendamiseks näib olevat mõistlik järgmine lähenemisviis:


klass Tehing ( // baasklass kõigile

avalik: // tehingud

virtual void logTransaction() const = 0; // käivitab tüübist sõltuva

// logi sissekanne

Transaction::Transaction() // konstruktori rakendamine

( // baasklass

logTransaction();

klass BuyTransaction: avalik tehing ( // tuletatud klass

// seda tüüpi tehingud

klass SellTransaction: avalik tehing ( // tuletatud klass

virtual void logTransaction() const = 0; // kuidas logida

// seda tüüpi tehingud


Vaatame, mis juhtub järgmise koodi käivitamisel:


Ostutehing b;


Ilmselgelt kutsutakse välja BuyTransaction konstruktor, kuid esmalt tuleb kutsuda Transaction konstruktor, kuna põhiklassi kuuluvad objekti osad konstrueeritakse enne tuletatud klassi kuuluvaid osi. Tehingu konstruktori viimane rida kutsub välja virtuaalfunktsiooni logTransaction ja siit algavad üllatused. Siin kutsutakse välja logTransactioni versioon, mis on määratletud klassis Tehing, mitte aga BuyTransactionis, kuigi loodava objekti tüüp on BuyTransaction. Baasklassi ehitamise käigus ei kutsuta välja tuletatud klassis defineeritud virtuaalseid funktsioone. Objekt käitub nii, nagu kuuluks baastüüpi. Lühidalt, baasklassi ehitamise ajal ei eksisteeri virtuaalseid funktsioone.

Sellel näiliselt ootamatul käitumisel on hea põhjus. Kuna põhiklassi konstruktoreid kutsutakse enne tuletatud klassi konstruktoreid, ei ole tuletatud klassi andmeliikmed põhiklassi konstruktori käitamise ajal veel lähtestatud. See võib põhjustada ebamäärast käitumist ja siluri tundmist. Juurdepääs objekti osadele, mida pole veel lähtestatud, on ohtlik, mistõttu C++ seda võimalust ei paku.

On veelgi põhimõttelisemaid põhjuseid. Sel ajal, kui baasklassi konstruktor töötab tuletatud klassi objekti loomisega, siis objekti tüüp on baasklass. Seda ei käsitle mitte ainult virtuaalsed funktsioonid, vaid ka kõik muud keelemehhanismid, mis kasutavad käitusajal tüübiteavet (näiteks punktis 27 kirjeldatud operaator dynamic_cast ja operaator typeid). Kui meie näites töötab tehingukonstruktor ja lähtestab objekti BuyTransaction baasosa, on see objekt tüüpi Tehing. Nii käsitlevad seda kõik C++ osad ja see on loogiline: objekti BuyTransactioni osad pole veel lähtestatud, seega on kindlam eeldada, et neid pole üldse olemas. Objekt ei ole tuletatud klassi objekt enne, kui algab viimase konstruktori täitmine.

Sama kehtib ka hävitajate kohta. Niipea kui tuletatud klassi hävitaja hakkab täitma, eeldab see, et sellesse klassi kuuluvad andmeliikmed pole määratletud, seega eeldab C++, et neid enam ei eksisteeri. Kui sisestame põhiklasside destruktori, muutub meie objekt baasklassi objektiks ja kõik C++ osad – virtuaalsed funktsioonid, operaator dynamic_cast jne – käsitlevad seda nii.

Ülaltoodud koodinäites kutsub tehingukonstruktor otse virtuaalset funktsiooni, mis on selles reeglis kirjeldatud põhimõtete selge rikkumine. Seda rikkumist on lihtne tuvastada, mistõttu mõned kompilaatorid hoiatavad (ja teised mitte; vt hoiatuste arutelu punktis 53). Kuid isegi ilma sellise hoiatuseta ilmub viga suure tõenäosusega enne käitusaega, kuna funktsioon logTransaction klassis Tehing on kuulutatud puhtalt virtuaalseks. Kui see pole kuskil defineeritud (ebatõenäoline, aga võimalik – vt punkt 34), siis selline programm ei linki: linker ei leia Transaction::logTransaction vajalikku teostust.

Konstruktori või destruktori töötamise ajal ei ole alati lihtne tuvastada virtuaalset funktsioonikutset. Kui tehingul on mitu konstruktorit, millest igaüks teeb sama tööd, siis peaksite oma programmi kavandama nii, et see väldiks koodi dubleerimist, pannes ühise lähtestamisosa, sealhulgas logiTransactioni kutse privaatsesse mittevirtuaalsesse lähtestamisfunktsiooni, öelge init:


klassi tehing(

( init(); ) // mittevirtuaalse funktsiooni kutsumine

Virtual void logTransaction() const = 0;

logTransaction(); // ja see on virtuaalne kõne

// funktsioonid!


Põhimõtteliselt on see kood sama, mis ülal, kuid see on salakavalam, kuna tavaliselt kompileeritakse ja lingitakse ilma hoiatusteta. Sel juhul, kuna logTransaction on tehinguklassi puhtalt virtuaalne funktsioon, katkestab enamik käitusaegseid süsteeme programmi selle väljakutsumise ajal (tavaliselt väljastades vastava teate). Kui aga logTransaction on "tavaline" virtuaalne funktsioon, millel on rakendus klassis Transaction, kutsutakse see funktsioon välja ja programm jätkab rõõmsalt, pannes teid mõtlema, miks tuletatud klassiobjekti käivitamisel kutsuti välja logTransactioni vale versioon. loodud. Ainus viis selle probleemi vältimiseks on veenduda, et ükski konstruktor ja hävitaja ei kutsuks virtuaalseid funktsioone, kui objekti luuakse või hävitatakse, ja et kõik funktsioonid, mida nad kutsuvad, järgivad sama reeglit.

Kuid kuidas saate veenduda, et tehinguhierarhias mis tahes objekti loomisel kutsutakse välja logi-tehingu õige versioon? On selge, et objekti virtuaalse funktsiooni kutsumine konstruktoritelt ei ole hea.

Sellele probleemile on erinevaid lahendusi. Üks on muuta funktsioon logTransaction klassis Tehing mittevirtuaalseks ja seejärel nõuda tuletatud klassi konstruktoritelt logi kirjutamiseks vajaliku teabe edastamist Tehingu konstruktorile. See funktsioon võib seejärel turvaliselt kutsuda mittevirtuaalset logTransactionit. Nagu see:


klassi tehing(

explicit Transaction(const std::string& logiinfo);

void logTransaction(const std::string& logiinfo) const; // Nüüd -

// mittevirtuaalne

// funktsioon

Tehing::Tehing(const std::string& logiteave)

logTransaction(loginfo); // Nüüd -

// mittevirtuaalne

klass Ostutehing: avalik tehing(

Ostutehing( parameetrid)

: Tehing(createLogString( parameetrid)) // edastage teave

(...) // logisse kirjutamise eest

... // aluse konstruktorile

static std::string createLogString( parameetrid);


Teisisõnu, kui te ei saa põhiklassi konstruktorist virtuaalseid funktsioone välja kutsuda, saate seda kompenseerida, edastades tuletatud konstruktorist vajaliku teabe põhiklassi konstruktorile.

Selles näites pange tähele privaatse staatilise funktsiooni createLogString kasutamist rakenduses BuyTransaction. Abifunktsiooni kasutamine baasklassi konstruktorile edastatava väärtuse loomiseks on sageli mugavam (ja paremini loetav) kui liikmete initsialiseerimiste pika loendi jälgimine, et põhiklassile vajalik edastada. Selle funktsiooni staatiliseks muutmisega väldime ohtu, et viidatakse kogemata BuyTransaction klassi initsialiseerimata andmeliikmetele. See on oluline, sest asjaolu, et need andmeliikmed pole veel defineeritud, on peamine põhjus, miks te ei saa konstruktoritelt ja destruktoritelt virtuaalseid funktsioone kutsuda.