Waarom zijn virtuele methoden nodig? Virtuele functie. Puur virtuele functie

Sergej Malysjev (ook bekend als Michalytsj)

Deel 1. Algemene theorie van virtuele functies

Als je naar de titel van dit artikel kijkt, denk je misschien: "Hmm! Wie weet niet wat virtuele functies zijn..." Als dat zo is, kun je hier gerust stoppen met lezen.

En voor degenen die net de fijne kneepjes van C++ beginnen te begrijpen, maar bijvoorbeeld al basiskennis hebben over zoiets als overerving, en iets hebben gehoord over polymorfisme, is het direct zinvol om dit materiaal te lezen. Als u virtuele functies begrijpt, heeft u de sleutel tot het ontsluiten van de geheimen van succesvol objectgeoriënteerd ontwerp.

Over het algemeen is de stof niet erg moeilijk. En alles wat hier besproken wordt, is ongetwijfeld terug te vinden in boeken. Het enige probleem is dat je waarschijnlijk geen volledige presentatie van het hele probleem in een of twee boeken zult vinden. Om over virtuele functies te schrijven, moest ik 6 verschillende publicaties ‘bestudeeren’. En zelfs in dit geval pretendeer ik helemaal niet volledig te zijn. In de lijst met referenties geef ik alleen de belangrijkste aan, de referenties die mij hebben geïnspireerd in de stijl van presentatie en inhoud.

Ik besloot al het materiaal in 3 delen te verdelen.
Laten we het in het eerste deel proberen te begrijpen algemene theorie virtuele functies. In het tweede deel zullen we de toepassing ervan (en hun kracht!) bekijken aan de hand van een min of meer praktijkvoorbeeld. Welnu, in het derde deel zullen we het hebben over zoiets als virtuele destructors.

Dus wat is het?

Laten we eerst onthouden hoe u in klassiek C-programmeren een data-object aan een functie kunt doorgeven. Hier is niets ingewikkelds aan, u hoeft alleen maar het type object in te stellen dat wordt doorgegeven op het moment dat u de functiecode schrijft. Dat wil zeggen, om het gedrag van objecten te beschrijven, is het noodzakelijk om hun type van tevoren te kennen en te beschrijven. De kracht van OOP is in dit geval dat je virtuele functies kunt schrijven, zodat het object zelf bepaalt welke functie het moet aanroepen terwijl het programma draait.

Met andere woorden: met behulp van virtuele functies bepaalt het object zelf zijn gedrag (zijn eigen acties). De techniek om virtuele functies te gebruiken wordt polymorfisme genoemd. Letterlijk betekent polymorfisme het hebben van vele vormen. Een object in uw programma kan feitelijk niet slechts één klasse vertegenwoordigen, maar veel verschillende klassen als ze door overerving gerelateerd zijn aan een gemeenschappelijke basisklasse. Welnu, het gedrag van objecten van deze klassen in de hiërarchie zal natuurlijk anders zijn.

Nou, nu ter zake!

Zoals u weet, kan een pointer naar een basisklasse volgens de regels van C++ verwijzen naar een object van deze klasse, maar ook naar een object van elke andere klasse die is afgeleid van de basisklasse. Het begrijpen van deze regel is erg belangrijk. Laten we eens kijken naar een eenvoudige hiërarchie van bepaalde klassen A, B en C. A zal onze basisklasse zijn, B zal worden afgeleid (gegenereerd) van klasse A, en C zal worden afgeleid van B. Zie de afbeelding voor uitleg.

In een programma kunnen objecten van deze klassen bijvoorbeeld op deze manier worden gedeclareerd.

Een object_A; //verklaring van een object van type A
B-object_B; //verklaring van een object van type B
C-object_C; //verklaring van een object van type C

Volgens deze regel een aanwijzer van type A kan naar elk van deze drie objecten verwijzen. Dat wil zeggen, dit zal waar zijn:


point_to_Object=&object_C; // wijs het adres van object C toe aan de aanwijzer

Maar dit klopt niet meer:

In *punt_naar_Object; // declareer een pointer naar een afgeleide klasse
point_to_Object=&object_A; //u kunt geen pointer toewijzen aan het adres van het basisobject

Hoewel de point_to_Object-aanwijzer van het type A* is en niet van C* (of B*), kan deze verwijzen naar objecten van het type C (of B). Misschien wordt de regel duidelijker als je object C beschouwt als een speciaal soort object A. Een pinguïn is bijvoorbeeld een speciaal soort vogel, maar het is nog steeds een vogel, ook al vliegt hij niet. Uiteraard werkt deze relatie tussen objecten en verwijzingen slechts in één richting. Een object van type C is een speciaal type object A, maar object A is niet een speciaal type object C. Terugkerend naar pinguïns kunnen we gerust zeggen dat als alle vogels een speciaal type pinguïn zouden zijn, ze eenvoudigweg niet in staat zouden zijn vliegen!

Dit principe wordt vooral belangrijk wanneer virtuele functies worden gedefinieerd in klassen die verband houden met overerving. Virtuele functies hebben precies dezelfde vorm en zijn op dezelfde manier geprogrammeerd als de meeste normale functies. Alleen hun aankondiging wordt gedaan met trefwoord virtueel. Onze basisklasse A kan bijvoorbeeld een virtuele functie declareren v_functie().

klasse A
{
publiek:
virtuele leegte v_function(void);//function beschrijft bepaald gedrag van klasse A
};

Een virtuele functie kan worden gedeclareerd met parameters en kan, net als elke andere functie, een waarde retourneren. Een klasse kan zoveel virtuele functies declareren als u nodig heeft. En ze kunnen zich in elk deel van het klaslokaal bevinden: gesloten, open of beschermd.

Als je in klasse B, afgeleid van klasse A, een ander gedrag moet beschrijven, dan kun je een virtuele functie declareren, ook wel genoemd v_functie().

klasse B: publiek A
{
publiek:
virtuele leegte v_function(void);//vervangingsfunctie beschrijft iets
//nieuw gedrag van klasse B
};

Wanneer een klasse zoals B een virtuele functie definieert die dezelfde naam heeft als een virtuele functie van zijn bovenliggende klasse, wordt de functie een override-functie genoemd. De virtuele functie v_function() in B vervangt de virtuele functie met dezelfde naam in klasse A. In feite is alles wat ingewikkelder en komt het niet neer op een simpele samenloop van namen. Maar hierover iets later meer, in de sectie "Enkele subtiliteiten van toepassing".
Nou, nu het allerbelangrijkste!

Laten we terugkeren naar de pointer point_to_Object van type A*, die verwijst naar object object_B van type B*. Laten we de instructie die de virtuele functie v_function() aanroept op het object waarnaar wordt verwezen eens nader bekijken punt_naar_Object.

Een *point_to_Object; // declareer een verwijzing naar de basisklasse
point_to_Object=&object_B; // wijs het adres van object B toe aan de aanwijzer
point_to_Object->;v_function(); //roep de functie aan

De point_to_Object pointer kan het adres van een object van type A of B opslaan. Dit betekent dat tijdens de uitvoering deze operator point_to_Object-gt;v_function(); roept een virtuele functie van de klasse aan op het object waarin deze zich bevindt op dit moment verwijst. Als point_to_Object verwijst naar een object van type A, wordt een functie aangeroepen die tot klasse A behoort. Als point_to_Object verwijst naar een object van type B, wordt een functie aangeroepen die tot klasse B behoort. Dus dezelfde instructie roept een functie van de klasse van aan het object dat wordt aangesproken. Dit is de actie die wordt bepaald tijdens de uitvoering van het programma.

Dus wat levert dit ons op?

Het is tijd om te kijken: wat bieden virtuele functies ons? Over de theorie van virtuele functies in algemene schets wij hebben een kijkje genomen. Het is tijd om na te denken over een reële situatie waarin u het kunt begrijpen praktische betekenis onderwerp in de echte wereld van programmeren.

Een klassiek voorbeeld (naar mijn ervaring - in 90% van alle literatuur over C++) dat voor dit doel wordt gegeven, is schrijven grafisch programma. Er wordt een hiërarchie van klassen opgebouwd, zoiets als “punt -gt lijn -gt; platte figuur -gt; En we beschouwen een virtuele functie, bijvoorbeeld Draw(), die dit allemaal tekent... Saai!

Laten we eens kijken naar een minder academisch, maar toch grafisch voorbeeld. (Klassiek! Waar kan ik eraan ontsnappen?). Laten we proberen hypothetisch te bekijken welk principe erin kan worden ingebed computerspel. En niet zomaar een game, maar de basis van elke (ongeacht 3D of 2D, cool of zo-zo) shooter. Schutters, om het simpel te zeggen. Ik ben niet bloeddorstig in het leven, maar, zondig, ik hou er soms van om te schieten!

Dus besloten we een coole shooter te maken. Wat heb je eerst nodig? Natuurlijk wapens! (Nou ja, misschien niet de eerste. Het maakt niet uit.) Afhankelijk van het onderwerp waarover we zullen schrijven, zal zo'n wapen nodig zijn. Misschien wordt het een set van een eenvoudige knuppel tot een kruisboog. Misschien van een haakbus tot een granaatwerper. Of misschien zelfs van een blaster tot een desintegrator. We zullen snel zien dat dit juist is wat niet belangrijk is.

Omdat er zoveel mogelijkheden zijn, moeten we een basisklasse creëren.

klasse Wapen
{
publiek:
... //er zullen gegevensleden zijn die bijvoorbeeld kunnen worden beschreven als
//de dikte van de knuppel en het aantal granaten in de granaatwerper
//dit deel is niet belangrijk voor ons

virtuele leegte Use1(void);//meestal - linker knop muizen
virtuele leegte Use2(void);//meestal - rechter knop muizen

... //er zullen nog wat meer gegevensleden en methoden zijn
};

Zonder op de details van deze klasse in te gaan, kunnen we zeggen dat de functies Use1() en Use2() misschien wel de belangrijkste zijn, die het gedrag (of het gebruik) van dit wapen beschrijven. Deze klasse kan elk type wapen voortbrengen. Er zullen nieuwe gegevensleden worden toegevoegd (zoals het aantal rondes, vuursnelheid, energieniveau, bladlengte, etc.) en nieuwe functies. En door de functies Use1() en Use2() opnieuw te definiëren, zullen we het verschil in het gebruik van wapens beschrijven (voor een mes kan dit slaan en gooien zijn, voor een machinegeweer kan dit enkelvoudig en burst-schieten zijn).

De verzameling wapens moet ergens worden opgeslagen. Blijkbaar is de eenvoudigste manier om dit te doen het organiseren van een reeks aanwijzers van het type Weapon*. Laten we voor de eenvoud aannemen dat dit zo is mondiale reeks Wapens, voor 10 soorten wapens, en alle wijzers worden om te beginnen op nul geïnitialiseerd.

Wapen *Wapen; //reeks verwijzingen naar objecten van het wapentype

Door aan het begin van het programma dynamische objecten (wapensoorten) te maken, voegen we er verwijzingen naar toe aan de array.

Om aan te geven welk wapen in gebruik is, zullen we een array-indexvariabele aanmaken, waarvan de waarde zal veranderen afhankelijk van het geselecteerde type wapen.

int TypeWapen;

Als resultaat van deze inspanningen zou de code die het gebruik van wapens in het spel beschrijft er als volgt uit kunnen zien:

if(Linkermuisklik) Arms-gt;Gebruik1();
anders Wapens->Gebruik2();

Alle! We hebben code gemaakt die schieten-schieten-oorlog beschrijft voordat we zelfs maar besloten welke soorten wapens zouden worden gebruikt. Bovendien. We hebben nog geen enkel echt type wapen! Een bijkomend (soms heel belangrijk) voordeel is dat deze code afzonderlijk kan worden samengesteld en in een bibliotheek kan worden opgeslagen. Later kun jij (of een andere programmeur) nieuwe klassen uit Weapon afleiden, deze opslaan in de Arms-array en ze gebruiken. Hiervoor is geen hercompilatie van uw code vereist.

Merk vooral op dat deze code niet vereist dat u de exacte gegevenstypen specificeert van de objecten waarnaar wordt verwezen door de Arms-aanwijzers, alleen dat ze zijn afgeleid van Weapon. Objecten bepalen tijdens runtime welke Use()-functie ze moeten aanroepen.

Enkele subtiliteiten van toepassing

Laten we wat tijd besteden aan het probleem van het vervangen van virtuele functies.

Laten we teruggaan naar het begin: naar de saaie klassen A, B en C. Klasse C bevindt zich momenteel helemaal onderaan de hiërarchie, aan het einde van de erfelijkheidslijn. In klasse C kunt u op precies dezelfde manier een vervangende virtuele functie definiëren. Bovendien is het helemaal niet nodig om het virtuele trefwoord te gebruiken, aangezien dit de laatste klasse in de overervingslijn is. De functie werkt al en is geselecteerd als virtueel. Maar! Maar als je een bepaalde klasse D uit klasse C wilt verwijderen, en zelfs het gedrag van de functie v_function() wilt veranderen, dan zal er niets van terecht komen. Om dit te doen, moet in klasse C de functie v_function() als virtueel worden gedeclareerd. Vandaar de regel, die als volgt geformuleerd kan worden: “eens virtueel, altijd virtueel!” Dat wil zeggen: het sleutelwoord virtueel is beter Gooi het niet weg – wat als het van pas komt?

Nog een subtiliteit. Een afgeleide klasse kan geen functie definiëren met dezelfde naam en dezelfde set parameters, maar met een ander retourtype dan de virtuele functie van de basisklasse. In dit geval zal de compiler vloeken in de fase van het compileren van het programma.

Volgende. Als je in een afgeleide klasse een functie introduceert met dezelfde naam en hetzelfde retourtype als een virtuele functie van de basisklasse, maar met een andere set parameters, dan zal deze functie van de afgeleide klasse niet langer virtueel zijn. Zelfs als u het tagt met het virtuele trefwoord, zal het niet zijn wat u had verwacht. In dit geval zal bij gebruik van een pointer naar de basisklasse elke waarde van deze pointer de functie van de basisklasse aanroepen. Denk aan de regel over functieoverbelasting! Het is eenvoudig verschillende functies. Je krijgt een compleet andere virtuele functie. Over het algemeen zijn dergelijke fouten zeer ongrijpbaar, aangezien beide vormen van notatie zeer acceptabel zijn en er in dit geval geen hoop is op compilerdiagnostiek.

Daarom nog een regel. Bij het vervangen van virtuele functies is een volledige overeenkomst van parametertypen, functienamen en retourwaardetypen in de basis- en afgeleide klassen vereist.

En nog een ding. Een virtuele functie kan alleen een niet-statische componentklassefunctie zijn. Kan niet virtueel zijn mondiale functie. Een virtuele functie kan in een andere klasse tot vriend worden verklaard. Maar we zullen het in een ander artikel over vriendelijke functies hebben.

Dat is alles voor deze keer.

In het volgende deel zul je het volledig zien functioneel voorbeeld het eenvoudigste programma, dat alle punten laat zien waar we het over hadden.

Als u vragen heeft, schrijf dan, wij zullen het uitzoeken.

Puur virtuele functies

Het virtuele functiemechanisme wordt gebruikt in gevallen waarin het nodig is een functie in de basisklasse te plaatsen die in afgeleide klassen anders moet worden uitgevoerd. Preciezer gezegd: niet alleen de enige functie uit de basisklasse moet anders worden uitgevoerd, maar elke productieklasse heeft zijn eigen versie van deze functie nodig.

Voordat u de mogelijkheden van virtuele functies uitlegt, moet u er rekening mee houden dat klassen die dergelijke functies bevatten, kunnen worden afgespeeld bijzondere rol bij objectgeoriënteerd programmeren. Daarom dragen ze speciale naam- polymorf. .

Geen enkele functie kan virtueel zijn, maar alleen niet-statische componentfuncties van een klasse. Zodra een functie als virtueel is gedefinieerd, creëert het opnieuw definiëren ervan in een afgeleide klasse (met hetzelfde prototype) een nieuwe virtuele functie in die klasse, zonder dat de virtuele specificatie hoeft te worden gebruikt.

Een afgeleide klasse kan geen functie definiëren met dezelfde naam en dezelfde set parameters, maar met een ander retourtype dan de virtuele functie van de basisklasse. Dit resulteert in een fout tijdens het compileren.

Als u in een afgeleide klasse een functie introduceert met dezelfde naam en hetzelfde retourtype als een virtuele functie in de basisklasse, maar met een andere set parameters, dan zal deze afgeleide klassefunctie niet virtueel zijn. In dit geval wordt, met behulp van een pointer naar de basisklasse, voor elke waarde van deze pointer een aanroep naar de functie van de basisklasse uitgevoerd (ondanks de virtuele specificatie en de aanwezigheid van een vergelijkbare functie in de afgeleide klasse).

Methoden (functies)

Virtuele methoden worden gedeclareerd in basisklasse met het virtuele trefwoord, en kan worden overschreven in een afgeleide klasse. De prototypes van virtuele methoden in zowel de basisklasse als de afgeleide klassen moeten hetzelfde zijn.

Door gebruik te maken van virtuele methoden kunt u het mechanisme implementeren late binding, waarbij de bepaling van de aan te roepen methode plaatsvindt tijdens runtime in plaats van tijdens compilatie. In dit geval hangt de aangeroepen virtuele methode af van het type object waarvoor deze wordt aangeroepen. Bij vroege binding, die wordt gebruikt voor niet-virtuele methoden, wordt bepaald welke methode moet worden aangeroepen tijdens het compileren.

In de compilatiefase wordt een tabel met virtuele methoden gebouwd en in de uitvoeringsfase wordt het specifieke adres ingevoerd.

Bij het aanroepen van een methode met behulp van een pointer naar een klasse geldt het volgende: volgende regels:

  • voor een virtuele methode wordt de methode aangeroepen die overeenkomt met het type object waarnaar de aanwijzer verwijst.
  • voor een niet-virtuele methode wordt de methode aangeroepen die overeenkomt met het type aanwijzer zelf.

Het volgende voorbeeld illustreert het aanroepen van virtuele methoden:

Klasse A // Basisklassedeclaratie (public: virtual void VirtMetod1(); // Virtuele methode void Metod2(); // Niet-virtuele methode 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(); // Aanroepen van de VirtMetod-methode van klasse B pB->VirtMetod1(); // Roep de VirtMetod-methode van klasse B pA->Metod2(); // Aanroepen van de Method2-methode van klasse A pB->Metod2(); // Roep de Method2-methode van klasse B aan)

Het resultaat van de uitvoering van dit programma zal zijn volgende lijnen:

B::VirtMetod1 genoemd, B::VirtMetod1 genoemd, A::Metod2 genoemd, B::Metod2 genoemd

Een puur virtuele functie is een virtuele functie die is gespecificeerd met een initialisatiefunctie

Bijvoorbeeld:

Virtuele leegte F1(int) =0;

Een klassedeclaratie kan een virtuele destructor bevatten die wordt gebruikt om een ​​object te verwijderen bepaald type. Er is echter geen virtuele constructor in C++. Een alternatief waarmee u objecten van een bepaald type kunt maken is virtuele methoden, waarin een constructor wordt aangeroepen om een ​​object van deze klasse te maken.

Een andere wijziging van de basisklasse leidt tot onverwachte gevolgen. Deze wijziging bestaat uit het wijzigen van de functiespecificatie van het basisklasselid. We gebruiken (voor de eerste keer!) de virtuele specificatie in een functiedeclaratie. Functies die met de virtuele specificatie zijn gedeclareerd, worden virtuele functies genoemd. De introductie van virtuele functies in de basisklassedeclaratie (slechts één specificatie) heeft zulke belangrijke implicaties voor de objectgeoriënteerde programmeermethodologie dat we opnieuw een gewijzigde declaratie van klasse A zullen presenteren:

Klasse A (openbaar: virtueel int Fun1(int); );

Eén extra specificatie in de functiedeclaratie en (voorlopig) geen wijzigingen meer in de declaraties van afgeleide klassen. Zoals altijd heel simpel belangrijkste functie(). Daarin definiëren we een pointer naar een object van de basisklasse, stellen we deze in op een object van het afgeleide type, waarna we de functie Fun1() aanroepen met behulp van de pointer:

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

Indien niet voor de virtuele specificatie, het resultaat van het uitvoeren van de aanroepexpressie

PObj->Fun1(1);

zou voor de hand liggen: zoals u weet, wordt de keuze van de functie bepaald door het type aanwijzer.

De virtuele specificatie verandert echter alles. De functiekeuze wordt nu bepaald door het type object waarop de basisklasse-pointer wordt ingesteld. Als een afgeleide klasse een niet-statische functie declareert waarvan de naam, het retourtype en de parameterlijst dezelfde zijn als die van een virtuele functie van de basisklasse, roept het uitvoeren van de aanroepexpressie een lidfunctie van de afgeleide klasse aan.

Er moet meteen worden opgemerkt dat de mogelijkheid om een ​​lidfunctie van een afgeleide klasse aan te roepen met behulp van een pointer naar de basisklasse niet betekent dat het mogelijk is om een ​​object “van bovenaf” te observeren vanaf een pointer naar een object van de basisklasse. klas. Niet-virtuele ledenfuncties en gegevens zijn nog steeds niet beschikbaar. En dit kan heel eenvoudig worden geverifieerd. Om dit te doen, probeert u gewoon te doen wat we al een keer hebben gedaan: een lidfunctie aanroepen van een afgeleide klasse die onbekend is in de basisklasse:

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

Het resultaat is negatief. De aanwijzer is, net als voorheen, alleen geconfigureerd voor basisfragment object van een afgeleide klasse. Toch is het mogelijk om functies van een afgeleide klasse aan te roepen. Er was eens, in de secties gewijd aan de beschrijving van constructors, de lijst met regulerende acties die door een constructor worden uitgevoerd tijdens de conversie van een toegewezen geheugenfragment naar een klassenobject. Onder deze activiteiten werd de initialisatie van virtuele functietabellen genoemd.

U kunt proberen de aanwezigheid van deze virtuele functietabellen te detecteren met behulp van de grootte van de bewerking. Natuurlijk hangt alles hier af van de specifieke implementatie, maar volgens ten minste In de Borland-versie van C++ neemt een representatief object van een klasse die verklaringen van virtuele functies bevat meer geheugen in beslag dan een object van een vergelijkbare klasse waarin dezelfde functies worden gedeclareerd zonder de virtuele specificatie.

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

Het afgeleide klassenobject krijgt dus een extra element: een verwijzing naar de tabel met virtuele functies. Het schema van een dergelijk object kan als volgt worden weergegeven (we duiden de verwijzing naar de tabel aan met de identificatie vptr, de tabel met virtuele functies met de identificatie vtbl):

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

In ons nieuwe objectdiagram is het geen toeval dat de verwijzing naar de tabel (een array van één element) met virtuele functies slechts door een stippellijn wordt gescheiden van het fragment van het object dat de basisklasse vertegenwoordigt. Het bevindt zich in het gezichtsveld van dit fragment van het object. Dankzij de beschikbaarheid van deze pointer kan de virtuele functie-operator Fun1

PObj->Fun1(1);

kan als volgt worden weergegeven:

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

Hier is alles alleen op het eerste gezicht verwarrend en onbegrijpelijk. In feite is er geen enkele uitdrukking in deze operator die ons onbekend is.

Er staat letterlijk dit:

BEL DE FUNCTIE OP INDEX 0 VAN DE TABEL MET VIRTUELE FUNCTIES vtbl (we hebben slechts één element in deze tabel), WAARVAN KAN HET STARTADRES WORDEN GEVONDEN DOOR DE INDEX vptr.

DEZE POINTER IS OP ZIJN beurt TOEGANKELIJK VIA DE POINTER pObj, GECONFIGUREERD VOOR HET MYAC-OBJECT. DE FUNCTIE WORDT DOORGEGAAN TWEE (!) PARAMETERS, WAARVAN DE EERSTE HET ADRES VAN HET MyAC-OBJECT IS (de waarde voor de this pointer!), DE TWEEDE IS EEN INTEGER WAARDE GELIJK AAN 1.

Een aanroep naar een functie van een basisklasselid wordt geleverd door een gekwalificeerde naam.

PObj->A::Fun1(1);

In deze verklaring laten we de virtuele functietabelservices achterwege. Tegelijkertijd informeren we de vertaler over onze bedoeling om een ​​lidfunctie van de basisklasse aan te roepen. Het mechanisme voor het ondersteunen van virtuele functies is strikt en zeer strikt gereguleerd. De verwijzing naar de tabel met virtuele functies is noodzakelijkerwijs opgenomen in het “bovenste” basisfragment van het afgeleide klasseobject. De pointertabel bevat de adressen van lidfuncties van het fragment op het “laagste” niveau dat declaraties van deze functie bevat.

We wijzigen opnieuw de declaratie van de klassen A, AB en declareren een nieuwe klasse ABC.

Het wijzigen van de klassen A en AB komt neer op het declareren van nieuwe lidfuncties daarin:

Klasse A (openbaar: virtuele int Fun1(int-sleutel); virtuele int Fun2(int-sleutel); ); ::::: int A::Fun2(int sleutel) ( 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; }

Deze klasse bevat een declaratie van de lidfunctie Fun1, die in de indirecte basisklasse A als een virtuele functie wordt gedeclareerd. Bovendien erft deze klasse van de directe basis de lidfunctie Fun2. Deze functie wordt in basisklasse A ook als virtueel gedeclareerd. We verklaren een representatief object van klasse ABC:

ABC MijnABC;

Het diagram kan als volgt worden weergegeven:

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

De virtuele functietabel bevat nu twee elementen. We stellen de objectaanwijzer van de basisklasse in op het MyABC-object en roepen vervolgens de lidfuncties aan:

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

In dit geval is het onmogelijk om de lidfunctie AB::Fun1() aan te roepen, omdat het adres ervan niet voorkomt in de lijst met virtuele functies en eenvoudigweg niet zichtbaar is vanaf het hoogste niveau van het MyABC-object waarnaar de pObj verwijst. wijzer is ingesteld. De tabel met virtuele functies wordt door de constructor gebouwd op het moment dat het object van het overeenkomstige object wordt gemaakt. Uiteraard zorgt de vertaler ervoor dat de constructor dienovereenkomstig wordt gecodeerd. Maar de vertaler kan de inhoud van de virtuele functietabel voor een specifiek object niet bepalen. Dit is een runtime-taak. Totdat de virtuele functietabel voor een specifiek object is gebouwd, kan de overeenkomstige lidfunctie van de afgeleide klasse niet worden aangeroepen. Dit is eenvoudig te verifiëren na een nieuwe wijziging van de klassendeclaratie.

Het programma is klein, dus het is logisch om de tekst volledig weer te geven. Laat u niet misleiden door de toegangsoperatie tot klassecomponenten::. De discussie over de problemen die met deze operatie gepaard gaan, moet nog komen.

#erbij betrekken klasse A (openbaar: virtueel int Fun1(int sleutel); ); int A::Fun1(int-sleutel) ( 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->Plezier1(2); // Roep ABC::Fun1() aan.

//pObj->Fun2(2); // Deze functie is niet toegankelijk via een pointer!!!

ABC MijnABC;

pObj->A::Fun1(2); // A::Fun1() aangeroepen. )

Nu op het moment dat het MyABC-object wordt aangemaakt

vanuit de constructor van klasse AB (en deze wordt aangeroepen vóór de constructor van klasse ABC), zal de functie A::Fun1() worden aangeroepen. Deze functie behoort tot klasse A. Het MyABC-object is nog niet volledig gevormd, de tabel met virtuele functies is nog niet gevuld en er is nog niets bekend over het bestaan ​​van de functie ABC::Fun1(). Nadat het MyABC-object uiteindelijk is gevormd, de virtuele functietabel is gevuld en de pObj-aanwijzer is ingesteld op het MyABC-object, is het aanroepen van de functie A::Fun1() via de pObj-aanwijzer alleen mogelijk met behulp van de volledig gekwalificeerde naam van dit object. functie:

PObj->Fun1(1); // Dit is een functieaanroep ABC::Fun1()! pObj->A::Fun1(1); // Uiteraard is dit een functieaanroep A::Fun1()!

Houd er rekening mee dat het rechtstreeks aanroepen van de Fun1-lidfunctie vanuit het MyABC-object een soortgelijk resultaat oplevert:

MijnABC.Fun1(1); // Roep de functie ABC::Fun1() aan.

En een poging om de niet-virtuele functie AB::Fun2() aan te roepen via een pointer naar een basisklasseobject mislukt. Er staat geen adres voor deze functie in de tabel met virtuele functies en het is onmogelijk om vanaf het bovenste niveau van het object “naar beneden te kijken”.

//pObj->Fun2(2); // Dat kun je niet doen!

Het resultaat van de uitvoering van dit programma demonstreert duidelijk de specifieke kenmerken van het gebruik van virtuele functies. Slechts een paar regels...

Fun1(125) van A. Fun1(1) van ABC. Fun1(5) van ABC. Fun2(1) uit AB. Fun1(1) van A. ========== Fun1(2) van ABC. Fun1(2) van A.

Aan de andere kant is het duidelijk dat als je een vervangende functie kunt aanroepen, je direct “via” deze functie toegang hebt tot alle functies en gegevensleden van de afgeleide klasse en vervolgens “bottom-up” tot alle niet-privéfuncties. en gegevensleden van directe en indirecte basisklassen. In dit geval komen alle niet-privégegevens en functies van de basisklassen beschikbaar via de functie.

En nog een klein voorbeeld dat de verandering demonstreert in het gedrag van een representatief object van een afgeleide klasse nadat een van de functies van de basisklasse virtueel wordt.

#erbij betrekken klasse A ( publiek: 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 ();"<

Aan het begin wordt de virtuele specificatie in de functiedefinitie A::xFun() van commentaar voorzien. Het proces van het uitvoeren van een programma bestaat uit het definiëren van een representatief object objB van een afgeleide klasse B en het aanroepen van de lidfunctie funA() voor dit object. Deze functie is overgenomen van de basisklasse, het is er één en het is duidelijk dat de identificatie ervan geen problemen veroorzaakt voor de vertaler. Deze functie behoort tot de basisklasse, wat betekent dat op het moment dat deze wordt aangeroepen, de besturing wordt overgedragen “naar het hoogste niveau” van het objB-object. Op hetzelfde niveau bevindt zich een van de functies met de naam xFun(), en het is deze functie waarover de besturing wordt overgedragen tijdens de uitvoering van de aanroepexpressie in de hoofdtekst van de functie funA(). Bovendien is het eenvoudigweg onmogelijk om vanuit de functie funA() een andere functie met dezelfde naam aan te roepen. Op het moment dat de vertaler de structuur van klasse A analyseert, heeft hij helemaal geen idee van de structuur van klasse B. De functie xFun(), lid van klasse B, blijkt onbereikbaar vanuit de functie funA().

Maar als u de virtuele specificatie in de definitie van de functie A::xFun() verwijdert, wordt er een vervangingsrelatie tot stand gebracht tussen twee functies met dezelfde naam, en zal het genereren van het object objB gepaard gaan met het maken van een tabel van virtuele functies, volgens welke de vervangende functie, een lid van klasse B, zal worden aangeroepen. Om de vervangen functie aan te roepen, moet een functie de gekwalificeerde naam gebruiken:

Ongeldig A::funA () ( xFun(); A::xFun(); )

Virtuele functie

Virtuele methode (virtuele functie) - bij objectgeoriënteerd programmeren een methode (functie) van een klasse die kan worden overschreven in onderliggende klassen, zodat de specifieke implementatie van de aan te roepen methode tijdens runtime wordt bepaald. De programmeur hoeft dus niet het exacte type object te kennen om ermee te kunnen werken via virtuele methoden: het is voldoende om te weten dat het object behoort tot de klasse of afstammeling van de klasse waarin de methode is gedeclareerd.

Virtuele methoden zijn een van de belangrijkste technieken voor het implementeren van polymorfisme. Hiermee kunt u algemene code maken die zowel met objecten van de basisklasse als met objecten van een van de onderliggende klassen ervan kan werken. Tegelijkertijd definieert de basisklasse de manier om met objecten te werken, en elk van zijn nakomelingen kan een specifieke implementatie van deze methode bieden. In sommige programmeertalen, bijvoorbeeld in het Engels. puur virtueel) of abstract. Een klasse die ten minste één dergelijke methode bevat, zal ook abstract zijn. Een object van zo'n klasse kan niet worden gemaakt (in sommige talen is dit mogelijk, maar het aanroepen van een abstracte methode leidt tot een fout). De erfgenamen van een abstracte klasse moeten implementaties bieden voor al haar abstracte methoden, anders zullen het op hun beurt abstracte klassen zijn.

Voor elke klasse die ten minste één virtuele methode heeft, a virtuele methodetabel. Elk object slaat een verwijzing op naar een tabel van zijn klasse. Om een ​​virtuele methode aan te roepen, wordt het volgende mechanisme gebruikt: er wordt een pointer naar de overeenkomstige tabel met virtuele methoden uit het object gehaald, en daaruit, met een vaste offset, een pointer naar de implementatie van de methode die voor deze klasse wordt gebruikt. Bij gebruik van meervoudige overerving of interfaces wordt de situatie iets ingewikkelder vanwege het feit dat de tabel met virtuele methoden niet-lineair wordt.

Voorbeeld

Voorbeeld van een virtuele functie in Delphi

Heel vaak wordt vergeten dat virtuele methoden worden overschreven met behulp van een trefwoord overschrijven. Hierdoor wordt de methode gesloten. In dit geval zal er geen methodevervanging in VMT plaatsvinden en zal de vereiste functionaliteit niet worden verkregen.

Deze fout wordt bijgehouden door de compiler, die een overeenkomstige waarschuwing geeft.

Een vooroudermethode aanroepen vanuit een overschreven methode

Het kan nodig zijn om een ​​vooroudermethode aan te roepen in een overschreven methode.

Laten we twee klassen declareren. Voorvader:

Voorouder = klas privé beschermd publiek(Virtuele procedure.) procedure Virtuele procedure ; virtueel; einde;

en zijn nakomeling (Descendant):

TAfstammeling = klas(TAvoorouder) privé beschermd publiek(Virtuele procedure-overlap.) procedure Virtuele procedure; overschrijven; einde;

Het aanroepen van een vooroudermethode wordt geïmplementeerd met behulp van het trefwoord ""inherited""

procedure TDescendant.VirtualProcedure; beginnen geërfd; einde;

Het is de moeite waard eraan te denken dat in Delphi de destructor moet worden overschreven. ""overschrijven""; en een oproep bevatten naar de destructor van de voorouder

TAfstammeling = klas(TAvoorouder) privé beschermd publiek vernietiger Vernietigen; overschrijven; einde; vernietiger TAfstammeling. Vernietigen; beginnen geërfd; einde;

In C++ is het niet nodig om de constructor van de voorouder aan te roepen en de destructor moet virtueel zijn. Voorouderdestructors worden automatisch opgeroepen. Om een ​​vooroudermethode aan te roepen, moet u de methode expliciet aanroepen:

Klasse Ancestor (public: virtuele leegte function1 () (printf ("Ancestor::function1" ); ) ); klasse Descendant: public Ancestor ( public : virtuele leegte function1 () ( printf ("Descendant::function1" ); Ancestor::function1 () ; // hierdoor wordt de tekst "Ancestor::function1" afgedrukt } } ;

Om een ​​voorouderconstructor aan te roepen, moet u de constructor specificeren:

Klasse Descendant: public Ancestor ( public : Descendant() : Ancestor() ; ) ;

Zie ook

Koppelingen

  • C++ FAQ Lite: Virtuele functies in C++ (Engels)

Wikimedia Stichting.

2010.

Laat ik beginnen met te herhalen: je moet geen virtuele functies aanroepen terwijl constructors of destructors actief zijn, omdat die oproepen niet zullen doen wat je denkt dat ze zullen doen, en je zult niet tevreden zijn met de resultaten. Als u een Java- of C#-programmeur bent, let dan speciaal op deze regel, omdat C++ zich in dit opzicht anders gedraagt.


Laten we aannemen dat er een hiërarchie van klassen bestaat voor het modelleren van ruiltransacties, dat wil zeggen orders om te kopen, verkopen, enz. Het is belangrijk dat deze transacties gemakkelijk te controleren zijn, dus elke keer dat er een nieuw transactieobject wordt gemaakt, moet er een passende invoer worden ingevoerd. worden gemaakt in het auditlogboek. De volgende aanpak om dit probleem op te lossen lijkt redelijk:

class Transaction ( // basisklasse voor iedereen

openbaar: // transacties

virtuele leegte logTransaction() const = 0; // voert typeafhankelijk uit

// opnemen in het protocol

Transaction::Transaction() // constructorimplementatie

( // basisklasse

logTransactie();

klasse BuyTransaction: openbare transactie ( // afgeleide klasse

// transacties van dit type

klasse SellTransaction: openbare transactie ( // afgeleide klasse

klasse BuyTransaction: openbare transactie ( // afgeleide klasse


virtuele leegte logTransaction() const = 0; // hoe inloggen


Laten we eens kijken wat er gebeurt als we de volgende code uitvoeren:


Het is duidelijk dat de BuyTransaction-constructor zal worden aangeroepen, maar de Transaction-constructor moet eerst worden aangeroepen omdat de delen van het object die tot de basisklasse behoren, worden geconstrueerd vóór de delen die tot de afgeleide klasse behoren. De laatste regel van de Transaction-constructor roept de virtuele functie logTransaction aan, en dit is waar de verrassingen beginnen. Hiermee wordt de versie van logTransaction aangeroepen die is gedefinieerd in de klasse Transaction, en niet in BuyTransaction, ook al is het type object dat wordt gemaakt BuyTransaction. Tijdens de constructie van de basisklasse worden virtuele functies die in de afgeleide klasse zijn gedefinieerd, niet aangeroepen. Het object gedraagt ​​zich alsof het tot het onderliggende type behoort. Kortom, er zijn geen virtuele functies op het moment dat de basisklasse wordt geconstrueerd.

Er is een goede reden voor dit schijnbaar onverwachte gedrag. Omdat basisklasseconstructors worden aangeroepen vóór afgeleide klasseconstructors, worden de gegevensleden van de afgeleide klasse nog niet geïnitialiseerd wanneer de basisklasseconstructor wordt uitgevoerd. Dit kan ongedefinieerd gedrag en nauwe bekendheid met de debugger veroorzaken. Toegang krijgen tot delen van een object die nog niet zijn geïnitialiseerd is gevaarlijk, dus C++ biedt deze optie niet.

Er zijn nog fundamentelere redenen. Terwijl de constructor van de basisklasse werkt aan het maken van een object van de afgeleide klasse, wordt het type object is basisklasse. Niet alleen behandelen virtuele functies dit als zodanig, maar dat geldt ook voor alle andere taalmechanismen die type-informatie gebruiken tijdens runtime (bijvoorbeeld de dynamic_cast-operator beschreven in Regel 27 en de typeid-operator). In ons voorbeeld, terwijl de transactieconstructor actief is en het basisgedeelte van het BuyTransaction-object initialiseert, is dit object van het type Transaction. Dit is hoe alle delen van C++ ermee omgaan, en dat is logisch: de BuyTransaction-gerelateerde delen van het object zijn nog niet geïnitialiseerd, dus het is veiliger om aan te nemen dat ze helemaal niet bestaan. Een object is geen object van een afgeleide klasse totdat de constructor van laatstgenoemde begint met uitvoeren.

Hetzelfde geldt voor destructors. Zodra de destructor van een afgeleide klasse begint met uitvoeren, wordt aangenomen dat de gegevensleden die tot die klasse behoren, ongedefinieerd zijn, dus gaat C++ ervan uit dat ze niet langer bestaan. Wanneer we de basisklasse-destructor invoeren, wordt ons object een object van de basisklasse, en alle delen van C++ - virtuele functies, dynamic_cast-operator, enz. - behandelen het als zodanig.

In het bovenstaande codevoorbeeld heeft de transactieconstructor rechtstreeks toegang tot een virtuele functie, wat een duidelijke schending is van de principes die in deze regel worden beschreven. Deze overtreding is gemakkelijk te detecteren en daarom geven sommige compilers een waarschuwing (terwijl andere dat niet doen; zie punt 53 voor een bespreking van waarschuwingen). Maar zelfs zonder een dergelijke waarschuwing zal de fout zich waarschijnlijk vóór runtime manifesteren, omdat de logTransaction-functie in de Transaction-klasse puur virtueel wordt verklaard. Tenzij het ergens gedefinieerd is (onwaarschijnlijk, maar mogelijk - zie Item 34), zal een dergelijk programma niet linken: de linker zal de noodzakelijke implementatie van Transaction::logTransaction niet vinden.

Het is niet altijd eenvoudig om een ​​virtuele functieaanroep te detecteren terwijl een constructor of destructor actief is. Als een transactie meerdere constructors heeft die elk dezelfde taak uitvoeren, moet u uw programma zo ontwerpen dat codeduplicatie wordt voorkomen door het gemeenschappelijke initialisatiegedeelte, inclusief de aanroep van logTransaction, in een privé, niet-virtuele initialisatiefunctie te plaatsen, bijvoorbeeld init:


klasse Transactie(

(init(); ) // roep een niet-virtuele functie aan

Virtuele leegte logTransaction() const = 0;

logTransactie(); // en dit is een virtuele oproep

// functies!


Deze code verschilt conceptueel niet van de bovenstaande, maar is verraderlijker omdat deze gewoonlijk zonder waarschuwing wordt gecompileerd en gekoppeld. Omdat logTransaction in dit geval een puur virtuele functie is van de klasse Transaction, zullen de meeste runtimesystemen het programma afbreken (meestal met een bericht) wanneer het wordt aangeroepen. Als logTransaction echter een “normale” virtuele functie is die een implementatie heeft in de Transaction-klasse, dan zal die functie worden aangeroepen en zal het programma vrolijk blijven draaien, waardoor je je afvraagt ​​waarom de verkeerde versie van logTransaction werd aangeroepen bij het maken van een afgeleide klasse voorwerp. De enige manier om dit probleem te vermijden is ervoor te zorgen dat geen van de constructors en destructors virtuele functies aanroept bij het maken of vernietigen van een object, en dat alle functies die ze aanroepen dezelfde regel volgen.

Maar hoe kunt u ervoor zorgen dat de juiste versie van log-Transaction wordt aangeroepen bij het maken van een object in de Transactiehiërarchie? Het is duidelijk dat het aanroepen van een virtuele functie van een object vanuit constructors niet geschikt is.

Er zijn verschillende opties om dit probleem op te lossen. Eén daarvan is om de logTransaction-functie niet-virtueel te maken in de Transaction-klasse, en vervolgens te eisen dat de constructors van de afgeleide klasse de informatie doorgeven die nodig is voor het loggen naar de Transaction-constructor. Deze functie kan dan veilig een niet-virtuele logTransaction aanroepen. Iets als dit:


klasse Transactie(

expliciete transactie(const std::string& logininfo);

void logTransaction(const std::string& logininfo) const; // Nu -

// niet-virtueel

// functie

Transactie::Transactie(const std::string& logininfo)

logTransactie(loginfo); // Nu -

// niet-virtueel

klasse BuyTransaction: publieke transactie (

Kooptransactie( parameters)

: Transactie(createLogString( parameters)) // geef informatie door

(...) // voor opname in het protocol

... // naar de basisconstructor

statische std::string createLogString( parameters);


Met andere woorden: als u geen virtuele functies kunt aanroepen vanuit de basisklasseconstructor, kunt u dit compenseren door de benodigde informatie door te geven aan de basisklasseconstructor van de afgeleide constructor.

In dit voorbeeld ziet u het gebruik van de statische privéfunctie createLogString in BuyTransaction. Het gebruik van een helperfunctie om een ​​waarde te creëren die moet worden doorgegeven aan de constructor van de basisklasse is vaak handiger (en beter leesbaar) dan het moeten opzoeken van een lange lijst met lidinitialisaties om de basisklasse door te geven wat deze nodig heeft. Door deze functie statisch te maken, vermijden we het gevaar dat er onbedoeld wordt verwezen naar niet-geïnitialiseerde gegevens van de klasse BuyTransaction. Dit is belangrijk omdat het feit dat deze gegevensleden nog niet zijn gedefinieerd de belangrijkste reden is waarom virtuele functies niet kunnen worden aangeroepen vanuit constructors en destructors.