Föreläsning 5
Problemet Vi har sett att vi kan ersätta de metoder vi ärver från överklassen med egen funktionalitet (polymorfism) class Bil : public Fordon { Bil(Person & owner) : Fordon(owner) {} // Den nya versionen av GetPosition skall // returnera fordonets position förskjutet // 3 enheter i bägge axlar Point GetPosition() { return Point(m_position).Offset(3,3); } };
Problemet Vad händer då en funktion som tar emot ”Fordon” får en ”Bil” som parameter? void fordonsinfo(Fordon & fordon) { // vad händer om fordon är ett objekt av // klassen ”Bil”? // skriver vi ut positionen som ges av // ”Fordon::GetPosition()” eller // ”Bil::GetPosition()”? cout << fordon.GetPosition(); };
Problemet – svar Metoden ”Fordon::GetPosition” används –Detta beror på att allt funktionen fordonsinfo vet om är att det är ett fordon som har skickats som parameter. Problemet orsakas av statisk bindning –Default-beteende i C++ –Vid kompileringstillfället bestäms det att ”Fordon::GetPosition()” skall användas
Problemet – önskat resultat I de flesta fall vill man att objektets mest aktuella metod skall exekveras, inte en gammal metod från någon överklass I vårat fall vill vi att ”Bil::GetPosition()” skall användas istället
Problemet – lösning Om man istället vid exekveringstillfället (run-time) bestämmer vilken funktion som skall användas kan vi få önskat resultat Dynamisk bindning –Vi vill använda oss av metoduppslagning
Problemet – lösning forts. C++ erbjuder en lösning för dynamisk bindning av metoder –Nyckelordet ”virtual” framför en metod säger åt C++ att metoduppslagning skall användas för denna specifika metod –”virtual” metoder ”ärvs”, dvs är en metod virtuell i överklassen så är den virtuell i underklasserna
Exempel (virtuell metod) class Fordon { public: Fordon() : m_position(0,0) {} virtual Point GetPosition() { return m_position; } protected: Point m_position; }; class Bil : public Fordon { public: Bil() {} // Inget anrop av Fordon() behövs virtual Point GetPosition() { return Point(m_position).Offset(3,3); } };
Problemet – följder Dynamisk bindning är inte lika effektivt som statisk bindning –stor anledning till att statisk bindning är default i C++ Dagens processorer hanterar dynamisk bindning mycket effektivt –Inte längre lika stor nackdel
Husdjur class Pet { public: virtual void mess_up() { // Släpp hår överallt } protected: Color m_color; };
Kanin class Rabbit : public Pet { public: // vi ärver alla metoder från Pet protected: Carrot m_favoriteCarrot; };
Hund class Dog : public Pet { public: void eat_cat(); virtual void mess_up() { // Bit sönder saker // Kräks på nya mattan } protected: Bone m_favoriteBone; };
Katt class Cat : public Pet { public: void scratch_sofa(); virtual void mess_up() { // släpa in möss // gör dessutom som alla andra Pet::mess_up(); } protected: };
Pure virtual Antag att man inte kan bestämma beteende för en virtuell funktion i en överklass –Exempelvis: Pet::mess_up() har ingen bestämd definition. Metoden kan då deklareras som rent virtuell (pure virtual) –mess_up() = 0; Tvingar alla underklasser att implementera metoden mess_up()
Exempel (pure virtual) class Pet { public: virtual void mess_up() = 0; protected: Color m_color; }; Vi finner inget passande standardbeteende för när husdjur stökar till –mess_up() blir en pure virtual metod
Exempel (pure virtual) forts. Vilket även innebär att alla underklasser måste implementera mess_up() class Rabbit : public Pet { public: virtual void mess_up() { // Släpp hår överallt } protected: Carrot m_favoriteCarrot; };
Abstrakt överklass Klass som innehåller en/flera rent virtuella metoder Klasser med rent virtuella metoder kan ej instansieras –Exempel: Husdjur, hur skulle detta objekt se ut? Underklass måste komplettera klassen för att kunna instansieras Referenser/pekare till objekt av överklassen kan ändå anropa de rent virtuella metoderna
Rent abstrakt överklass Alla medlemmar är rent virtuella (pure virtual) Definierar ett gränssnitt som arvtagare måste implementera (för att kunna instansieras)
Virtuella destruktorer När man använder virtuella metoder i en klass bör man ta som vana att även deklarera destruktorn virtuell. Om man misstänker att klassen skall användas polymorft bör man även skapa en virtuell destruktor Att inte skapa en virtuell destruktor kan i vissa fall få förödande konsekvenser
Lillhjärna class Cerebellum // Lillhjärnan { public: ~Cerebellum() { cout << ”Signing off”; } virtual void react() { fight(); eat(); sleep(); reproduce(); } void fight(); void sleep(); void eat(); void reproduce(); };
Storhjärna class Cerebrum : public Cerebellum // Storhjärnan { public: Cerebrum() { m_mem = new Memory(); } ~Cerebrum() { delete m_mem; } virtual void react() { switch(think()) { case 0: eat(); break; case 1: sleep(); break; case 2: reproduce(); break; } } int think(); // return appropriate action protected: Memory *m_mem; };
Destruktorhaveri void main() { Cerebellum * lill = new Cerebellum(); Cerebrum * stor = new Cerebrum(); squash(lill); // anropar lill-destruktor squash(stor); // anropar lill-destruktor // missar alltså stor-destruktorn pga att // destruktorn inte är virtual } void squash(Cerebellum * brain) { // statisk bindning (~Cerebellum) delete brain; }
Lösning Lösningen på problemet med felaktigt destruktoranrop är givetvis att göra destruktorn virtual
Virtuellt arv När man ärver multipelt från två eller fler klasser med gemensamma överklasser uppstår datadubblering För att undvika detta kan man ärva klasser virtuellt, vilket gör att alla klasser i arvshierarkin endast finns med i en uppsättning Fordon BilBåt Amfibiebil
Exempel (virtuellt arv) class Fordon { public: Fordon(Person & owner); Point GetPosition() { return m_position; } Person & GetOwner() { return m_owner; } protected: Point m_position; Person & m_owner; };
Exempel (virtuellt arv) class Bil : public virtual Fordon { public: Bil(int age) { m_age = age; } void Print() { cout << ”Bil, årsmodell ” << m_model; } protected: int m_model; }; class Bat : public virtual Fordon { public: Bat(int length) { m_length = length; } void Print() { cout << ”Båt, längd ” << m_length; } protected: int m_length; };
Exempel (multipelt arv) class Amfibiebil : public Bil, public Bat { public: Amfibiebil(int age, int length) : Bil(age), Bat(length) {} // för att undvika namnkonflikter måste vi // ersätta metoden Print void Print() { Bil::Print(); Bat::Print(); } }; Klassen ärver multipelt precis som tidigare (får nu en uppsättning fordonsegenskaper)
Sammanfattning – virtuellt arv Virtuellt arv måste specificeras redan på den nivån i klasshierarkin där de ärver från samma klass. Dubblering av data kan undvikas genom att ärva ”virtual” Varje datamedlemsaccess sker virtuellt –Prestandaförsämring, värre än virtuella funktioner
Interface-klasser Klasser utan datamedlemmar Mestadels rent virtuella metoder –Kan ha enklare virtuella metoder med implementation Undviker problem vid multipelt arv eftersom ingen data kan dubbleras Ärv maximalt en klass med implementation, resten Interface-klasser Namnkollisionsproblematiken kvarstår dock
Sammanfattning Polymorf metod (polymorfism) –”metod med många former” –Egenskapen att kunna ha flera olika beteenden beroende på objekt –Ersättning av metoder i underklasser Statisk bindning –Kompilatorn bestämmer vid kompileringstillfället vilken metod som skall anropas
Sammanfattning forts. Dynamisk bindning –Programmet avgör vilken metod som skall anropas först när programmet kör (run- time) –Åstadkoms i C++ genom ”virtual” –Inte lika effektivt som statisk bindning, men inte så allvarligt på dagens processorer