14.
Klasser och objekt
1. Objekt.
2. Klasser.
9. Klassarv.
10. Klasshierarki.
11. Utbyte av medlemsfunktion.
Ett objekt är enligt svensk grammatik en sak eller ett föremål, någonting konkret eller abstrakt. Objektet har diverse attribut, det vill säga egenskaper som beskriver det. Det kan vara färg, storlek, ålder etcetera. Man kan också oftast göra olika saker med ett objekt. Vanligt är även att det kan göra ett och annat självt. Objektet har metoder för sådant som ska göras. En bil kan startas, svängas, stannas etcetera. Den kan blinka själv om vi aktiverar körriktningsvisaren, och motorn behöver bara kontrolleras, när den väl är igång. Den kan själv tända tändstiften och suga in bensin etcetera i rätt takt. Vi kan alltså dela in objektets egenskaper i två kategorier, attribut och metoder.
Definition: Ett objekt är någonting konkret eller abstrakt. Objektet har vanligen attribut och metoder. Attributen beskriver objektets 'utseende' medan metoderna beskriver vad objektet kan göra, på kommando eller på egen hand. |
Övningsuppgift 14.1.1:
· En klocka har olika attribut, till exempel typ, och olika metoder.
· Man kan ställa en klocka så att den visar rätt tid. Är detta ett attribut eller en metod?
· Ett gammalt väggur brukar kunna slå ett antal slag varje timme, för att tala om vilken timme det är. Är detta en metod som den utför på kommando, eller utför klockan detta på egen hand?
·
Räkna upp några olika tänkbara värden för
attributet 'typ' som nämndes i första punkten:
_________________________________________________________
_________________________________________________________
_________________________________________________________
·
Räkna upp några andra tänkbara attribut
för en klocka:________________
_________________________________________________________
_________________________________________________________
_________________________________________________________
·
Räkna upp några andra tänkbara metoder
för en klocka:_______________
_________________________________________________________
_________________________________________________________
_________________________________________________________
Man kan betrakta det mesta i vår omgivning som objekt. När man skriver ett datorprogram måste man identifiera de olika objekt som programmet kommer att arbeta med. Ska man skriva ett lönesystem finner man genast att man kommer att arbeta med objekt för de olika anställda, objekt för själva lönen, objekt för de konton som lönen kommer ifrån, och kanske de bankkonton lönerna betalas ut genom. Det finns inga fasta regler för vilka objekt man 'måste' identifiera, utan det är behovet och arbetsmetoderna som styr.
Skriver man till exempel ett objektorienterat ritprogram finner man behov av olika geometriska figurer, pennor, penslar, paletter etcetera.
Övningsuppgift 14.1.2:
· Tänk dig att du planerar ett objektorienterat ritprogram. Du funderar på att definiera objekt för geometriska figurer, pennor och penslar. Diskutera gärna följande punkter med dina kurskamrater, och fyll sedan i:
·
Räkna upp några tänkbara attribut för en
geometrisk figur:______________
_________________________________________________________
_________________________________________________________
_________________________________________________________
·
Räkna upp några tänkbara attribut för en
penna:_____________________
_________________________________________________________
_________________________________________________________
_________________________________________________________
·
Räkna upp några tänkbara attribut för en
pensel:_____________________
_________________________________________________________
_________________________________________________________
_________________________________________________________
Andra objekt kan vara till exempel en matematisk beskrivning av en geometrisk figur, ett banklån, en personuppgift, ett matrecept etcetera. När vi baserar våra program på olika objekt kallas denna programmeringsmetod Objekt-Orienterad Programmering, OOP. Som vi ska se i nästa avsnitt beskrivs ett objekt av en sorts mall, vilken kallas en klass, vilket i sin tur blir class på engelska.
När man skapat en klass har man skapat en ny datatyp. Objektet skapar man genom att deklarera en variabel av den nya datatypen, på samma sätt som vi deklarerar vanliga variabler med hjälp av de enkla datatyperna 'int', 'char' etcetera. Vi ska se mer även på detta i nästa avsnitt.
Definition: Ett objekt beskrivs av en klass (engelska class). Klassen beskriver en ny datatyp. Man skapar ett objekt genom att deklarera den som en variabel av denna nya datatyp. |
Övningsuppgift 14.1.3:
· Räkna upp några andra tänkbara objekt för vilka du kan tänka dig såväl olika attribut som metoder:
·
Objekt:___________________________________________________
Attribut:___________________________________________________
_________________________________________________________
_________________________________________________________
Metoder:_______________________________________________________
_________________________________________________________
Objekt:___________________________________________________
Attribut:___________________________________________________
_________________________________________________________
_________________________________________________________
Metoder:_______________________________________________________
_________________________________________________________
Objekt:___________________________________________________
Attribut:___________________________________________________
_________________________________________________________
_________________________________________________________
Metoder:_______________________________________________________
_________________________________________________________
För att göra ovanstående abstrakta diskussion något mer konkret kan vi diskutera ett exempel med en matematisk modell av en geometrisk figur: en cirkel. Vi börjar med att arbeta med den på traditionellt vis. Vi skapar en struktur för de variabler vi är intresserade av, och lägger till de funktioner som behövs.
En cirkel har, matematiskt sett, tre variabler: en radie, en omkrets samt en yta. Dessa tre variabler hänger intimt samman med varandra genom enkla samband, enkla matematiska beräkningar. Man kan t.ex. med hjälp av en känd radie beräkna omkretsen genom sambandet:
omkrets = 2 * PI * radie;
Där PI kan vara definierad som en konstant 3.141592654:
#define PI 3.141592654
Man kan också konstatera att:
yta = PI * radie * radie;
Därmed har vi ett recept på objektet Cirkel:
I. Ett antal attribut (variabler):
· radie
· omkrets
·
yta
II. Ett antal metoder (funktioner):
· beräkna omkrets från radie
·
beräkna yta från radie
Naturligtvis kan man tänka sig att ha fler variabler och funktioner i ett objekt som kallas cirkel, till exempel kanske vi vill beräkna radien om ytan är känd, eller så vill vi kanske kunna rita upp cirkeln på bildskärmen, och då kan det vara önskvärt med färg på cirkeln, tjocklek på linjen, fyllnadsfärg etcetera, till exempel om man gör ett objektorienterat ritverktyg.
Sammanfattning:
Ett objekt är en komplex variabel, vilken beskrivs av en klass. En klass är en egendefinierad datatyp.
Definition: Ett objekt kan beskrivas av sina attribut, till exempel längd, bredd, färg etcetera, och av de operationer objektet kan utföra, till exempel beräkna yta, byt färg samt rita sig på en bildskärm. En sådan beskrivning kallas en klass (engelska class). Klassen i sig är inte ett objekt, bara en mall som beskriver hur ett objekt ska se ut. När vi deklarerar en klass skapar vi en ny datatyp. |
En skillnad mellan C och C++ är att man designade C++ speciellt på så sätt att det skulle vara så effektivt som möjligt att skapa egendefinierade typer, vilka enkelt kunde handha datakontroll, datasäkerhet, flexibilitet, egen version av olika operatorer etcetera.
I C++ skapar man både den nya datatypen och de operationer som är avsedda för data i en och samma modul, en klass, 'class'. En klass består av databeskrivningar, attributen, och beskrivningar av de operationer som utförs på data, metoderna.
En klass är en mall för ett objekt på samma sätt som en struktur är en mall för en egendefinierad strukturtyp. Den första skillnaden man lägger märke till ligger i klassens förmåga att ta med funktioner i beskrivningen.
En klassdeklaration liknar en strukturdeklaration, åtminstone till sin huvudsakliga utformning. Skillnaden ligger i att man byter ut nyckelordet 'struct' mot 'class', samt att man kan ha många fler finesser i klassen, till exempel att man kan gömma data i en klass. De mest slående är som sagt att man kan ha funktioner i klassen.
Låt oss testa hur långt vi kommer med en vanlig struktur när vi försöker beskriva cirkeln vi diskuterade ovan:
struct cirkel
{
float fRadie;
float fOmkrets;
float fYta;
};
Man kan inte initiera en variabel i en struktur, till exempel PI som borde initieras till 3.141592654. Eftersom strukturen bara är en mall för hur variablerna ska läggas upp, talar vi här bara om typer, inte variabler. Variablerna skapas först när man deklarerar en variabel av den nya struct-typen. Lägg märke till att detta även gäller för klasser. Däremot kan man som vi senare ska se lägga in initieringar när själva objektet skapas. I en struktur är det alltså meningslöst att ta med en variabel för PI, medan det kan vara användbart i en klass.
Med hjälp av denna mall kan vi skapa variabler av typen 'struct cirkel':
struct cirkel
scFrisbee;
struct cirkel
scRing;
Bägge dessa variabler har sin egen radie, sin egen omkrets och sin egen yta. Man kan t.ex. använda elementoperatorn för att lägga in en radie för Frisbee:
scFrisbee.fRadie = 24.0F;
Vad de däremot inte har är inbakade funktioner för beräkning av omkrets och yta, vilket skulle kunna göras när radien är känd.
I vanlig C++ programmering lägger man nu till fristående funktioner, vilka kan anropas med anropsparametrar och svara med returvärde:
float RadieTillYta(float r)
{
return PI * r * r;
}
Ovanstående funktion skulle kunna anropas i huvudprogrammet på till exempel nedanstående sätt (om vi redan deklarerat strukturen cirkel någonstans) . Man skulle då kunna använda elementoperatorn för att nå de olika elementen:
void main()
{
struct cirkel scRing;
scRing.radie = 12.0F;
scRing.yta = RadieTillYta(scRing.radie);
}
Men vi skulle kunna göra på ett annat sätt, vilket faktiskt bäddar för problem:
void main()
{
struct cirkel scRing;
float Radie;
Radie = 12.0F;
scRing.fRadie = Radie;
...
scRing.fYta = RadieTillYta(Radie);
}
Vad skulle hända om vi råkade ändra variabeln 'Radie' mellan de två sista raderna. Vi kan ju t.ex. göra ändringar i programmet där vi flyttar lite på koden, eller gör ett funktionsanrop mellan dessa två rader, och får variabeln ändrad. Då har vi en struktur med inkoncistent data, det vill säga data som inte passar ihop.
Tidigare nämndes att man kan gömma data i en klass. Vi ska strax titta på denna metod, men just nu konstaterar vi att ovanstående exempel gav oss ett problem, och det här med att gömma data faktiskt löser detta problem. Hur, ska vi se senare. Att gömma data kallas inkapsling, engelska: encapsulation.
Definition: I en klass kan man gömma data genom att kapsla in variablerna. Detta kallas inkapsling, datagömning eller encapsulation. Detta förhindrar felaktig användning. Korrekt användning sker via metoder. |
Vi som programmerar i C++ har tillgång till klasser, och nu ska vi definiera vår första klass. Vi ska skapa en klass som kan hantera vår cirkel. En sådan klass kan göras på många olika sätt, och här nedan demonstreras ett av dessa. I kommande exempel varierar vi metoderna litet. Innan vi diskuterar detaljerna kan vi ta en titt på hur hela klassen deklareras, vi ska se på det så kallade klasshuvudet:
class CCirkel
{
public:
CCirkel(float r);
void SetRadie(float r);
float GetRadie(void);
void VisaYta(void);
void VisaOmkrets(void);
~CCirkel();
private:
void BeraknaYtaOchOmkrets(void);
float m_Radie;
float m_Omkrets;
float m_Yta;
float m_PI;
};
Detta var mycket på en gång, men låt oss dissekera klassen och se på en bit i taget. Till att börja med har vi en övergripande struktur som överensstämmer med en struct-beskrivning:
struct Cirkel
{
variabeldeklarationer...
};
class CCirkel
{
deklaration av variabler och funktioner...
};
Vi har alltså bytt ut nyckelordet 'struct' mot 'class'. Därefter kommer skillnaden att alla klasser av hävd får ett extra inledande C i namnet, för att man ska se att det är en klass. Detta är inte nödvändigt, men det är en bra praxis. Inom parentes sagt har Microsoft hållit sig till denna standard, medan Borland har valt prefixet 'T', för 'type'. (Borland är en konkurrent till Microsoft, och levererar ett eget utvecklingsverktyg för C++ och Windowsprogrammering.)
I strukturdefinitionen kan vi bara lägga in variabler, medan vi har både variabler och funktioner och/eller funktionsprototyper i klassdefinitionen. I klassen CCirkel finns endast funktionsprototyper, vilket är det vanligaste. Själva funktionskoden, den så kallade implementationen eller genomförandet deklarerar man vanligen efter klasshuvudet, vilket vi som sagt kallar ovanstående klassdeklaration.
Definition: Ett objekt beskrivs i en mall. Mallen kallas klass. Metoderna i klassen brukar inte deklareras i klassbeskrivningen, utan förekommer vanligen som prototyper. Den delen av klassbeskrivningen kallas klasshuvud. Klasshuvudet avslutas med ett semikolon. Efter klasshuvudet skrivs funktionerna i sin helhet. |
Om vi betraktar den del där deklarationerna finns ser
vi två nya nyckelord, 'public:' och 'private:'.
class CCirkel
{
public:
deklaration av variabler och funktioner...
private:
deklaration av inkapslade variabler och funktioner...
};
Observera att vi kan betrakta varje objekt som ett eget sammanhang, ett scope. Följande gäller för ovanstående nyckelord:
· Alla funktioner och variabler som deklarerats efter 'public:' kan användas direkt i det sammanhang där objektet är känt, de är externt kända.
· Alla funktioner och variabler som deklarerats efter 'private:' är gömda för det sammanhang där objektet är deklarerat, de blir inkapslade i de objekt som skapats med denna klass som mall.
· Man kan använda 'public:' och 'private:' omväxlande, de fungerar som switchar som tillgängliggör respektive gömmer funktioner och variabler (metoder och attribut).
· När inget annat angivits gäller 'private:', men skriv ändå ut ordet 'private:' för tydlighets skull.
· Det finns ett tredje nyckelord som heter 'protected:', vilket får sin mening först vid klassarv (se avsnitten 'Kompilerade klasser samt 'Klassarv'). En medlem som är 'protected:' i en förälderklass kan bli tillgänglig i en ärvande, men vara gömd för de objekt man skapar med hjälp av den ärvande klassen.
Deklarationerna gäller vanliga variabeldeklarationer, funktionsdeklarationer eller funktionsprototyper (funktionerna måste i så fall deklareras senare) samt två speciella funktioner som har samma namn som klassen:
public:
CCirkel();
...
~CCirkel();
Dessa två funktioner kallas 'constructor' respektive 'destructor' . De måste inte finnas med, men finns vanligen ändå. De behöver inte innehålla någon kod. Finns det någon kod, så är det den kod som man vill ska utföras när man skapar ett objekt, respektive när man tar bort det.
Vi hade ju ett litet problem när vi deklarerade vår cirkel med hjälp av en struktur. Det gick inte att vara säker på att de olika variablerna hade värden som 'passade ihop'. En viss radie motsvarar alltid en viss omkrets och en viss yta. När man skapar ett objekt baserat på en klass, skulle det vara bra om man genast får kontroll över att variablerna har korrekta värden. Därför har man skapat constructorn.
Till att börja med konstaterar vi att dessa ligger under 'public:'. De måste nämligen kunna anropas 'utifrån'. De har samma namn som klassen, men den sista har ett '~' framför namnet. (Detta tecken kallas 'tilde' och finns till höger om 'Å' på tangentbordet. Man måste hålla nere 'Alt Gr'. Tecknet syns inte förrän man skriver nästa tecken.)
Constructorn anropas automatiskt när ett objekt skapas. Man kan till exempel passera ett argument för radien till constructorn, och låta denna beräkna korrekta värden för omkrets och yta. Härigenom har man gjort så att det inte går att skapa ett objekt med inkoncistent data.
Om man sedan har kapslat in alla variabler är man säker på att det inte går att förändra variablerna så att objektet får inkoncistent data. Ska man kunna förändra objektets attribut lägger man till metoder för dessa, så kallade accessmetoder. SetRadie() är tänkt som en sådan metod. Genom att anropa den ska man kunna förändra radien. Om man nu ser till så att SetRadie() uppdaterar övriga variabler på samma sätt som vi behöver göra i constructorn, så har vi även här sett till så att objekten alltid har koncistent data.
Uppenbarligen är constructorn vanligt förekommande. Destructorn är däremot inte lika vanlig, men förekommer oftare ju mer komplexa klasser man skapar. Tanken är att man ska ha en väldefinierad punkt att 'städa' efter sig när objektet ska förstöras. Om man till exempel skapat något med hjälp av 'new', så behöver man ta bort det med hjälp av 'delete'. Av den anledningen har man sett till så att destructorn anropas automatiskt när objektet förstörs, till exempel när programmet lämnar det sammanhang objektet skapades i.
Definition: En constructor anropas automatiskt när objektet skapas. En destructor anropas automatiskt när objektet tas bort. |
Constructor och destructor får inte ha någon returtyp. De har ingenstans att returnera något värde. De får inte ens deklareras med typen 'void'. Anger man någon returtyp, om så bara 'void', får man ett kompileringsfel.
Låt oss titta vidare i vårt klasshuvud. Resten är de funktioner och variabler som vi vill att objekt som skapas med denna klass som mall ska innehålla. Dessa funktioner och variabler kallas medlemmar: medlemsfunktioner och medlemsvariabler. Man brukar använda 'm_' i början av medlemsvariablerna. Medlemsfunktioner kallas också ibland för 'metoder', den term vi använt ovan, liksom medlemsvariablerna ibland kallas 'attribut'.
Normalt brukar man definiera alla klasser i headerfiler, en headerfil för varje klass och en tillämpningsfil för medlemsfunktionerna till varje klass. Tillämpningsfilen kallas på engelska implementation file. I detta exempel skulle vi lagt klassdefinitionen av CCirkel i en fil kallad cirkel.h och medlemsfunktionerna i cirkel.cpp. Om man har större projekt har man dessutom en separat tillämpningsfil för huvudprogrammet. Nu nöjer vi oss dock med att lägga allt i samma fil:
Först eventuella kompileringsdirektiv. Vi kommer att använda 'cout', så vi måste ta med filen 'iostream.h'. Därefter kommer klasshuvudet enligt ovanstående. Därefter kommer implementationen av metoderna, och till sist skriver vi vårt huvudprogram, main(), där vi ska testa vår nya klass.
Nu kan vi ta ett par exempel på hur de olika funktionerna kan se ut. Vi börjar med vår constructor. Den anropas som sagt automatiskt när ett objekt skapas, och den tar ett argument för radien. Radien ska lagras, m_PI initieras och m_Yta beräknas:
// Constructor för klassen CCirkel:
CCirkel::CCirkel(float r)
{
// Initiera m_Radie med angivet värde.
m_Radie = r;
// Initiera m_PI:
m_PI = 3.14159F;
// Initiera m_Yta:
BeraknaYtaOchOmkrets();
}
Som synes ska man ange en radie när objektet skapas. Här sätts medlemsvariabeln m_Radie till angivet värde redan i constructorn. Anropet till BeraknaYtaOchOmkrets() garanterar att vi inte skapar ett objekt där m_Radie, m_Yta och m_Omkrets inte stämmer överens med varandra. Det var ju detta som den gamla metoden med struktur och lösa funktioner inte kunde garantera. Här är alltså lösningen. I klasshuvudet finns en prototyp som heter SetRadie(). Den är uppenbarligen avsedd att ändra cirkelns radie. I den funktionen måste man också anropa BeraknaYtaOchOmkrets() om vi ska behålla denna fördel.
Lägg också märke till att det är tillåtet att initiera variabler i metoderna: m_PI tilldelas här värdet 3.141592654. Detta utförs alltså först när respektive objekt skapas. Kom ihåg att det inte gick att initiera variabler i en struct-definition eftersom denna bara är en mall, inte egentliga variabler, så dessa kunde inte initieras. Metoderna är dock kod, som kopieras in i minnet vid första objekt man definierar av klassens typ, och kan alltså innehålla initieringar.
Destructorn innehåller däremot ingen kod. Vi får ändå skriva den tomma funktionen, och här gör vi det bara för att se hur det kan se ut. Det är vanligt att man skapar en tom destructor för att senare fylla i kod:
// Tom destructor:
CCirkel::~CCirkel()
{
}
Om man deklarerat en prototyp för destructorn in klasshuvudet och sedan inte deklarerat själva implementationen (den du ser här ovan) kommer kompilatorn inte att se något fel. Länkaren däremot vill lägga in ett anrop där objektet förstörs, vilket till exempel kan vara där main() tar slut, om objektet skapats i main(). Då söker länkaren efter implementationen, och ger oss ett felmeddelande som anger att den inte hittar funktionen. Detta kan verka förvirrande eftersom vi inte själva anropat den. Skriv därför alltid destructorns implementation om du skrivit en prototyp i klasshuvudet.
Lägg märke till att vi talar om vilken klass funktionen tillhör genom att ange klassens namn före funktionens namn, åtskilda av ett dubbelt kolon (::), en så kallad räckviddsoperator (Scope Resolution Operator). Därigenom är det alltså möjligt att ha samma namn i flera olika klasser, man kan alltid skilja dem åt på detta sätt.
När man deklarerade en lokal variabel med samma namn som en global dito, blev den globala variabeln 'osynlig' inom räckvidden för den miljö (funktion) där den deklarerades. I C++ har vi möjlighet att referera till den globala variabeln i alla fall, genom att ange räckviddsoperatorn före variabelnamnet. Den fungerar som en metod att sträcka sig utanför aktuellt sammanhang, och här har vi utökat metoden genom att 'titta in' i sammanhanget för klassen CCirkel.
Då kan vi fylla på med resten av funktionerna. Vi vill gärna kunna ändra radien i efterhand:
// SetRadie() ändrar radien på ett existerande objekt:
void CCirkel::SetRadie(float r)
{
// Lagra angiven radie i m_Radie:
m_Radie = r;
// Se till att radie, yta och omkrets stämmer överens:
BeraknaYtaOchOmkrets();
}
Vi har också en funktion som endast används internt i klassen, nämligen funktionen BeraknaYtaOchOmkrets(). Den är därför deklarerad under 'private:' i klasshuvudet. Så här kan vi till exempel skriva implementationen av BeraknaYtaOchOmkrets(). Lägg märke till att det inte syns här att den är deklarerad som private:
// Funktion som beräknar ytan och omkretsen från radien.
void CCirkel::BeraknaYtaOchOmkrets()
{
m_Yta = m_PI * m_Radie * m_Radie;
m_Omkrets = m_PI * m_Radie * 2;
}
Vi har en funktion för att visa storleken på ytan:
// Utskriftsfunktion, visar ytan:
void CCirkel::VisaYta(void)
{
cout << "Cirkelns yta är: " << m_Yta << '\n';
}
Och därmed har vi nästan alla bitarna klara. Nu är det bara att prova detta genom att skapa objekt i main(), vilket vi ska göra strax, i nästa avsnitt: Skapa objekt.
Som nämnts anropas destructorn när objektet förstörs. Detta sker alltså när programmet lämnar det sammanhang där objektet skapades, eller, om objektet skapats med hjälp av 'new', när man gör 'delete' på det. Här har man möjlighet att städa sådant som behöver städas, t.ex. om man allokerat minne kan man som sagt återlämna det här.
Observera än en gång att både constructor och
destructor är typlösa. De har ingenstans att returnera något värde, och man
får inte ens ange att de är av typen 'void'. En constructor returnerar inte ett
värde, den skapar ett objekt. En destructor returnerar inte heller ett värde,
den förstör ett objekt.
Man skapar ett objekt på samma sätt som man deklarerar en variabel av en viss typ. Om vi har deklarerat klassen CCirkel kan vi deklarera t.ex. objektet Frisbee på följande sätt:
CCirkel Frisbee(250.0F);
Skillnaden ligger i att vi kan passera en variabel till constructorn, i detta fall ett värde av typen float, som ska lagras i m_Radie. Jämför detta med hur vi skapar en heltalsvariabel:
int iTal;
Detta sker alltså på den plats där vi vill använda objektet. I vårt fall blir det i main(). Vår källkodsfil bör därför nu innehålla i nämnd ordning:
· Includesatser och dylikt efter behov.
· Klasshuvudet.
· Funktionernas implementering i valfri ordning.
· Huvudfunktionen main().
Vi kan fortsätta med att skapa fler objekt. Här nedan en ring:
CCirkel Ring(12.5F);
Varje gång vi skapar ett objekt används klassen som mall för att lägga upp medlemsfunktionerna, medlemsvariablerna, samt att anropa det nya objektets constructor.
Varje objekt blir alltså ett fristående objekt. Ändrar man i det ena objektets medlemsvariabler påverkar det inte andra objekt.
Alltså kan vi be t.ex. medlemsfunktionen VisaYta() i respektive objekt att tala om för oss vilken yta respektive cirkel har. Det gör vi i nästa avsnitt.
Funktionerna däremot delas av alla objekt. För t.ex. tre olika objekt av klassen CCirkel förekommer alla variabler i tre uppsättningar tillsammans med respektive objekt, medan funktionerna lagras i endast en uppsättning i minnet. Detta kallas för att man återanvänder koden. Vi märker inte någonting av detta, för oss verkar det som om alla objekt har var sin uppsättning funktioner. När ett objekt aktiverar en funktion meddelar det nämligen funktionen vilket objekts medlemmar den ska arbeta med.
Vi skapade två objekt ovan. I minnet bör alltså nu finnas två uppsättningar attribut och en uppsättning metoder:
I nästa avsnitt ska vi titta på hur man kan anropa metoderna i respektive objekt.
I tidigare avsnitt skapade vi två objekt, Frisbee och Ring, av klassen CCirkel. Nu är det fritt fram att använda de publika medlemsfunktionerna VisaYta() och SetRadie(). Vi kan t.ex. visa hur stor yta varje objekt har genom att anropa metoden VisaYta() i respektive objekt. Man använder helt enkelt elementoperatorn på samma sätt som när man vill använda olika element i en struktur:
Frisbee.VisaYta();
Ring.VisaYta();
Om vi vill ändra radien i objektet ring kan vi göra det med hjälp av metoden SetRadie(), och på samma sätt som tidigare se vad detta ger för yta:
Ring.SetRadie(10.0F);
Ring.VisaYta();
Nu har vi diskuterat så mycket teori att det är nödvändigt att testa allt ovanstående. Allt vi diskuterat hittills kan skrivas in i en enda fil om man vill. Gör man detta så är det en bra metod att börja med klasshuvudet. Glöm inte att avsluta klassbeskrivningen med ett semikolon! Därefter deklarerar man metoderna, och sist kommer main(). All kod som behövs till nedanstående övningsuppgift finns i ovanstående avsnitt (3 - 5). Observera att utskriftens två första rader nedan är en kombination av utskrift från main() (objektens namn) och utskriften i VisaYta().
Övningsuppgift 14.5.1:
· Skapa projektet cirkel.
· Deklarera klassen CCirkel.
· Skriv ett program som använder klassen CCirkel på samma sätt som diskuterats ovan.
· Utskriften ska se ut så här:
· Observera att texter som "Frisbee: " och "Ring: " skrivs ut från main(). Endast utskriften "Cirkelns yta är: (värde)" kommer från klassen.
I klasshuvudet finns en funktionsprototyp som heter GetRadie(). Den har vi varken använt eller skrivit implementering till. I nedanstående uppgift kan du testa att själv göra detta. Meningen är att funktionen ska returnera m_Radie, och detta kan man använda i en utskriftssats i main(). Vill du skriva elegant kan du anropa GetRadie() i utskriftssatsen i stället för att mellanlagra värdet i en temporär variabel. Den utskrift som talar om att radien ändras ska finnas i main(), liksom all annan utskrift utom den i VisaYta().
Övningsuppgift 14.5.2:
· Öppna projektet cirkel.
· Lägg till en implementering av funktionen GetRadie().
· Testa i main att läsa av radien för objektet Ring före och efter det att vi ändrat den.
· Utskriften bör nu se ut så här:
Övningsuppgift 14.5.3:
· Öppna projektet cirkel om det inte redan är öppet.
· I klassen CCirkel finns även en prototyp för funktionen VisaOmkrets(). Skriv implementationen på egen hand och testa att använda den i main().
Överkurs:
De attribut som deklarerats under private: kan som sagt inte användas direkt på objektet via elementoperatorn. Detta kallas inkapsling, engelska encapsulation. De metoder som används för att komma åt attribut som deklarerats under private: kallas ibland accessfunktioner. Det är i accessfunktionerna man bygger in de kontroller som garanterar att objektet innehåller korrekt data. Vi ska utveckla detta vidare i ett exempel med klassen CDatum, men först en varning.
De som läst noga om referensvariabler bör nog varnas för att inte vara alltför kreativa: Använd inte accessfunktioner med referensvariabel som returvärde. Det öppnar den inkapslade medlemsvariabeln för direkt påverkan, eftersom funktionsanropen blir en alias för medlemsvariabeln. Om man t.ex. skapar accessfunktionen GetRadie() så här:
// GetRadie har referens till int som returvärde:
int
&GetRadie()
{
return m_Radie;
}
Då skulle man kunna ändra m_Radie i objektet Ring så
här:
Ring.GetRadie() = 17;
Detta förstör inkapslingen av data, vilket var en av huvudidéerna
med OOP. I övrigt tar vi inte upp referensvariabler i denna kurs.
Överkursuppgift 14.5.4:
· Projekt klot.
· Skapa en klass i separat fil klot.h.
· Klassen ska ta emot radien och specifika vikten (densiteten).
· Man ska kunna fråga efter diameter, omkrets, volym samt massa.
· Skapa en main() i klot.cpp, vilken skapar ett standardklot med diametern 1 meter och specifika vikten 1 kg / liter.
·
Man ska bli presenterad en meny där man
kan välja mellan att:
1 - Ange ny radie.
2 - Ange ny specifik vikt.
3 - Visa uppgifter.
4 - Avsluta.
·
Om man väljer 'Visa uppgifter' ska man få
följande skärmutskrift:
Ett klot med radien radie
m och specifika
vikten specifik vikt kg/l har
följande data:
Diameter: diameter m.
Omkrets.: omkrets m.
Massa...: massa kg.
Det är vanligt att man överlagrar constructorn. En orsak kan vara att man vill kunna skapa objekt på olika villkor. Om vi tittar på exemplet med CCirkel, så skulle vi kunna förse ett objekt med en standardradie när objektet skapas, i stället för att man anger en radie. Man kanske vill bestämma radien vid ett senare tillfälle, men vi ska absolut inte tillåta att objekt skapas med felaktiga data. En cirkel kan t.ex. inte ha en negativ radie. Man skulle då kunna lägga till en constructor som inte tar några argument alls. En sådan constructor kallas default constructor, den förvalda constructorn eller standardconstructorn.
Vilken radie ska man då ange för en cirkel om man vill ge den ett korrekt värde? Frågar man en matematiker finns det en självklar standardradie, nämligen den i enhetscirkeln. Denna har en radie av 1. Alltså lägger vi till en andra constructor i CCirkel, en som inte tar några argument, och deklarerar sedan dess kod:
CCirkel::CCirkel()
{
m_Radie = 1.0F;
m_PI = 3.141592654F;
BeraknaYtaOchOmkrets();
}
Som synes ser den i övrig exakt likadan ut som den constructor vi redan deklarerat.
Övningsuppgift 14.6.1:
· Skapa en överlagrad konstruktor till CCirkel enligt ovanstående modell.
· Testa den från main(). Om man frågar efter ytan borde den bli 3.141592654.
Och nu ett exempel med datum. Först ska vi se hur man löste saker och ting med traditionella metoder. När man skriver på 'vanligt' sätt kan man tänkas vilja representera ett datum med en struktur, sedan skapa en strukturvariabel, och initiera dess element:
struct datum
{
int aar;
int manad;
int dag;
}
struct datum foedelse_datum;
foedelse _datum.aar = 1963;
foedelse _datum.maanad = 5;
foedelse _datum.dag = 17;
Nu kan man ju inte skriva ut detta datum bara genom att skicka 'foedelse_datum' till cout. Man måste antingen skicka varje element för sig, eller skriva en egen utskriftsfunktion (som i sin tur kan använda cout). Låt oss kalla den visa_datum():
void visa_datum(struct datum * d)
{
static char cMaanad[][10] =
{"Januari", "Februari", "Mars",
"April", "Maj", "Juni",
"Juli", "Augusti", "September",
"Oktober", "November", "December"};
cout << d->dag << cMaanad[(d->maanad) - 1] << d->aar;
}
Vill man till exempel jämföra två datum måste man jämföra ett element i taget, och använda viss logik i jämförelsen. Om man jämför 1995-12-10 med 1996-01-05 är både dag och månad lägre för det andra datumet trots att det första inträffade tidigare. På samma sätt som vid utskrift kan man skapa en egen funktion som tar två pekare till struct datum som argument, och returnerar ett svar. Skulle svaret kanske kunna fungera som returvärdet från strcmp()?
Övningsuppgift 14.7.1:
· Skapa ovanstående datum-struktur.
· Skriv en jämförelsefunktion. Slå upp strcmp() i hjälpen och använd samma modell för returvärdet. (Du ska alltså inte använda strcmp(), du ska bara använda samma modell för returvärdet!)
· Låt ditt program be användaren ange två datum, mata in dessa i två struct datum-variabler och jämför dem med din funktion.
· Programmet ska sedan tala om vilket datum som inträffade först. Om datumen är lika ska det rapporteras i stället.
· Programmet ska fortsätta med att ta emot datumpar tills användaren anger en nolla som svar. (Du kan även använda metoden med 'smart cin-användning om du kommer ihåg hur den fungerar.)
Metoden att använda en struktur har två uppenbara nackdelar:
Man har ingen garanti för att data är korrekt. Strukturen ovan kan t.ex. innehålla ett datum 31 februari 9119. Man kan då inte lätt se vilken del av programmet som skapat detta felaktiga datum. Det kan även vara något som kompilerats i förväg, där vi kanske inte ens har källkoden. Användaren kanske slår fel när han matar in datum.
När man väl börjat använda 'datum' i sina program har man bundit sig till det format man bestämt. Det går inte lätt att byta format till t.ex. någon form av packat datumformat, som man gjort i time_t. (Slå upp time_t i hjälpen så får du se ett exempel på packat datumformat.)
Skulle man dessutom få in ett månadsnummer utanför intervallet 1 - 12, skulle utskriftsrutinen uppföra sig konstigt i och med att vi använder månadsnummer som index i en lista.
Men nu kan vi ju det här med klasser, så vi skapar
raskt en sådan för vårt datum. Den skulle kunna se ut så här:
class CDatum
{
public:
CDatum(int a, int m, int d);
void VisaDatum();
private:
int m_Aar;
int m_Maanad;
int m_Dag;
};
Här finns i stort sett vad vi hade ovan, men vi måste
naturligtvis även skriva funktionerna. I klassdeklarationen brukar man bara
skriva funktionsprototyper, för tydlighets skull. Dessutom bryr vi oss inte om
destructorn denna gång. Vi har alltså två metoder constructorn och
utskriftsfunktionen. Constructorn tar tre argument, år, månad och dag.
Innan vi går vidare är det på sin plats att diskutera
en mera vanlig strategi för de filer i vilka en klass beskrivs. Själva
klasshuvudet, vilket vi ser ovan, brukar stå i en headerfil. Den kan vi i detta
fall kalla datum.h.
Implementationen, det vill säga den kod som står i funktionerna, skriver man i
en implementeringsfil, och den bör i så fall heta datum.cpp. Den ska man kunna kompilera för sig, och den ska därför
tas med i projektet. Själva main() skrivs i en fil som har samma 'förnamn' som
projektet. Vi kommer att skapa ett projekt för att testa klassen CDatum med,
så låt oss kalla projektet 'datumtest', och därmed skrivs main() i en fil som
heter datumtest.cpp. Båda dessa
filer måste naturligtvis innehålla satsen #include "datum.h" om det hela ska
fungera. Även datumtest.cpp ska tas
med i projektet. När man kompilerar ett sådant projekt som har mer än en källkod
kommer man att se att kompilatorn kompilerar filerna en i taget.
Nu tittar vi på metoderna. Constructorn, CDatum,
anropas ju automatiskt när objektet skapas. Argument till denna funktion kan
anges vid deklaration av objektet:
CDatum foedelse_dag(1963, 5, 17);
Vi kan fortsätta med att deklarera funktionerna.
Constructorn förses med kontroller som gör att inget 'omöjligt' datum kan
komma in i klassen:
// Constructor:
CDatum::CDatum(int
a, int m, int d)
{
// Lista med antal dagar per månad:
static int ant_dagar[12] = {31, 28, 31, 30, 31, 30,
31, 31, 30, 31, 30, 31};
// Kontrollera årtalet:
if(a < 100) a += 1900;
if(a < 1900) a = 1900;
if(a > 2100) a = 2100;
// Kontrollera månadsnumret:
if(m < 1) m = 1;
if(m > 12) m = 12;
// Kontrollera dagnumret med hjälp av listan ovan:
if(d < 1) d = 1;
if(m == 2 && !(a % 4)) //
Februari, skottår.
{
if(d > 29) d = 29;
}
else
{
if(d > ant_dagar[m - 1]) d = ant_dagar[m - 1];
}
// Lagra det kontrollerade datumet i objektets attribut:
m_Aar = a;
m_Maanad = m;
m_Dag = d;
}
Vi kan fortsätta med implementationen av VisaDatum():
void CDatum::VisaDatum()
{
char cMaanad[12][10] =
{"Januari", "Februari", "Mars",
"April", "Maj", "Juni",
"Juli", "Augusti", "September",
"Oktober", "November", "December"};
cout << m_Dag << ' ' << cMaanad[m_Maanad - 1]
<< ' ' << m_Aar << '\n';
}
Sedan är det bara att använda klassen. Vi har ännu
inte skapat några objekt, så vi börjar med detta. Vi kan skriva en main() i
filen datumtest.cpp vilken skapar
två objekt, ett med korrekt datum och ett med felaktigt datum, för att sedan
skriva ut dem så att vi kan se att det blir korrekta datum:
void main()
{
CDatum idag(96, 07, 02); // Korrekt.
CDatum fel(96, 21, 00); // Felaktigt.
idag.VisaDatum();
fel.VisaDatum();
}
Och så här blir utskriften:
I klassen CDatum finns nu bara enklast möjliga
felkorrigering. Man kan naturligtvis göra den mer utförlig, varna användaren
etc. I detta exempel demonstreras bara att ett felaktigt datum inte kan ta sig
in i klassens medlemsvariabler. Observera att de är gömda under nyckelordet
'private', så att man inte heller kan ändra dem utifrån. Klassen har 100%
kontroll över deras integritet.
Detta är t.ex. inte möjligt att göra i main():
m_Dag =
-2; // 'undeclared identifier'
idag.m_Dag = -2; // 'error C2248: 'm_Dag' : cannot access
private member declared in class 'cdatum''
Övningsuppgift 14.7.2:
· Skapa ett nytt projekt DatumTest för klassen CDatum.
· Försök att skapa klassen på egen hand, utan att titta i häftet.
· Vi kommer att bygga vidare på denna klass i kommande avsnitt, så var noga med att använda rätt namn och rätt filnamn.
· Klasshuvudet ska stå i en fil som ska heta datum.h.
· Funktionerna ska beskrivas i en fil som ska heta datum.cpp.
· Huvudfunktionen main() ska stå i en fil som ska heta datumtest.cpp.
· Glöm inte att inkludera datum.h i båda cpp-filerna.
· Glöm inte att ta med båda cpp-filerna i projektet.
· Du behöver inte hantera skottår om du inte vill, alla Februari får vara 28 dagar.
· Lägg till en överlagrad constructor som inte tar några argument, och låt den initiera datum till den förste januari 1984.
· Testa att skapa tre objekt i main, ett med korrekt datum, ett med ett felaktigt datum samt ett utan datum. Skriv ut alla tre datum på skärmen.
· Så här kan det se ut:
Det är inte alltid samma person som skriver en klass som sedan använder den. Tvärtemot är det mycket vanligt att den som använder en klass bara får en headerfil med klasshuvudet i, samt en objektsfil där metoderna finns kompilerade.
Om du gjort klassen CDatum enligt beskrivning så är det just det du nu har. Filen datum.h innehåller klasshuvudet och filen datum.obj i debugkatalogen, eller releasekatalogen, innehåller den kompilerade koden. Vi vill helst ha releaseversionen av klassen så du bör ändra projektinställningen till release. Detta gör du i projektverktygsfältet med en kontroll som ser ut så här:
Som du ser står där just nu 'DatumTest - Win32 Debug' i kombinationsrutan. Klicka på listpilen och byt till 'DatumTest - Win32 Release'.
Innan du kompilerar behöver vi göra en sak till. Som du kanske kommer ihåg från avsnittet 'Deklarera klasser' så fanns det tre nyckelord för datagömning: public:, private: samt protected:. Vi kommer i nästa avsnitt att göra ett arv från klassen CDatum, och då är det nödvändigt att nyckelordet private: byts ut mot protected:, för att den ärvande klassen ska få tillgång till medlemmarna.
När du bytt ut private: mot protected: kan du kompilera klassen. Du behöver inte använda 'build'. Det räcker med 'compile datum.cpp'. Därefter kan man använda klassen i sin kompilerade form i vilket projekt som helst.
Övningsuppgift 14.8.1:
· Kompilera klassen CDatum exakt enligt ovanstående instruktioner.
· Vår kompilerade klass är färdig! Låt oss skapa ett nytt projekt. Det nya projektet ska heta NyDatum.
· Kopiera filen C:\CCpp\DatumTest\datum.h till C:\CCpp\NyDatum\datum.h.
· Kopiera filen C:\CCpp\DatumTest\release\datum.obj till C:\CCpp\NyDatum\datum.obj.
· Kopiera filen C:\CCpp\DatumTest\DatumTest.cpp till C:\CCpp\NyDatum\NyDatum.cpp.
· Inkludera NyDatum.cpp och datum.obj i projektet. Observera att du måste byta filter i filvalsdialogrutan för att kunna se objektsfilen.
· Kompilera och länka projektet. Allt ska fungera som förut.
När du länkade projektet kan du ha fått en varning:
LINK : warning LNK4098: defaultlib
"LIBC" conflicts with use of other libs; use /NODEFAULTLIB:library
Detta händer om man länkar objketsfiler som är av olika typer, och med typ avses i detta fallet om filen är kompilerad med eller utan debug. Vår kompilerade klass är ju kompilerad utan debug, och det är ju som det ska vara. Om vi kompilerar det nya projektet för release, kommer vi inte att få varningen. Detta innebär att vi har hindrat den som använder klassen från att titta i klasssen med hjälp av debuggern.
Det är möjligt att leverera klasserna i båda versionerna. Det är just det som Microsoft gör med en del av sina klasser. Då har de även lämnat med källkod, så att debuggern kan visa steg för steg hur klassen arbetar. Vi nöjer oss dock i detta exempel med det vi har. Vi kan köra debug, men när vi gör det kan man bara utföra objektens funktioner som ett enda programsteg.
Om du fått andra problem, kan du börja med att kontrollera att du kopierat rätt, och att du tagit med rätt filer i projektet. Så här ska 'FileView' se ut om allt är rätt:
Definition: En kompilerad klass är en klass som beskrivs av ett klasshuvud i en headerfil och en färdigkompilerad objektskod. |
I nästa avsnitt ska vi se hur man kan bygga vidare på en klass, trots att man inte har tillgång till källkoden. Konceptet kallas klassarv.
Men om vi nu vill göra egna tillägg i de klasser vi köpt? (MFC står för Microsoft Foundation Classes och innehåller grundläggande klasser för windowsprogrammering. Vi får t.ex. ett tomt programfönster, men behöver lägga till diverse funktioner, menyer etc.)
Låt oss ta ett enkelt exempel. Vår klass CDatum kan ju egentligen bara tre saker: Den kan skapa ett förvalt datum, den kan kontrollera, eventuellt justera och skapa ett angivet datum, och den kan skriva ut datum enligt ett visst format.
Man kan tänka sig många tillägg till detta. Det går till exempel att anpassa olika operatorer så att de får en meningsfull betydelse. Operatorn '++' skulle kunna öka m_Dag med ett. Om m_Dag blir för stort ska m_Dag bli 1 och m_Maanad ökas med 1. Om m_Maanad blir större än 12 ska m_Manad bli 1 och m_Aar i sin tur ökas med ett. Detta är ju inte vad ++operatorn normalt gör.
Vi ska dock inte gå så djupt in i ämnet eftersom detta är en nybörjarkurs, så vi uppfinner ett enklare behov i stället. Vi vill kunna skriva ut datum i formatet ÅÅÅÅ-MM-DD med en ny utskriftsfunktion som vi kallar 'VisaKortDatum()'.
Det går dock inte att ändra i klassen, eftersom vi inte har tillgång till källkoden. Försök absolut inte att ändra något i klasshuvudet! Gör du det så stämmer inte klasshuvudet med den objektskod vi har.
Lösningen på problemet heter klassarv. Man kan därigenom skapa en ny klass, men utan att skriva om allt som fanns i den gamla klassen. Man ärver bara från den gamla klassen och gör sedan de tillägg man vill ha. Låt oss skapa en ny klass. Klassen kan heta CNyDatum. Man kan eventuellt skapa den i två nya filer, men för enkelhets skull deklarerar vi den ovanför main(). I main() ändrar vi så att den nya klassen används i stället. Så här ser filen NyDatum.cpp ut efter ändring:
#include
<iostream.h>
#include
"datum.h"
class CNyDatum
: public CDatum
{
public:
CNyDatum();
CNyDatum(int a, int m, int d);
void VisaKortDatum();
};
CNyDatum::CNyDatum():CDatum()
{
}
CNyDatum::CNyDatum(int
a, int m, int d):CDatum(a, m, d)
{
}
void
CNyDatum::VisaKortDatum()
{
cout << m_Aar << '-';
if(m_Maanad < 10)
cout << '0';
cout << m_Maanad << '-';
if(m_Dag < 10)
cout << '0';
cout << m_Dag << '\n';
}
void main()
{
CNyDatum idag(96, 07, 02); // Korrekt.
CNyDatum fel(96, 21, 00); // Felaktigt.
CNyDatum def;
idag.VisaKortDatum();
fel.VisaKortDatum();
def.VisaKortDatum();
cout << '\n';
}
Övningsuppgift 14.9.1:
· Gör ovanstående tillägg.
· Kompilera, länka och testa.
· Utskriften ska se ut så här:
Nu har vi en del att diskutera. För det första så angavs själva arvet så här:
class CNyDatum : public CDatum
Kolon anger arv, och nyckelordet 'public' används för att ange att man vill ha högsta tillgänglighet från förälderklassen, vilket man kallar den klass som man ärver av. Vi ska strax titta med på denna namnsättning.
Nästa sak vi lägger märke till är att vi definierar nya constructorer. Detta på grund av att det finns en speciell syntax för att anropa och eventuellt passera argument till förälderklassens konstruktor:
CNyDatum::CNyDatum():CDatum()
{
}
CNyDatum::CNyDatum(int
a, int m, int d):CDatum(a, m, d)
{
}
Vi ska strax titta på hur man kan anropa andra metoder i en förälderklass. Först ska vi titta på begreppet klasshierarki.
Man kan rita upp förhållandet mellan CDatum och CNyDatum på följande sätt:
Detta kallas för att rita upp dess hierarki. (Inte harakiri!) Föräldern behöver inte avlida för att barnet ska få ärva.
I övrigt liknar faktiskt en klasshierarki ett släktträd:
Man kan se vilka klasser som kan anropa vilka medlemsfunktioner genom att studera klasshierarkin. En klass kan anropa en metod i en klass ovanför, men inte i en klass nedanför.
Klass E kan alltså anropa metoder i alla klasser utom i klasserna D och F. Klass C kan anropa metoder i klass A och B, men inte i klass D, E eller F.
Den klass man ärver från kallas förälderklassen. Den ärvande klassen kallas arvinge eller barnklass. På engelska kallas de parent class och child class. Det finns en benämning till, nämligen basklass, på engelska base class, vilket betecknar den förälderklass som är 'anfader' i en klassherarki. I Exemplet ovan är alltså Klass A basklassen. Klass B och C är förälderklasser, men det är även Klass A, som därigenom har två roller. Klasserna D, E och F är arvingar, men det är även klasserna B och C vilka även de får dubbla roller. Detta är värre än TV-serien 'Dynasty'.
11. Utbyte av medlemsfunktion.
Innan vi lämnar detta sista kapitel i kurshäftet vill jag bara demonstrera en sak till. Vi har visserligen endast kompilerad kod för metoden VisaDatum(), men vi kan 'köra över' den genom att deklarera en ny metod med samma namn i vår ärvande klass CNyDatum. Detta har flera användningsområden, men här går vi in på det enklaste. Låt oss vara 'missnöjda' med utskriften. Metoden VisaKortDatum() har vi ju kontroll över, så den kan vi 'piffa upp' efter tycke och smak. Om man gillar stjärnor kan man göra så här:
void CNyDatum::VisaKortDatum()
{
cout <<
"\n**************\n";
cout << " " << m_Aar << '-';
if(m_Maanad < 10)
cout << '0';
cout << m_Maanad << '-';
if(m_Dag < 10)
cout << '0';
cout << m_Dag << \n';
cout <<
"**************\n\n";
}
Då blir utskriften litet elegantare:
Men om vi vill göra motsvarande ändring i VisaDatum(), måste vi då skriva om hela metoden? Naturligtvis kan man göra det, men det går att anropa den ifrån vår egen version av VisaDatum(). Så här kan man göra, först en ny funktionsprototyp i klasshuvudet till CNyDatum:
void VisaDatum();
Därefter deklarerar vi koden. Lägg märke till att man kan anropa en förälderklass' metod genom att ange klassnamnet och räckviddsoperatorn före metodanropet:
void CNyDatum::VisaDatum()
{
cout << "\n*******************\n ";
CDatum::VisaDatum();
cout << "*******************\n\n";
}
Om man sedan ändrar i main() så att VisaDatum() används kan det se ut så här:
Övningsuppgift 14.11.1:
· Öppna projektet NyDatum om det inte redan är öppet.
· Gör ovanstående tillägg och ändring.
· Kompilera, länka och testa, utskriften ska bli som ovan.
Övningsuppgift 14.11.2:
· Öppna projektet NyDatum om det inte redan är öppet.
· Skriv på egen hand en metod med vars hjälp man kan ändra det datum objektet innehåller. Kalla metoden SetDatum(). Du kan kopiera kod från filen CDatum.cpp i projektet DatumTest, så slipper du skriva alla tester igen.
· Testa att skapa ett objekt i main() för vilket datum inte anges.
· Anropa SetDatum() med ett korrekt datum, och skriv ut detta.
· Anropa SetDatum() med ett felaktigt datum, och skriv ut detta.
· Kompilera, länka och testa. Kontrollera så att det felaktiga datumet korrigeras.
Övningsuppgift 14.11.3:
· Öppna projektet NyDatum om det inte redan är öppet.
· Skriv på egen hand en metod som avgör om det årtal som objektet innehåller är ett skottår. Kalla metoden SkottAar(). Metoden ska returnera värdet 0 om det inte är skottår, annars -1.
· Låt användaren ange år månad och dag, vilka sparas i lokala variabler i main().
· Skapa ett CDatum-objekt och ange de lokala variablerna som argument.
· Anropa en utskrift så att användaren ser om angivet datum korrigerats.
· Testa metoden SkottAar() i main() genom att infoga ett anrop till den som villkor i en if-sats. Om villkoret är uppfyllt ska texten "Det är ett skottår." skrivas ut. Om villkoret inte är uppfyllt ska texten "Det är inte ett skottår." skrivas ut.
· Kompilera, länka och testa. Så här kan det se ut: