5. Arv och Polymorfism

 

 

·     Hantering av närbesläktade klasser.

 

·     Arvtagarens konstruktor.

 

·     Typomvandling på objekt och pekare.

 

·     Listor och basklasspekare.

 

·     Virtuella funktioner.

 

·     Multippelt arv.

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 


Hantering av närbesläktade klasser.

 

Vi har tidigare provat på ett enkelt klassarv när vi i kapitlet ‘Klasser och objekt’, avsnittet ‘Klassarv’ lät klassen CNyRektangel ärva av CRektangel. I det fallet gjorde vi enklast tänkbara arv. I detta avsnitt ska vi gå ett litet steg längre.

 

Hur ska vi hantera en situation där vi har nästan samma typ av data i olika objekt, och många gemensamma operationer? Vi kan t.ex. skapa klasser för att hantera anställda vid ett företag, och dessa anställda kan skilja sig t.ex. genom att några har timlön medan andra har fast lön, och försäljarna har provisions­baserad lön.

 

Gemensamt har de att de alla har namn och anställningsuppgifter, samt att alla vill ha sin lön utbetald.

 

Då skulle man kunna börja med en klass avsedd att hantera de generella delar­na. Låt oss kalla den CPerson (Inte Edward, CPersson CDan dö):

 

class CPerson

{

public:

    CPerson();

    CPerson(const char *namn);

    ~CPerson();

    char *GetNamn() const;

    char *GetAnr() const;

private:

    char m_Namn[31];

    char m_Anr[11];

};

 

Naturligtvis skulle det behövas mer data här, personnummer, adress etc. men vi håller exemplet enkelt och tar bara med namn och anställningsnummer.

 

Om vi nu behöver en klass för de som jobbar för timlön, kan vi ärva av CPerson:

 

class CJobbare : public CPerson

{

public:

    CJobbare(const char *namn);

    void SetTimLon(float timlon);

    void SetTimmar(float timmar);

private:

    float m_TimLon;

    float m_Timmar;

};


Här har vi en konstruktor som tar emot namnet och den ska naturligtvis skicka det vidare till förälderklassens konstruktor. Vi har också tillägg i form av två variabler för timlön och arbetade timmar.

 

Skulle vi nu skapa ett objekt för en ‘jobbare’ så har vi tillgång till alla medlem­mar i såväl CJobbare som CPerson:

 

CJobbare HissPojken(”Stefan Shapiro”);

char *AnstNr;

 

HissPojken.SetTimmar(40.0F);  // Medlem i CJobbare

AnstNr = HissPojken.GetAnr(); // Medlem i CPerson

 

Det är dock inte tillåtet att försöka använda en medlem som är privat i CPerson från en metod i CJobbare:

 

void CJobbare::SkrivNamn() const

{

    cout << m_Namn; // Fel! m_Namn är privat i CPerson.

}

 

Skulle vi kunna göra detta skulle datainkapslingen kunna brytas bara genom att man ärvde från en klass till en egen klass.

 

Däremot är det ju meningen att man ska kunna nå privata medlemmar via accessfunktionerna. Så här skulle man kunna lösa ovanstående uppgift:

 

void CJobbare::SkrivNamn() const

{

    cout << GetNamn(); // OK! GetNamn() är en publik funktion.

}

 

Hur gör vi med säljaren då? Han/hon har ju också namn, anställningsnummer, lön och arbetstid men dessutom provision. Då behöver vi även en medlem att beräkna provisionen på, försäljningen. Alltså bör vi ärva från CJobbare:

 

class CSaljare : public CJobbare

{

public:

    CSaljare(const char *namn);

    void SetProvision(float provision);

    void SetSaljTotal(float saljtotal);

private:

    float m_Provision;

    float m_SaljTotal;

};

 

Nu har ett objekt som skapas med CSaljare som mall tillgång till alla medlem­mar i klasserna CPerson, CJobbare samt CSaljare antingen direkt (public:) eller via accessfunktioner (private:).
Nu är det bara chefen kvar! Han har ingen fast arbetstid. I hårda tider jobbar han så mycket som det behövs, i bättre tider spelar han golf (så mycket som det behövs). Han behöver bara sin lön, vilket inte alltid är så bara. Därför ärver vi direkt från CPerson (vilken antagligen var rätt så rik):

 

class CChef : public CPerson

{

public:

    CChef(const char *namn);

    void SetManadsLon(float manadslon);

private:

    float m_ManadsLon;

};

 

Observera att chefen har månadslön, inte timlön som CJobbare. Nu har vi en klasshierarki:

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 


Observera att CPerson fungerar som ‘basklass’ för mer än en ‘arvinge’. Gene­rellt gäller att en basklass kan vara förälder till valfritt antal arvingar, såväl genom direkt arv (CJobbare och CChef) som genom arv genom flera genera­tioner (CSaljare via CJobbare).

 

Observera att medlemmar i olika grenar av klasshierarkin inte ‘känner’ varand­ra, d.v.s. har inte tillgång till varandras medlemmar vare sig de är publika eller pri­vata. Således kan inte CChef använda m_Timmar i CJobbare vilket däremot CSaljare kan. Därigenom kan man återanvända medlemsnamn i olika grenar utan kon­flikt.


Metodbyte.

 

Vi har gjort detta tidigare, men då kallade jag avsnittet ‘Utbyte av medlems­funktion’. I detta fall passar ordet ‘metodbyte’ bättre in, inte bara för att ‘metod’ är ett effektivare namn för medlemsfunktion, utan även för att just vill byta metod.

 

Betänk hur man ska beräkna en löneutbetalning för en jobbare versus en säljare. I grund och botten är det samma metod, lön gånger tid, men säljaren blir nog rätt putt om vi drar in hans provision på grund av ‘programmeringstekniska’ or­saker. Vi duckar och byter metod i stället.

 

Först måste vi dock ha en grundläggande löneberäkning i CJobbare att bygga vidare på, en med lön gånger tid:

 

 

 

 

float CJobbare::BeraknaLoneutbetalning() const

{

    return m_TimLon * m_Timmar;

}

 

 

 

 

Klassen CSaljare byter sedan ut denna metod. Vi använder samma metod som i avsnittet ‘Utbyte av medlems­funktion’. Observera att vi måste använda räck­viddsoperatorn för att referera till förälderklassens funktion, vilken måste an­vändas när vi beräknar försäljarens grundlön eftersom den beräknas från upp­gifter i privata medlemmar i den klassen:

 

 

 

 

float CSaljare::BeraknaLoneutbetalning() const

{

    return CJobbare::BeraknaLoneutbetalning() +

           m_Provision * m_SaljTotal;

}

 

 

 

 

Detta är som sagt en vanlig metod när man byter ut medlemsfunktioner. Vi kommer att se exempel på detta i MFC, när vi lär oss skriva Windowsprogram baserade på dessa.

 


Nu kan vi använda objekt av två olika typer och beräkna deras lön med samma syntax:

 

 

 

 

// Skapa två objekt av olika typ.

CJobbare a1(”Tony Curtis”);

CSaljare a2(”Roger Moore”);

...

// Erforderliga uppgifter fylls i objekten.

...

// Skriv utbetalning.

cout << a1.GetNamn() << ”, Lön: ”
     << a1.BeraknaLoneutbetalning() << ‘\n’;

cout << a2.GetNamn() << ”, Lön: ”
     << a2.BeraknaLoneutbetalning()<< ‘\n’;

 

 

 

 

Observera att vi fortfarande kan beräkna grundlönen för objektet a2 ovan, det är bara atta använda räckviddsoperatorn:

 

a2.CJobbare::BeraknaLoneutbetalning();


Arvtagarens konstruktor.

 

När man skapar ett objekt med en klass som har ärvt av en annan klass som mall kommer detta objekt att innehålla alla medlemmar i förälderklassen, för­utom medlemmarna i den klass den skapas efter. När du väl klurat ut vad jag menar med detta undrar du säkert varför jag upprepar mig. Jo, vi får inte glöm­ma att även förälderklassens medlemmar måste initieras. Alltså måste vi anropa även dess konstruktor.

 

Var gör vi det då? I arvingens konstruktor naturligtvis (hade ni väntat er något annat?). För att underlätta för oss finns det en speciell syntax för att anropa förälderns konstruktor i arvingens dito. Vi kan titta på konstruktorerna i våra klasser CJobbare och CSaljare:

 

CJobbare::CJobbare(const char *namn) : CPerson(namn)

{

    m_TimLon = 0.0F;

    m_Timmar = 0.0F;

}

 

CSaljare::CSaljare(const char *namn) : CJobbare(namn)

{

    m_Provision = 20.0F;

    m_SaljTotal = 0.0F;

}

 

Observera att CSaljare anropar konstruktorn i CJobbare, inte den i CPerson. Medlemmarna i CJobbare måste också initieras. CJobbare kommer ändå att anropa konstruktorn i CPerson, så att alla led initieras. Som synes skickas namnet vidare från CSaljare via CJobbare utan åtgärd till CPerson där det slutligen lagras i m_Namn.

 

Chefen behandlas lika summariskt som jobbaren i detta fall. Han får ju sin kompensation i golfen i stället. Skriv själv den konstruktorn.

 

Konstruktorns funktionsheader innehåller anropet till förälderklassens konstruk­tor. Detta får till resultat att alla konstruktioner läggs på hög och väntar på sina föräldrar. I praktiken utförs alltså koden i basklassen först, och sedan generation för generation i kronologisk ordning. Själva objektets konstruktor utförs alltså sist. Tänk på detta om du använder publika medlemmar från föräldrar och för­fädrer i din konstruktor.

 

Om basklassen har en förvald konstruktor (inga argument) så måste vi inte anropa den.


Typomvandling på objekt och pekare.

 

Det är tillåtet att använda tilldelning mellan förälderklass och arvinge om det inte resulterar i odefinierat data. När man gör detta sker automatiskt en typom­vandling från arvingens typ till förälderns. ‘Överblivet’ data för vilka det inte finns medlemmar i förälderklassen kopieras inte:

 

CJobbare kontorist(””); // Får inget namn.

CSaljare dorrknackare(”Sture Svärdh”);

 

// Sture svärdhs son Sture Svärdh d.y. får jobb som kontorist.

kontorist = dorrknackare; // Namnet kopieras.

 

I ovanstående fall sker automatisk typomvandling av ‘dorrknackare’ från CSal­jare till CJobbare, varefter fälten m_Namn, m_Anr, m_TimLon och m_Timmar kopieras. Däremot tillåts inte det motsatta, eftersom det skulle lämna fält oiniti­erade, nämligen m_Provision och m_SaljTotal:

 

dorrknackare = kontorist; // Error, can not convert class...

 

Minnesregel:

·     Alla försäljare är arbetare.

·     Inte alla arbetare är försäljare.

 

Man kan också implicivt, d.v.s. automatiskt utan att ange det, typomvandla en pekare på samma sätt. När man deklarerar en pekare som ska peka på ett objekt eller en strukturvariabel skapar kompilatorn automatiskt en gömd lista där ob­jektets alla offset finns representerade, d.v.s. avståndet mellan objektets position i minnet och en viss namngiven medlem.

 

CPerson *p;

CJobbare a1(”Roger Moore”);

CSaljare a2(”Tony Curtis”);

CChef    a3(”Bill Gates”);

 

p = &a1; // Typomvandling CJobbare->CPerson

p = &a2; // Typomvandling CSaljare->CPerson

p = &a3; // Typomvandling CChef->CPerson

 

När man refererar till ett objekt via en pekare når man de medlemmar som känns igen av pekarens typ. I ovanstående fall kan vi alltså endast hämta m_Namn och m_Anr från de tre objekten via pekaren p.

 

cout << p->GetNamn() << ”, Lön: ”

     << p->BeraknaLoneutbetalning() << ‘\n’;

// Fel! Pekaren p saknar offset för BeraknaLoneutbetalning().

 

Däremot finns ju objekten fortfarande kvar orörda om vi vill göra något annat med dem.

 

Det är alltså viktigt att hålla reda på pekarens typ. Om man kallar en metod som finns på flera ställen i klasshierarkin väljs den som harmonierar med pekaren. Finns ingen sådan får man kompileringsfel. Kalla därför inte pekarna för t.ex. ‘p’ som jag gjort ovan. Det är bättre att märka pekaren med typen i dess namn, t.ex. pJobbare, pSaljare, pChef etc.

 

Man kan explicivt typomvandla på ‘fel håll’ när man arbetar med pekare. Det är dock programmerarens ansvar att inte använda de extra offsets som inte finns i det objekt den pekar på:

 

// Detta är OK endast om p verkligen pekar på ett objekt

// av typen CChef!

cout << p->GetNamn() << ", Lön: "

     << ((CChef*)p)->BeraknaLoneutbetalning() << ‘\n’;

 

Skulle p peka på ett objekt av annan typ kan detta framkalla ‘memory viola­tion’, d.v.s. operativsystemet stoppar vårt program med hänvisning till att det försökt använda minne som det inte självt allokerat, eller andra slumpmässiga fel.

 

Kompilatorn har ingen metod att varken upptäcka eller varna oss för sådana misstag, so, check your targets! Man vet aldrig vart en ‘vild’ pekare pekar. Eller som dom mycket riktigt fick det till:

 

-Se upp med vart du pekar med den där slangbellan!

-Lugn chefen. Hon är säkrad, gamla Bettan.


Listor och basklasspekare.

 

Vare sig man använder länkade listor eller vanliga dito är det praktiskt att an­vända en pekare deklarerad med basklassens typ för att komma åt grunddata i listans olika element. Man kan t.ex. använda en pekare avsedd för CPerson till att peka på alla objekt i en lista, trots att listan innehåller objekt av alla de typer vi ärvt från den klassen, CJobbare, CSaljare och CChef.

 

Dessutom har vi nytta av att det är tillåtet att typomvandla från en pekare av­sedd för någon av de ärvande klasserna till en pekare mot basklassen. Om vi t.ex. skulle skapa en klass som hanterar en länkad lista för alla typer av personer på företaget, så behöver vi bl.a. en funktion som lägger in nya objekt. En sådan funktion behöver naturligtvis ha adressen till det objekt som ska läggas in i lis­tan, och den erhålls via en pekare. Vilken typ av pekare ska funktionen ta som argument? Jo, basklassens pekare:

 

class CPersonLista

{

public:

    CPersonLista();

    int Ny(CPerson* nyperson);

    ...

private:

    ...

};

 

Observera att vi inte deklarerat hela klassen här, jämför ‘TeleLista’ i kapitel 4.

 

När vi sedan använder listan får vi tillgång till implicit typomvandling när vi anropar Ny:

 

// Skapa listobjekt och objektspekare.

CPersonLista Anstallda;

CJobbare *pJobbare;

CSaljare *pSaljare;

CChef *pChef;

 

// Skapa ett par objekt.

pJobbare = new CJobbare(”Urban Turban”);

pSaljare = new CSaljare(”Bengt Ärligh);

pChef = new CChef(”Stig Nilsson”);

 

// Lägg in dem i listan.

Anstallda.Ny(pJobbare); // Typomvandling CJobbare* -> CPerson*

Anstallda.Ny(pSaljare); // Typomvandling CSaljare* -> CPerson*

Anstallda.Ny(pChef);    // Typomvandling CChef* -> CPerson*

 

Naturligtvis kan man p.s.s. skapa en lista enligt tidigare exempel i kapitel 4, men denna gången använder vi basklasspekaren:


void SkrivLista(CPersonLista inlista)

{

    CPerson * aktuell;

    CPersonFunktioner PersonFunktioner(inlista);

    aktuell = PersonFunktioner.GetFirst();

    cout << aktuell->GetAnr() << ' '

         << aktuell->GetNamn() << '\n';

    while(aktuell = PersonFunktioner.GetNext())

    {

        cout << aktuell->GetAnr() << ' '

             << aktuell->GetNamn() << '\n';

    }

}

 

Som vi ser fungerar det på samma sätt (förutsatt att vi skapat en vän-klass som heter CPersonFunktioner och som innehåller de funktioner vi behöver). Skill­naden ligger i uttrycken:

 

aktuell = PersonFunktioner.GetFirst()

 

och

 

aktuell = PersonFunktioner.GetNext()

 

Skillnaden syns inte på uttrycken, men beroende av vilken typ av objekt p pekar på när ett sådant uttryck utförs sker typomvandling automatiskt.

 

Denna metod ger alltså bara tillgång till de medlemmar som finns deklarerade i klassen CPerson. Vi skulle t.ex. inte kunna beräkna säljarens lön och ta med den i listan.

 

Detta skulle kunna göras om det fanns en metod att införa funktionen Berakna­Loneutbetalning() redan i basklassen. Men gjorde vi det skulle vi ju få samma algoritm för löneberäkning för alla anställda, och det skulle inte Säljaren gilla, han vill gärna ha en förmånligare beräkning än jobbaren.

 

Detta löser vi med s.k. ‘Skenbara’ funktioner, ‘Virtual functions’ eller som alla andra förstör vårt svenska språk genom illegal språkimport: ‘Virtuella funktion­er’. Vi tar och tittar på dessa i nästa avsnitt.


Virtuella funktioner.

 

Virtuella funktioner är metoder avsedda att bytas ut i ärvande klasser. När man sedan anropar en virtuell funktion via en pekare som pekar på basklassen kom­mer i stället den ärvande klassens funktion att anropas. Detta är alltså tvärt emot hur vanliga metoder uppträder.

 

Man deklarerar en virtuell funktion genom att ange nyckelordet ‘virtual’ först i funktionsprototypen/deklarationen i en basklass. Man måste sedan inte ange detta nyckelord när man deklarerar ärvande klass, det blir implicivt en virtuell funktion när man byter ut en dito.

 

Följande funktioner kan inte deklareras som virtuella:

 

·     Statiska medlemsfunktioner.

·     Globala funktioner.

 

Observera att man fortfarande kan anropa basklassens funktion på vanligt sätt, m.h.a. räckviddsoperatorn. Detta är det vanligt att man gör inifrån den funktion i den ärvande klassen som byter ut den i basklassen.

 

Nu skulle vi kunna lägga till en virtuell funktion för vår klass CPerson. Vi dek­larerar en prototyp i klasshuvudet:

 

virtual float BeraknaLoneutbetalning() const;

 

Vi skulle eventuellt kunna tänkas specificera en metod i en ärvande klass som byter ut BeraknaLoneutbetalning(), vilken gör något som måste städas upp i destruktorn. Därför är det säkrast att vi förser CPerson med en virtuell dest­ruktor i stället för den vanliga. När ett objekt av en ärvd klass går ur ‘scope’ kallas normalt först den ärvda klassens konstruktor, och sedan basklassens. Har man då något som städas m.h.a. ‘delete’ kan det innebära att man försöker frigöra samma minne två gånger (är vi nu där igen...)

 

Lägg till nyckelordet ‘virtuell’ framför den ock­så. Nu ska vi beskriva basklassens BeraknaLoneutbetalning(). Den kan behövas om man anropar funktionen för ett objekt där funktionen inte finns. Lämpligt är att man bara returnera värdet 0,0:

 

float CPerson::BeraknaLoneutbetalning() const

{

    return 0.0F;

}

 

Därmed kan vi skriva en lönelista enligt samma modell som personallistan i föregående avsnitt:

 

void SkrivLoneutbetalningar(CPersonLista inlista)

{

    CPerson * aktuell;

    CPersonFunktioner PersonFunktioner(inlista);

    aktuell = PersonFunktioner.GetFirst();

    cout << aktuell->GetAnr() << ' '

         << aktuell->GetNamn() << ' '

         << aktuell->BeraknaLoneutbetalning() << '\n';

    while(aktuell = PersonFunktioner.GetNext())

    {

        cout << aktuell->GetAnr() << ' '

             << aktuell->GetNamn() << ' '

             << aktuell->BeraknaLoneutbetalning() << '\n';

    }

}

 

Här kommer olika versioner av BeraknaLoneutbetalning() att anropas beroende av vilken typ av objekt pekaren aktuell pekar på.

 

Om man inte vill skriva en onödig funktion i basklassen, d.v.s. om den ändå inte gör något, kan man skriva en s.k. ‘äkta virtuell funktion’, ‘pure virtual function’. Då skriver man ingen funktionsdeklaration, bara en prototyp i klasshuvudet. Prototypen efterföljs av ett likhetstecken och en nolla:

 

virtual float BeraknaLoneutbetalning() const = 0;

 

Den äkta virtuella funktionen fungerar endast som flerformig (polymorf) plats­hållare för utbyte (override). En klass som innehåller äkta virtuella funktioner kan inte användas för att skapa objekt, endast för arv! Klassen sägs nu vara ‘abstrakt’. Ärvande klasser måste byta ut funktionen, annars blir de också abstrakta.

 

Observera att man kan tillämpa detta även om man inte har tillgång till källkod­en för basklassen. Detta genom s.k. dynamisk bindning. Man kan även lägga till nya klasser för nya typer och länka dessa till projektet utan att man ens har till­gång till källkoden för det program som anropar de virtuella funktionerna.

 

 

 

 

 

 

 

 



Övningsuppgift:

·      När du läst igenom detta kapitel så här långt skapar du ett nytt projekt Firma.

·      Skapa de klasser som diskuterats ovan, dock ej CKonsult.

·      Varje klass ska ha sin egen headerfil till klassbeskrivningen och sin egen implementeringsfil till metodbeskrivningarna.

·      Imlementeringsfilerna behöver innehålla includesatser för motsvarande headerfil, och filen Firma.cpp, som innehåller main, behöver includesatser för alla klasshuvuden.

·      Testa varje klass efterhand p.s.s. som här i häftet, d.v.s. enligt följande:

·      CPerson och CJobbare: skapa objekt och använd elementoperatorn för att läsa anställningsnumret och att skriva ut ‘<namn> - <anställningsnummer> (se sidan 3).

·      CJobbare och CSaljare: skapa objekt och använd elementoperatorn för att ange timlön och timmar för bägge objekten samt total försäljning för säljaren. Skriv ut en lönelista (se sidan 5).

·      CChef: p.s.s. endast ett objekt, skriv lönelistan (en rad).

·      Prova att kopiera ett objekt av typen CSaljare till ett tomt objekt av typen CJobbare och skriv ut det senare för att se att namnet följt med. (Här träder impliciv typomvandling in, det som finns extre i CSaljare kopieras inte. Se sidan 7.)

·      Testa impliciv typomvandling via pekare. Skapa en pekare av typen CPerson och tre objekt av de tre övriga typerna. Kopiera adressen för ett objekt till pekaren. Pekaren kan nå metoder definierade i CPerson, men kräver expliciv typomvandling för att kunna nå övriga metoder i objektet. Testa genom att fylla i lönegrundande uppgifter för objektet och skriva ut en rad till en lönelista. Upprepa ovanstående för de övriga två objekten (se sidan 7).

·      Innan du fortsätter med nästa uppgift, vilken grundar sig på denna, bör du kanske be din handledare om ett facit till denna uppgift, Firma (1), så att du inte påbörjar den med felaktiga förutsättningar.

 

Övningsuppgift:

·      Öppna projektet Firma om det inte redan är öppet.

·      Börja med att skapa en lista som kan innehålla pekare av typen CPerson (lista på anställda) m.h.a. CPersonLista, en ny klass som du skapar enligt en modell motsvarande CTeleLista i kapitel 4. Du behöver även en klass CPersonFunk­tioner samt helst en separat headerfil med en deklaration av max antal poster i listan (så att man bara behöver ändra på ett ställe i listan om man behöver utöka listan). Vi arbetar alltså inte med en länkad lista.

·      Byt ut innehållet i main() enligt nedan.

·      Det ska det finnas en liten meny där man kan välja att mata in anställda, skriva ut en lista på anställda, att mata in uppgifter om försäljning per säljare, samt att skriva ut lönelistan.

·      Använd en basklasspekare för att anropa metoder i listans objekt. Om en metod saknas i det objekt listans aktuella post avser, kommer basklassens metod att anropas i stället. Detta händer t.ex. om man försöker ange provisionsgrundande försäljning för ett objekt av typen CChef eller Jobbare, därför måste basklassen innehålla en virtuell metod att använda när detta inträffar. Den metoden kan eventuellt via en utskrift varna användaren för att operationen inte gick att utföra: ”Denna person har inte provision!”, och kan i så fall inte vara äkta virtuell.

·      Den metod som beräknar en löneutbetalning däremot, kan med fördel vara äkta virtuell.

·      En jobbare som arbetar 40.0 timmar i veckan får betalt för 8 timmar per dag. För enkelhets skull förutsätter vi att det är exakt 4 veckor i varje månad. Vi hanterar bara 5-dagars arbetsvecka, ingen övertid och ingen frånvaro, också det för enkelhets skull.

 


Multippelt arv.

 

Vi har tidigare bara sysslat med arv från en förälderklass. Det är också tillåtet i C++ att ärva från flera klasser. Man skriver bara fler klasser åtskilda med kom­matecken. Om vi t.ex. anställer en försäljningschef som själv även säljer, d.v.s. han är både chef och säljare:

 

class CSaljChef : public CSaljare, public CChef

{

...

}

 

CSaljChef ärver alla medlemmar i såväl klassen CSaljare som CChef. Man får inte ange samma klass flera gånger i listan, det skulle ge oss multippel deklara­tion av medlemmar. Detta problem uppstår i och för sig ändå, eftersom såväl CSaljare som CChef i sin tur har ärvt från samma klass, CPerson. Vi har däri­genom dubbel uppsättning av dess medlemmar. Metoderna finns ändå bara i en uppsättning i minnet, men samma problem uppstår för dessa att veta vilka med­lemmar de ska arbeta mot.

 

Man kan därför inte direkt använda varken medlemmar eller metoder, utan man måste specificera genom att via räckviddsoperatorn tala om via vilken klass vi avser att nå basklassen:

 

cout << SaljChef1.CChef::GetNamn();

 

Samma problem kan uppstå även om vi inte ärver av samma basklass mer än en gång, nämligen om vi ärver av två olika klasser/basklasser i vilka det finns medlemmar definierade med samma namn. Problemet löses på samma sätt.

 

Ärver vi samma basklass mer än en gång måste vi se upp med vad som händer vid typomvandling. Vi kan t.ex. tänka oss vad som händer om vi vill omvandla en pekare avsedd för klassen CSaljChef till att vara avsedd för CPerson:

 

CPerson *pPerson;

CSaljChef * pSaljChef;

 

pPerson = pSaljChef // Error ambiguous reference...

 

Kompilatorn vet inte vilken kopia av basklassens medlemmar som ska använd­as. Man kan lösa konflikten m.h.a. en expliciv typomvandling:

 

pPerson = (CChef*)pSaljChef;

 

Inte ens försäljningschefer håller sig med dubbla namn och anställningsnum­mer, så det är ju slöseri med utrymme att ha dubbla medlemmar. Man kan und­vika detta genom att deklarera basklassen som en ‘virtuell basklass’. Detta gör man genom att använda nyckelordet ‘virtual’ i de klasser som ärver direkt av basklassen:

 

class CJobbare : public virtual CPerson

{

...

};

 

class CChef : public virtual CPerson

{

...

};

 

Det är alltså bara dessa klasser som behöver använda nyckelordet ‘virtual’. Klassen CSaljare ärver inte direkt från CPerson. Man säger att CPerson är en indirekt basklass till CSaljare.

 

Nu har CSaljChef endast en kopia av basklassen. Denna metod kräver dock rätt så mycket extra bearbetning, och rekommenderas inte om man vill skriva pro­gram som ska vara optimala med avseende på snabbhet.