Objektorienteret programmering i C++


Indhold

Indledning…………………………………………………………………………………………………….. 2

Hvordan en klasse erklæres……………………………………………………………………………… 2

Angivelse af funktioner……………………………………………………………………………………. 3

Constructor…………………………………………………………………………………………………… 3

Erklæring af et objekt……………………………………………………………………………………… 4

Requests (kald af medlemsfunktioner)………………………………………………………………… 5

Konstante objekter………………………………………………………………………………………… 5

Multiple constructors………………………………………………………………………………………. 6

Alternativ assignment-metode i constructors………………………………………………………… 7

Operator overloading……………………………………………………………………………………… 7

Friend-funktioner…………………………………………………………………………………………. 11

Dynamisk instantiering af objekter……………………………………………………………………. 12

Copy constructor…………………………………………………………………………………………. 13

Destructor…………………………………………………………………………………………………… 14

Kald af funktioner gennem pointere………………………………………………………………….. 15

Templates…………………………………………………………………………………………………… 16

Nedarvning…………………………………………………………………………………………………. 18

Polymorfi……………………………………………………………………………………………………. 21

Multipel nedarvning………………………………………………………………………………………. 22

Virtuelle nedarvning………………………………………………………………………………………. 23

Abstrakte klasser…………………………………………………………………………………………. 25

Statiske funktioner………………………………………………………………………………………… 26

Indledning

Objekt-orienteret programmering er ikke bare en ny måde at programmere på. Det er en helt ny måde at tænke på. I stedet for at se sit program som opbygget af algoritmer, der kan opdeles i funktioner, skal man tænke på sit program som bestående af objekter, der alle har specielle egenskaber. At lære at udnytte de objekt-orienterede muligheder i sit programmeringssprog er ikke det store problem, men ændringen af tankegangen kommer først med tiden.

 

Grundstenen i objekt-orienteret programmering er – ikke overraskende – objekter. Objekter er imidlertid ikke bare objekter; de er objekter af en bestemt type. Objekters type kaldes deres klasse.

 

Det er muligt at lave flere objekter af samme klasse. Disse vil så – til at starte med – være identiske. Med tiden vil de naturligvis udvikle sig forskelligt alt efter hvilke funktioner, der bliver kaldt på dem.

 

Hvordan en klasse erklæres

class Cirkel {

  private: //Private variable

    int radius;

  public: //Member functions

    Cirkel(int r = 0); //Constructor

    void SetRadius (int);

    int GetAreal();

    int GetOmkreds();

};

Eksempel 1

 

Eksempel 1 viser, hvordan en klasse erklæres. Som det ses starter man med det reserverede ord class. Derefter gives navnet på klassen eller typen, hvorefter en krøllet parentes følger.

 

Klasser kan indeholde to forskellige ting: Variable og funktioner. De er opdelt i to forskellige grupper. De private variable og funktioner og de offentlige variable og funktioner. Det er hovedsageligt variable, der er private, mens de fleste funktioner er offentlige, men det modsatte kan også være tilfældet.

 

De private variable indledes med ordet private:. Herefter følger variabel-definitionerne, ganske som i en almindelig funktion. Det specielle ved private variable er, at de kun kan tilgås fra de funktioner, der er medlemmer af klassen. Konceptet med private variable kaldes indkapsling. Det vil sige, at de data, der kun vedrører selve objektet, er afskærmet fra resten af programmet.

 

De offentlige variable og funktioner indledes med ordet public:. Disse kan tilgås fra hele programmet – hvordan vender vi tilbage til.

 

Funktioner i en klasse kaldes medlemsfunktioner eller member functions. Et funktionskald kaldes en request.

 

Bemærk at der skal være et semikolon efter den afsluttende krøllede parentes.

 

Da den private variabel radius ikke kan tilgås af andre funktioner end medlemsfunktionerne er det nødvendigt, at have en funktion, der sætter en ny radius. Den kaldes i dette eksempel SetRadius. Dette kan måske virke meget uhensigtsmæssigt, men det er faktisk en af fordelene ved objekter. Da SetRadius er den eneste funktion, der ændrer variablen radius ved vi, at hvis radius pludseligt antager en forkert værdi, så må ændringen være sket gennem funktionen SetRadius – det letter fejlretningen.

 

I praksis vil der ofte være andre fordele ved indkapslingen. Man kan for eksempel forestille sig, et konto-objekt, der har en funktion, der tilskriver rente. Hvis reglerne for rentetilskrivning skal ændres, er det kun nødvendigt at ændre denne ene funktion, og da kontoens saldo er privat, er det ikke muligt, at en travl programmør falder for fristelsen til lige at lægge et par procent til saldoen, og så er den rentetilskrivning ordnet.

Angivelse af funktioner

I erklæringen af klassen skrives funktioner bare med deres prototype. Når selve funktionen skal angives, skal man imidlertid først angive, hvilken klasse funktionen hører til. Funktionshovedet vil have følgende format:

 

<returtype> <klasse>::<funktionsnavn> (<parametre…>)

 

Eksempler på dette kan ses i eksempel 2. Herefter følger funktionskroppen som i en almindelig funktion. Dobbelt-kolonet, der adskiller klasse og funktionsnavn, kaldes scope.

Constructor

Som det ses i eksempel 1, står der “Constructor” i kommentaren efter medlemsfunktionen Cirkel(int r=0). En constructor er en funktion, der kaldes automatisk, når der oprettes et objekt af klassen. Det normale arbejde for constructor-funktionen er at initialisere variable, og det er også det, der er formålet med denne constructor. Parameteren r angiver radius – på den måde kan cirkel-objektet i programmet initialiseres til at have en startværdi.

 

Hvis der ikke gives en startværdi, bliver r sat til default-værdien 0. Herefter udføres den kode, der står i funktionen Cirkel::Cirkel.

 

Constructors skal altid have det samme navn som klassen.

 

Af eksempel 1 ses det også, at der ikke angives nogen returtype for constructor’en. Det betyder ikke som normalt, at returtypen er int. Constructors kan ikke returnere noget, og derfor angiver man ingen returtype.

 

 

#include <iostream.h>

#include <math.h>

 

#define pi M_PI

 

class Cirkel {

  private: //Private variable

    int radius;

  public: //Member functions

    Cirkel(int r = 0); //Constructor

    void SetRadius (int);

    int GetAreal();

    int GetOmkreds();

};

 

Cirkel::Cirkel(int r)

{

  radius = r;

}

 

void Cirkel::SetRadius (int r)

{

  radius = r;

}

 

int Cirkel::GetAreal()

{

  return pi*radius*radius;

}

 

int Cirkel::GetOmkreds()

{

  return 2*pi*radius;

}

 

void main()

{

  Cirkel rund(5);

 

  cout << “Omkredsen er ” << rund.GetOmkreds() << endl;

}

 

Eksempel 2.

Erklæring af et objekt

Et objekt erklæres på samme måde som alle andre variable. Det vil sige på formen

<datatype> <navn>

 

Hvis man i stedet for at definere en int ved navn x, vil definere et objekt af typen Cirkel med navnet rund, skriver man altså ikke int x; men Cirkel rund;

Ligesom man kunne initialisere simple variable (fx int x=5;)  kan man også initialisere komplekse variable som objekter, hvis klassen tillader det. Det gør den i dette tilfælde (constructeren tager parameteren r). Derfor kan man, hvis man vil initialisere rund med radius 5, skrive Cirkel rund(5);

 

Requests (kald af medlemsfunktioner)

Når man har brug for at benytte en af de offentlige funktioner, der findes i objektet sender man et request. Det gøres på samme måde som man foretager et funktionskald. Blot skal man huske at angive, hvilket objekt man kalder funktionen i. Bemærk, at det skal være objektet (altså variablen), man angiver, og ikke klassen. Objektnavnet og funktionsnavnet adskilles med punktum (der udtales “dot”), så requesten har følgende format:

 

<objekt>.<funktion>(<parametre>);

 

Et eksempel på en request ses i main-delen af eksempel 2.

Konstante objekter

Som alle andre variable kan objekter også gøres konstante. Det gøres ved at skrive const foran deres definition. Fx const Cirkel rund(5);

 

Det vil i det tilfælde ikke være muligt at ændre nogen variable i objektet. Deraf kan man slutte at funktionen SetRadius heller ikke må kaldes, da den jo netop ændrer variablen radius (rent teknisk kan det godt lade sig gøre, men det giver advarslen “Non-const function Cirkel::SetRadius(int) called for const object”). Derimod skulle man tro, at det ville være helt i orden at kalde funktionerne GetOmkreds og GetAreal, da de jo kun læser radius, hvilket jo er helt legalt at gøre med konstante variable. Sådan er det imidlertid ikke. Når et objekt er konstant er det hele objektet, der er konstant. Det vil sige, at et kald til GetOmkreds vil give en advarsel i stil med den som et kald til SetRadius gav.

 

Disse problemer kan imidlertid omgås, så man kun får advarslen, når man virkeligt har brug for den – når man virkeligt foretager en ændring af et konstant objekt. Det gør man ved at tilføje ordet const efter alle de medlemsfunktioner, der ikke modificerer variable. I vores eksempel vil klasse-erklæringen komme til at se ud som i eksempel 3. Derudover skal man huske også at tilføje const til funktionshovedet, når selve funktionen skrives.

 

class Cirkel {

  private: //Private variable

    int radius;

  public: //Member functions

    Cirkel(int r = 0); //Constructor

    void SetRadius (int);

    int GetAreal() const;

    int GetOmkreds() const;

};

Eksempel 3.

 

Multiple constructors

 

Som vi tidligere har set, kan constructoren have default-værdier. Men betragt eksemplet med nedenstående constructor i class’en Point:

 

Point::Point (int x = 0; int y = 0);

 

Hvis objekter af klassen instantieres med to parametre er der ingen problemer – det er der heller ikke, hvis der slet ikke angives parametre; så sættes de begge til nul. Men hvad, hvis der kun angives et parameter? Så antager C++, at det er det første parameter, der overføres, og de resterende sættes til default-værdier. I dette tilfælde vil det altså betyde, at y sættes til nul. Det er meget godt, hvis det var meningen, at y skulle sættes til default-værdien, men hvad, hvis vi hellere ville have haft, at det var x, der blev sat til default-værdien? Den eneste måde det kan gøres på med default-parametre er at bytte om på parametrene, og det virker meget ulogisk.

 

For at afhjælpe dette problem, kan man definere flere constructors[1]. I vores cirkel-eksempel vil vi altså kunne definere to constructors i stedet for den ene, vi har med default-værdier. Så kommer erklæringen af klassen og implementationen af de to constructors til at se således ud:

 

class Cirkel

{

  private: //Private variable

    int radius;

  public: //Member functions

    Cirkel(); //Constructor uden parametre

    Cirkel(int r); //Constructor med radius som parameter

 

    void SetRadius (int);

    int GetAreal();

    int GetOmkreds();

  };

 

  Cirkel::Cirkel ()

  {

    radius = 0;

  }

 

  Cirkel::Cirkel(int r)

  {

    radius = r;

  }

Eksempel 4.

 

Alternativ assignment-metode i constructors

 

Tidligere har vi i constructorens kode skrevet de assignments, der skulle foretages. Det er også muligt at skrive dem på en anden måde. Fx kan vores constructor med parametren r omskrives til følgende:

 

 

 

  Cirkel::Cirkel(int r) : radius ( r )

  {

  }

Eksempel 5.

 

Denne form for assignments kan kun anvendes i constructoren.

 

Nu da størrelsen på constructoren er begrænset så den faktisk kan skrives på en linie, bliver det interessant at se, at man kan have koden til constructoren stående i klassens erklæring. Det kaldes inline-kode.

 

class Cirkel

{

  private: //Private variable

    int radius;

  public: //Member functions

    Cirkel() : radius(0) {}; //Constructor uden parametre

    Cirkel(int r) : radius(r) {}; //Constructor med radius som parameter

 

    void SetRadius (int);

    int GetAreal();

    int GetOmkreds();

};

Eksempel 6.

 

Det er smag og behag, hvilken metode man foretrækker, men her vil vi for læsevenlighedens skyld fortsætte med den “gamle” metode.

 

Operator overloading

Operator overloading vil sige, at man tillader brug af operatorer på sine objekter. Normalt vil nedenstående jo ikke være tilladt:

 

Cirkel a,b;

 

if (a == b)

cout << “Ens”;

 

Det er imidlertid muligt at fortælle C++-compileren, at den, når den møder udtrykket “==” skal udføre en bestemt funktion. For at gøre det, skal man bruge nogle forud-bestemte funktionsnavne. Det generelle syntaks herfor er:

 

<returtype> operator <operator> (<parametre>);

 

Antallet af parametre bruges til at kende forskel på unære og binære operatorer. Fx er den eneste forskel på

 

int operator– ();

og

int operator– (int);

 

at  den øverste er unært minus (fx -5), mens den nederste er binært minus (fx 3-5).

 

Det er ikke alle klasser, der kan gøre fornuftigt brug af alle operatorer, og så kan man bare nøjes med at inkludere de fornuftige – så kommer compileren med en fejlmeddelelse, hvis man prøver at bruge nogen af de andre.

 

I vores eksempel vil operatoren =, == og + være fornuftige (der kan selvfølgeligt findes andre, men de tre giver os mulighed for at se lidt af hvert).

 

= sætter vores cirkel lig med en anden cirkel – det vil sige, at radius sættes til radius af den anden cirkel.

 

== sammenligner om radius på to cirkler er ens og returnerer en boolsk værdi.

 

+ lægger ikke to cirkler sammen, men lægger en konstant til radius.

 

Erklæringen af klassen med de tre operatorer og implementationen heraf kunne se således ud:

 

 

class Cirkel

{

  private: //Private variable

    int radius;

  public: //Member functions

    Cirkel(); //Constructor uden parametre

    Cirkel(int r); //Constructor med radius som parameter

 

    void SetRadius (int r);

    int GetAreal();

    int GetOmkreds();

 

    //Operators

    Cirkel& operator= (Cirkel);

    int operator== (Cirkel);

    Cirkel operator+ (int);

};

 

 

Cirkel& Cirkel::operator= (Cirkel c)

{

  radius = c.radius;

  return *this;

}

 

int Cirkel::operator== (Cirkel c)

{

  return radius == c.radius;

}

 

Cirkel Cirkel::operator+ (int x)

{

  return Cirkel(radius + x);

}

Eksempel 7.

 

Umiddelbart kan de forskellige returtyper måske virke forvirrende, men prøv at se på et par eksempler.

 

Cirkel A(5), B; //Opret to cirkler – en med radius 5 og en med radius 0

 

B = A;

 

Her ser vi to objekter af samme klasse med en operator i mellem. Først skal det understreges, at det altid er objektet til venstre for en binær operator, der får funktionskaldet. I dette tilfælde vil det altså være B’s funktion operator=, der kaldes.

 

Denne funktion skal primært sørge for, at B’s radius sættes lig med A’s radius. Man kunne på den baggrund argumentere for, at den ikke behøvede at returnere noget. Imidlertid er det jo sådan i C++, at assignments har en værdi. Man kan fx skrive X = Y = 5, hvilket medfører, at både Y sættes til 5 og X sættes til værdien af assignmentet Y = 5, hvilket vil sige 5. Ergo sættes både X og Y til 5.

 

Hvis vi ønsker, at det samme skal gælde for objekter af cirkelklassen, fx C = B = A, skal B = A altså returnere værdien B.

 

At returtypen er en reference skyldes blot, at dataene fra det ene objekt ikke skal kopieres over i et andet – det har ingen betydning i dette eksempel, men ved klasser med store mængder private data, kan man spare tid på denne måde.

 

Det mest interessante ved funktionen er nok at den returnerer *this. this er en pointer til det aktuelle objekt, altså i dette tilfælde objektet på venstre-siden af lighedstegnet. At der sættes en stjerne foran, skyldes blot at det er værdien og ikke adressen, der skal returneres (at det er adressen, der bliver returneret, fordi resultatet returneres per reference, er en anden sag).

 

Hvis vi vælger at sammenligne de to cirkler kan det gøres med nedenstående if-struktur:

 

if (A==B)

cout << “Ens”;

else

cout << “Uens”;

 

Her skal operator== returnere en boolsk værdi (dvs 0 eller 1) alt efter om de to cirklers radius er ens. Det gøres i dette tilfælde ved at returnere resultatet af sammenligningen af de to radia.

 

operator+ derimod skal returnere en variabel af typen Cirkel. Det ses af nedenstående eksempel:

 

B = A+2;

 

Her skal udtrykket A+2 evalueres først. Det gøres ved et kald til A’s operator+ (A står på venstre side af operatoren). Herefter skal resultatet af denne evaluering lægges over i B (hvilket i øvrigt sker ved et kald til B’s operator=). Da den eneste værdi vi kan sætte lig med en cirkel er en anden cirkel, må returtypen af operator+ altså være en cirkel.

 

Returneringen fra operator+ viser også, at man godt kan kalde en klasses constructor direkte. Returtypen herfra er et objekt af klassen.

 

En pudsig ting ved operator-funktioner, der returnerer objekter er, at man kan kalde member-funktioner fra assignments. Fx vil linien

 

cout << (c+1).GetOmkreds();

 

udskrive omkredsen af en cirkel med en radius en større end c’s (men vil ikke ændre c’s radius!)

 

Ligesom det var muligt at have multiple constructors er det også muligt at have flere implementationer af den samme operator. Det foregår efter præcis samme princip som multiple constructors. På den måde vil det være muligt – udover at lægge en int til en cirkel – at lægge to cirkler sammen, eller måske også en cirkel og en float. Hvis man arbejder med flere forskellige klasser kan man endda skrive funktioner, der gør det muligt at sammenligne, addere osv. de forskellige klasser.

 

Det kan måske virke overraskende, at de forskellige operator-funktioner kan tilgå variablen radius i de cirkler de får over som parametre. Det skyldes, at memberfunktioner i en klasse, altid kan tilgå de private variable (og funktioner) som objekter af samme klasse har.

 

Det er ikke alle operatorer, der kan overloades, men de fleste kan. Nedenstående skema, viser hvilke der kan og ikke kan. Nogle af operatorerne vender vi tilbage til i afsnittet om friend-funktioner.

 

Følgende operatorer kan overloades:

+ * / % ^ & |
~ ! = += -= *=
/= %= ^= &= |= <<  >>  <<=
>>= == != <= >= && || ++
-> , -> [] () new delete

 

Følgende operatorer kan ikke overloades:

. .* :: ?: sizeof

 

Friend-funktioner

 

Af og til kan man have brug for at tillade enkelte funktioner udenfor klassen at tilgå private variable og funktioner. Det gør man ved at inkludere funktionsnavnet i klassens erklæring og tilføje det reserverede ord friend foran.

 

En typisk anvendelse af friend-funktioner er når man vil overloade en operator, hvor ens objekt står på højre siden. Betragt for eksempel nedenstående linie:

 

cout << C;

 

Her vil det være cout, der får funktionskaldet, og funktioner i cout vil normalt ikke kunne tilgå private variable i cirklen C. Derfor vælger vi at gøre cout’s funktion til en friend af vores klasse, og vi skriver selv implementationen til udskriftsrutinen. Husk at cout blot er en variabel af typen ostream.

 

Erklæringen af klassen og implementationen af udskriftsrutinen ser således ud:

 

 

class Cirkel

{

  private: //Private variable

    int radius;

  public: //Member functions

    Cirkel(); //Constructor uden parametre

    Cirkel(int r); //Constructor med radius som parameter

 

    void SetRadius (int r);

    int GetAreal();

    int GetOmkreds();

 

    //Operators

    void operator= (Cirkel);

    int operator== (Cirkel);

    Cirkel operator+ (int);

    friend ostream& operator<< (ostream& ostr, Cirkel C);

};

 

 

ostream& operator<< (ostream& ostr, Cirkel C)

{

  ostr << C.radius;

  return ostr;

}

 

Eksempel 8.

 

Bemærk at funktionen ikke er medlem af klassen Cirkel – det vil sige, at man ikke skal inkludere klassenavnet i funktionshovedet.

 

At returtypen er en ostream skyldes, at man kan lave lænkede outputs. Fx

 

cout << “Radius er ” << C << endl;

 

Hvis udskriften af C ikke returnerede en ostream, ville de ikke være muligt at sende endl i samme strøm.

 

Den ostream, der tages som parameter er den man skal sende udskriften til. Man skal altså være opmærksom på ikke af gammel vane at skrive cout. Hvis man anvender cout i stedet for ostream vil funktionen kun kunne bruges til udskrift på skærmen og ikke til fx udskrift til fil.

 

I dette tilfælde skal objektet også tages som parameter. Det skyldes, at denne funktion jo ikke tilhører klassen – hvis vi skal kende objektet, er vi nødt til at få det med som parameter.

 

Dynamisk instantiering af objekter

Dynamisk allokering af hukommelse er ikke nogen speciel objektorienteret funktion, men det er medtaget her, fordi det afføder nogle interessante problemer i forhold til objekter.

 

Hvis vi i stedet for vores klasse Cirkel, der kun kan indeholde information om en cirkel, ønsker at lave en klasse med flere cirkler – CirkelList – kan vi opbevare informationerne om de forskellige cirkler i et privat array af typen Cirkel. Størrelsen på dette array kan imidlertid variere, da vi lader det være op til brugeren af klassen at bestemme, hvor mange cirkler, der skal være plads til. Derfor har vi brug for at allokere hukommelsen dynamisk. Vi lader brugeren angive antallet af cirkler når han instantierer objektet. Hvis der ikke angives nogen værdi, vil der blive lavet plads til 10 cirkler. Så vil erklæringen af klassen og constructoren komme til at se således ud:

 

 

class CirkelList

{

  private:

    Cirkel *Liste;

  public:

    CirkelList(int antal=10);

};

 

CirkelList::CirkelList (int Antal)

{

  if (Antal < 1)

  return;

  Liste = new Cirkel[Antal];

  count = 0;

}

Eksempel 9.

 

Bemærk, at Liste, der skal være det array, der indeholde de forskellige cirkler, blot er en pointer – den får først tildelt hukommelse i constructoren.

 

Copy constructor

Det er tidligere nævnt, at constructoren kaldes, når et objekt bliver instantieret. Prøv at betragte nedenstående eksempel:

 

 

  void main()

  {

    Cirkel rund(5);

 

    CirkelList List1;

    List1.Add (rund);

 

    CirkelList List2=List1;

  }

Eksempel 10

 

Her bliver List2 instantieret og bliver omgående sat lig med List1. Det betyder, at alle List1’s variable bliver kopieret over til List2. Det gælder således også pointeren List1.Liste, der peger på placeringen af cirklerne i hukommelsen. Det medfører, at det ikke er to forskellige lister vi har, men to referencer til den samme liste af cirkler. I sådanne tilfælde har man brug for at kalde en såkaldt copy constructor. Den kaldes automatisk, når der kan være behov for at kopiere data i constructoren. Udover ovenstående eksempel er det når et objekt gives med som parameter i et funktionskald (men ikke, hvis parameteren gives som pointer eller som reference).

 

Copy constructoren ser således ud:

 

class CirkelList

{

  private:

    Cirkel *Liste;

    int size, count; //Max. størrelse og antal indtastede

  public:

    CirkelList(int antal=10); // Standard constructor

    CirkelList (const CirkelList &cl); // Copy constructor

    CirkelList& operator= (CirkelList cl);

    void Add (Cirkel c); //Tilføjer cirkel

};

 

CirkelList::CirkelList (const CirkelList &cl)

{

  count = cl.count;

  size = cl.size;

  Liste = new Cirkel [size];

  for (int i = 0; i < count; i++)

  Liste[i] = cl.Liste[i];

}

Eksempel 11

 

Det interessante ved copy constructoren ses allerede i funktionshovedet. Som enhver anden constructor har den ingen returtype. Derudover skal parametren var const og referenceparameter. Parametertypen skal naturligvis være en instans af klassen – det er jo højresiden i tildelingen. I eksemplet fra Eksempel 10 vil parametren altså være List1.

 

Da objekter af samme klasse automatisk er friends kan copy contructoren uden videre tilgå parametrens private data, såsom size og count.

 

Bemærk også, at copy constructoren kun kan tage den ene parameter. Det betyder fx at nedenstående sætning ikke er tilladt:

 

CirkelList List2(5)=List1;

 

Destructor

 

Det kan næsten høres af navnet: Destructor er det modsatte af constructor. Destructoren kaldes, når et objekt skal fjernes. Det kan enten skyldes at dets scope er udløbet (for lokale variable), eller at objektet er blevet instantieret dynamisk og nu bliver fjernet ved hjælp af delete kommandoen. Endeligt kan det også skyldes, at programmet er afsluttet.

 

Da C++ ikke har garbage collection er det op til objekterne selv at frigive den hukommelse, de har allokeret dynamisk. Den hukommelse, der er tildelt statisk – det vil sige til variable, der ikke er pointere – bliver automatisk frigivet og skal ikke frigives i destructoren.

 

Nedenstående er et eksempel på en destructor i klassen CirkelList.

 

 

class CirkelList

{

  private:

    Cirkel *Liste;

    int size, count; //Max. størrelse og antal indtastede

 

  public:

    // Constructors

    CirkelList(int antal=10);

    CirkelList (const CirkelList &cl);

 

  // Destructor

  ~CirkelList ();

 

  // Operatorer

  CirkelList& operator= (CirkelList cl);

 

  // Andre funktioner

  void Add (Cirkel c); //Tilføjer cirkel

};

 

CirkelList::~CirkelList()

{

  if (Liste)

  delete[] Liste;

}

Eksempel 12

 

Ligesom constructoren, har destructoren ingen returtype. Dens navn skal være klassens navn med en tilde (~) foran. Destructoren tager aldrig parametre. I dette eksempel skal den kun frigive det hukommelse, der er tildelt pointeren Liste. Først kontrollerer den dog, at pointeren ikke er null, hvilket kunne være tilfældet, hvis en illegal parameter var overført til constructoren, eller hvis der ikke havde været tilstrækkelig hukommelse til instantiering.

 

Bemærk i øvrigt den sære notation med de kantede parenteser efter delete. Det angiver, at hele array’et skal slettes. Var de der ikke, var kun det første element blevet fjernet.

 

Kald af funktioner gennem pointere

 

Hvis man har en pointer til et objekt og gerne vil kalde et af objektets funktioner, bruger man i stedet for det vanlige punktum en pil (->) som scope-angivelse. Se nedenstående eksempel.

 

 

  void main()

  {

    Cirkel rund(5), rund2;

 

    CirkelList *List1;

    List1 = new CirkelList();

    List1->Add (rund);

    delete List1;

  }

Eksempel 13

 

Bemærk, at der først laves en pointer til et CirkelList-objekt, hvorefter det instantieres ved hjælp new. Herefter kaldes funktionen Add med kommandoen List1->Add(); Slutteligt fjernes objektet med delete. Grunden, til at man er nødt til at bruge pilen, er, at man ikke kan dereferenciere objekter. Ellers kunne man jo have klaret sig med kommandoen *List1.Add(rund); Det er ikke tilladt.

Templates

 

Ligesom man i C++ kan lave generiske funktioner (template-funktioner) kan man også lave template-klasser. I en template-klasse, bygger man klassen til at kunne benytte flere forskellige slags datatyper – det er herefter op til den enkelte programmør at angive i koden, hvilken datatype, der skal benyttes.  Hvis vi i stedet for vores CirkelList klasse, ville have en klasse, der bare skulle være en liste, og ellers ikke bekymre sig om, hvad den var en liste af, kunne vi passende gøre den til en template-klasse. Så ville funktionerne komme til at se således ud:

 

 

template <class T>

class List

{

  private:

    T *Liste;

    int size, count; //Max. størrelse og antal indtastede

 

  public:

    // Constructors

    List(int antal=10);

    List(const List<T> &l);

 

    // Destructor

    ~List ();

 

    // Operatorer

    List<T>& operator= (List<T> l);

 

    // Andre funktioner

    void Add (T c); //Tilføjer element

};

 

template <class T>

List<T>::List (int Antal)

{

  if (Antal < 1)

  return;

  Liste = new T[Antal];

  count = 0;

}

 

template <class T>

List<T>::List (const List<T> &l)

{

  count = l.count;

  size = l.size;

  Liste = new T [size];

  for (int i = 0; i < count; i++)

  Liste[i] = l.Liste[i];

}

 

template <class T>

List<T>::~List()

{

  if (Liste)

  delete[] Liste;

}

 

template <class T>

List<T>& List<T>::operator= (List<T> l)

{

  size = size < l.size ? size : l.size; //Vælg mindste størrelse

 

  for (int i = 0; i < size; i++)

  Liste[i] = l.Liste[i];

 

  return *this;

}

 

template <class T>

void List<T>::Add (T c)

{

  Liste[count++] = c;

}

 

void main()

{

  List<int> IntegerListe (5);

  IntegerListe.Add (6);

 

  List<Cirkel> *CirkelListe;

  Cirkel rund(5);

 

  CirkelListe = new List<Cirkel>();

  CirkelListe->Add (rund);

  delete CirkelListe;

}

Eksempel 14

 

Det bærende i template-klasser er udtrykket template <class T>, der skrives foran selve klassen og alle medlemsfunktioner. Faktisk er T bare en pladsholder for en hvilken som helst type, og T kan derfor erstattes af et hvilket som helst andet navn.

 

Da T er pladsholder for den type, der skal bruges i klassen, ser man, at Liste ikke længere er en pointer til Cirkel, men en pointer til T. 

 

Klassens rigtige navn er nu List<type>, og da den type vi arbejder med er T, skal den klasse, der skal overføres til copy constructoren var List<T>. Det samme gør sig gældende i alle funktionshovederne. Her er klasseangivelsen på venstre side af scope-tegnet List<T>. Generelt kan konverteringen af en specifik klasse til en generel template-klasse opdeles i tre punkter:

 

  • indsæt template <class pladsholder> foran klassedefinitionen og alle medlemmers funktionshoveder.
  • erstat referencer til den specifikke type til pladsholder.
  • erstat alle referencer til klassen til klasse<pladsholder>.

 

I programmets main-del, ses hvordan listen instantieres. Først laves objektet IntegerList, der er en liste af integers. Dét angives ved at skrive int i kantede parenteser efter klassenavnet. Det ses nu, at int har taget pladsholderen T’s plads, og int vil således blive indsat alle de steder, der i objektet er referencer til T. Det ses også, at Add-funktionen, der tager en T som parameter, nu tager en int.

 

Senere i programmet laves en CirkelListe, der er en liste af typen Cirkel – den svarer stort set til den CirkelList klasse, der er blevet lavet tidligere. Her vises, hvordan template-klasser allokeres dynamisk – igen skal man lige huske typen i kantede parenteser – ellers er systemet det samme.

 

Nedarvning

 

En af de bedste ting ved objekt-orienteret programmering er muligheden for nedarvning. Her kan man tage det bedste fra en klasse og udvide det med ny kode, for at danne en ny klasse. Vi kan for eksempel lave en Kvadrat-klasse, som vi senere kan nedarve fra for at lave en cirkel-klasse, da en cirkel er et “kvadrat med runde hjørner”.

 

Kvadrat-klassen er triviel og ser således ud:

 

 

class Kvadrat

{

  private:

    int width;

 

  public:

    Kvadrat();

    Kvadrat(int w);

 

    int omkreds();

    int areal();

    int getWidth();

};

 

Kvadrat::Kvadrat()

{

  Kvadrat(0);

}

 

Kvadrat::Kvadrat(int w)

{

  width = w;

}

 

int Kvadrat::omkreds()

{

  return 4*width;

}

 

int Kvadrat::areal()

{

  return width*width;

}

 

int Kvadrat::getWidth()

{

  return width;

}

Eksempel 15

 

Den klasse man nedarver fra kaldes en super-klasse, og den klasse, der nedarver kaldes en sub-klasse. Super-klasse og sub-klasse er relative begreber. Det vil sige, at en klasse kan være sub-klasse i forhold til den klasse, den nedarver fra, men den er super-klasse i forhold til de klasse, der nedarver fra den.

 

Bemærk, at super-klassen skal have en default-constructor uden parametre. Denne bliver automatisk kaldt, når der laves instanser af dens sub-klasser. I dette eksempel kalder default-constructoren den rigtige constructor med parameteren 0. I øvrigt skal det også bemærkes, at klassens variable ikke er private, men protected. Ligesom private variable, kan protected variable ikke tilgås udenfor klassen, men forskellen er, at sub-klasser har adgang til protected data – det har de ikke til private.

 

Sub-klasser har automatisk alle super-klassens funktioner. Den kan selv bestemmer, om den vil udvide, erstatte eller beholde disse funktioner. Hvis sub-klassen vælger at ændre en af super-klassens funktioner, kaldes det overriding.

 

I sub-klassens definition skal kun prototypen på de funktioner, som sub-klassen selv tilføjer eller ændrer stå. Da Cirkel-klassen skal ændre såvel areal som omkreds skal de begge med. Derudover er der naturligvis en constructor. Funktionen getWidth skal ikke ændres, og den medtages derfor ikke.

 

 

class Cirkel : public Kvadrat

{

  public:

    Cirkel (int radius);

    int omkreds();

    int areal();

};

 

Cirkel::Cirkel (int radius)

{

  width = radius;

}

 

int Cirkel::omkreds()

{

  return 2*PI*width;

}

 

int Cirkel::areal()

{

  return PI*width*width;

}

Eksempel 16

 

Selve nedarvningen foregår allerede i den første linie. Efter klassens navn sættes et kolon, hvorefter der skrives public og den klasse, den skal nedarve fra. Herefter har Cirkel-klassen adgang til variablene height og width, der blandt andet bruges i constructoren.

 

Det blev tidligere nævnt, at super-klassen skulle have en default-constructor, der automatisk bliver kaldt. Det foregår i den rækkefølge, at den øverste super-klasses contructor kaldes først, hvorefter constructoren i samtlige sub-klasser kaldes. Den rækkefølge er ikke tilfældig. Var sub-klassens constructor blevet kaldt først, ville den havde sat width og height til radius. Herefter ville super-klassens constructor blive kaldt, og width og height ville blive sat til 0.

 

For at få det på det rene med det samme: destructorerne kaldes i den modsatte rækkefølge. Det vil sige, at sub-klassens destructor kaldes først, hvorefter super-klassernes destructorer kaldes.

 

Polymorfi

 

Man kan sige, at der nu eksisterer to ens funktioner: int areal(). Deres funktionshoved er ens, og det vil således ikke være muligt for compileren, at kende forskel på dem – men der er jo rimelig stor forskel. Heldigvis er det sådan, at hvis man har en instans af en sub-klasse, så bliver det sub-klassens funktion, der kaldes. Betragt derimod nedenstående eksempel:

 

 

void main()

{

  Kvadrat *k;

  k = new Cirkel(5);

  cout << k->areal();

}

Eksempel 17

 

Her ses det ikke blot, at det er muligt at sætte en pointer til en super-klasse til at pege på en sub-klasse. Eksemplet stiller også spørgsmålet om, hvilken areal-funktion, der kaldes. Umiddelbart, skulle man tro, at det er Cirkel-klassens areal-funktion, der kaldes, men det er ikke tilfældet. Derimod er det Kvadrat-klassens areal-funktion, der bliver udført. Det skyldes, at det er den eneste funktion, Kvadrat-pointeren kender.

 

For at undgå dette må vi fortælle compileren, at hvis sub-klassen selv implementerer en funktion, er det den, der skal udføres i stedet. Det gøres ved at skrive virtual foran funktionens erklæring. Dette skal køres i super-klassen, og ikke i sub-klassen, som jo ellers er den, der har problemet.

 

I Kvadrat-klassen skulle der altså ikke have stået

int areal();

men

virtual int areal();

 

Multipel nedarvning

 

C++ understøtter det, der kaldes multipel nedarvning. Det vil sige, at en klasse nedarver fra mere end en klasse. Vi kunne for eksempel forestille os, at Cirkel-klassen også nedarver fra en Ellipse-klasse. Ellipse-klassen definition ses i nedenstående eksempel.

 

 

class Ellipse

{

  protected:

    int width, height;

 

  public:

    Ellipse();

    Ellipse(int w, int h);

 

    int areal();

    int omkreds();

 

    int getWidth();

    int getHeight();

};

Eksempel 18

 

Hvis Cirkel-klassen skal nedarve fra såvel Kvadrat-klassen som Ellipse-klassen, tilføjes “, public Ellipse” til nedarvningslinien.

 

Hvis koden nu forsøges kompileret, får man imidlertid en fejlmeddelelse, når man prøver at tilgå variablen width i Cirkel-klassen: “Member is ambiguous.” Det skyldes at compileren ikke kan kende forskel på width fra Kvadrat-klassen og width fra Ellipse-klassen. Det samme gælder referencer til funktionen getWidth().

 

Problemet kan klares ved at angive scope foran henvisningerne. Så kommer constructoren for eksempel til at se således ud:

 

 

Cirkel::Cirkel (int radius)

{

  Kvadrat::width = radius;

}

Eksempel 19

 

Det samme kan gøres i funktionskaldet til getWidth(). Så kommer det til at se således ud:

 

 

void main()

{

  Cirkel c(5);

  cout << c.omkreds() << endl << c.Kvadrat::getWidth();

}

 

Eksempel 20

 

Pænt er det ikke, men det virker.

 

Virtuelle nedarvning

 

Multipel nedarvning giver i midlertid også et andet problem. Hvis klassen A er super-klasse for både klasserne B og C, og D nedarver fra såvel B og C, vil funktionerne i A blive nedarvet to gange. Nedenstående klassediagram viser nedarvningerne.

 

 

 

 

Lad os antage, at klasen A har en protected variabel test. B har en funktion setTest, der lægger en værdi i variable test. C har en funktion getTest, der returnerer værdien af test-variablen, mens D bare har funktionen run, der starter eksemplet. C++ koden til de fire klasser ser således ud:

 

 

class A

{

  protected:

    int test;

};

 

class B : public A

{

  public:

    void setTest(int t)

    {

      test = t;

    }

};

 

class C : public A

{

  public:

    int getTest()

    {

      return test;

    }

};

 

class D : public B, public C

{

  public:

    void run()

    {

      setTest (5);

      cout << getTest();

    }

};

Eksempel 21

 

Man skulle tro, at udskriften ville vise 5, men den viser faktisk 8394 eller et eller andet andet tilfældigt tal. Det skyldes, at variablen test bliver nedarvet to gange: både gennem B og gennem C. Når funktionen setTest i B bliver kaldt, sætter den sit eksemplar af test til 5, mens C’s getTest-funktion returnerer værdien af sin egen test, og den er uændret.

 

For at undgår dette må man bruge virtuel nedarvning. Her sørger compileren for, at kun en version af hver variabel og funktion nedarves. I C++ laver man virtuel nedarvning ved at bruge det reserverede ord virtual.

 

Virtual nedarvning foretages ved at skrive virtual foran de klasser, der nedarves fra. Bemærk at dette ikke har noget som helst med virtuelle funktioner at gøre (ud over, at det er det samme reserverede ord, man bruger).

 

class A

{

  protected:

    int test;

};

 

class B : virtual public A

{

  public:

    void setTest(int t)

    {

      test = t;

    }

};

 

class C : virtual public A

{

  public:

    int getTest()

    {

      return test;

    }

};

 

class D : public B, public C

{

  public:

    void run()

    {

      setTest (5);

      cout << getTest();

    }

};

Eksempel 22

 

Abstrakte klasser

 

Når man laver en super-klasse, kan man komme ud for, at de klasser, der nedarver fra den, godt nok har en funktion, der hedder det samme og gør nogenlunde det samme, men alligevel kan de ikke bruge den samme kode – funktionen skal simpelthen skrives for hver enkelt sub-klasse.

 

Da funktionsnavnet findes i alle sub-klasserne, vil det være naturligt, at det også findes i super-klassen. Så vil der imidlertid komme det problem, at hvis en eller anden laver en instans af superklassen og prøver at kalde den pågældende funktion, sker der ikke noget.  Derfor kan man erklære en funktion pure virtual. Det angiver, at der i den klasse ikke findes kode til den funktion. Klasser, der indeholder bare en pure virtual funktion, kaldes abstrakte klasser, og dem er det ikke muligt at lave objekter af.

 

Hvis en klasse arver fra en abstrakt klasse, skal den selv implementere de pure virtuelle funktioner. Gør den ikke det, bliver den selv en abstrakt klasse.

 

En funktion erklæres pure virtual ved at skrive virtual foran den og = 0 efter den. Det fremgår af eksempel 23.

 

class A

{

  virtual void b() = 0;

};

Eksempel 23

 

Statiske funktioner

 

Normalt skal der jo oprettes en instans (et objekt) af en klasse, før dens funktioner kan kaldes. Man kan imidlertid lave statiske funktioner. Disse kan kaldes blot ved at angive klassen. Således ved MyClass::f() kalde den statiske funktion f() i klassen MyClass.

 

Eksempel 24 viser, hvordan statiske funktioner erklæres. Det skal bemærkes, at statiske funktioner kun må kalde funktioner, der også er statiske. Ligeledes skal alle variable, der bruges af en statisk funktion, være statiske.

 

class MyClass

{

  private:

    static int x;

  public:

    static void f();

};

 

int MyClass::x = 0;

 

void MyClass::f()

{

  x*=2;

}

Eksempel 24

 

Af eksemplet ses det også, hvordan statiske funktioner initialiseres. Det gøres ved følgende syntaks: type klasse::variabel = værdi;

 


[1] Man kan faktisk bruge teknikken til at definere flere forskellige typer af alle slags funktioner, men her ser vi kun på mulighederne for multiple constructors.

Comments are closed.