Göm menyn

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.

Angående vector i uppgifterna

Tänk inte för mycket på vector, då uppgifterna handlar om arv och polymorfi inte vector!
Läs endast detta avsnitt om du känner dig förvirrad över vector. Det ska gå att lösa uppgifterna utan att veta hur vector fungerar men det kan ändå vara bra att ha en grundläggande förståelse för allt som händer i koden.

I vissa av dessa uppgifter används något som kallas en vector, det är en typ av lista som finns definierat i C++ som tillåter oss att lägga in värden av en specifik datatyp längst bak i listan.

För att få tillgång till vector måste vi inkludera paketet <vector>. Det fulla namnet av en vector är std::vector.

Nedan följer ett exempel på hur vector fungerar:

      
    vector<int> v { 1, 2, 3 }; // lägg in värden 1, 2 och 3 i listan

    v.push_back(4); // lägg in värdet 4 längst bak i listan

    int first_element { v[0] }; // ta ut ett element på en viss position med [ ]

    // loopa igenom hela listan
    for (int i{0}; i < v.size(); ++i)
    {
        cout << v[i] << endl;
    }
      
    

Vi kan specificera vilken datatyp som vectorn innehåller genom att stoppa in det innanför <...>, det kan vara vad som helst, inklusive våra egna klasser. Exempel:

      
    class My_Class
    {
    public:
        int function() 
        { 
            return 1; 
        }
    };

    vector<My_Class> v{};
    v.push_back(My_Class{}); // skapa en temporär My_Class och stoppa in den i v
    int result { v[0].function() }; // vi kan komma åt medlemmar från v[0] med . operatorn
      
    

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
    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<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

Du har klasserna:
    
    class Race
    {
    public:
      virtual void eat() = 0;
      ...
    };

    class Human : public Race
    {
    public:
      void eat() override;
      ...
    };

    class Orc : public Race
    {
    public:
      void eat() override;
      ...
    };
    
Nu skapar du:
    
    class Uruk_Hai : public Human, public Orc
    {
    public:
      ...
    };

    Uruk_Hai* uruk = new Uruk_Hai;
    uruk->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<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: Christoffer Holm, Simon Ahrenstedt
Senast uppdaterad: 2023-10-26