Virtuell metod

Den aktuella versionen av sidan har ännu inte granskats av erfarna bidragsgivare och kan skilja sig väsentligt från versionen som granskades den 9 december 2017; kontroller kräver 23 redigeringar .

En virtuell metod ( virtuell funktion ) är en metod (funktion) av en klass i objektorienterad programmering som kan åsidosättas i understigande klasser så att den specifika implementeringen av metoden som ska anropas kommer att bestämmas vid körning. Programmeraren behöver alltså inte känna till den exakta typen av ett objekt för att kunna arbeta med det genom virtuella metoder: det räcker att veta att objektet tillhör klassen eller avkomling av klassen där metoden deklareras. En av översättningarna av ordet virtuell från engelska kan vara "faktisk", vilket är mer passande i betydelsen.

Användning

Virtuella metoder är en av de viktigaste teknikerna för att implementera polymorfism . De låter dig skapa gemensam kod som kan fungera både med objekt av basklassen och med objekt av någon av dess underklasser. I det här fallet definierar basklassen ett sätt att arbeta med objekt, och vilken som helst av dess arvingar kan tillhandahålla en konkret implementering av detta sätt.

Definitionsmetod

Vissa programmeringsspråk (till exempel C++ , C# , Delphi ) kräver att du uttryckligen anger att denna metod är virtuell. På andra språk (t.ex. Java , Python ) är alla metoder virtuella som standard (men bara de metoder för vilka detta är möjligt; till exempel i Java kan metoder med privat åtkomst inte åsidosättas på grund av synlighetsregler).

Basklassen kanske inte tillhandahåller implementeringar av den virtuella metoden, utan deklarerar bara dess existens. Sådana metoder utan implementering kallas "pure virtual" (översatt från engelska  pure virtual ) eller abstrakt. En klass som innehåller minst en sådan metod kommer också att vara abstrakt . Ett objekt av en sådan klass kan inte skapas (på vissa språk är det tillåtet, men att anropa en abstrakt metod kommer att resultera i ett fel). Arvtagare av en abstrakt klass måste tillhandahålla [1] en implementering för alla dess abstrakta metoder, annars kommer de i sin tur att vara abstrakta klasser. En abstrakt klass som endast innehåller abstrakta metoder kallas ett gränssnitt .

Implementering

Tekniken att anropa virtuella metoder kallas också "dynamisk (sen) bindning". Detta innebär att metodnamnet som används i programmet är associerat med ingångsadressen för en viss metod dynamiskt (under programkörning) och inte statiskt (under kompilering), eftersom det vid kompilering i allmänhet är omöjligt att avgöra vilken av existerande metodimplementeringar kommer att kallas.

I kompilerade programmeringsspråk görs dynamisk länkning vanligtvis med hjälp av en virtuell metodtabell , som skapas av kompilatorn för varje klass som har minst en virtuell metod. Elementen i tabellen innehåller pekare till implementeringar av virtuella metoder som motsvarar denna klass (om en ny virtuell metod läggs till i den efterkommande klassen läggs dess adress till i tabellen, om en ny implementering av den virtuella metoden skapas i ättlingklass fylls motsvarande fält i tabellen med adressen till denna implementering). För adressen för varje virtuell metod i arvsträdet finns det således en fast förskjutning i den virtuella metodtabellen. Varje objekt har ett tekniskt fält som, när objektet skapas, initieras med en pekare till tabellen över virtuella metoder i sin klass. För att anropa en virtuell metod tas en pekare till motsvarande tabell över virtuella metoder från objektet, och från den, med en känd fast offset, en pekare till implementeringen av metoden som används för denna klass. När man använder multipelt arv blir situationen något mer komplicerad på grund av att den virtuella metodtabellen blir icke-linjär.

Ett exempel på en virtuell funktion i C++

Ett exempel i C++ som illustrerar skillnaden mellan virtuella och icke-virtuella funktioner:

Antag att en basklass Animal(djur) kan ha en virtuell metod eat(ät, ät, ät). En underklass (barnklass) Fish(fisk) kommer att åsidosätta metoden eat()annorlunda än en underklass Wolf(varg) skulle åsidosätta den, men du kan anropa den eat()på vilken instans som helst av en klass som ärver från klassen Animaloch få det beteende som är eat()lämpligt för den underklassen.

Detta tillåter programmeraren att bearbeta en lista med klassobjekt Animalgenom att anropa en metod på varje objekt eat()utan att tänka på vilken underklass det aktuella objektet tillhör (det vill säga hur ett visst djur äter).

En intressant detalj av virtuella funktioner i C++ är standardbeteendet för argument . När du anropar en virtuell funktion med ett standardargument, tas funktionens kropp från det verkliga objektet, och värdena för argumenten är av typen referens eller pekare.

klass Djur { offentliga : void /*icke-virtuell*/ move () { std :: cout << "Detta djur rör sig på något sätt" << std :: endl ; } virtual void eat () { std :: cout << "Djur ät något!" << std :: endl ; } virtuell ~ Animal (){} // destructor }; klass Wolf : public Animal { offentliga : void move () { std :: cout << "Wolf walks" << std :: endl ; } void eat ( void ) { // metoden eat åsidosätts och även virtuell std :: cout << "Varg äter kött!" << std :: endl ; } }; int main () { Djur * zoo [] = { ny varg (), nytt djur ()}; för ( Djur * a : zoo ) { a -> flytta (); a -> äta (); ta bort en ; // Eftersom destruktorn är virtuell, kommer förstöraren för dess klass att kallas för varje //-objekt } returnera 0 ; }

Slutsats:

Detta djur rör sig på något sätt Varg äter kött! Detta djur rör sig på något sätt Djur äta något!

Ett exempel på en analog av virtuella funktioner i PHP

Motsvarigheten i PHP är användningen av sen statisk bindning. [2]

class Foo { public static function baz () { return 'water' ; } offentlig funktion __construct () { echo static :: baz (); // sen statisk bindning } } class Bar utökar Foo { public static function baz () { return 'fire' ; } } ny foo (); // skriver ut 'vatten' ny Bar (); // skriver ut "eld"

Ett exempel på en virtuell funktion i Delphi

polymorfism av Object Pascal-språket som används i Delphi. Tänk på ett exempel:

Låt oss förklara två klasser. Förfader:

TAncestor = klass privat skyddad offentlig {Virtuell procedur.} procedur VirtualProcedure ; virtuell; procedur StaticProcedure ; slutet;

och dess ättling (Descendant):

TDescendant = klass (TAncestor) privat skyddad offentlig {Åsidosättande av virtuell procedur.} procedur VirtualProcedure; åsidosätta; procedur StaticProcedure; slutet;

Som du kan se deklareras en virtuell funktion i förfaderklassen - VirtualProcedure. För att dra fördel av polymorfism måste den åsidosättas i efterkommande.

Implementeringen ser ut så här:

{TAncestor} procedur TAncestor.StaticProcedure; Börja ShowMessage('Ancestor statisk procedur.'); slutet; procedur TAncestor.VirtualProcedure; Börja ShowMessage('Ancestor virtuell procedur.'); slutet; {TDscendant} procedur TDescendant.StaticProcedure; Börja ShowMessage('Descendent statisk procedur.'); slutet; procedur TDescendant.VirtualProcedure; Börja ShowMessage('Descendant åsidosättande procedur.'); slutet;

Låt oss se hur det fungerar:

procedure TForm2.BitBtn1Click(Avsändare: TObject); var MyObject1: TAncestor; MyObject2: TAncestor; begin MyObject1 := TAncestor .Create; MyObject2 := TDescendant .Create; Prova MyObject1.StaticProcedure; MyObject1.VirtualProcedure; MyObject2.StaticProcedure; MyObject2.VirtualProcedure; till sist MyObject1.Free; MyObject2.Free; slutet; slutet;

varLägg märke till att vi i avsnittet har deklarerat två objekt MyObject1och MyObject2typer TAncestor. Och när MyObject1de skapade skapade de hur TAncestor, men MyObject2hur TDescendant. Detta är vad vi ser när vi klickar på knappen BitBtn1:

  1. Ancestor statisk procedur.
  2. Ancestor virtuell procedur.
  3. Ancestor statisk procedur.
  4. Åsidosättande av ättlingsprocedur.

För MyObject1allt det är klart kallades de specificerade procedurerna helt enkelt. Men för MyObject2detta är det inte så.

Samtalet MyObject2.StaticProcedure;resulterade i "Ancestor static procedure.". När allt kommer omkring deklarerade vi MyObject2: TAncestor, och därför kallades StaticProcedure;klassförfarandet TAncestor.

Men samtalet MyObject2.VirtualProcedure;ledde till ett samtal VirtualProcedure;implementerat i efterkommande ( TDescendant). Detta hände för att det MyObject2inte skapades som TAncestor, utan som TDescendant: . Och den virtuella metoden åsidosattes. MyObject2 := TDescendant.Create; VirtualProcedure

I Delphi implementeras polymorfism med hjälp av vad som kallas en virtuell metodtabell (eller VMT).

Ganska ofta glömmer man bort virtuella metoder att åsidosätta med override. Detta gör att metoden stängs . I det här fallet kommer metodsubstitution inte att ske i VMT och den nödvändiga funktionaliteten kommer inte att erhållas.

Detta fel spåras av kompilatorn, som utfärdar en lämplig varning.

Ett exempel på en virtuell metod i C#

Ett exempel på en virtuell metod i C#. I exemplet används nyckelordet för att basege åtkomst till en metod på den a()överordnade (bas) klassen A .

class Program { static void Main ( sträng [] args ) { A myObj = new B (); Konsol . ReadKey (); } } // Basklass A public class A { public virtual string a () { return "fire" ; } } //Godycklig klass B som ärver klass A klass B : A { public override string a () { return "water" ; } public B () { //Visa resultatet som returneras av den åsidosatta metoden Console . ut . WriteLine ( a ()); //water //Skriv ut resultatet som returneras av metoden för den överordnade klassen Console . ut . WriteLine ( bas.a ( ) ); //eld } }

Anropa en förfadermetod från en åsidosatt metod

Det kan vara nödvändigt att anropa en förfadermetod i en åsidosatt metod.

Låt oss förklara två klasser. Förfader:

TAncestor = klass privat skyddad offentlig {Virtuell procedur.} procedur VirtualProcedure ; virtuell; slutet;

och dess ättling (Descendant):

TDescendant = klass (TAncestor) privat skyddad offentlig {Åsidosättande av virtuell procedur.} procedur VirtualProcedure; åsidosätta; slutet;

Anropet till förfadermetoden implementeras med hjälp av nyckelordet "ärvt"

procedur TDescendant.VirtualProcedure; börja ärvt; slutet;

Det är värt att komma ihåg att i Delphi måste förstöraren nödvändigtvis överlappas - "åsidosätta" - och innehålla ett anrop till förfäderförstöraren

TDescendant = klass (TAncestor) privat skyddad offentlig förstörare Destroy; åsidosätta; slutet; förstörare TDescendant. Förstöra; börja ärvt; slutet;

I C++ behöver du inte anropa förfaderns konstruktör och destruktor, destruktorn måste vara virtuell. Ancestor destructors kommer att anropas automatiskt. För att anropa en förfadermetod måste du uttryckligen anropa metoden:

klass Anfader { offentliga : virtual void function1 () { printf ( "Ancestor::function1" ); } }; klass Ättling : offentlig Anfader { offentliga : virtuell void funktion1 () { printf ( "Descendant::function1" ); Ancestor :: funktion1 (); // "Ancestor::function1" kommer att skrivas ut här } };

För att anropa en förfaderkonstruktör måste du ange konstruktorn:

klass Ättling : offentlig Anfader { offentliga : Descendant () : Ancestor (){} };


Fler exempel

Första exemplet klass Anfader { offentliga : virtual void function1 () { cout << "Ancestor::function1()" << endl ; } void function2 () { cout << "Ancestor::function2()" << endl ; } }; klass Ättling : offentlig Anfader { offentliga : virtual void function1 () { cout << "Descendant::function1()" << endl ; } void function2 () { cout << "Descendant::function2()" << endl ; } }; Descendant * pekare = ny Descendant (); Ancestor * pointer_copy = pekare ; pekare -> funktion1 (); pekare -> funktion2 (); pointer_copy -> funktion1 (); pointer_copy -> funktion2 ();

I det här exemplet definierar klassen Ancestortvå funktioner, den ena är virtuell och den andra inte. Klassen Descendantåsidosätter båda funktionerna. Det verkar dock som att samma anrop till funktioner ger olika resultat. Resultatet av programmet blir som följer:

Descendant::function1() Descendant::function2() Descendant::function1() Ancestor::function2()

Det vill säga information om typen av objekt används för att bestämma implementeringen av den virtuella funktionen, och den "korrekta" implementeringen anropas, oavsett typ av pekare. När en icke-virtuell funktion anropas styrs kompilatorn av pekaren eller referenstypen, så två olika implementeringar anropas function2()trots att samma objekt används.

Det bör noteras att i C++ är det möjligt att om nödvändigt specificera en specifik implementering av en virtuell funktion, faktiskt kalla den icke-virtuellt:

pekare -> Ancestor :: funktion1 ();

för vårt exempel, kommer att mata ut Ancestor::function1() , och ignorerar typen av objekt.

Andra exemplet klass A { offentliga : virtuell int - funktion () { retur 1 ; } int get () { returnera denna -> funktion (); } }; klass B : offentlig A { offentliga : int function () { retur 2 ; } }; #include <iostream> int main () { Bb ; _ std :: cout << b . () << std :: endl ; // 2 returnerar 0 ; }

Även om klass B inte har en get() - metod , kan den lånas från klass A , och resultatet av denna metod kommer att returnera beräkningarna för B::function() !

Tredje exemplet #include <iostream> använder namnutrymme std ; struktur IBase { virtuell void foo ( int n = 1 ) const = 0 ; virtuell ~ IBase () = 0 ; }; void IBase::foo ( int n ) const { cout << n << "foo \n " ; } IBase ::~ IBase () { cout << "Base destructor \n " ; } struct Härledd final : IBase { virtual void foo ( int n = 2 ) const override final { IBase :: foo ( n ); } }; void bar ( const IBase & arg ) { arg . foo (); } int main () { bar ( härledd ()); returnera 0 ; }

Det här exemplet visar ett exempel på att skapa ett IBase-gränssnitt. Med hjälp av exemplet med ett gränssnitt visas möjligheten att skapa en abstrakt klass som inte har virtuella metoder: när destructor deklareras som ren virtuell och dess definition är gjord av klasskroppen, försvinner möjligheten att skapa objekt av en sådan klass , men förmågan att skapa ättlingar till denna förfader finns kvar.

Utdata från programmet kommer att vara: 1 foo\nBase destructor\n . Som vi kan se togs standardvärdet för argumentet från typen av länk, och inte från den faktiska typen av objektet. Precis som förstöraren.

Det sista nyckelordet indikerar att en klass eller metod inte kan åsidosättas, medan åsidosättande indikerar att en virtuell metod explicit åsidosätts.

Se även

Anteckningar

  1. Virtuella funktioner . Hämtad 16 september 2020. Arkiverad från originalet 24 september 2020.
  2. PHP: Sen statisk bindning - Manual . php.net. Hämtad 5 november 2016. Arkiverad från originalet 8 november 2016.

Länkar