Uppgifter relaterade till arv och polymorfi
Följande uppgifter behandlar den del av objektorienterat tänkesätt -
och implementation av detta i C++ - som låter dig som programmerare
skapa ett "standard" grundkoncept som sedan kan "importeras" i mer
specialicerade klasser, både för att lagra mer data och få ett
beteende som skiljer sig från grundklass och systerklasser.
Uppgift 1
I den här uppgiften gäller följande fakta, inget mer och inget mindre.
Ett fordon har ett antal hjul (kan vara 0) och en topphastighet. Alla
fordon kan köras, t.ex. från A till B. Ett passagerarfordon har även
det en topphastighet och att antal hjul, men dessutom ett antal
sittplatser och kan utrymmas. Ett fraktfordon har slutligen (utöver
topphastighet och antal hjul) en maxlast i kilogram och kan lastas
samt lastas av.
Beskriv klasserna fordon, passagerarfordon och fraktfordon i C++ med
hjälp av publikt arv. Vänta med konstruktorer till uppgift (2).
Uppgift 2
Skapa lämpliga (alla medlemmar ska initieras) konstruktorer för
klasserna i uppgift (1). För alla fordonstyper är det givet att
topphastigheten (i konstruktorn) begränsas till 100km/h om antalet
hjul överstiger 6. Koden för denna kontroll ska inte upprepas.
Uppgift 3
Till varje klass i uppgift (1), lägg till en funktion som returnerar
en kort beskrivning av fordonstypen. Lös detta med polymorfi utan att
lägga till ytterligare medlemsvariabler.
Du behöver bara redovisa det du lägger till i varje klass och du får i
denna uppgift lägga till funktionsdefinitionen direkt i
klassdefinitionen.
Uppgift 4
Antag du har följande klasser:
class Binary_Operator
{
public:
virtual double evaluate(double a, double b) const { return 0.0; }
};
class Multiply : public Binary_Operator
{
public:
virtual double evaluate(double a, double b) const { return a * b; }
};
class Add : public Binary_Operator
{
public:
virtual double evaluate(double a, double b) const { return a + b; }
};
int main()
{
vector v{ Multiply(), Add() };
for ( Binary_Operator o : v )
{
cout << o.evaluate(5.0, 3.0) << endl;
}
return 0;
}
Programmet skriver nu ut:
0
0
Förklara hur programmet (inte) fungerar och visa de ändringar du
behöver göra för att få polymorfin att fungera och programmet att
skriva ut:
15
8
En bra start är att göra basklassens evaluate
"pure virtual" vilket
kommer få till följd att kompilatorn reagerar på nästa fel. Detta görs
med " = 0;
" istället för funktionsdefinition och är mer rimligt
eftersom det inte finns någon uppenbar implementation till en icka
namngiven binär operator.
Uppgift 5
Du har följande klasser:
class Menu_Item
{
public:
Menu_Item(string const& t) : title(t) {}
virtual void execute() = 0;
private:
string title;
};
class Menu : public Menu_Item
{
public:
Menu(string const& t) : Menu_Item(t) {}
~Menu() { // delete all items in list }
void add_menu_item(Menu_Item* i) { item_list.push_back(i); }
void execute() { // user chose one menu item and execute i }
private:
vector item_list;
};
Genom att skapa en subklass till Menu_Item
för varje önskat
menyalternativ går det att bygga upp ett menysystem:
Menu_Item* start = new Menu("Start");
start->add_menu_item(new Load_Game("Läs in sparat spel"));
start->add_menu_item(new Save_Game("Spara spelet"));
start->add_menu_item(new Highscore("Poängställning"));
Menu_Item* options = new Meny("Alternativ...");
start->add_menu_item(options);
options->add_menu_item(new Keyboard_Settings("Tangentbordsalternativ"));
options->add_menu_item(new Graphic_Settings("Grafikalternativ"));
options->add_menu_item(new Sound_Settings("Ljudalternativ"));
start->execute();
delete start;
Varför är det speciellt viktigt att ha en virtuell destruktor i
klassen Menu_Item
?
Lägg till virtuell destruktor och förklara skillnaden med och utan
virtuell destruktor.
Uppgift 6
Du har klasserna:
class Race
{
public:
virtual void eat() = 0;
...
};
class Human : public Race
{
public:
void eat();
...
};
class Orc : public Race
{
public:
void eat();
...
};
Nu skapar du:
class Uruk_Hai : public Human, public Orc
{
public:
...
};
Uruk_Hai* uruk = new Uruk_Hai;
uru->eat();
Rita ett klassdiagram och förklara utifrån det vad du nu har för
problem med dina Uruk_Hai
. Visa sedan hur du i C++11 kan lösa
problemet med ett using-direktiv i din Uruk_Hai
-klass.
Du märker att inte alla problem är lösta om du sedan försöker med:
Race* r = new Uruk_Hai;
Det problemet går att lösa med virtuellt arv men lämnas till en annan
kurs. Undvik hellre multipelt arv i denna form som bästa lösning.
Uppgift 7
Arvsituationen i uppgift (6) är att be om problem. Bättre nytta av
multipelt arv kan vi ha med så kallade mix-in-classes, eller
egenskapsklasser på svenska. Visa hur du med en gemensamma basklassen
Race och egenskapsklasserna Sun_Insensitive
, Strong
, Intelligent
och
Repulsive
kan skapa klasserna Human
, Orc
och Uruk_Hai
utan att i någon
ras få med basklassen mer än en gång. Du behöver inte visa några
deklarationer inuti klasserna.
Visa sedan hur du i följande funktion kan avgöra om du har med en
motbjudande gäst att göra:
int eat_dinner(Race* dinner_guest)
{
// Tips: dynamic_cast är mycket mer användbart än typeid
}
Uppgift 8
I ett spel har du en klass som då och då ändrar status. Varje
statusändring kan påverka ett antal andra olika klasser som då kan
behöva uppdateras. Ett exempel kan vara en datamängd som samtidigt
visualiseras som cirkeldiagram, stapeldiagram, graf och tabell. Så
fort den underliggande datamängden ändras måste de klassinstanser som
hanterar vardera diagram uppdatera sin bild.
Du vill nu definiera ett gränssnitt (interface) som deklarerar de
funktioner som behövs för att en klass skall bli övervakningsbar
(kunna skicka notiser om ändringar till de som lyssnar) och de
funktioner som krävs för att vara observatör (kunna få meddelanden om
ändringar eller "lyssnare").
En klass som skall vara observatör implementerar nu de funktioner som
är deklarerade i gränssnittet för en sådan. En pekare till observatören
läggs till en lista hos den klass som skall övervakas. När sedan den
övervakade klassen ändras anropar den helt enkelt en funktion som går
igenom listan med observatörer och meddelar var och en om att någon
ändring skett.
Typiskt sett har en observatör en funktion, t.ex. "notify" som den
övervakade klassen kan anropa efter ändring, och den övervakade
klassen har funktioner för att lägga till och ta bort observatörer och
en funktion som går igenom listan och anropar "notify" för varje
observatör.
Gränssnittet för en observatör är nu de deklarationer som behövs för att
standardisera hur observatören ska få notis om ändringar i det
övervakade objektet.
Gränssnittet för det övervakade objektet är de deklarationer som behövs
för att hantera observatörer (genom att använda funktionerna i
observatörernas interface.
Därmed kan alla klasser som implementerar (definierar funktionerna i)
gränssnitt läggas till i listan för alla klasser som kan övervakas.
För att göra en klass till observatör behöver den bara "ärva in"
gränssnitt för en observatör och implementera funktionerna i det, och
en klass som behöver bli övervakningsbar kan "ärva in" funktionerna i
det gränssnitt (som dock kan ha färdiga definitioner enligt nedan
exempel).
Egenskapsinterfacet som gör något övervakningsbart skulle se ut
ungefär som följer:
class Observable
{
public:
Observable() : obs_list() {}
void add_observer(Observer* o) { obs_list.push_back(o); }
void remove_observer(Observer* o) { ... }
protected:
void notify_all()
{
for (Observer* o : obs_list)
o->notify();
}
private:
vector obs_list;
};
class Data : public Observable
{
...
};
Visa hur du deklarerar ett interface för en observatör (används som
Observer
i koden ovan) i C++. Förklara sedan hur du skapar t.ex. en
klass för stapeldiagram (bar chart) att vara en observatör och lägger
till den i en instans av den övervakade Data-klassen. Du behöver bara
visa de delar som har med observatörklassen att göra, och du kan
använda "..." som ersättning för andra detaljer.
Vi kan även notera att Observable-klassen inte tar ansvar för några
pekare själv, utan får dem utifrån. Följaktligen bör den inte heller
köra delete. Ansvaret för delete faller enligt god regel på den klass
eller funktion som ursprungligen körde new. Det är ju inte alls säkert
att pekarna i "obs_list" ens erhölls via new.