TDP004 Objektorienterad programmering
Förberedelseuppgifter Arv och Polymorfi
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 den en topphastighet och att antal hjul, men dessutom ett antal sittplatser där passagerarna har möjlighet att lämna fordonet. 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:
double evaluate(double a, double b) const override { return a * b; }
};
class Add : public Binary_Operator
{
public:
double evaluate(double a, double b) const override { return a + b; }
};
int main()
{
vector<Binary_Operator> v{ Multiply{}, Add{} };
for ( int i{0}; i < v.size(); ++i )
{
cout << v[i].evaluate(5.0, 3.0) << endl;
}
return 0;
}
Programmet skriver nu ut:
0 0Fö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 8En 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<Menu_Item*> 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
CRC står för Class, Responsibility, Collaboration. I vilket sammanhang används detta och varför är det viktigt?Uppgift 7
OOA och OOD är metoder för att komma fram till vilka klasser som bör finnas i ett objektorienterat program. Förklara hur man går tillväga i en objektorienterad analys. Vad är viktigast att tänka på i varje steg?Uppgift 8
Förklara med ett eget exempel i C++ skillnaden mellan klassförhållandena composition, aggregation, arv och association.Uppgift 9
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<Observer*> 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.
Sidansvarig: Eric Ekström
Senast uppdaterad: 2024-11-01