16.07.2013 Views

Kapitel 1: Introduktion [kapitel]

Kapitel 1: Introduktion [kapitel]

Kapitel 1: Introduktion [kapitel]

SHOW MORE
SHOW LESS

Create successful ePaper yourself

Turn your PDF publications into a flip-book with our unique Google optimized e-Paper software.

Forord<br />

Objekt-orienteret programmering er blevet den helt store trend i løbet af de<br />

sidste to-tre år. Ikke fordi det er en ny teknik, men fordi adskillige nye<br />

systemer og sprog for nylig er udviklet til direkte at understøtte den. Desværre<br />

er termen OOP mange steder et hi-tech synonym for "god", og bruges i flæng<br />

som superlativ i forbindelse med markedsføring og reklame.<br />

Det er i sig selv ikke et væsentligt problem, men kan lede til, at mange får en<br />

forkert forståelse for det ny paradigme og dets konsekvenser. Objekt-orienteret<br />

teknologi er ikke bare et nyt værktøj, som med (u)sædvanlig lethed kan<br />

integreres i programudviklingen. Det kræver en ny tankegang, en ny opfattelse<br />

af programkonstruktion og projektstyring, en revideret opfattelse af<br />

arbejdsgangen fra design til færdigt produkt.<br />

På det punkt er objekt-orienteret teknologi på et tidligt stadie. Størstedelen af<br />

litteraturen koncentrerer sig enten om specifikke sprog eller om forslag til<br />

designmetoder og formel specifikation. De fleste har svært at kombinere de ny<br />

metoder med deres egen hverdag som programmører og softwareingeniører,<br />

når de ikke er i stand til at relatere dem til de sprog og værktøj, de benytter i<br />

traditionelle miljøer. I denne bog forsøger jeg at beskrive både et konkret<br />

system, programmeringssproget C++, og rene formelle metoder for programkonstruktion<br />

efter objekt-orienterede principper, med det formål at både reelle<br />

eksempler og underbyggende teori bliver klargjort for læseren.<br />

Og det er nødvendigt med en bred forståelse. Objekt-orienteret programudvikling<br />

har så mange fortrin fremfor traditionelle teknikker, at de kan være<br />

svære at overskue. Det tager tid at blive fortrolig med den anderledes måde at<br />

tænke på. Derfor er denne bog bygget op omkring forskelle mellem kendte<br />

fremgangsmåder og objekt-orienterede. Med konkretiserede opstillinger af<br />

kendte, reelle problemer vises og bevises fordelen ved den objekt-orienterede<br />

praksis. Jeg mener nemlig, at den eneste måde at lære en ny programmeringsteknik<br />

på, er at skrive programmer efter den.<br />

Men tro ikke, at det blot drejer sig om at skrive programmer. Det ligger i<br />

ordet paradigmeskift, at der er brug for en kovending i selve indstillingen.


ii<br />

Indlærte procedurer skal aflæres igen; det er svært og tidskrævende. Jeg vil<br />

anslå, at det typisk tager en til to måneder at sætte sig ind i tankegangen, to til<br />

tre måneder at lære at skrive programmer efter den og op til seks måneder at<br />

blive virkelig produktiv. Men tiden er givet godt ud og vil hurtigt komme<br />

tilbage, når den virkelige gevinst ved OOP viser sig: genbrug af software. Et<br />

nyt projekt anskues som en modifikation af et tidligere projekt, ikke som en<br />

helt ny udfordring.<br />

Jeg vil gerne rette en tak til Sten Lawætz, Christian Ravn og Peter Raabye for<br />

råd og forslag, til Borland Scandinavia for hjælp til fordanskning af alle de nye<br />

termer og til Niels Persson for engagement og udholdenhed. Særlig tak til<br />

Bjarne Stroustrup for hans arbejde med udviklingen af C++, et sprog med<br />

virkelige muligheder.<br />

Forord til anden udgave<br />

Der er en stigende udvikling indenfor objekt-orienteret teknologi. Gennem det<br />

sidste år er dusinvis af professionelle oversættersystemer og CASE-værktøj<br />

nået markedet, og objekt-orienteret analyse og design bliver mere og mere<br />

formaliseret. De relationelle databaser taber markedsandele til objektorienterede<br />

databaser og der tilbydes kurser som aldrig før.<br />

C++ har også udviklet sig. Da første udgave af denne bog udkom, var dybere<br />

kendskab til C++ en sjældenhed. I dag er det en velset kvalifikation for<br />

jobansøgende programmører. Som ventet har C++ vundet indpas i de fleste<br />

udviklingsafdelinger, hvor C før var det herskende sprog. Lærekurven for<br />

tusindvis af programmører er begyndt.<br />

For C++'s vedkommende findes desværre stadig ikke en færdig standard.<br />

ANSI-komiteen, der arbejder på en samlet pakke af omkring 25 objektorienterede<br />

standarder (X3-gruppen) forventer først at være færdig omkring<br />

sensommeren 1993. Der er imidlertid visse nyheder i sproget, som forventes at<br />

være sikre i den kommende standard, blandt andet de parameteriserede typer.<br />

Denne nye udgave behandler sproget, som der foreligger som produkt fra<br />

AT&T i version 3.0. Der er nye afsnit om skabeloner, og XCLklassebiblioteket<br />

bygger også nu på typeparameterisering. Fejlbehandling, som<br />

er beskrevet i appendiks A, ventes ikke at blive godtaget af ANSI, og behandles<br />

derfor ikke i bogens hovedafsnit.<br />

Der er også lagt mere vægt på objekt-orienteret analyse i <strong>kapitel</strong> 5, hvor et<br />

afsnit om genbrugs- og abstraktionsmekanismer gennemgår, hvordan større<br />

projekter gribes an. Derudover er alle bogens afsnit blevet revideret, rettet for<br />

fejl og mangler samt opdateret til den nye version.<br />

Tak rettes til Sten Lawætz, Christian Ravn, Christian Gabe, Torben Hansen


og Peter Berggreen for konstruktiv kritik. Errare humanum est.<br />

Frederiksberg, oktober 1991 og januar 1993<br />

Maz Spork<br />

(halgrim@diku.dk)<br />

iii


Indhold<br />

KAPITEL 1INTRODUKTION<br />

1.1 Hvorfor en ny programmeringsmetode? 1<br />

1.1.1 Lineær programmering 2<br />

1.1.2 Struktureret programmering 3<br />

1.1.3 Modulærprogrammering 4<br />

1.2 Dataabstraktion 4<br />

1.3 Objekt-orienteret programmering 5<br />

1.4 Programmeringssproget C++ 7<br />

1.5 Terminologi og konventioner 9<br />

1.6 Organiseringen af denne bog 10<br />

KAPITEL 2C++<br />

2.1 <strong>Introduktion</strong> 13<br />

2.2 Udviklingsmiljøet 14<br />

2.2.1 Erklæring og definition 15<br />

2.2.2 Præprocessering 15<br />

2.2.3 Typesystemet 17<br />

2.2.4 Standard-input/output 17<br />

2.3 Grundlæggende syntaks 18<br />

2.3.1 Kommentarer 18<br />

2.3.2 Afstand 19<br />

2.3.3 Navne 19<br />

2.3.4 Sætninger 20<br />

2.3.5 Udtryk 21<br />

2.4 Typer 21<br />

2.4.1 Symbolske variable 23<br />

2.4.2 Fundamentale typer 26<br />

2.4.3 Pointer-typer 28<br />

2.4.4 Reference-typer 29<br />

2.4.5 Vektor-typer 29<br />

i


ii<br />

2.4.6 Literale konstanter 30<br />

2.4.7 Symbolske konstanter 33<br />

2.4.8 Opremsninger 34<br />

2.4.9 Brugerdefinerede typer 35<br />

2.4.10 Typekonvertering 36<br />

2.5 Operatorer 39<br />

2.5.1 Aritmetiske operatorer 39<br />

2.5.2 Bitvise operatorer 40<br />

2.5.3 Logiske og relationelle operatorer 41<br />

2.5.4 Tildelings-operatorer 42<br />

2.5.5 Reference-operatorer 43<br />

2.5.6 Særlige operatorer 45<br />

2.5.7 Evalueringsrækkefølge 47<br />

2.5.8 Præcedens 47<br />

2.6 Funktioner 48<br />

2.6.1 Skop, blokke og levetid 50<br />

2.6.2 Prototyper 51<br />

2.6.3 void 52<br />

2.6.4 main() 52<br />

2.6.5 Funktionskald 53<br />

2.6.6 Afledte typer som parametre 54<br />

2.6.7 Returværdier 57<br />

2.6.8 Rekursion 59<br />

2.6.9 Overstyrede funktionsnavne 59<br />

2.6.10 Underforståede parametre 60<br />

2.6.11 Uspecificerede parametre 61<br />

2.6.12 inline-funktioner 62<br />

2.6.13 Evalueringsrækkefølge 62<br />

2.7 Kontrolstrukturer 63<br />

2.7.1 if/else 63<br />

2.7.2 while/do 65<br />

2.7.3 for 66<br />

2.7.4 break, continue og goto 67<br />

2.7.5 switch/case 68<br />

2.8 Datastrukturer 70<br />

2.8.1 Medlemsreferencer 72<br />

2.8.2 Bitfelter og union'er 72<br />

2.8.3 Anonyme strukturer 74<br />

2.8.4 Indlejrede strukturer 75<br />

2.8.5 Funktioner som medlemmer 77<br />

2.8.6 Fremad-referencer 78


2.8.7 Dereference af medlemmer 79<br />

2.9 Lageradministration 80<br />

2.9.1 Lagringsklasser 81<br />

2.9.2 new og delete 82<br />

2.10 Præprocessering 84<br />

2.10.1 Inkludering af headerfiler 84<br />

2.10.2 Makroekspansioner 84<br />

2.10.3 Betinget oversættelse 88<br />

2.11 Mere om pointere, vektorer og referencer 90<br />

2.11.1 Pointere 90<br />

2.11.2 Pointeraritmetik 91<br />

2.11.3 Vektorer 92<br />

2.11.4 Initiering af vektorer 94<br />

2.11.5 Vektorer og pointere 95<br />

2.11.6 Pointere og vektorer i funktioner 98<br />

2.11.7 Pas på med pointere 100<br />

2.11.8 Pointere og vektorer i datastrukturer 101<br />

2.11.9 Pointere og nul 102<br />

2.11.10 Pointere og void 103<br />

2.11.11 Pointere og typekonvertering 103<br />

2.11.12 Pointere og const 104<br />

2.11.13 Referencer 106<br />

2.11.14 Typedefinitioner 109<br />

2.12 I/O og standardbiblioteket 110<br />

2.12.1 Output 111<br />

2.12.2 Input 112<br />

2.12.3 Filbaseret I/O 112<br />

2.13 Opgaver til <strong>kapitel</strong> 2 113<br />

2.14 Referencer og udvalgt litteratur 115<br />

KAPITEL 3DATAABSTRAKTION<br />

3.1 <strong>Introduktion</strong> 117<br />

3.1.1 Biblioteker 119<br />

3.1.2 Grænseflader 119<br />

3.2 Et eksempel-problem 120<br />

3.2.1 En løsning i C 121<br />

3.2.2 Gennemgang af C-løsningen 124<br />

3.3 Klasse-begrebet 127<br />

3.3.1 Medlemsfunktioner 129<br />

iii


iv<br />

3.3.2 Underforståede medlemsreferencer 131<br />

3.3.3 Overstyrede funktionsnavne 131<br />

3.3.4 Underforståede parametre 132<br />

3.3.5 Indkapsling 133<br />

3.3.6 struct, class og union 136<br />

3.3.7 Klasser og skop 137<br />

3.3.8 Private medlemsfunktioner 138<br />

3.3.9 Klasse-baseret vs. objekt-baseret indkapsling 138<br />

3.3.10 Klasser som moduler 139<br />

3.3.11 Indkapslede typeerklæringer 140<br />

3.3.12 Anonyme klasser 142<br />

3.3.13 Statiske medlemsvariable 143<br />

3.3.14 Statiske metoder 145<br />

3.3.15 Konstante objekter 146<br />

3.3.16 Konstante metoder 148<br />

3.3.17 Overstyrede konstante metoder 150<br />

3.3.18 Første løsning i C++ 152<br />

3.4 Initiering og nedbrydning af objekter 154<br />

3.4.1 Konstruktøren 155<br />

3.4.2 Hvornår kaldes en konstruktør 156<br />

3.4.3 Konstruktører uden parametre 158<br />

3.4.4 Konstruktører med ét parameter 159<br />

3.4.5 Konstruktører med flere parametre 159<br />

3.4.6 Klasser med flere konstruktører 159<br />

3.4.7 Konstruktører med underforståede parametre 160<br />

3.4.8 Initieringslister 161<br />

3.4.9 Destruktøren 164<br />

3.4.10 Hvornår kaldes en destruktør 165<br />

3.4.11 Anden løsning i C++ 166<br />

3.4.12 Kopiering af objekter 169<br />

3.4.13 Kopi-konstruktøren 171<br />

3.4.14 Statiske objekter 172<br />

3.4.15 Pointere og referencer til objekter 172<br />

3.4.16 Dynamiske objekter 174<br />

3.4.17 Vektorer af objekter 176<br />

3.5 Overstyring af operatorer 178<br />

3.5.1 Operator-funktioner 179<br />

3.5.2 Hvad kan overstyres 180<br />

3.5.3 Overblik 181<br />

3.5.4 Overstyring af fortegns-operatorer 182<br />

3.5.5 Overstyring af binære operatorer 183


3.5.6 Overstyring af relationelle operatorer 183<br />

3.5.7 Overstyring af tildelings-operatorer 184<br />

3.5.8 Overstyring af særlige operatorer 185<br />

3.5.9 Retningslinier for operator-overstyring 186<br />

3.5.10 Operator-funktioner som klassemedlemmer 187<br />

3.5.11 friend-funktioner 188<br />

3.5.12 Mere om kopiering af objekter 190<br />

3.5.13 Midlertidige objekter 192<br />

3.5.14 Overstyring af indekserings-operatorer 193<br />

3.5.15 Overstyring af funktionskalds-operatorer 196<br />

3.5.16 Overstyringer af operator-funktioner 197<br />

3.5.17 Overstyringer i standardbiblioteket 199<br />

6.5.18 Mere om this 200<br />

3.5.19 Tredje løsning i C++ 203<br />

3.6 Brugerdefineret typekonvertering 204<br />

3.6.1 Konverterings-funktioner 206<br />

3.6.2 Eksplicit konvertering 207<br />

3.6.3 Implicit konvertering 208<br />

3.6.4 Konstruktører og konvertering 208<br />

3.6.5 Konverteringer i flere led 209<br />

3.6.6 Objektets underforståede værdi 211<br />

3.6.7 Fjerde løsning i C++ 212<br />

3.7 Klasser og pointere 216<br />

3.7.1 Pointere til medlemsdata 216<br />

3.7.2 Pointere til medlemsfunktioner 217<br />

3.8 Parameteriserede typer 218<br />

3.8.1 Eksempler på parameterisering 218<br />

3.8.2 Skabeloner 221<br />

3.9 Design af abstrakte datatyper 226<br />

3.9.1 Samspil mellem klasser 226<br />

3.9.2 Fejlbehandling i abstrakte datatyper 234<br />

3.9.3 Brugerdefineret lageradministration 237<br />

3.9.4 Objekt-I/O 242<br />

3.9.5 Samarbejde med andre sprog 244<br />

3.10 Anvendelser for abstrakte datatyper 247<br />

3.10.1 Klassen LinkedList 247<br />

3.10.2 Klassen AssocArray 250<br />

3.10.3 Klassen Random 254<br />

3.10.4 Klassen Set 256<br />

3.11 Opgaver til <strong>kapitel</strong> 3 262<br />

3.12 Referencer og udvalgt litteratur 264<br />

v


KAPITEL 4OBJEKT-ORIENTERET PROGRAMMERING<br />

vi<br />

4.1 <strong>Introduktion</strong> 265<br />

4.1.1 Det objekt-orienterede paradigme 266<br />

4.1.2 Terminologi 268<br />

4.2 Et eksempel-problem 270<br />

4.2.1 En løsning med abstrakte datatyper 271<br />

4.2.2 Klassen Koordinat 271<br />

4.2.3 Klassen Transformation 274<br />

4.2.4 Klassen Figur 278<br />

4.2.5 Klasserne Punkt, Linie og Firkant 278<br />

4.2.6 Gennemgang af ADT-løsningen 282<br />

4.2.7 En forbedret ADT? 283<br />

4.3 Arv 286<br />

4.3.1 Type-ækvivalens 286<br />

4.3.2 Lineær arv 288<br />

4.3.3 Tilgangsspecifikationer 291<br />

4.3.4 Tilgang fra klient-kode 294<br />

4.3.5 protected tilgang 299<br />

4.3.6 Tvetydigheder ved lineær arv 301<br />

4.3.7 Konstruktion af afledte objekter 303<br />

4.3.8 Destruktion af afledte objekter 307<br />

4.3.9 Typekonvertering under arv 308<br />

4.3.10 En løsning med nedarvning 309<br />

4.3.11 Problemer med lineær arv 316<br />

4.4 Polymorfe klasser 319<br />

4.4.1 En isotrop oftattelse af klasser 320<br />

4.4.2 Bindingstidspunkter 321<br />

4.4.3 Virtuelle funktioner 322<br />

4.4.4 Dynamisk binding som indkapsling 325<br />

4.4.5 Virtuelle metoder i detaljer 328<br />

4.4.6 Polymorfe metoder 330<br />

4.4.7 Polymorfe datastrukturer 333<br />

4.4.8 Initiering af polymorfe klasser 335<br />

4.4.9 Nedbrydning af polymorfe klasser 336<br />

4.4.10 Abstrakte klasser 337<br />

4.4.11 En løsning med virtuelle metoder 340<br />

4.5 Multipel arv 345<br />

4.5.1 Multipel arv med uafhængige baseklasser 347


4.5.2 Tvetydigheder ved multipel arv med uafhængige baseklasser 351<br />

4.5.3 Multipel arv med relaterede baseklasser 354<br />

4.5.4 Virtuelle baseklasser 356<br />

4.5.5 Initiering og nedbrydning af multipelt afledte objekter 363<br />

4.5.6 Typekonvertering under multipel arv 366<br />

4.5.7 Multipel arv og polymorfi 369<br />

4.5.8 Virtuelle klasser og polymorfi 376<br />

4.5.9 Problemer med virtuelle klasser 378<br />

4.6 Opgaver til <strong>kapitel</strong> 4 379<br />

4.7 Referencer og udvalgt litteratur 380<br />

KAPITEL 5OBJEKT-ORIENTERET PROGRAMKONSTRUKTION<br />

5.1 <strong>Introduktion</strong> 381<br />

5.2 Hvornår skal man bruge objekter? 385<br />

5.2.1 Overvejelser om klasser 385<br />

5.2.2 Fremgangsmåder 389<br />

5.2.3 Organisering af kildetekst 392<br />

5.3 Organisering af klasser 395<br />

5.3.1 Overvejelser om arv 396<br />

5.3.2 Alternativer til arv 402<br />

5.3.3 Medlemsvariable 410<br />

5.3.4 Metoder 411<br />

5.3.5 Adgangskontrol 416<br />

5.3.6 Polymorfi og genbrug 422<br />

5.3.7 Generalitet 424<br />

5.3.8 Overvejelser om multipel arv 425<br />

5.3.9 Overblik 432<br />

5.4 Grænseflader 434<br />

5.4.1 Grænsefladen til klienten 435<br />

5.4.2 Grænsefladen til den afledte klasse 437<br />

5.5 Klasser som processer 439<br />

5.5.1 Algoritmers transparens 439<br />

5.5.2 Datastrukturers transparens 440<br />

5.5.3 Modulærprogrammering 442<br />

5.5.3 Differentialprogrammering 442<br />

5.5.4 Funktionoider 443<br />

5.5.5 Delte objekter 445<br />

5.5.6 Arv og brugerdefineret lageradministration 445<br />

5.6 Containerklasser 448<br />

vii


5.6.1 Objekters ejerforhold 448<br />

5.6.2 Polymorfe containere 449<br />

5.6.3 Programmering med containere 451<br />

5.6.4 Gennemløb af containere 454<br />

5.7 Bestandighed 455<br />

5.7.1 Objekters levetid 456<br />

5.7.2 Implementation af bestandige objekter 457<br />

5.7.3 Administration af bestandige objekter 460<br />

5.7.4 OODBMS 462<br />

5.8 Klassebiblioteker 464<br />

5.8.1 Klassebibliotekets anvendelse 464<br />

5.8.2 Klassebibliotekets indhold og opbygning 466<br />

5.8.3 Topologi 467<br />

5.9 Genbrugsmekanismer i C++ 469<br />

5.9.1 Domæneananlyse 471<br />

5.9.2 Abstraktionsmekanismer 473<br />

5.9.3 Granularitet og genbrug 477<br />

5.9.4 Identifikation af abstraktioner og entiteter 478<br />

5.9 Udviklingsmæssige overvejelser 479<br />

5.10.1 Evolutionære ændringer 480<br />

5.10.2 Dokumentation 481<br />

5.10.3 Udviklingstekniske følger 482<br />

5.11 Opgaver til <strong>kapitel</strong> 5 483<br />

5.12 Referencer og udvalgt litteratur 483<br />

KAPITEL 6STANDARDBIBLIOTEKET I C++<br />

viii<br />

6.1 <strong>Introduktion</strong> 485<br />

6.1.1 Klasser og objekter i biblioteket 487<br />

6.2 Standard I/O 489<br />

6.2.1 Output 490<br />

6.2.2 Input 492<br />

6.3 Formatering 496<br />

6.3.1 Manipulatorer 500<br />

6.3.2 Formatering uden I/O 502<br />

6.4 Strømstatus og fejlbehandling 505<br />

6.5 Filer 507<br />

6.6 Brugerdefinerede udvidelser 513<br />

6.6.1 Klassespecifikke overstyringer af in- og outputoperatorerne 514<br />

6.6.2 Brugerdefinerede manipulatorer 515


6.7 Opgaver til <strong>kapitel</strong> 6 517<br />

6.8 Referencer og udvalgt litteratur 518<br />

KAPITEL 7KLASSEBIBLIOTEKET XCL<br />

7.1 <strong>Introduktion</strong> 519<br />

7.1.1 Funktionalitet i XCL 520<br />

7.1.2 Opbygning af klassehierarkiet 521<br />

7.1.3 Hierarkiet og standardbiblioteket 522<br />

7.2 Abstrakte klasser 522<br />

7.2.1 Klassen Object 523<br />

7.2.2 Klassen ContainerBase 526<br />

7.2.3 Klassen Container 527<br />

7.2.4 Klassen Sortable 530<br />

7.3 Værktøjsklasser 531<br />

7.3.1 Klassen Nil 532<br />

7.3.2 Klassen ObjectManager 534<br />

7.3.3 Klassen SharedObject 536<br />

7.3.4 Klassen Iterator 537<br />

7.4 Generelle datatyper 538<br />

7.4.1 Klassen String 539<br />

7.4.2 Klassen Complex 541<br />

7.5 Containerklasser 541<br />

7.5.1 Klassen GenericList::GenericNode 542<br />

7.5.2 Skabelonklassen GenericList 542<br />

7.5.3 Skabelonklassen GenericArray 546<br />

7.5.4 Klassen LinkedList 546<br />

7.5.5 Klassen Association 549<br />

7.5.6 Klassen Dictionary 550<br />

7.5.7 Klassen Queue 552<br />

7.5.8 Klassen DeQueue 554<br />

7.5.9 Klassen Stack 555<br />

7.5.10 Klassen Array 556<br />

7.5.11 Samarbejde mellem containerklasser 558<br />

7.6 Objekt-I/O 559<br />

7.7 Udvidelser og tilpasninger 564<br />

7.8 Opgaver til <strong>kapitel</strong> 7 570<br />

7.9 Referencer og udvalgt litteratur 570<br />

ix


APPENDIKS A FREMTIDSUDSIGTER 573<br />

A.1 ANSI C++ 573<br />

A.2 Fejlbehandling 574<br />

A.3 Parameteriserede typer 580<br />

A.4 Typeidentifikation på kørselstidspunktet 580<br />

APPENDIKS BGODE RÅD TIL C-PROGRAMMØRER 583<br />

APPENDIKS C ANDRE OBJEKT-ORIENTEREDE SPROG 595<br />

C.1 Hvornår er man objekt-orienteret? 595<br />

C.2 Pascal-varianter 596<br />

C.3 Eiffel 597<br />

C.4 Smalltalk 597<br />

C.5 Actor 599<br />

APPENDIKS D OBJEKT-ORIENTERET ORDBOG 601<br />

APPENDIKS EBIBLIOGRAFI 607<br />

APPENDIKS FKILDETEKST 611<br />

STIKORDSREGISTER 659<br />

x


Figurer<br />

1-1 Placeringen af C++ blandt andre vigtige sprog 8<br />

2-1 Afhængigheder mellem filer 16<br />

2-2 Intervaller for signed og unsigned char-typer 27<br />

2-3 Præcisionen af kommatal centreres omkring 1 og -1 28<br />

3-1 Ejerforhold for statisk datamedlem 144<br />

3-2 Grafisk repræsentation af en hægtet liste 248<br />

4-1 Lineær arv, multipel arv og hierarki af klasser 268<br />

4-2 Grafisk transformation 274<br />

4-3 Et associativt opbygget hierarki 287<br />

4-4 Nedarvning fra Figur 290<br />

4-5 Det afledte objekts reelle indhold 309<br />

4-6 Ny acyklisk orienteret graf (dag), hvor Firkant nedarver fra Linie 316<br />

4-7 Skjulte datamedlemmer i polymorfe klasser (konceptuel model) 329<br />

4-8 Et kald til en virtuel funktion. 329<br />

4-9 Multipel arv 346<br />

4-10 Multipel arv i fauna-hierarkiet 347<br />

4-11 Himmellegeme-delhierarki 349<br />

4-12 Multipel arv i Figur-hierarkiet 350<br />

4-13 Multipel arv med relateret baseklasse 354<br />

4-14 Multipel arv med urelaterede baseklasser 355<br />

4-15 Et tvetydigt baseobjekt i en hægtet liste 359<br />

4-16 Et entydigt baseobjekt i en hægtet liste 360<br />

4-17 Sammenblanding af virtuelle og ikke-virtuelle baseklasser 364<br />

4-18 Virtuelle funktioner som "klister" mellem baseklasser 371<br />

4-19 En incidensmatrix implementeret med hægtede lister 372<br />

4-20 Arvetræ for en incidensliste (adjacency list) 374<br />

4-21 En grafstruktur til implementation i AdjacencyNode-subhierarkiet 374<br />

4-22 En graf repræsenteret ved hægtede lister 375<br />

5-1 Afhængigheder mellem moduler i LinkedList-hierarkiet 393<br />

5-2 Generisk kode som klister-element mellem klienter og klasser 423<br />

xi


xii<br />

5-3 Modulær dekomposition og rekomposition 425<br />

5-4 Hierarki af klasser i et vinduessystem 429<br />

5-5 Dualisme i klassens grænseflade 435<br />

5-6 Brugerdefineret typekonvertering har polymorfe egenskaber 427<br />

5-7 En adressetabel udgør programmørkontrolleret polymorfi 461<br />

5-8 Klassetopologisk opbygning: enkelt træ eller skov af træer 467<br />

5-9 Balancen i et objekt-orienteret design 473<br />

6-1 Standardbibliotekets klassehierarki 487<br />

7-1 Hierarkisk fordeling af klasserne i XCL 520<br />

7-2 Den interne (fundamentale) repræsentation af hægtede lister i XCL 545<br />

7-3 Struktur over liste af printerkøer 559


Tabeller<br />

2-1 Eksempler på typer på forskellige processorer 26<br />

2-2 Aritmetiske operatorer 39<br />

2-3 Bitvise operatorer 40<br />

2-4 Logiske og relationelle operatorer 41<br />

2-5 Kombinerede tildelings-operatorer 42<br />

2-6 Reference-operatorer 42<br />

2-7 Særlige operatorer 46<br />

2-8 Operatorernes associativitet, prioritet og syntaks 49<br />

3-1 Funktionsoversigt i C-løsningen 121<br />

4-1 Den afledte klasses tilgang ved arv med forskellige tilgange 300<br />

5-1 Et eksempel på et semantisk katalog over klasser 429<br />

6-1 Klasser i standardbiblioteket 487<br />

6-2 Objekter i standardbiblioteket 488<br />

7-1 Propagation gennem dumpOn()-metoder i komplekst objekt 562<br />

xiii


Programeksempler<br />

xiv<br />

3-1 Kommatals-aritmetik 120<br />

3-2 En C-specifikation på et komplekst tal 122<br />

3-3 En C++-specifikation af et komplekst tal 152<br />

3-4 En C++-specifikation af et komplekst tal med kons- og destruktører 167<br />

3-5 En C++-specifikation af et komplekst tal med operatoroverstyringer 203<br />

3-6 En C++-specifikation af et komplekst tal med typekonverteringer 212<br />

3-7 Dynamiske strenge 227<br />

3-8 En hægtet liste 248<br />

3-9 En associativ liste 250<br />

4-1a En koordinat-ADT 271<br />

4-1b En todimensionel transformationsklasse 275<br />

4-1c En generel figur-klasse 278<br />

4-1d En konkret figur-klasse 279<br />

4-1e Flere konkrete figur-klasser 280<br />

4-2 En kombineret figur-klasse 283<br />

4-3 En generel figur-baseklasse 310<br />

4-4 En nedarvet punkt-klasse 312<br />

4-5 En nedarvet linie-klasse 312<br />

4-6 En nedarvet rektangel-klasse 314<br />

4-7 En indirekte nedarvet rektangel-klasse 314<br />

4-8 Anvendelse af nedarvede klasser 315<br />

4-9 Vektor af pointere til baseklasser 318<br />

4-10 Figurklasser med virtuelle funktioner 324<br />

4-11 Typeuafhængighed for Roter() og Forstoer() 327<br />

4-12a En klasse, der både er klient og arving 333<br />

4-12b Polymorfe datastrukturerer i anvendelse 334<br />

4-13 Virtuelt kald fra konstruktør 336<br />

4-14 En abstrakt figur-klasse 340<br />

4-15 Virtualiseret version af eksempel 4-12 341<br />

4-16 Eksplicitte kopi-rutiner 343<br />

4-17 Et generelt himmellegeme 347


4-18 Multipel arv med urelaterede baseklasser 354<br />

4-19 Hægtet liste med polymorfe hægter 356<br />

4-20a En sorteret liste med polymorfe hægter 357<br />

4-20b En indekseret liste med polymorfe hægter 358<br />

4-21 En sorteret, indekseret polymorf liste 358<br />

4-22 Konkretisering af hægte-klassen 362<br />

4-23 Multipel arv med virtuelle funktioner 369<br />

4-24a En abstrakt incidenshægte 372<br />

4-24b En headnode, en egentlig knude i grafen 373<br />

4-24c En knude i grafen, ref. til en HeadNode 373<br />

5-1a Vektor-repræsenteret stak af heltal 416<br />

5-1b Hægtet liste-repræsenteret stak af heltal 417<br />

5-2a En endimensionel mængde-klasse 420<br />

5-2b En todimensionel mængde-klasse 420<br />

5-3 En generel lineær semafor 444<br />

5-4 Lageradministrationsklassen CustMemMgr 445<br />

5-5 Et generelt, associérbart objekt 452<br />

5-6 Anvendelse af associérbare lister 453<br />

6-1 iostream anvendt på fundamentale typer 490<br />

6-2 Uformateret I/O med iostream 491<br />

6-3 Søgning i et istream-objekt 495<br />

6-4 Filtrering af kommentarer fra kildetekst 495<br />

6-5 Brugerdefinerede formateringer 497<br />

6-6 Anvendelse af manipulatorer 500<br />

6-7 In-core formatering 502<br />

6-8 Klientkontrolleret buffering i ostrstream 503<br />

6-9 Konvertering fra tegn til objekter 504<br />

6-10 Anvendelse af strømmenes statusflag 507<br />

6-11 Læsning og skrivning af filer med fstream 509<br />

6-12 Ordtælling i filer med fstream 510<br />

6-13 Kopiering af filer med fstream 510<br />

6-14 Genanvendelse af samme strøm-objekter 511<br />

xv


<strong>Introduktion</strong><br />

Denne bog handler om problemløsning. Når vi skriver programmer på vores<br />

computere, søger vi at finde en løsning på et problem, som vi har defineret. Der<br />

findes utallige værktøj til at hjælpe os i denne proces, både teoretiske metoder<br />

og fysiske værktøj. Men der er sket noget væsentligt de sidste par år. Førhen<br />

var vi glade, når problemet var løst, og programmerne virkede, som de skulle.<br />

Hvert projekt fulgte samme procedure, og programmøren måtte hver gang<br />

genopfinde hjulet for at tilpasse løsningen til problemdefinitionen. I dag står<br />

nogle ting mere klart om fremtiden for software engineering: programmerne er<br />

efterhånden så komplekse, at de hidtidige metoder for udvikling er for svage.<br />

Med 100 programmører i et udviklingsprojekt er antallet af fejl store og<br />

kilderne er svære at finde. Vi kan ikke længere forsvare at starte fra bunden,<br />

hver gang vi begynder på et nyt projekt.<br />

1.1 HVORFOR EN NY PROGRAMMERINGSMETODE?<br />

Mange metoder har gennem tiden været brugt som hjælp i store projekter. Problemet har dog<br />

været, at disse metoder har krævet for meget af den individuelle programmør. Det har været<br />

programmørens ansvar at overholde reglerne, men metoderne har ikke kunnet tvinge ham eller<br />

hende til det. Dertil kommer, at redskaberne (sprogene, CASE-værktøjerne, de integrerede<br />

miljøer) altid har været bagud i forhold til omfanget af de problemer de er lavet til at løse. Først<br />

efter problemet er blevet defineret, er redskabet blevet udviklet.<br />

Objekt-orienteret programudvikling (OOP) har mange indbyggede løsninger på disse<br />

problemer. Ideen er ikke ny, men har været kendt og brugt i over 20 år. Der er dog en forskel på,<br />

om et sprog tillader en bestemt form for udviklingsmetode, og om det understøtter denne metode.<br />

Det er muligt at skrive objekt-orienterede programmer i C eller Pascal, men det kræver en stor<br />

indsats fra programmørens side. Disse sprog understøtter ikke denne metode, og gør<br />

programmeringsmiljøet ustabilt. Til gengæld kræver objekt-orienteret programmering en ganske<br />

anden måde at angribe problemerne på.<br />

Hvad er det helt konkret, der er i vejen med sprog som C og Pascal - vi har løst vores problemer<br />

xvi<br />

1


i disse sprog i to årtier med gode resultater. Der er intet i vejen med for eksempel procedurale<br />

programmeringssprog, når der tænkes på de problemer, de er beregnet til at løse. Men store,<br />

komplekse programmer som OS/2 vidner om, at morgendagens problemer ikke kan løses med<br />

lethed, når vi bruger de konventionelle programmeringssprog og -metoder. Disse problemer<br />

omfatter<br />

• at udviklingsprojekter, som kræver en stor skare af programmører, er svære at<br />

kontrollere. Det er med de konventionelle metoder svært at sikre, at de regler, der<br />

fremsættes, overholdes af alle.<br />

• at allerede skrevne programmer er svære at genbruge, fordi de er for specialiserede. Det<br />

er i konventionelle sprog vanskeligt at skrive generiske programmer, der senere kan<br />

tillægges en konkret betydning.<br />

• at fejlfindingsprocesser er svære, når selve programmeringssproget ikke understøtter<br />

udviklingsmetoden direkte. Det er aldrig entydigt hvor og af hvem en fejl kommer fra.<br />

• at kildekoden til programmet, som ofte består af mange filer, skal gennemgå store<br />

modifikationer rent behandlingsmæssigt for at lave en mindre ændring. Det er farligt,<br />

fordi man risikerer at introducere nye fejl i allerede gennemtestet kode.<br />

Lad os tage en kort opremsning af programmeringsmetodernes historie, før vi kommer ind på<br />

objekt-orienteret programmering.<br />

1.1.1 Lineær programmering<br />

Computerprogrammering er en ung videnskab. De første programmérbare computere er under 40<br />

år gamle, og gennem disse år har udviklingen vist en klar tendens: at programmere er blevet mere<br />

og mere et spørgsmål om behov for programmøren som menneske. De første programmer blev<br />

skrevet i binær kode, som blev tastet ind i computeren ad mekanisk vej med fysiske kontakter.<br />

Senere, da lagerkapaciteten blev forøget, blev reelle højniveau-computersprog udviklet, og<br />

programmøren kunne nu i stedet for binær kode tænke i rækker af kommandoer, som mindede<br />

mere om naturligt sprog, og var lettere at arbejde med.<br />

De første sprog var designet til at løse mindre opgaver - for det meste mindre trivielle<br />

udregninger - og det var derfor ikke særdeles krævende (efter vore dages begreber) for sproget at<br />

behandle disse programmer. Desuden var programmerne sjældent på over et par hundrede linier<br />

store. Men som computernes styrke og kapacitet voksede, voksede opgaverne med dem (en af de<br />

universelle love om computere). De tidlige lineære programmeringssprog kunne ikke bruges til<br />

disse nye opgaver. De lineære programmer bestod ofte af en eksekveringssekvens fra start til slut,<br />

uden kald til subrutiner. Når programmet ændrede sekvens, var det i form af ubetingede hop, men<br />

hvorfor og hvordan sekvensen ændrede sig, var svær at se klart. Alle data var globale, og kunne<br />

følgelig manipuleres fra alle steder i programmet. Det var klart ikke muligt at skrive bare semikomplekse<br />

programmer i disse sprog, der mest af alt minder om de sprog, der findes på mindre<br />

1.1 Hvorfor en ny programmeringsmetode? 17


programmérbare lommeregnere i dag.<br />

1.1.2 Struktureret programmering<br />

Det blev hurtigt klart, at nye sprog måtte udvikles for at klare de nye opgaver, som de hurtigere<br />

og bedre computere kunne udføre (en anden universel lov: vi er altid bagud i software, forud i<br />

hardware og visioner). I slutningen af 60'erne kom revolutionen med struktureret programmering,<br />

hvor programmerne opdeles i mindre dele efter de opgaver, de udfører. Mindre funktioner og<br />

procedurer varetager mere specialiserede opgaver, og indgår i en helhed i en større opgave.<br />

Funktionerne har så lidt med hinanden at gøre som muligt, og indeholder egne, lokale data.<br />

Information kan flyde fra funktion til funktion efter formelle regler. Hver lille funktion kan<br />

opfattes som et miniprogram, som, når det bliver samlet med de andre, blev til en applikation.<br />

Målet var at gøre livet lettere for programmøren og samtidig gøre programmerne nemmere at<br />

læse og vedligeholde. Et struktureret program skrives ved at bryde opgaven op i mindre, logisk<br />

sammenhængende dele, og skrive disse dele hver for sig. Det gør det lettere at isolere problemer,<br />

fordi en fejl kan føres tilbage til den funktion, som behandler en bestemt del-opgave. Selve<br />

sproget gav en vis støtte til programmøren, som nu havde en chance for at forstå, hvad der<br />

foregik. Selve eksekveringssekvensen blev mere tydelig i kildeteksten.<br />

Med struktureret programmering blev et andet, meget kraftigt koncept introduceret, nemlig<br />

abstraktion. Abstraktion defineres i struktureret programmering som muligheden for at kunne<br />

bruge en facilitet uden at skulle bekymre sig om, hvordan den udfører sit arbejde. I et struktureret<br />

program er det nok at vide at en given funktion udfører en operation, ikke hvordan den gør det -<br />

så længe den virker kan den bruges (og genbruges) af programmøren. Dette kaldes funktionel<br />

abstraktion og er hjørnestenen i struktureret programmering.<br />

I dag er struktureret programmering og struktureret design ikke til at komme udenom. Alle<br />

programmeringssprog understøtter de faciliteter, der kendetegner struktureret programmering,<br />

selv BASIC er blevet struktureret! Det er ikke svært at forestille sig grunden til dette: Struktureret<br />

programmering er en bedre metode end lineær programmering til at udvikle og vedligeholde<br />

programmer.<br />

Til gengæld voksede opgaverne igen. Problemet blev nu, at der ingen logisk sammenhæng var<br />

mellem kode og data, og at større projekter med mange programmører havde svære tider med at<br />

få deres respektive dele af et stort projekt til at kunne tale sammen.<br />

1.1.3 Modulærprogrammering<br />

Modulærprogrammering løser en del af disse problemer. I de fleste konventionelle proceduralt<br />

orienterede sprog er det, som vi skal se, muligt at skjule irrelevante data fra klienten (i denne bog<br />

benyttes termen klient om den programmør, der anvender og bygger videre på de programmer,<br />

som andre konstruerer). Denne form for dataabstraktion er et skridt på vejen mod mere solide<br />

programmer, fordi kode og data klumpes sammen i enheder. I C er det muligt at opdele<br />

programmet i et antal separate kildefiler, der indeholder statiske data, som kun kendes i den fil de<br />

er defineret i - heraf modulariteten. Bruges objektkoden fra dette modul af andre, kan de statiske<br />

18 <strong>Introduktion</strong> 1


data ikke ses - de er private for modulet. Sproget Modula-2 går et skridt videre og har en formel<br />

tilgangsmetode til private data, nemlig en sprogunderstøttet import- og eksportfacilitet af<br />

moduler. Mens struktureret programmering tilstræber at associere data, som har med bestemt<br />

kode at gøre, med denne kode, tvinger modulærprogrammering programmøren til dette. Eller<br />

rettere: data skal isoleres med koden, og kun koden skal kunne være tilgængelig for andre.<br />

Dette har vist sig at være en god idé. Men modulærprogrammering var mere en<br />

programmeringsmetode end et programmeringssystem. Ideen med at samle kode og data er først<br />

for alvor virkningsfuld, når den bliver brugt på den rigtige måde, men hvis sproget ikke støtter<br />

metoden, vil der altid være faldlemme og ulemper. Der er derfor brug for en mere klar metode til<br />

afgrænsning af grænseflader, afhængigheder og tilgang.<br />

1.2 DATAABSTRAKTION<br />

Dataabstraktion gør for data, hvad funktionel abstraktion gør for kode. Ideen med modulariteten<br />

er at samle programmer og data, der har med hinanden at gøre, under samme tag. Dette kendes<br />

fra C og Pascal, hvor struct- og record-konstruktioner gøres lokale til den funktion, der<br />

bruger dem. C++ tillader, at vi samler data og kode i klumper for dermed at begrænse disse datas<br />

virkefelter eller skop til de relevante funktioner. Dette kaldes indkapsling, og sikrer, at<br />

datastrukturer kun manipuleres direkte af den kode, der har lov til det. Andre dele af programmet<br />

må kalde en funktion for at ind- eller udlæse disse data. Hermed kan ansvaret for en inkonsistens<br />

eller redundans i data føres tilbage til de dele af programmet, der ejer dem. Ingen uvedkommende<br />

kan røre ved dem - de kan slet ikke se dem. Hermed gøres programmerne sikrere og mere<br />

robuste.<br />

Dette introducerer dog et andet problem. På grund af klientens tvang til indirektion bliver<br />

dennes programmer sjældent læsbare. Ægte dataabstraktion udvider derfor dette koncept med en<br />

entydig grænseflade, så klienten ser de omhandlede data som en ny datatype, der kan manipuleres<br />

på generel vis, ligeledes defineret af designeren af typen.<br />

Dataabstraktion er en vigtig grundpille i forståelsen af objekt-orienteret programmering.<br />

Normalt har kun en oversætterdesigner mulighed for at skabe reelle nye datatyper. I de sprog, vi<br />

kender, findes allerede abstrakte datatyper, for eksempel C's double. Denne decimaltalstype<br />

findes sjældent direkte i hardware, og oversætteren erstatter hver manipulation med et<br />

underforstået funktionskald. I C-udtrykket<br />

a = b * c + d,<br />

hvor alle variable er af typen double, vil den færdige objektkode indeholde underforståede<br />

kald til en multiplikations- og en additionsrutine. Men vi har lov at bruge samme syntaks i vores<br />

udtryk, som var det en heltalsberegning. Operatorerne har dermed forskellige betydninger, som<br />

afhænger af den semantiske kontekst, og siges at have polymorfe egenskaber. Dette foregår<br />

selvfølgelig uden programmørens indblanding, og vi tænker ikke på det til daglig.<br />

Det er denne abstraktion, der i C++ er indbygget i selve sproget. Som programmører kan vi<br />

designe nye datatyper og lade klienten anvende dem som var de indbyggede eller fundamentale<br />

typer. Vi specificerer blot den grænseflade, vi ønsker typen skal have, og beder klienten om ikke<br />

1.1.3 Modulærprogrammering 19


at bekymre sig om hvordan den reelt er implementeret. De nye typer, vi designer, kalder vi<br />

abstrakte, fordi de i realiteten kan være af enhver art. Udtrykket abstrakte datatyper er en smule<br />

misvisende, fordi typerne ikke forekommer abstrakte i brug, men derimod aldeles konkrete. Det<br />

skal forstås sådan, at klienten kan abstrahere fra implementationen af typen og koncentrere sig<br />

om sit eget problem.<br />

1.3 OBJEKT-ORIENTERET PROGRAMMERING<br />

Jeg har nævnt, at objekt-orienteret programmering kræver en ny holdning til opbygning af<br />

programmer. Skabelsen af abstrakte datatyper og grænsefladen til deres brug nødvendiggør, at<br />

programkonstruktionen må foretages særdeles anderledes. I stedet for at tænke på, hvordan<br />

bestemte data manipuleres, som det gøres i proceduralt orienteret programmering, skal vi<br />

fokusere på, hvad der skal gøres med disse data. Hvor vi hidtil har benyttet os af datastrukturer og<br />

kontrolstrukturer som definition på et program, skal vi bruge ideen om objekter og beskeder eller<br />

signaler objekterne imellem. Vi kan omskrive Niklaus Wirths frase datastrukturer + algoritmer<br />

= programmer til objekter + beskeder = programmer.<br />

Men hvad er et objekt egentlig? Det indeholder de nødvendige data eller datastrukturer for at<br />

beskrive et emne eller en entitet, tilsammen med de funktioner - metoder - der skal til for at<br />

manipulere emnet. Mulighederne er med denne simple definition endeløse: et objekt kan<br />

indeholde en intern repræsentation af en bil, en skatteyder eller et psykofarmaka samt metoder til<br />

at udregne kilometer/liter-forhold, fastsætte restskat eller bestemme bivirkninger. De fleste<br />

objekt-orienterede sprog har en konstruktion til definition af et objekt. I C++ (og i de fleste andre<br />

objekt-orienterede systemer) kaldes den for en klasse, og termerne klasse, abstrakt datatype og<br />

brugerdefineret type benyttes i denne bog i reglen med samme betydning. Klasser udgør altså en<br />

definition på en type, som kan instantieres i et objekt. En klasse og et objekt er ikke det samme;<br />

et objekt er en forekomst af en klasse. Betegnelserne objekt, forekomst og variabel har i C++ stort<br />

set den samme mening.<br />

En stor del af objekt-orienterede sprogs styrke ligger i deres evne til at nedarve egenskaber fra<br />

klasse til klasse. En generisk overordnet klasse kan nedarve sine egenskaber (data og metoder) til<br />

en underordnet klasse, som så kan bygge videre på den. Den overordnede klasse, også kaldet<br />

baseklassen, indeholder meget generelle strukturer, mens den underordnede eller afledte klasse<br />

indeholder mere specifikke strukturer. For eksempel kan en overordnet klasse repræsentere et<br />

transportmiddel, hvilket jo er et meget generelt koncept. Herefter kan en underordnet klasse, der<br />

nedarver egenskaberne fra transportmiddel-klassen, beskrive en bil, mens en anden beskriver et<br />

fly. Det kan endda ske i flere led, hvorved vi får adskillige mellemliggende klasser -<br />

transportmiddel nedarver til køretøj, båd og flyvemaskine, som så igen nedarver til henholdsvis<br />

personbil, bus og lastvogn, speedbåd, sejlbåd og færge samt propelfly, jetfly og svævefly. Den<br />

resulterende struktur kaldes et klassehierarki.<br />

Hvad er så den fordel ved arbejdet med klassehierarkier? Et hierarki set fra bunden består af<br />

klasser af stigende generisk karakter. Generiske klasser kan bruges som overordnede for nye<br />

klasser, som kan påføres hierarkiet som de bliver nødvendige. Denne form for<br />

differentialprogrammering gør, at de tidligere definerede klasser (med grænseflader) ikke<br />

behøver at gennemgå modifikationer, og vi opnår et vigtigt mål, nemlig genbrug af kode. Når vi<br />

20 <strong>Introduktion</strong> 1


kan genbruge dele af programmer, vi allerede har skrevet uden at ændre i dem, har vi sparet tid.<br />

Ikke kun i selve programmeringen, men i både analyse, design og test. En anden fordel ved<br />

klassehierarkier er, at typen af et specifikt objekt - objektets reelle klasse - ikke nødvendigvis<br />

behøver være kendt på oversættelsestidspunktet. Ved en teknik, der kaldes sen eller dynamisk<br />

binding tillades identifikationen af det faktiske objekt fra et hierarki af relaterede objekter at vente<br />

til kørselstidspunktet. Det giver programmøren mulighed for at sende en besked til et objekt uden<br />

at kende den konkrete forekomst af objektet. For eksempel behøver programmøren kun viden<br />

om, at en bestemt objekt-reference peger på et transportmiddel. Et bestemt funktionskald vil først<br />

blive bundet til det objektet af den faktiske type (personbil, speedbåd osv.), når det foretages<br />

under kørslen, og ikke under oversættelsen. I C++ realiseres dette med virtuelle funktioner og<br />

abstrakte klasser, som tillader denne polymorfi. Teknikken lader os således benytte funktioner og<br />

programmoduler, der virker og er skrevet til et specifikt formål få nye anvendelser, igen uden at<br />

ændre i dem.<br />

Nøgleordet er genericitet. Programudviklingen i fremtiden skal fokusere klart på, hvordan<br />

systemerne kan opdeles i moduler af forskellig generisk karakter. Disse moduler, klasser,<br />

delhierarkier eller komplette hierarkier bliver til et slags software-modstykke til integrerede<br />

kredse, som man kan sammenstykke til en komplet applikation. Denne fremgangsmåde er<br />

grunden til, at designfasen skal koncentreres om en differentiering af substansen i de forskellige<br />

programdele.<br />

1.4 PROGRAMMERINGSSPROGET C++<br />

Programmeringssproget C++ bruges som referencesprog i denne bog, og dele af den kan i vidt<br />

omfang bruges som lærebog i sproget. C++ er opfundet og først implementeret af danskeren<br />

Bjarne Stroustrup i starten af firserne på det amerikanske telefonselskab AT&Ts Bell<br />

Laboratories. C++ er en efterfølger til sproget C, og er på nær nogle få områder kompatibelt med<br />

ANSI C - den første implementation af sproget hed endda C with Classes. Det giver Cprogrammører<br />

mulighed for at komme i gang med det samme og gå inkrementalt til værks med<br />

de nye faciliteter. C findes på næsten alle systemer, hvilket begrænser de teknologiske barrierer<br />

mod at installere C++. Standardiseringen af C++, foretaget af den amerikanske nationale<br />

standardiseringskomite ANSI, giver os garanti for en ensartethed mellem implementationer af<br />

sproget på forskellige systemer. ANSI C++ har været undervejs siden 1986 og foreligger nu i en<br />

form, der ligger meget tæt på AT&Ts version 3.0 af sproget.<br />

C++ er ikke det første sprog, som understøtter de objekt-orienterede principper. Xerox'<br />

Smalltalk er for eksempel et fuldt integreret objekt-orienteret programmeringsmiljø, Ada fra<br />

USA's forsvarsministerium gør meget ud af dataabstraktion og LISP tillader sen binding. C++ har<br />

"lånt" meget fra bla. Simula og Algol, men har nogle klare fordele frem for andre objektorienterede<br />

sprog, idet det i henhold til definitionen på paradigmet er meget rent. Samtidig kan<br />

det, fordi det bygger direkte på C, anvendes til systemprogrammering og opbygning af meget<br />

store og komplekse programmer. C++ er, ligesom C, et minimalt sprog - der findes mindre end<br />

50 reserverede ord - men samtidig meget alsidigt og effektivt.<br />

Mange programmører er aldrig hoppet på C-vognen, fordi de efter en evaluering har skønnet, at<br />

sproget har alt for mange faldgruber og giver anledning til alt for mange fejl. C++ afhjælper<br />

1.3 Objekt-orienteret programmering 21


meget af dette, men bibeholder stadig de fordele, som C-programmører holder af deres sprog for.<br />

Den basale syntaks er den samme, og nyhederne ændrer ikke sprogets ansigt væsentligt. Det er<br />

anslået, at en fuld forståelse for objekt-orienteret programmering og C++ tager omkring et år,<br />

men samtidig at udbyttet vil spare meget mere tid.<br />

Figur 1-1: Placeringen af C++ blandt andre vigtige sprog.<br />

22 <strong>Introduktion</strong> 1


I appendiks B gennemgås C++ specielt for C-programmører. Her forklares, hvordan de<br />

væsentligste problemer i C kan overkommes med elegance og lethed i C++, og hvordan man som<br />

C-programmør bør forholde sig til et skift til C++.<br />

1.5 TERMINOLOGI OG KONVENTIONER<br />

Denne bog er bygget over skeletter af C++ kode. For at skelne mellem brødtekst og tekst med<br />

syntaktisk relevans (programtekst), bruges forskellige skrifttyper. Når der i brødteksten refereres<br />

til dele af programteksten, benyttes en ensartet skrifttype for at understrege, at der er<br />

tale om den entydige skrivemåde.<br />

Alle programmører har sine egne præferencer hvad angår formatering af kildetekst, og<br />

forfatteren er ingen undtagelse. I alle kode-eksempler benyttes en indryknings-teknik som denne:<br />

void strcpy (char* dst, const char* src) {<br />

while (*src) {<br />

*dst = *src;<br />

dst++, src++;<br />

}<br />

*dst = '\0';<br />

}<br />

I mange eksempler findes desuden en "tom kommentar":<br />

// ...<br />

for at vise, at der mangler programtekst på dette sted. Denne programtekst behøves ikke for at<br />

forstå eksemplet, men skal selvfølgelig skrives for at få eksemplet til at køre. De fleste eksempler<br />

kunne med lethed håndoptimeres med øget effektivitet som mål, men med læsbarheden som<br />

offer. Det er vigtigere, at en illustration i form af et program kan forstås uden besvær.<br />

Når funktionsnavne bliver omtalt i teksten, vil de blive fulgt af en tom parantesomslutning, som<br />

i en henvisning til funktionen strcpy() ovenfor. Parametre medskrives ikke i brødteksten;<br />

parenteserne er kun til at identificere et funktionsnavn.<br />

Der anvendes generelt versaler til symbolske konstanter. For eksempel,<br />

const int SECONDSPERHOUR = 3600;<br />

og små bogstaver til ikke-konstante objekter af fundamentale datatyper, som i<br />

long time = 1000000;<br />

mens navne på brugerdefinerede datatyper altid starter med en versal. Hvis navnet består af flere<br />

ord, begynder disse også med versaler - ikke separeret med understregninger, som det er kutyme i<br />

C, men derimod som biversaler. For eksempel,<br />

1.4 Programmeringssproget C++ 23


class Matrix;<br />

class Complex;<br />

class ObjectManager;<br />

class AssociativeArray;<br />

Navne på forekomster af brugerdefinerede typer følger samme regler som navne på<br />

brugerdefinerede typer - blot er det første bogstav ikke skrevet med stort. For eksempel,<br />

Matrix metaData;<br />

Complex fourierSeries [100];<br />

ObjectManager theManager;<br />

AssociativeArray diskCache;<br />

Eftersom objekt-orienteret programmering først nu er ved at blive udbredt i større stil, findes<br />

ingen standardiserede danske oversættelser af de mange nye ord, der beskriver principperne og<br />

metoderne. De danske ord, jeg har valgt at bruge i denne bog er taget fra flere de facto standarder<br />

fra bla. danske softwarehuse, forlag, fagblade samt forslag fra danskere, der benytter C++ i deres<br />

daglige arbejde. Der findes en ordforklaring i appendiks D, så betydningen af eventuelle for<br />

læseren nye og mystiske ord kan slås op.<br />

Slutteligt skal det nævnes, at litteraturhenvisninger og referencer nævnes i kantede parenteser<br />

med den primære forfatters efternavn samt årstallet på udgivelsen og et eventuelt seriebogstav<br />

hvis der er tvetydigheder. I appendiks E findes en bibliografi.<br />

24 <strong>Introduktion</strong> 1


1.6 ORGANISERINGEN AF DENNE BOG<br />

Det er en vanskelig proces at skifte arbejdsmodel. Alle softwarefolk, der bruger veldefinerede<br />

metoder og indarbejdede værktøj har det dårligt med at skulle skifte til noget andet. Det faktum,<br />

at der findes så mange forskellige systemer betyder, at en helt generel indføring i objektorienteret<br />

tankegang uden fokus på det konkrete og dagligdags er svær at skabe. Af den grund<br />

henvender denne bog sig bedst til dem, der i forvejen kender til C og arbejdsgangen bag<br />

programudvikling med dette sprog. Og som konsekvens af dette er store dele af bogen centreret<br />

om konkrete forskelle mellem C og C++, ikke som konstruktioner i selve sproget, men som<br />

betydningen af brugen af disse konstruktioner i lyset af de programmeringsmæssige problemer,<br />

de søger at løse.<br />

Bogen er opdelt i syv kapitler og seks appendices. Hvert <strong>kapitel</strong> slutter med et sæt opgaver og<br />

en begrebsliste, som kan bruges til at sikre, at materialet fra kapitlet er blevet forstået korrekt. Et<br />

løsningssæt til opgaverne findes som separat publikation.<br />

<strong>Kapitel</strong> 2 er en uformel introduktion til selve programmeringssproget C++, dets syntaks,<br />

datatyper og kontrolstrukturer og forskellige mønstre i programmeringen, der følger med.<br />

Sådanne mønstre kaldes undertiden idiomer, fordi de ikke er dele af definitionen på sproget, men<br />

er sæt af uskrevne regler for brugen af det. Et eksempel på en idiomatisk konstruktion fra C (og<br />

C++) er<br />

while (*q++ = *p++);<br />

som kopierer en streng. Den er ikke en del af C, men vil være kendt som en fundamental<br />

byggeklods for kyndige C-programmører. Det er netop byggeklodser som disse, der gør et sprog<br />

anvendeligt og som essentielt differentierer programmeringssprog af samme type, fordi de<br />

essentielt følger samme regler men har forskellig syntaks og dermed forskellige idiomatiske<br />

manifestationer. <strong>Kapitel</strong> 2 søger at opridse disse idiomer.<br />

<strong>Kapitel</strong> 3 indeholder en gennemgang af begrebet dataabstraktion og fører læseren gennem<br />

koncepterne bag programmering med abstrakte datatyper. En bred forståelse for dataabstraktion<br />

er nødvendig for anvendelsen af mange objekt-orienterede principper og bliver i dette <strong>kapitel</strong> sat<br />

skarpt op mod de muligheder for abstraktion, der ligger i ældre sprog. Med udgangspunkt i et<br />

eksempel-problem gennemføres en løsning med sproglige konstruktioner fra C++ lidt efter lidt -<br />

inkrementalt - så det bliver gjort klart, hvor fordelene og faldlemmene ligger. Denne<br />

fremgangsmåde, som bygger på en problemstilling og et løsningsmønster, der er forkert for så at<br />

vise, hvorfor det er forkert og hvordan det kan gøres anderledes, kaldes løsning per absurdum.<br />

Jeg viser således ikke blot, hvordan tingene bør gøres, men også hvordan de ikke bør gøres.<br />

Kapitlet slutter med en gennemgang af designfasen i udviklingen af abstrakte datatyper og de nye<br />

anvendelsesområder, de lægger op til. Til sidst findes et antal konkrete eksempler på anvendelser.<br />

I <strong>kapitel</strong> 4 åbnes for substansen i anvendelsen af C++, nemlig for objekt-orienteret<br />

programmering. Jeg har valgt en pragmatisk indgangsvinkel til introduktionen af hele<br />

paradigmet, og starter derfor fra en programmeringsmæssig vinkel og går i <strong>kapitel</strong> 5 mere<br />

analytisk til værks. Også i <strong>kapitel</strong> 4 benyttes en eksempel-problemstilling, som gradvist løses<br />

med objekt-orienterede metoder som de fremlægges, så læseren får mulighed for at se fejlene ved<br />

de traditionelle løsningsmodeller. Specifikt forsøges problemerne først løst med abstrakte<br />

1.5 Terminologi og konventioner 13


datatyper for dernæst at vise, at det i et større perspektiv ikke lever op til alle vore krav. Kapitlet<br />

behandler de konkrete sproglige konstruktioner i C++ såsom arv, grænseflader, typekonvertering,<br />

polymorfi og multipel arv. Særligt behandles de problemer, som de forskellige faciliteter bærer<br />

med sig.<br />

<strong>Kapitel</strong> 5 lægger som nævnt en mere formel linie til det objekt-orienterede paradigme og<br />

fokuserer på fasemodeller og organisation. Jeg mener, det er til læserens fordel at have noget<br />

praktisk og håndgribeligt i baghovedet før en principiel og metodisk diskussion om anvendelser<br />

og teknikker kan begyndes. I kapitlet gennemgås således de overvejelser for systemkonstruktion,<br />

som indføringen af objekt-orienteret programmering gør nødvendig, især med henblik på at<br />

opridse de muligheder og konsekvenser, de medfører.<br />

<strong>Kapitel</strong> 6 er en gennemgang af standardbiblioteket i C++, det sæt af klasser og funktioner til<br />

tegnbaseret I/O, som følger med de fleste oversættere. Dette bibliotek analyseres ud fra to vinkler,<br />

dels så læseren får en detaljeret forståelse for opbygningen og dermed kan udnytte det til fulde og<br />

dels, fordi opbygningen af klassehierarkiet bag biblioteket er en god kandidat for et studie.<br />

I <strong>kapitel</strong> 7 præsenteres et konkret lille klassebibliotek, XCL, som indkapsler traditionelle<br />

datastrukturer og algoritmer og som kan bruges som udgangspunkt for videre programudvikling.<br />

Klassebiblioteket er af generel karakter, er hardware-uafhængigt og kan bruges som case study<br />

for opbygningen af klassebiblioteker. Hele biblioteket gennemgås med referencer bagud i bogen<br />

til de overvejelser, der har gjort sig gældende for specifikke klasser og implementationer.<br />

Biblioteket findes i sin helhed i appendiks F.<br />

De seks appendices indeholder henholdsvis en oversigt over alternative implementationer af<br />

C++ med mulige fremtidige udvidelser og tilretninger, en samling af gode råd til Cprogrammører,<br />

der ønsker at skifte til C++, en kort oversigt over andre objekt-orienterede<br />

systemer på markedet, en liste over de danske oversættelser af engelsk fagsprog, der bruges i<br />

bogen samt en bibliografi og den komplette kildetekst til klassebiblioteket fra <strong>kapitel</strong> 7.<br />

14 <strong>Introduktion</strong> 1


C++<br />

Dette <strong>kapitel</strong> giver en generel, men koncis, indføring i sproget C++. Hvis<br />

læseren allerede kender C++'s syntaks og regler for kontrol- og datastrukturer,<br />

kan kapitlet springes over, mens det for C-kyndige programmører bør<br />

gennemlæses med henblik på de få fundamentale forskelle mellem de to sprog.<br />

Det er nødvendigt at være fortrolig med syntaksen i C++ for at kunne forstå<br />

eksemplerne senere i bogen. Kendes hverken til C++ eller C, henledes<br />

opmærksomheden på litteraturlisten i slutningen af kapitlet. Det formodes, at<br />

læseren kender de grundlæggende principper i procedurale sprog, samt fysiske<br />

metoder og værktøjer i udviklingsprocessen.<br />

2.1 INTRODUKTION<br />

En stor del af C++'s styrke kommer fra, at det er afledt direkte fra C og derfor har en god base for<br />

at udvikle sig i mange udviklingsmiljøer. De første oversættere blev implementeret som<br />

translatører, dvs. programmer, som konverterer C++-kode til standard C-kode. Dermed var det<br />

muligt at starte med udvikling i C++ med det samme. Den originale translatør, cfront, som blev<br />

udviklet af Bjarne Stroustrup fra det amerikanske telefonselskab AT&T, er i dag flyttet til næsten<br />

alle kendte systemer.<br />

I takt med den øgede interesse omkring C++ begynder nu "rene" oversættere at komme på<br />

markedet. Disse oversættere springer C-leddet over, og genererer objektkoden direkte med to<br />

fordele til følge: oversættelsen sker hurtigere, og det bliver muligt at køre en symbolsk debugger<br />

på koden, hvilket letter udviklingsarbejdet betragteligt.<br />

For en vordende C++ programmør giver arven fra C en anden fordel, nemlig at han kan gå<br />

inkrementalt til værks med de nye faciliteter. Han kan være produktiv samtidig med, at han<br />

tilegner sig viden om et nyt sprog - det ses sjældent. De fleste eksempler i denne bog er bygget<br />

op, så en C-programmør let vil kunne se, hvad der foregår og relatere det til sin viden om dette<br />

sprog.<br />

Men hvem henvender et objekt-orienteret sprog i C++'s skikkelse sig til? Først må det gøres<br />

klart, at C++ ikke er et stort 4-generationssprog. Det er designet ud fra en minimalist-filosofi, som<br />

siger, at "hvad programmøren selv kan programmere sig ud af, skal sproget ikke indeholde." C++<br />

viderefører dermed ideen fra C, som ligeledes kun indeholder hvad er absolut nødvendigt.<br />

Dernæst skal det siges, at C++ er et generelt programmeringssprog, som ikke favoriserer<br />

1.5 Terminologi og konventioner 15<br />

2


estemte problemer og løsningsmodeller fremfor andre. Vi kan skrive alle typer programmer i<br />

C++, det være sig indenfor simulation, kunstig intelligens, procestyring, systemprogrammel,<br />

kommunikation, grafiske brugerflader osv. Som eksempel kan nævnes, at AT&T har omskrevet<br />

UNIX-operativsystemet i C++, mens FN bruger sproget til udvikling af simulationsprogrammel<br />

til u-landsprojekter. Pointen er, at programmøren gennem sprogets faciliteter selv må skabe de<br />

nødvendige "byggeklodser" for sit eget softwareprojekt. En konsekvens af denne filosofi er, at<br />

C++ ikke indeholder faciliteter for I/O, men derimod appellerer til generalitet og flytbarhed ved<br />

at forudsætte implementering af I/O ved hjælp af sproget selv.<br />

Det er svært at spå om, hvilket objekt-orienteret sprog vil komme til at dominere i fremtiden.<br />

Det har nemlig vist sig, at der er større forskelle end først antaget mellem objektorienterede<br />

systemer, selvom de ofte sættes i samme bås. Dog har C++ to væsentlige fordele frem for mange<br />

af konkurrenterne: det følger helt igennem de objekt-orienterede koncepter uden kompromiser,<br />

og det er samtidig et system-orienteret sprog, som man kan skrive "rigtige" programmer i. Man<br />

kan argumentere for, at et sprog som Smalltalk er mere holistisk, og derfor lettere at forstå, men<br />

Smalltalk kan ikke bruges reelt til systemudvikling. Ligeledes er Ada ikke helt "rent", og er tillige<br />

for tungt at danse med på mindre systemer. I appendiks C findes en oversigt over andre objektorienterede<br />

systemer.<br />

2.2 UDVIKLINGSMILJØET<br />

Kildeteksten skrives normalt som tekstfiler i et redigeringsværktøj. Disse filer bliver af<br />

oversætteren konverteret til objektkode, som indeholder dels den færdige målkode til en bestemt<br />

maskinarkitektur og dels en symboltabel over funktioner og variable, der evt. skal bruges udefra.<br />

Der er ofte flere objektfiler i et projekt. Et program består ofte af flere "moduler" som oversættes<br />

separat, og programmet benytter sig hyppigt af standardrutiner, som ligger i et standardbibliotek.<br />

Objektfilerne kædes sammen af en lænker, som finder og binder alle referencer filerne imellem.<br />

Lænkerens output er et køreklart program, der startes af en loader, der normalt er integreret i<br />

operativsystemet.<br />

Der er fire principielle grunde til at fordele et program på flere fysiske kildefiler. For det første<br />

nedsættes oversættelsestiden drastisk, da al kildetekst ikke skal oversættes, hver gang en mindre<br />

ændring foretages. Dernæst kan et system ikke altid rumme en komplet oversættelse rent<br />

hukommelsesmæssigt, hvis al kildekode findes i samme fil. For det tredje kan programmøren<br />

opdele programmet i "moduler", som hvert repræsenterer en omsluttet del af hele programmet.<br />

Og sidst ses fordelen af oversætter-lænker konceptet mere tydeligt, når to eller flere<br />

programmører arbejder på samme projekt. Det er for meget besvær at skulle redigere i den<br />

samme fysiske fil under udviklingen. En ekstra fordel ved separat lænkning er, at det tillader os at<br />

blande objektmoduler fra forskellige sprog sammen i samme program.<br />

2.2.1 Erklæring og definition<br />

Et velskrevet C++-program består af to dele: erklæringer og definitioner. Disse to dele er<br />

konceptuelt fordelt på separate filer. Erklærings-delen indeholder alle deklarationer af navne på<br />

16 <strong>Introduktion</strong> 2.1


funktioner eller data i kildefilen, som skal kunne ses af fremmede kildefiler, mens definitionsdelen<br />

indeholder den faktiske kode. Når et navn skal ses udefra, kaldes det offentligt. Erklæringsfilerne<br />

kaldes normalt header-filer, og ender af den grund ofte i ".h", mens definitions-filerne<br />

indeholder kildetekst, og har normalt enten endelsen ".c", ".cp", ".cpp" (for c-plus-plus) eller<br />

".cxx" (to skæve plusser).<br />

C++ har et meget stærkt typesystem og derfor meget strenge regler for brugen af typer. Derfor<br />

behøves en specifikation af for eksempel funktioners parameterlister og returtyper. Ligger<br />

funktionen fysisk i samme kildefil hvor den refereres fra, er det intet problem, så længe<br />

erklæringen kommer før kaldet. Hvis funktionen imidlertid ligger i en anden fysisk kildefil, og<br />

først bindes af lænkeren, skal en eksplicit erklæring indeholdes i kildefilen. Under gennemløbet<br />

af kildekoden (normalt kun et enkelt), har programmøren via et specielt såkaldt direktiv lov til at<br />

"inkludere" andre filer i den aktuelle. Dette bruges næsten udelukkende til inkludering af headerfiler,<br />

som specificerer grænsefladen til den objektfil, man ønsker at binde til. I figur 2-1 ses to<br />

kildefiler, proces.cpp og klient.cpp. proces.cpp tænkes at indeholde funktioner<br />

og data, som klient.cpp skal bruge. Header-filen proces.h sikrer, at klient.cpp får<br />

den rigtige grænseflade til funktioner og data i proces.cpp. proces.cpp inkluderer<br />

imidlertid også header-filen, for derigennem at undgå redundans og sikre konsistens.<br />

2.2.2 Præprocessering<br />

Inklusion af filer foretages af den del af oversætteren, som hedder præprocessoren.<br />

Præprocessoren arbejder ikke med kildetekst som syntakstekst, men bruges til forskellige<br />

opgaver før den egentlige oversættelse. Kommandoer til præprocessoren skal skrives på en linie<br />

for sig selv i kildeteksten, med start på det første tegn på linien, som skal være et #, se afsnit<br />

2.10.<br />

Blandt præprocessorens muligheder ligger definition og ekspansion af makroer samt betinget<br />

oversættelse. Makroer er blot tegnsekvenser, som erstattes af andre sekvenser, før selve<br />

oversættelsen tages op, men kan også bruges til at holde værdier, som har betydning for selve<br />

oversættelsen, dvs. symboler, som ikke har noget direkte at gøre med det færdige, oversatte<br />

program. Inklusion af andre filer (normalt header-filer) i en given kildefil foretages med<br />

#include-direktivet, efterfulgt af navnet på filen, som skal substitueres på dette sted. Filnavnet<br />

skal omsluttes af enten klammer eller citationstegn, for henholdsvis en standard-header, som<br />

ligger i standard-kataloget for headerfiler, og bruger-skrevne header-filer, som ligger i det<br />

aktuelle katalog eller via et stinavn givet i sætningen.<br />

2.2.1 Erklæring og definition 17


Figur 2-1: Afhængigheder mellem filer<br />

Makroer og betinget oversættelse spiller nært sammen. Betinget oversættelse betyder, at en del<br />

af kildeteksten skal springes over og slet ikke skal ses af selve oversætteren. Der er flere<br />

umiddelbare grunde til, at der eventuelt skal foretages betinget oversættelse under programmørens<br />

kontrol via specielle præprocessor-direktiver, som minder meget om de generelle<br />

if/else-strukturer, vi finder i selve sproget. Først kan en given kildetekst rumme muligheden<br />

for at kunne producere kode til forskellige miljøer eller maskinkonfigurationer. Ved at<br />

fremskynde sådanne betingelser fra kørselstidspunktet til oversættelsestidspunktet opnås både<br />

sikrere og mindre programmer. Et eksempel på dette er kode, som bruges under fejlretningen i<br />

programmerne - kode, som ikke skal være til stede i produktionsversioner. Disse kan med fordel<br />

gøres til genstand for betinget oversættelse, så de med en enkelt ændring i kildeteksten helt kan<br />

udelades fra programmet. Det er vigtigt at skelne betingelserne i præprocessoren fra de<br />

forskellige betingede kontrolstrukturer i C++ (afsnit 2.7), da der er tale om to helt forskellige ting:<br />

betingelser på oversættertidspunktet og på kørselstidspunktet. Betinget oversættelse er kun et<br />

værktøj for udvikleren. #define erklærer makroer (afsnit 2.10), #if sætter betingelsen for<br />

oversættelse indtil næste #endif eller #else.<br />

2.2.3 Typesystemet<br />

Som nævnt er C++ baseret på et stærkt typesystem, som kræver at der er overensstemmelse<br />

18 Udviklingsmiljøet 2.2


mellem typerne af de objekter, der relateres i programmerne. I modsætning til konventionelle<br />

programmeringssprog udvider C++ typesystemet, så programmøren kan konstruere egne,<br />

specialdesignede typer - såkaldte brugerdefinerede typer eller abstrakte datatyper. For disse<br />

typer gælder samme regler som for de indbyggede typer i sproget - de fundamentale eller<br />

konkrete typer. Det giver sproget en styrke, som tillader en ensartet syntaks og forståelse for<br />

programmernes og datamodellernes struktur og anvendelse.<br />

2.2.4 Standard-input/output<br />

En væsentlig detalje i C++ (og C), som kun findes i få andre sprog, er som nævnt fraværet af<br />

indbyggede faciliteter for tegnbaseret input og output. Som følge af minimalist-designet kan<br />

programmøren selv skrive den nødvendige kode.<br />

Dog findes visse funktioner i et standardbibliotek, som findes på alle C++-systemer. Dette<br />

bibliotek definerer fire kanaler eller strømme for I/O, nemlig standard input, standard output,<br />

standard fejl-output samt en bufferet version af sidstnævnte. Disse strømme er normalt bundet til<br />

tastaturet og skærmen, men kan omdirigeres ved kørselstidspunktet. Når et C++-program kalder<br />

en standard-funktion, ved denne, hvor eventuelle data skal sendes videre. Fordi biblioteket netop<br />

er standardiseret, sikrer denne metode, at C++-kode er relativ flytbar, altså at den kan flyttes fra et<br />

system (for eksempel MS-DOS) til et andet (for eksempel UNIX) uden de store kvaler. Og da<br />

oversætteren ikke behøver at vide noget om I/O, gør det arbejdet lettere for oversætterdesigneren,<br />

hvis oversætteren skal flyttes. Det betyder også, at det står applikationsprogrammøren frit at<br />

skabe sin egen I/O som han lyster.<br />

Standardbiblioteket indeholder også et antal generelle funktioner og konstanter, som kan bruges<br />

af applikationerne. Mere om dem i slutningen af kapitlet.<br />

2.3 GRUNDLÆGGENDE SYNTAKS<br />

Kildetekst i C++ gennemløbes en enkelt gang (single-pass) af oversætteren og analyseres på den<br />

måde fra venstre mod højre. C++-programmerne skrives også i en først-A-så-B-stil, hvor de<br />

trinvise instruktioner udgør programmerne. Det simpleste C++-program ser således ud:<br />

#include <br />

main () {<br />

cout


Ethvert C++-program starter sin kørsel i main. De krøllede parenteser, Tuborg-parenteserne,<br />

hører sammen og omslutter en blok, i dette tilfælde indholdet af funktionen main. Mere om<br />

funktioner senere i kapitlet.<br />

Linie 4 sender strengen "Goddag C++-verden!\n" til standardoutput-strømmen. En<br />

streng i C++ består af en række enkelte tegn i sekvens, omsluttet af citationstegn. "\n" i<br />

slutningen af strengen er et newline-tegn, og betyder, at følgende output skal fortsætte fra næste<br />

linie (det, nogen stadig kalder vognretur). Mere om strenge senere.<br />

2.3.1 Kommentarer<br />

De fleste programmeringssprog giver programmøren mulighed for indsættelse af kommentarer<br />

direkte i kildeteksten. Denne form for lavniveau-dokumentation er meget brugbar, da enkelte dele<br />

af programmet kan uddybes i flydende tekst. Kommentarer ignoreres af oversætteren, og bruges<br />

alene til at forklare andre (eller én selv), hvad der foregår.<br />

Kommentarer i C++ skrives på en af to måder, enten som enkelt linie eller som en hel blok af<br />

tekst:<br />

// dette er en kommentar, der ender nu<br />

/* dette er en kommentar, som fylder<br />

tre linier<br />

*/<br />

De to skråstreger // fortæller oversætteren, at resten af linien er en kommentar, som skal<br />

ignoreres. Tekst, der er omsluttet af /* ... */ ignoreres også, og kan spænde over flere<br />

linier. Den første form er arvet fra BCPL (en fjern forfader til C++), mens den sidste er arvet fra<br />

C.<br />

2.3.2 Afstand<br />

Kildetekstens elementer separeres af afstandstegn. Afstandstegn er linieskift, sideskift og<br />

vognretur og mellemrum (ASCII 10, 12, 13 og 32). Reglerne for brugen af afstand er, at navne<br />

skal separeres, hvis meningen ellers går tabt. Dette sker dog sjældent i C++. Eksemplet i afsnit<br />

2.3 kunne se således ud, hvis afstandstegn ikke benyttedes:<br />

#include<br />

main(){cout


finde rundt i sin egen kode).<br />

Brugen af afstand har for de fleste programmører en æstetisk betydning. Man "formaterer" sin<br />

kildetekst, så meningen (semantikken) lettere kan forstås. Indholdet af funktioner, løkker osv. bør<br />

"indrykkes", så det kan overskues, og så slutningen let kan lokaliseres. Brug afstand og<br />

kommentarer med fornuft; det kan betale sig.<br />

2.3.3 Navne<br />

Hovedparten af et program består af brugerdefinerede navne på data og funktioner. Der er visse<br />

syntaktiske regler for disse navne: de skal udelukkende indeholde alfanumeriske tegn, og skal<br />

ydermere starte med et bogstav. Alfanumeriske tegn er alle tal (0 til 9), alle bogstaver (a til z, A<br />

til Z - altså ikke danske tegn) samt understregningstegnet (_). Eksempler på lovlige navne er<br />

Beaujolais minvar3 MO9CA _operatornew MON9CA<br />

mens ulovlige navne for eksempel kan være<br />

4NIC8 @@@@ 2MAT AMOUNT$<br />

Heltal%<br />

C++ har et antal reserverede navne, som har prædefinerede betydninger i sproget. Syntaksen i<br />

C++ tillader ikke disse at blive brugt som brugerdefinerede navne. De reserverede navne er:<br />

asm auto break case catch<br />

char class const continue default<br />

delete do double else enum<br />

extern float for friend goto<br />

if inline int long new<br />

operator overload private protected public<br />

register return short signed sizeof<br />

static struct switch template this<br />

throw typedef union unsigned virtual<br />

void volatile while<br />

Navne skal være unikke i C++, dvs. det er ikke tilladt at bruge samme navn i to eller flere<br />

kontekster, hvis det kan lede til tvetydigheder. Det er dog tilladt at benytte ens navne, hvis der<br />

ikke opstår konflikter, hvilket indebærer, at de eksisterer separat fra hinanden i programmet under<br />

hvert sit skop - se afsnit 2.6.1.<br />

2.3.4 Sætninger<br />

Programmer opbygges af sætninger, som beskriver de enkelte databehandlinger. Sætninger er<br />

2.3.2 Afstand 21


derfor interessante, fordi det er dem, som rent faktisk udfører arbejdet. Syntaksen i C++ kræver,<br />

at der skal et semikolon (;) efter hver sætning. Således er et semikolon for sig selv en "tom<br />

sætning":<br />

; // intet sker her<br />

Der er en klar konsistens i syntaksen for, hvornår der skal semikolon, som afviger en smule fra<br />

andre semikolon-brugende sprog som Pascal og Modula-2, der bruger semikolon som generel<br />

separator. C++ bruger afstand som separator, og har derfor kun brug for semikolon som<br />

afslutning på sætninger. Grunden til, at denne diskussion er interessant er, at C++ tillader dig at<br />

blande sætninger sammen på en meget fri måde, som ikke kendes fra andre sprog:<br />

while ( (a = getchar()) != EOF) putchar (a);<br />

Denne sætning er en løkke, som gennemløbes indtil en afbrydelses-tilstand er sand, eller mere<br />

korrekt, indtil en gentagelses-tilstand bliver falsk. Hele linien er een sætning, som består af en<br />

løkke med en betingelse, en tildeling og et funktionskald. Sætninger er således afsluttede,<br />

enkeltstående sekvenser i programmet, som kan forstås og beskrives i en enkelt omgang. Der skal<br />

ikke semikolon efter andet end hele sætninger.<br />

2.3.5 Udtryk<br />

I afsnit 2.4 tager vi fat på de forskellige datatyper i C++, men først skal vi se på, hvordan disse<br />

typer arbejder sammen. I C++ sammensættes forekomster af forskellige typer i udtryk, som består<br />

af et eller flere objekter. Objekterne i et udtryk kaldes for operanter (undertiden operander) og de<br />

sættes sammen med operatorer. Operatorerne styrer, hvordan operanterne skal fortolkes i forhold<br />

til hinanden eller i forhold til sammenhængen. Operatorer, der arbejder på én operant kaldes<br />

fortegns-operatorer, mens de, der arbejder på to operanter kaldes binære operatorer. Et eksempel<br />

på et udtryk er<br />

a<br />

som simpelthen er objektet a. Et sammensat udtryk, hvor to operanter ganges sammen med<br />

multiplikations-operatoren * kan se således ud:<br />

a * b<br />

mens et udtryk, der består af to subudtryk kan eksemplificeres ved<br />

a * b + c * d<br />

2.4 TYPER<br />

22 Grundlæggende syntaks 2.3


I C++ klassificeres alle data efter deres type. Type-begrebet betyder, at programmøren kan<br />

relatere data med andre data, så oversætteren forstår det, og er i C++ udbygget til at omfatte<br />

brugerdefinerede typer. At arbejde med datatyper er i grunden et spørgsmål om, hvordan et<br />

bestemt mønster skal fortolkes i en given sammenhæng. For når det kommer til stykket, er<br />

computerens lager organiseret i lineært adresserede bits, nuller eller ettaller. Lange rækker af<br />

nuller og ettaller er svære at fortolke og arbejde med for programmører, fordi der ikke ydes hjælp<br />

til at forstå strukturen i rækken.<br />

På generelle computere beskrives lageret normalt i bytes (på dansk undertiden benævnt<br />

oktetter), som er enheder af 8 binære cifre eller bits. I realiteten er lageret underlagt processorens<br />

arkitektur, og er organiseret i maskin-ord, som er det mindste antal bits, som på én gang kan<br />

adresseres. Maskinord er en kvantitet, som er bestemt af computerens arkitektur, og afhænger af<br />

processorens databusbredde. Deres format er derfor hardware-afhængig og ikke entydig at<br />

fortolke. For eksempel vil sekvensen af bits<br />

1111000000000001<br />

på en Intel-processor repræsentere decimaltallet 496, mens det på en Motorola-arkitektur vil være<br />

61441 † . For at sikre, at programmer har en rimelig grad af flytbarhed, overtager oversætteren<br />

behandlingen af de hardware-afhængige aspekter ved at indføre typer, som netop er karakteriseret<br />

som hardware-uafhængige. En type er således en abstraktion af et numerisk, matematisk eller<br />

andet begreb, som gør det lettere for programmøren at overskue en problemstilling i selve<br />

programmets kildetekst. Eller med andre ord: en given bitstrøm er umulig at behandle uden at<br />

kende strømmens type. Derfor et typesystem.<br />

C++ har et antal indbyggede datatyper, som alle er arvet fra C. Typerne beskriver et antal<br />

lagerenheder af fast længde samt de operationer, der kan udføres på dem. En type er således en<br />

prægeplade for faktiske objekter af den type. Typen kan ikke manipuleres, men det kan objektet,<br />

og vi siger derfor, at et objekt er en forekomst af en bestemt type.<br />

Som nævnt i afsnit 2.2.3 indeholder C++ et antal fundamentale typer samt muligheden for at<br />

udvide med egne brugerdefinerede typer. Senere i afsnit 2.4 behandles de forskellige<br />

fundamentale typer i detaljer, men for at lette forståelsen i de mellemliggende afsnit skal et par af<br />

dem kort opremses: typerne char, int og long repræsenterer alle heltal og forskellen<br />

mellem dem er deres præcision - altså antallet af værdier, de kan indeholde. De arbejder sammen<br />

med og kan tildeles konstante værdier - for eksempel tallet 42 - og er normalt forbundne med<br />

henholdsvis tegn, små heltal og store heltal.<br />

2.4.1 Symbolske variable<br />

† Det hænger sammen med, at Motorola-processorer arbejder efter little-endian princippet, dvs. det mest<br />

betydningsfulde ciffer står først i lageret. På en Intel-maskine er det omvendt, da den har det mindst betydningsfulde<br />

først, og lægger tal ud i lageret på en big-endian måde. Disse benævnelser stammer fra Johnathan Swifts fortælling<br />

om Gulliver's Rejser, i hvilken der berettes om en uoverensstemmelse over, hvorvidt man bør spise et æg fra den<br />

store eller den lille ende.<br />

2.3.5 Udtryk 23


I C++, som i de fleste andre programmeringssprog, udføres databehandlinger gennem<br />

manipulationer af variable. Det letter programmeringen, fordi variablene giver os en symbolsk<br />

abstraktion i form af et navn. Gennem navnet (på engelsk variablens identifier) kan vi manipulere<br />

variablen (eller objektet) på forskellige måder og med sprogets kontrolstrukturer kan vi<br />

strukturere brugen af variablene, så kompleksiteten minimeres.<br />

Et objekt og en variabel har stort samme betydning i objekt-orienteret programmering. Den lille<br />

spidsfindige forskel er, at en variabel henviser til en syntaksmæssig størrelse i kildeteksten (en<br />

variabel med et eller andet navn) mens et objekt betyder en forekomst af en bestemt type (et<br />

objekt med en eller anden type). I dette afsnit taler vi om syntaks, som er rettet mod objekternes<br />

navne, og bruger derfor variable.<br />

Variable repræsenteres af symboler. En C++-variabel har en unikt navn (afsnit 2.3.3), og skal<br />

erklæres som en variabel af en bestemt type før den kan bruges. Erklæringen af en variabel v<br />

med en type T foretages ved<br />

T v;<br />

hvorefter v kan opfattes som en abstraktion af typen T, for hvilken gælder et sæt regler for bla.<br />

ind- og udlæsning, generel manipulation og oprettelse/nedbrydning. v er ydermere en symbolsk<br />

variabel, dvs. en abstraktion, som lader os arbejde med et navn, som associerer det, som<br />

variablen repræsenterer med et egentligt navn. Denne abstraktion tabes med det samme i<br />

målkoden, fordi det færdigtoversatte program ikke bibeholder symbolikken. Den er kun til gavn<br />

for udvikleren, som kan døbe en variabel et navn, som har forbindelse med dens indhold. For<br />

eksempel vil et godt navn til en variabel, som beskriver en temperatur være celcius † .<br />

Der er altså en forskel på de to former for abstraktion, som typesystemer giver programmøren:<br />

den ene giver hardwareuafhængighed, den anden giver syntaksfrihed. Tildelinger af variable<br />

foretages med et lighedstegn,<br />

celcius = 5;<br />

hvilket tildeler variablen celcius værdien 5.<br />

Der skelnes mellem to slags udtryk i tildelinger, nemlig udtryk til venstre for et lighedstegn (en<br />

lvalue, udtales "el-value") og udtryk til højre for (en rvalue, udtales "ar-value"). Venstre-udtryk<br />

skal kunne fortolkes som en endelig adresse, hvori en værdi kan placeres, mens højre-udtryk skal<br />

resultere i en håndgribelig værdi. Grunden til, at det er interessant er, at det højre-værdier og<br />

venstre-værdier differentierer adressérbare objekter.<br />

Symbolske variable er adressérbare, og har således to værdier, der beskriver dem. Den ene er<br />

variablens dataindhold mens den anden er dens adresse. Et udtryk resulterer altid i enten en højreværdi<br />

eller en venstre-værdi, men kun venstre-værdier (adressérbare objekter) kan stå til venstre<br />

for lighedstegnet. Højre-værdier kan kun stå til højre for lighedstegnet, til venstre giver de ingen<br />

mening. Som vi skal se i afsnit 2.4.6 er tallet 5 en literal konstant, som altid er en højre-værdi.<br />

Den er ikke adressérbar, hvorfor sætningen<br />

† Navnet temp er et dårligt valg, fordi det traditionelt associerer til det engelske temporary, altså midlertidig.<br />

24 Typer 2.4


5 = celcius;<br />

er meningsløs. Oversætteren vil ikke godtage denne sætning. Idet symbolske variable har to<br />

associerede værdier kan de stå både til højre og til venstre for lighedstegnet, og fortolkningen af<br />

dem afhænger derfor helt og aldeles af, om de i det aktuelle udtryk er venstre-værdier eller højreværdier.<br />

For eksempel,<br />

celcius = celcius + 10;<br />

Her fortolkes den første celcius som en venstre-værdi, altså variablens egentlige position,<br />

mens den anden fortolkes som dens faktiske og aktuelle værdi. Højre-værdier kan være en<br />

kombination af subudtryk, opdelt af operatorer. Operatorer bruges til at associere og samle<br />

variable i beregninger eller sammenligninger. Et sammensat udtryk kan se således ud:<br />

areal = hoejde * bredde; †<br />

hvor hoejde og bredde gennem en multiplikation resulterer i en højre-værdi. Venstreværdier<br />

kan også bestå af sammensatte udtryk, som vi skal se senere i dette <strong>kapitel</strong>.<br />

En tildeling kan forekomme på samme tid som en erklæring. Erklæringen og tildelingen<br />

long x;<br />

x = 1;<br />

kan skrives kortere som<br />

long x = 1;<br />

i de fleste tilfælde, afhængig af skop og type. Parenteser bruges i komplekse udtryk til at<br />

underkende de indbyggede regler for præcedens mellem for eksempel additions- og multiplikationssubudtryk,<br />

som i<br />

a = (5 + 3) * 4;<br />

hvilket tildeler a produktet af 4 samt additionen mellem 5 og 3, altså 32. Skrives<br />

a = 5 + 3 * 4;<br />

vil den indbyggede præcedens mellem operatorerne (se tabel 2-2) gange 3 med 4, og så addere 5<br />

med resultatet 17. Værdierne kan specificeres på mange forskellige måder, som beskrives i afsnit<br />

† De danske bogstaver Æ, Ø, Å, æ, ø samt å kan ikke bruges i selve kildeteksten, da de ikke er lovlige tegn i navne.<br />

Derfor bruges de sammensatte AE, OE, AA osv. i stedet. Dette gælder ikke for kommentarer, der ignoreres af<br />

oversætteren.<br />

2.4.1 Symbolske variable 25


2.4.6. I C++ (og C) er syntaksen meget generel, og tillader placering af tildelinger og erklæringer<br />

næsten overalt. Dette leder i nogle tilfælde til sætninger, hvor meningen kan gå tabt for en hurtig<br />

læser. I udtrykket<br />

a = b = c + d;<br />

tildeles både a og b summen af c og d. Syntaksen bruges, når flere variable skal tildeles eller<br />

initieres med samme værdi. Parenteser kan benyttes i flerdelte tildelinger, hvorved de enkelte<br />

elementer tildeles værdien indenfor den parentes, de står. I udtrykket:<br />

a = (b = c) + d;<br />

tildeles a summen af c og d, mens b tildeles værdien af c alene, grundet parenteserne.<br />

Komma-tegnet har en særlig betydning i C++, idet det bruges til at sekvensere udtryk. To eller<br />

flere erklæringer og/eller tildelinger kan samles i en enkelt sætning, separeret af kommaer:<br />

int a, b = 2, c, d = 4; // 4 heltalserklæringer<br />

a = b, c = d; // to tildelinger<br />

Der er en spidsfindig forskel mellem initieringer (som int i = 5) og tildelinger (som i =<br />

5), som skal vise sig at være meget væsentlig i forbindelse med abstrakte datatyper. En initiering<br />

indebærer, at det pågældende objekt bliver konstrueret med en bestemt værdi mens en tildeling<br />

blot ændrer et eksisterende objektets værdi.<br />

2.4.2 Fundamentale typer<br />

De indbyggede datatyper i C++ kaldes også samlet de fundamentale typer. Denne betegnelse er<br />

opstået på grund af C++'s mulighed for at udvide typesystemet med brugerdefinerede datatyper.<br />

De fundamentale typer er de, som sproget er født med, og som oversætteren selv kender til. De 6<br />

fundamentale typer repræsenterer alle heltal eller kommatal med forskellig præcision: char,<br />

short, int, long, float og double. De fire første er heltals-typer, de sidste to kommatalstyper.<br />

Forskellen på de forskellige typer i de to "familier" er præcisionen, dvs. antallet af<br />

forskellige tal, der kan repræsenteres i den pågældende type. C++-standarden siger to ting om<br />

præcision, nemlig<br />

1. at en char er mindre eller lig en short, en short mindre eller lig en int, og en<br />

int mindre eller lig en long, samt at en float er mindre eller lig en double,<br />

char


float


Figur 2-2: Intervaller for signed og unsigned char-typer.<br />

En anden modifikator til en type er register-nøgleordet. C++ tillader i forskellige<br />

sammenhænge, at programmøren blander sig i oversætterens arbejde. For variable gælder det, at<br />

de i objektkoden behandles enten i CPU'ens interne registre alene, eller bliver skiftet ud mellem<br />

CPU'en og lageret. Med register før en erklæring fortæller man oversætteren, at den<br />

nærværende variabel bruges på en måde, som egner sig til intern CPU-behandling. Oversætteren<br />

vil så vidt muligt forsøge at følge dette råd, men giver dog ingen garanti. En registervariabel<br />

er oftest en afledt type, men kan også være en forekomst af en fundamental type. Der er<br />

ingen forskel i brugen i forhold til normalt erklærede variable.<br />

Erklæringen foretages således:<br />

register int a;<br />

I standardbiblioteket, der følger med de fleste oversættersystemer, findes en headerfil ved navn<br />

limits.h, som beskriver de absolutte størrelser, maksimal- og minimalværdier samt intervaller<br />

for de fundamentale typer i C++ i den pågældende implementation. Disse værdier er symbolske<br />

konstanter og kan bruges for at sikre, at en påkrævet præcision overholdes.<br />

2.4.3 Pointertyper<br />

Figur 2-3: Præcisionen af kommatal centreres omkring 1 og -1.<br />

C++ har en maskinnær opbygning. Af den grund findes både sproglige konstruktioner og afledte<br />

typer, der mere eller mindre lader den grundlæggende maskinarkitekur skinne igennem til<br />

programmøren. En af disse typer er pointeren.<br />

28 Typer 2.4


Pointere har den egenskab, at de peger på data i stedet for at indeholde data. Forskellen er<br />

temmelig subtil. Når en pointer peger på for eksempel en int, indeholder pointeren blot<br />

adressen på denne variabel. Er en anden pointer rettet mod en char, er indholdet adressen på<br />

denne variabel. Alle pointere er derfor i princippet helt ens: de indeholder en adresse i maskinens<br />

lager. Forskellen er måden, hvorpå de data, der befinder sig på adressen, behandles. En<br />

pointervariabel erklæres med en stjerne (*) efter typenavnet:<br />

int* iptr; // iptr er en pointer til en int<br />

Denne erklæring resulterer i en allokeret variabel, som indeholder plads nok til en adresse i<br />

lageret. Den allokerer ikke en int! Før den kan bruges, må den tildeles en værdi, for eksempel<br />

adressen på en anden variabel:<br />

int i; // i er en int<br />

iptr = &i; // iptr indeholder nu i's adresse<br />

Nu kan indholdet af variablen i også manipuleres indirekte gennem iptr, ved brug af<br />

operatoren *:<br />

*iptr = 5; // i (*iptr) har nu værdien 5<br />

int j = *iptr; // j har nu samme værdi som i (*iptr)<br />

Denne operation kaldes dereference og betyder, at det ikke er pointeren men de data, som den<br />

refererer, der er i fokus. Vi bruger pointere, fordi de tillader os at generalisere. En programdel,<br />

der benytter pointere kan arbejde på forskellige forekomster af data afhængig af pointernes<br />

værdier, mens en programdel, der arbejder direkte på forekomster er bundet til disse. Selve<br />

pointerbegrebet og ideen om indirektion er fundamentet for udvikling af generisk kode. Som vi<br />

skal se senere, kommer meget af styrken i objekt-orienteret programmering fra, at kompleksiteten<br />

i pointerarbejdet skjules fra programmøren. I afsnit 2.10 gennemgås alle de særlige forhold ved<br />

pointere, som på en og samme tid gør dem uvurderlige og besværlige at have med at gøre.<br />

2.4.4 Referencetyper<br />

Referencer opfører sig akkurat som pointere, med den ene undtagelse, at de indirekte data ikke<br />

skal derefereres. En reference er således en pointer, som har samme syntaks i tildelinger og<br />

udtryk som normale variable, men som peger på andre data. Man kan sige, at en reference er et<br />

alias for en anden variabel. For eksempel,<br />

char c; // c er en char<br />

char& cref = c; // cref refererer c<br />

Referencen cref kan nu bruges (på både højre og venstre side af en tildeling) som om den<br />

egentlig var c:<br />

2.4.3 Pointertyper 29


char ch = cref; // samme som char ch = c;<br />

cref = 'a'; // samme som c = 'a'<br />

Referencer benyttes mest i funktioners parameterlister, fordi det letter syntaksen, at de refererede<br />

data ikke eksplicit skal manipuleres gennem *-operatoren. Dette kommer til udtryk i overstyring<br />

af operatorer (afsnit 3.5), hvor referencer er helt nødvendige.<br />

Læg igen mærke til forskellen mellem tildeling og initiering af variable. Under initiering af en<br />

referencevariabel tildeles en værdi til selve referencen, mens det i senere tildelinger til referencen<br />

er den refererede variabel, der tildeles en værdi - altså to helt forskellige operationer i referencens<br />

sammenhæng, der ikke må forveksles.<br />

2.4.5 Vektor-typer<br />

En vektor er en samtidig erklæring af mere end én forekomst af samme type, som skal allokeres i<br />

sekvens, dvs. lineært i lageret. En vektorerklæring foretages med en angivelse af størrelsen på<br />

vektoren i kantede parenteser efter variabelnavnet, for eksempel<br />

char Buffer [100]; // hundrede char-variable<br />

En vektor er faktisk det samme som en pointer - en pointer kan tildeles en vektor. De ovenstående<br />

erklæringer vil resultere i, at Buffer får typen "vektor af char", men er egentlig blot en<br />

pointer til den første char i rækken af de hundrede. Forskellen i erklæringen er, at vektoren<br />

faktisk allokerer plads til data.<br />

Elementerne i vektoren kan refereres med indeks-operatoren, hvor indekset løber fra 0 til n-1,<br />

hvor n er antallet af elementer. Er vektoren for eksempel allokeret med 32 elementer, indekseres<br />

den fra 0 til 31 inklusive. For eksempel,<br />

char c = Buffer [11]; // det 12. element i Buffer<br />

Buffer [99] = '\n'; // det 100. element i Buffer<br />

Vektorer bruges i forbindelse med pointere, pointermanipulation og -aritmetik samt løkker, da<br />

gennemgange af vektorer i søgninger, sorteringer og opdelinger er de primære anvendelser. Mere<br />

om disse emner, samt om multidimensionale vektorer, i afsnit 2.10.<br />

2.4.6 Literale konstanter<br />

I de foregående afsnit er tal som 5 og 0.00000001 dukket op flere steder. Sådanne værdier kaldes<br />

literale konstanter og har ingen direkte type. De hedder literaler, fordi de helt bogstaveligt talt<br />

kun kan beskrives i form af den værdi, de har. Literale konstanter er derfor data, som står<br />

eksplicit beskrevet i kildeteksten og som bliver fysisk repræsenteret i objektkoden. C++ lader os<br />

arbejde med tre typer af konstanter, heltal, kommatal og tegn. Det er muligt at specificere disse<br />

30 Typer 2.4


tre typer på forskellige syntaksbestemte måder samt i forskellige talsystemer. Den oftest<br />

benyttede konstant er heltallet, som er uundværlig i næsten alle kontrolstrukturer:<br />

12345 (Decimal 12345)<br />

0x2000 (Hexadecimal 2000 - decimal 8192)<br />

0701 (Oktal 701 - decimal 449)<br />

'8' (Tegnet '8' - ASCII decimalværdi 56)<br />

Decimale konstanter er underforstået for heltal, og andre talsystemer må bruge en anden syntaks.<br />

Det ses, at hexadecimale konstanter har en 0x før selve konstanten, at oktale konstanter altid<br />

starter med et 0 samt at tegnkonstanter benytter apostrofer til at omslutte tegnet. Der skelnes<br />

ikke mellem store og små bogstaver i hexadecimale heltalskonstanter, ligesom x'et kan være et<br />

X. Tegnkonstanter bruges til at finde værdien af et bestemt tegn i det aktuelle tegnsæt, hvilket<br />

oftest er ASCII (American Standard Code for Information Interchange). Samme numeriske<br />

værdi kan altså opnås på mange måder. Tallet 42 kan beskrives som både 42, 0x2A og 052.<br />

Den binære repræsentation af en literal konstant afhænger af, hvor stort tallet er i forhold til<br />

præcisionen af de fundamentale typer i den pågældende oversætter. Uanset i hvilken<br />

sammenhæng en literal konstant anvendes - som operant i et subudtryk eller en tildeling - vil den<br />

fremstå i objektkoden i en form, som svarer til den type, der er nærmest. Konstanten 123 vil<br />

typisk være en short int, mens 123456 typisk vil være en long int. Afhængigt af<br />

udtrykket, som konstanten bruges i, vil oversætteren konvertere den konstante værdi til en given<br />

anden type. Typekonvertering beskrives i afsnit 2.4.11. Programmøren kan imidlertid kontrollere<br />

den absolutte størrelse på en literal konstant ved hjælp af en angivelse efter selve værdien. I<br />

følgende eksempel tvinges konstanten til at være en long, selvom den sandsynligvis kunne<br />

holdes i en int:<br />

99L (L betyder long-konstant)<br />

Det er naturligvis kun muligt at tvinge præcisionen opad; en konstant, som kun kan indeholdes i<br />

en long kan ikke tvinges ind i en short.<br />

Fortolkningen af den literale konstant som signed eller unsigned kan også kontrolleres<br />

ved hjælp af en angivelse efter konstanten. En heltalskonstant er underforstået signed, men<br />

kan tvinges til at være unsigned ved at klistre et U efter konstanten:<br />

50000U (50000 som unsigned int-konstant)<br />

Bogstaverne kan være enten store eller små og kan også blandes sammen. For eksempel,<br />

30lu (30 som unsigned long-konstant)<br />

Dette gælder også kommatal. Oversætteren forudsætter også her altid den mindst mulige<br />

præcision, når en konstant lagres, men det er muligt at tvinge den til at gemme en floatkonstant<br />

med<br />

2.4.6 Literale konstanter 31


eller<br />

65F (65 som kommatal)<br />

65.<br />

65.0<br />

hvor decimaltalskommaet (et komma i Danmark, men et punktum i udlandet) tvinger den literale<br />

konstant til at være et kommatal.<br />

Læg mærke til, at der ikke skelnes mellem float og double, når det gælder konstanter.<br />

Syntaksen er iøvrigt sjældent nødvendig, og bruges kun i forbindelse med specielle funktionskald<br />

og initieringer af datastrukturer. Det er et spørgsmål om at fortælle oversætteren, at en bestemt<br />

konstant ikke behøver mere præcision end man har behov for, så den ikke allokerer unødvendigt<br />

plads og bruger unødvendig tid til konvertering. Oversætteren vil iøvrigt advare om brugen af<br />

konstanter, der er for store i forhold til konteksten.<br />

Kommatal, som har typen double eller float, kan skrives som konstanter i enten normal<br />

eller "videnskabelig" form (med eksponent):<br />

3.1415 (Omtrent π)<br />

.5 (En halv)<br />

5.5e60 (Et meget stort tal)<br />

1E-77 (Et meget lille tal)<br />

Som beskrevet kan tegnkonstanter specificeres med omslutning af apostrofer. Selvom C++ har<br />

datatypen char, definerer sproget ikke, hvordan tegnsættet ser ud. Der er altså ingen<br />

konvention om, hvilke værdier de forskellige tegn (bogstaver, symboler osv.) har på målmaskninen.<br />

De fleste operativsystemer (UNIX, MS-DOS, OS/2) bruger dog ASCII-tegnsættet, og<br />

standardbiblioteket i C++ har derfor visse ASCII-relaterede funktioner. Men selve sproget<br />

definerer kun typen char som et heltal af tilstrækkelig båndbredde til at kunne rumme et tegn.<br />

Et 'a' vil have en anden numerisk værdi på en ASCII-maskine end på for eksempel en<br />

EBCDIC-maskine. For at sikre flytbarheden mest muligt kan tegnkonstanter indeholde et antal<br />

maskinuafhængige kontroltegn, for eksempel linieskift og tabulatorfunktion. Kontroltegn<br />

specificeres med et escape-tegn, den bagvendte skråstreg \:<br />

'\b' Backspace<br />

'\f' Sideskift (form feed)<br />

'\n' Linieskift (newline)<br />

'\r' Vognretur<br />

'\t' Horisontal tabulering<br />

'\v' Vertikal tabulering<br />

De faktiske tegn \, ' og " (baglæns-skråstreg, apostrof og citationstegn) skrives som<br />

32 Typer 2.4


tegnkonstant således:<br />

'\\' Baglæns-skråstregen \<br />

'\'' Apostrofen '<br />

'\"' Citationstegnet "<br />

Det er også muligt at udtrykke en numerisk tegnkonstant ved hjælp af escape-tegnet. Kun 8- og<br />

16-talssystemet kan bruges i escape-sekvenser i tegnkonstanter:<br />

'\0' Nul<br />

'\010' Oktal 10, den decimale værdi 8<br />

'\xFF' Hex FF, den decimale værdi 255<br />

Strenge, som vi kender dem fra andre sprog, er meget begrænsede i C++. Strengen er ikke en<br />

datatype i sig selv, men er repræsenteret som en lineær liste af char's, som slutter med '\0'<br />

samt en pointer, der peger på det først tegn i listen, og kan ikke manipuleres direkte af C++.<br />

Standardbiblioteket indeholder derfor en række funktioner, som arbejder på strenge. Det er vigtigt<br />

at huske, at lovlig en C++-streng altid slutter i '\0', og at denne terminator tælles med i<br />

lagerallokeringen. Derfor har strengen<br />

"kyrie eleison"<br />

en faktisk længde på 14 chars. Kontroltegn kan indsættes i strengkonstanter efter samme regler<br />

som for tegnkonstanter:<br />

"Første linie\nAnden linie\n"<br />

Vær opmærksom på, at 'a' ikke er ækvivalent til "a", idet sidstnævnte er en konstant på to<br />

chars, 'a' og '\0'. Læg også mærke til, at literale konstanter aldrig kan bruges på venstre<br />

side af et lighedstegn.<br />

2.4.7 Symbolske konstanter<br />

Hvis en variabel i C++ aldrig modificeres gennem hele programmet, kan (og bør) den erklæres<br />

som en symbolsk konstant. Hermed er det ikke tilladt at ændre dens værdi andre steder i<br />

programmet (den er således ikke variabel). Forskellen mellem symbolske og literale konstanter<br />

er, at en symbolsk konstant har en egentlig type og dermed samme semantiske egenskaber som<br />

en variabel, mens literale konstanter er "dumme". Symbolske konstanter kan også være nyttige,<br />

hvis der skal arbejdes med konstanter af typer, der ikke er fundamentale - afledte eller brugerdefinerede.<br />

Objekter erklæres konstante med nøgleordet const:<br />

const double pi = 3.1415926; // pi er altid pi<br />

const a = 5; // int-type underforstået<br />

2.4.6 Literale konstanter 33


En const skal altid initieres under erklæringen. Konstanter er brugbare i de tilfælde, hvor<br />

kildeteksten skal kunne oversættes med forskellige indbyggede afhængigheder. For eksempel<br />

kunne en konstant i kildeteksten specificere antallet af brugere i et flerbrugersystem, som sælges i<br />

forskellige licens-konfigurationer. Eller for eksempel værdierne af forskellige tegn:<br />

const brugere = 4; // 4-brugerversion<br />

const ACK = 6; // ASCII-koder<br />

const NAK = 21;<br />

const SOX = 2;<br />

const EOX = 3;<br />

Læg mærke til, at en int ikke er det samme som en const int. Overvej følgende kodefragment:<br />

const int i = 5; // ok: i tildeles en literal konstant<br />

int j = i; // ok: j tildeles 5<br />

i = j; // fejl: i er konstant<br />

const int k = j; // ok: tildeling ved erklæring<br />

Dette skyldes, at det ikke er muligt at dereferere (tage adressen på) en konstant type, hverken<br />

implicit eller eksplicit (ved tildeling eller ved brug af en operator). Symbolske konstanter er ikke<br />

adressérbare og kan følgelig (afsnit 2.4.1) ikke stå til venstre for et lighedstegn. Det faktum, at<br />

konstante typer er forskellige fra variable typer har sin største betydning i forbindelse med<br />

funktioner (afsnit 2.6) og interaktion med abstrakte datatyper (afsnit 3.3.15)<br />

C-kyndige læsere vil bemærke, at symbolske konstanter spiller en langt mere omfattende rolle i<br />

C++ end i ANSI C. De har for det første underforstået intern lænkning og kan altså ikke ses fra<br />

andre objektmoduler. Det betyder, at en symbolsk konstant kan placeres i en headerfil uden<br />

problemer fra lænkeren. For det andet kan de, på grund af kravet om explicit initiering, bruges i<br />

konstante udtryk, for eksempel som angivelse af størrelsen på en vektor, og erstatter altså i vid<br />

udstrækning makrodefinitionerne fra C.<br />

2.4.8 Opremsninger<br />

Et alternativ til const, som kan bruges til automatisk tildeling af værdier til symbolske<br />

konstanter, hedder opremsninger (på engelsk enumerations). En opremsning erklærer et antal<br />

symbolske konstanter og tildeler fortløbende værdier (startende ved 0) til disse. Fordelen er, at<br />

symbolerne automatisk får forskellige værdier, programmøren samtidig kan abstrahere fra de<br />

faktiske numeriske værdier og i stedet blot koncentrere sig om symbolerne. For eksempel,<br />

enum { running, ready, waiting };<br />

34 Typer 2.4


Denne sætning erklærer tre konstanter som medlemmer af opremsningen, running, ready<br />

og waiting, og tildeler værdierne 0, 1 og 2 til disse. Opremsninger kan navngives, og de<br />

individuelle konstanter kan, hvis det er nødvendigt, tildeles en værdi:<br />

enum Attrs {<br />

black, blue, red, magenta, green, cyan, yellow, white,<br />

brown = 0x20, grey, marmelade, purple, manderine,<br />

brightness = 0x40,<br />

flash = 0x80<br />

};<br />

Navnet Attrs opfører sig som et heltal, og kan bruges i type-erklæringer. Attrs er i den<br />

forstand en ny type, som har et antal lovlige konstant-tildelinger. black til white har de<br />

konstante værdier 0 til 7, brown til manderine har værdierne 32 til 36 (decimal),<br />

brightness har 64 og flash 128. Selvom det også er tilladt at tildele andre konstanter end<br />

medlemssymbolerne til en forekomst af en opremsning, er det ikke programmeringsteknisk<br />

fornuftigt, og oversætteren vil sandsynligvis give en advarsel om tildelinger mellem<br />

opremsningstyper og heltal. Et nyt medlem af Attrs bør erklæres i stedet for at benytte literale<br />

konstanter.<br />

Det anvendelige ved enum-konstruktioner er, at et antal relaterede konstanter kan opremses i<br />

en fart uden at skulle tildeles vilkårlige værdier, der adskiller dem fra hinanden. Er der for<br />

eksempel tale om at identificere forskellige byer i Danmark kunne man forestille sig, at hver by<br />

fik et nummer. Selve nummeret er ligegyldigt, det væsentlige er, at alle byer har et entydigt og<br />

unikt nummer. Det sikres ved brug af opremsninger, uden at der påføres kildeteksten støj i form<br />

af de faktiske værdier af de symbolske konstanter.<br />

2.4.9 Brugerdefinerede typer<br />

En brugerdefineret type er et særligt navn, der defineres af programmøren. Brugerdefinerede<br />

typer er fundamentet for objekt-orienteret programmering, fordi de tillader semantiske udvidelser<br />

af sproget. I dette afsnit vises blot, hvordan et brugerdefineret typenavn erklæres, mens<br />

anvendelsen og implementationen får vente til <strong>kapitel</strong> 3.<br />

En brugerdefineret type introduceres i typesystemet med enten class, struct, union,<br />

enum eller typedef. Resultatet bliver et nyt typenavn, som kan bruges til at instantiere<br />

variable, altså skabe reelle objekter med.<br />

class Matrix; // en matrix-type<br />

class Semaphore; // en semafor-type<br />

class BigNum; // en multipræcisionstype<br />

// ...<br />

BigNum b; // et BigNum-objekt<br />

2.4.8 Opremsninger 35


Matrix m; // et Matrix-objekt<br />

Semaphore s; // et semafor-objekt<br />

Brugerdefinerede typer følger alle regler, der gælder for fundamentale typer. Det er muligt at<br />

kontrollere typernes indre opførsel, konverteringer mellem typer og betydningen af operatorer i<br />

forbindelse med objekter af typerne. Alle disse forhold ved brugerdefinerede typer er her grebet<br />

ud af en sammenhæng, der beskrives fyldestgørende gennem <strong>kapitel</strong> 3.<br />

2.4.10 Typekonvertering<br />

Når et udtryk blander forekomster af forskellige typer sammen, konverteres de enkelte subudtryk<br />

til en fælles type efter visse regler. Der skelnes mellem tvungen (eller eksplicit) og underforstået<br />

(eller implicit) typekonvertering, som sker henholdsvis under programmørens og under<br />

oversætterens kontrol. Underforstået typekonvertering er et resultat af tvetydigheder mellem<br />

objekter af forskellige typer, som blandes i samme udtryk. Den fælles type er altid mindst af<br />

samme præcision som modtageren af udtrykket, altså variablen til venstre for lighedstegnet. Hvis<br />

denne type har større båndbredde end en af operanderne (for eksempel en int til en long),<br />

bibeholdes al information, men hvis det omvendte er tilfældet, tabes præcision. Følgende sidste<br />

tildeling resulterer altid i en implicit typekonvertering:<br />

double d = 3.5; // en double (ingen konvertering)<br />

int a = 100; // og en int (ingen konvertering)<br />

int b = a * d; // ganges sammen (konverteringer)<br />

Tildelingen b = a * d vil typekonvertere alle led i subudtrykket på højre side af<br />

lighedstegnet til den type, som alle andre typer går op i - dvs. den mest præcise type, der findes i<br />

udtrykket. Alle led, som ikke har denne type vil blive konverteret underforstået, hvilket for dette<br />

eksempel vil typekonvertere a til en double (fordi det er typen på d), gange med d,<br />

typekonvertere resultatet til en int (fordi det er typen på b), og tildele det til b. Læg her<br />

mærke til, at alle typekonverteringer arbejder på kopier af de variable, der findes i programmet -<br />

d beholder sin værdi på 3.5 selvom den bliver typekonverteret.<br />

Når information tabes i en underforstået typekonvertering, vil oversætteren normalt give en<br />

advarsel om det:<br />

double d = 2.33; // en double<br />

int a = d; // tildeles en int (a == 2)<br />

double e = a; // og tildeles tilbage (e == 2.0)<br />

Her typekonvertere en double i d's skikkelse til en int, hvorved en del af præcisionen<br />

(decimaltallet 0.33) tabes. a får værdien 2. Den efterfølgende e = a indeholder en<br />

underforstået typekonversion mellem en int og en double, hvilket ikke har betydning for<br />

præcisionen, da en double er "større" end en int. Men læg mærke til, at e ikke er lig d.<br />

36 Typer 2.4


I reelle udtryk med flere operanter forekommer der også underforstået konvertering:<br />

float f = 0.5;<br />

double d = 2.5;<br />

int a = 4;<br />

long l = f + a * d;<br />

Tildelingen til l vil medføre følgende underforståede typekonverteringer:<br />

1. Subudtrykket a * d har højest prioritet ifølge reglerne for operator-præcedens (afsnit<br />

2.5.7). Så først konverteres a til en double, fordi det omvendte ville resultere i tab af<br />

præcision. Det er altid det "mindste" af de to led, der konverteres.<br />

2. Så multipliceres den typekonverterede a (som double) med d.<br />

3. Nu skal f konverteres til en double, da det nye subudtryk består af en float og en<br />

double (resultatet af a * d).<br />

4. Så adderes den typekonverterede f (som double) med resultatet fra før.<br />

5. Det samlede resultat konverteres til den type, som modtagervariablen har, før den<br />

tildeles, altså til en long.<br />

Følgende regler gælder således under implicit typekonvertering:<br />

a. hvis en af operanterne er en long double, konverteres den anden operant også til en<br />

long double før operationen udføres. Ellers:<br />

b. hvis en af operanterne er en double, konverteres den anden operant også til en<br />

double før operationen udføres. Ellers:<br />

c. hvis en af operanterne er en float, konverteres den anden operant også til en float<br />

før operationen udføres. Ellers:<br />

d. hvis en af operanterne er en long, konverteres den anden operant også til en long.<br />

Ellers:<br />

e. hvis en af operanterne er en char eller en short, konverteres den til en int.<br />

Oversætteren vil normalt ved optimering fjerne eller transformere typekonverteringer, hvis det<br />

ikke har betydning for præcisionen. Givet<br />

float f = 2.5;<br />

double d = 2.5;<br />

2.4.10 Typekonvertering 37


int i = f + d;<br />

vil f blive konverteret til en double, fordi præcisionen afhænger af de to kommatal. Men hvis<br />

f var en int, vil d givetvis blive konverteret til en int, fordi resultatet også skal passe til en<br />

int, og decimalværdien i d ingen betydning har i udtrykket.<br />

Ved hjælp af tvungen typekonvertering (type-casting) kan objekter tvinges til at konvertere type<br />

under programmørens kontrol. Tvungen typekonvertering finder anvendelse, når et kombineret<br />

udtryk skal bruge specielle datastørrelser, hvis modtagerelementet ikke er en entydig type samt i<br />

forbindelse med pointermanipulation, og beskrives under afsnittene om unions og pointere.<br />

Objektet, der skal konverteres, sættes i parentes og skrives efter navnet på den type, der skal<br />

konverteres til:<br />

int d = int (float (i) + float (j));<br />

C++ tillader en anden syntaks, som findes i klassisk C og ANSI C, og som er medtaget for<br />

kompatibiltetens skyld:<br />

int i;<br />

long l = long (i); // normal C++ cast<br />

long l = (long) i; // ANSI C cast<br />

Den ny syntaks i C++ er en del mere konsistent i forhold til den måde, funktioner kaldes på, den<br />

måde, brugerdefinerede typer konstrueres på samt den måde, variable initieringes på i dynamiske<br />

allokeringer.<br />

2.5 OPERATORER<br />

Sættet af operatorer afgrænser metoderne, hvormed en eller flere datatyper kan behandles. En<br />

operator udfører dermed en operation på et eller flere objekter og en sammensætning af en eller<br />

flere operatorer og operanter udgør et udtryk. C++ benytter næsten alle tilgængelige specialtegn<br />

som operatorer, som kan opdeles i 5 hovedgrupper plus en gruppe for ekstra betydninger af<br />

særlige operatorer. De 5 hovedgrupper indeholder operatorer for aritmetiske, bitvise og logiske<br />

udtryk samt for tildelings- og reference-operatorer. Alle operanter, der manipuleres, kan igen<br />

være sammensatte udtryk med egne operatorer.<br />

Der skelnes iøvrigt mellem operatorer, der arbejder på en enkelt og på to operanter. De, som<br />

arbejder med en operant kaldes fortegnsoperatorer eller monadiske operatorer og kan for<br />

eksempel være en negation mens de, der arbejder med to operanter kaldes dyadiske operatorer og<br />

kan for eksempel være en addition. Der er også en enkelt triadisk operator, den særlige<br />

aritmetiske if/else.<br />

2.5.1 Aritmetiske operatorer<br />

De aritmetiske operatorer (regne-operatorerne) i C++ er vist i tabel 2-2.<br />

38 Typer 2.4


Operator Funktion<br />

+<br />

-<br />

*<br />

/<br />

%<br />

++<br />

--<br />

Addition<br />

Subtraktion<br />

Multiplikation<br />

Division<br />

Modulus (rest)<br />

Optælling<br />

Nedtælling<br />

Tabel 2-2: Aritmetiske operatorer.<br />

De fem første aritmetiske operatorer er dyadiske og kræver to operanter, en på hver side af<br />

operatoren:<br />

a + 5 // variablen a adderet med 5<br />

14 % 8 // resten efter 14 divideret med 8<br />

x * y // x gange y<br />

De to sidste, op- og nedtælling, arbejder på en enkelt operant, og adderer eller subtraherer "1" fra<br />

denne. Grunden til at "1" her skrives i anførselstegn er, at tallet et ikke altid betyder det samme,<br />

men afhænger af størrelsen på de data, der er tale om. Op- og nedtælling efter evaluering betyder,<br />

at operanten først ændres efter dens værdi er evalueret, mens op- og nedtælling før evaluering<br />

ændrer værdien før den evalueres. Dette kan eksemplificeres således:<br />

int a = 3, b = a; // erklær a og b som int med værdien 3<br />

a++; // læg 1 til a, nu lig 4<br />

int c = a++ * --b; // c = a * (b - 1), a = a + 1, b = b - 1<br />

Efter disse tre linier vil c have værdien 8, a værdien 5 og b værdien 2. I fjerde linie er værdien<br />

af a++ 4 (og ikke 5), fordi operatoren ++ kommer efter variablen, og dermed er en optællingsoperator<br />

med evaluering før optællingen. Værdien af --b er 2 (og ikke 3), fordi b først<br />

nedtælles, og så afgiver værdien i evalueringen.<br />

2.5.2 Bitvise operatorer<br />

Når vi taler om bitvise operatorer, arbejder vi med binær aritmetik. Her skal vi huske på, at alle<br />

data i computeren lagres som binære repræsentationer. Vi kan for eksempel derfor udnytte, at en<br />

char består af 8 bits.<br />

Overdreven eller forkert brug af bitvise operatorer kan lede til ikke-flytbar kode, fordi typernes<br />

størrelser ikke er absolut defineret. Der er imidlertid tidspunkter, hvor bitvise operationer ikke<br />

kan udelades.<br />

2.5.1 Aritmetiske operatorer 39


Operator Funktion<br />

&<br />

|<br />

^<br />

>><br />

2; // e == 5<br />

Binær aritmetik bruges bla. i kommunikation med hardware, for eksempel en seriel port eller en<br />

videocontroller. C++'s sæt af binære operatorer gør, at vi undgår at skulle skrive dele af<br />

programmet i assembler eller maskinkode.<br />

2.5.3 Logiske og relationelle operatorer<br />

De logiske operatorer er vigtige. Det er dem, vi bruger i sammenligninger af data, samt til tests af<br />

typen sand/falsk. Logiske operatorer kaldes også relationelle operatorer, fordi de opstiller en<br />

relation mellem to operanter og lader resultatet være en funktion af dette. De logiske operatorer er<br />

opridset i tabel 2-4.<br />

Operator Funktion<br />

&&<br />

||<br />

==<br />

><br />

<<br />

Logisk AND<br />

Logisk inklusiv OR<br />

Lig med<br />

Større end<br />

Mindre end<br />

40 Operatorer 2.5


=<br />


5 evalueres a to gange, hvilket er tidskrævende og besværligt. Den ækvivalente skrivemåde a<br />

+= 5 evaluerer kun a en gang. Tildelings-operatorerne kan også læses som "udfør operation o<br />

på variabel v og tildel til v." a += 5 læses som "læg 5 til a og tildel resultatet til a".<br />

2.5.5 Reference-operatorer<br />

Operator Funktion<br />

=<br />

+=<br />

-=<br />

*=<br />

/=<br />

%=<br />

&=<br />

|=<br />

^=<br />

>>=<br />


2.3.6, en lineær liste af forekomster. Vektorens navn er derfor en pointer til (adressen på) det<br />

første element i listen (med indeksværdien 0). Når en vektor erklæres med<br />

char vek [10];<br />

afsættes plads til 10 char-forekomster, som ligger efter hinanden i lageret og er nummereret<br />

fra 0 til 9, ikke fra 1 til 10. Når et udtryk som<br />

a = vek [5];<br />

evalueres, multipliceres indekset (her 5) med størrelsen på data-elementet (her en char) og<br />

adderes til vek (adressen på det første element). Resultatet er den adresse, som den ønskede<br />

værdi befinder sig på. vek[n] er derfor af den type, som bruges i erklæringen af vek og er<br />

kun et venstre-udtryk, hvis denne type er et venstre-udtryk.<br />

Dereference af pointere med * følger samme regel. Hvis en variabel v er erklæret som<br />

pointer til type T, vil udtrykket *v returnere en forekomst af typen T (*v's rvalue er en<br />

forekomst af T). For eksempel,<br />

char* a;<br />

// ...<br />

char b = *a;<br />

Når adresse-på operatoren påhæftes et objekt i et udtryk, vil udtrykkets rvalue være en pointer<br />

til det pågældende objekt:<br />

char a; // en normal char<br />

char* b; // en pointer til en char<br />

b = &a; // b tildeles adressen på a<br />

a = *b; // a tildeles b's dereference<br />

Adresse-på operatoren kan aldrig bruges på venstre side af et lighedstegn. Dereferenceoperatorerne<br />

* og & binder til højre (se afsnit 2.5.8), så<br />

char a = 2, b = 3, *c = &b;<br />

char d = a**c; // betyder a ganget med *c, altså 6<br />

Både vektorer og pointere forekommer i flere led eller dimensioner, og her følger erklæringssyntaksen<br />

stadig udtrykssyntaksen:<br />

char a; // char<br />

char* b; // pointer til char<br />

char** c: // pointer til pointer til char<br />

char*** d; // pointer til pointer til pointer til char<br />

2.5.5 Reference-operatorer 43


*b = a; // *b tildeles a<br />

**c = a; // **c tildeles a<br />

***d = a; // ***d tildeles a<br />

og måske lidt mere kryptisk:<br />

b = *c; // *c er "pointer til char"<br />

*d = c; // *d er "pointer til pointer til char"<br />

*c = &a; // *c er "pointer til char"<br />

**d = b; // **d er "pointer til char"<br />

Det afgørende er, at typerne til højre og venstre for lighedstegnet, når det gælder pointere,<br />

skal være ens. Man kan ikke tildele en pointer til en pointer til en char med en pointer til en<br />

char. Med andre ord, hvis c er erklæret som char** (pointer til pointer til char), så vil<br />

*d være en pointer til char og **d være en char.<br />

2.5.6 Særlige operatorer<br />

C++ har et antal ekstra operatorer, der enten arbejder i sammenhænge, vi ikke har<br />

introduceret, eller som har helt specielle idiomatiske betydninger. I tabel 2-7 vises disse ni<br />

operatorer, som bliver diskuteret nedenfor.<br />

Kun ::, ->* og .* findes i C++ og ikke i C. Aritmetisk if/else anvendes som en<br />

indbygget test i midten af andre udtryk, og kan bruges til at omgå de større kontrolstrukturer:<br />

a = b == c ? d : e;<br />

Denne sætning vil tildele a værdien af d, såfremt b er lig med c, ellers får a værdien fra<br />

e. Det skal forstås sådan, at hvad der kommer før spørgsmålstegnet er et logisk udtryk (sandt<br />

eller falsk), og at de to værdier til venstre og til højre for kolon-tegnet beskriver de to mulige<br />

valg. Eller hvis vi læser fra venstre mod højre: er det logiske udtryk sandt, bruges den første<br />

værdi, ellers den anden. Det kan være en fordel at bentyte paranteser til at gøre aritmetiske<br />

if/else-sætninger mere læselige:<br />

a = (b == c) ? (d) : (e);<br />

Sekvensoperatoren - kommaet - bruges til adskillelse af tildelings- og erklæringsudtryk.<br />

Brugen af denne operator garanterer, at udtrykkene evalueres fra venstre mod højre i den<br />

rækkefølge, de står beskrevet i kildeteksten (se næste afsnit). For eksempel,<br />

int a, b, c;<br />

x = 3, y = 4, z = 5;<br />

Resten af de særlige operatorer har betydninger i sammenhænge, vi endnu ikke har<br />

44 Operatorer 2.5


ehandlet. De benyttes i følgende forbindelser: Skop-opløsning bruges til at referere data i et<br />

andet logisk skop end det aktuelle. Operatorerne for medlems-valg bruges i sammenhæng<br />

med datastrukturer. Eksplicitte typekonverteringer kan være nødvendige, når en type absolut<br />

skal ændre "form" af en eller anden grund (for eksempel til en parameter). sizeof<br />

returnerer størrelsen af en type, både en indbygget og en brugerdefineret, og bruges ofte til at<br />

sikre flytbarheden i et program. sizeof er en operator, ikke en funktion, fordi den giver sit<br />

resultat som konstant på oversættertidspunktet.<br />

Operator Funktion<br />

?:<br />

,<br />

::<br />

-><br />

->*<br />

.<br />

.*<br />

()<br />

sizeof<br />

2.5.7 Evalueringsrækkefølge<br />

I et udtryk som<br />

i = a * b + c * d;<br />

Aritmetisk if/else<br />

Sekventierings-operator<br />

Skop-opløsning<br />

Medlems-reference (pointere)<br />

Medlems-dereference (pointere)<br />

Medlems-reference (forekomster)<br />

Medlems-dereference (forekomster)<br />

Tvungen type-konvertering<br />

Størrelse på type<br />

Tabel 2-7: Særlige operatorer.<br />

er det ikke defineret, i hvilken rækkefølge, C++ evaluerer subudtrykkene (a*b) og<br />

(c*d). Kun sekvensoperatoren (komma) samt logisk AND (&&) og OR (||) garanterer, at<br />

operanten eller subudtrykket til venstre evalueres før det til højre. Pas derfor på med udtryk<br />

som:<br />

int j = 2, i = j++ * j;<br />

hvor værdien af i bliver enten 4 eller 6, afhængig af den enkelte oversætter. Grunden til, at<br />

rækkefølgen ikke er lineær fra venstre mod højre er, at oversætteren skal have mulighed for at<br />

optimere den genererede kode i subudtrykket, hvilket ofte medfører en omstrukturering.<br />

Denne omstrukturering er ikke entydig, og gør det umuligt at forudsige rækkefølgen af<br />

evaluering.<br />

2.5.6 Særlige operatorer 45


2.5.8 Præcedens<br />

Operatorerne har forskellig prioritet eller præcedens i forhold til hinanden, hvilket ganske<br />

simpelt er for at undgå en masse parenteser i kildeteksten. Subudtryk bundet af operatorer<br />

med højere prioritet udføres før subudtryk med lavere prioritet. For eksempel bliver resultatet<br />

af<br />

int i = 4 + 3 * 2;<br />

at i får værdien 10, ikke 14, da * rangerer højere end + (som på en regnemaskine).<br />

Desuden binder operatorerne forskelligt (de har forskellig associativitet); fortegnsoperatorerne<br />

(!, &, ~ osv.) og tildelingsoperatorerne (=, +=, != osv.) binder til højreoperanten,<br />

mens alle andre operatorer binder til venstreoperanten. For eksempel,<br />

int i = 5, j = 4 * -i; // j == -20<br />

Her binder - til højre og * til venstre, så udtrykket fortolkes som (5) * (-i) og giver<br />

således resultatet -20. På samme måde vil de to stjerne-operatorer det i følgende udtryk<br />

fortolkes som dels en aritmetisk operator og en binær fortegnsoperator (dereferencen):<br />

int x = 10, z = 20;<br />

int* y = &z;<br />

int r = x**y; // r == 200<br />

I visse andre sprog er ** en operator, der står for potens. Den findes ikke i C++, men<br />

fortolkes som to enkeltstående operatorer, hvis betydning afhænger af konteksten. Stod der<br />

for eksempel z = x+**y var den første * en dereference og ikke en multiplikativ<br />

operator. I C++ skal potensopløftning implementeres af programmøren. Operatorernes<br />

præcedens og associativitet er vist i tabel 2-8.<br />

Til slut en generel advarsel om typiske fejlkilder i C++-programmer. Det er næsten altid på<br />

grund af en slåfejl eller syntaktisk misforståelse, at problemerne opstår, og det bliver desto<br />

meget værre af, at sådanne fejl ofte er syntaktisk lovlige i C++. Oversætteren gør, hvad den<br />

får besked på, og koden ser umiddelbart rigtig ud. Som eksempel kan følgende løkke vises,<br />

som tæller variablen i ned sålænge den er størren end 0 (while-løkker diskuteres i afsnit<br />

2.7.2):<br />

while (i =! 0) i--;<br />

Kan du se problemet? Det viser sig, at ulighedsoperatoren er vendt forkert (den hedder !=)<br />

så der i virkeligheden står "i tildeles den logiske negation af den literale konstant 0", hvilket<br />

jo altid er en sand værdi - og løkken fortsætter for evigt.<br />

46 Operatorer 2.5


2.6 FUNKTIONER<br />

Uanset C++'s muligheder indenfor objekt-orienteret programmering, bygger det syntaksmæssigt<br />

på det proceduralt orienterede sprog C. Funktioner er et nøglekoncept, fordi det<br />

tillader en dekomposition af kompleksiteten, hvorved databehandlinger af ekstern relevans<br />

kan samles i mindre grupper.<br />

En funktion består af et navn, en parameterliste og en returtype, samt kroppen af funktionen.<br />

Det følgende eksempel definerer en areal-funktion i to dimensioner:<br />

long areal (int bredde, int laengde) {<br />

long c = bredde * laengde;<br />

return c;<br />

}<br />

(P) (A) Operator Beskrivelse Eksempel<br />

17<br />

17<br />

16<br />

16<br />

16<br />

16<br />

15<br />

15<br />

15<br />

15<br />

15<br />

15<br />

15<br />

H<br />

V<br />

V<br />

V<br />

V<br />

V<br />

H<br />

H<br />

H<br />

H<br />

H<br />

H<br />

H<br />

::<br />

::<br />

()<br />

., -><br />

[]<br />

sizeof<br />

+, -<br />

++, --<br />

!<br />

~<br />

*, &<br />

()<br />

new, delete<br />

klasseskop<br />

globalt skop<br />

funktionskald<br />

medlemsvalg<br />

vektorindeksering<br />

angivelse af størrelse<br />

fortegnsminus/plus<br />

op- og nedtælling<br />

logisk negation<br />

bitvis negation<br />

dereference, adresse<br />

tvungen konvertering<br />

lageradministration<br />

a = Klasse::Medlem<br />

a = ::Data<br />

a = funktion ()<br />

a = S.m, a = S->m<br />

a = vek [n]<br />

a = sizeof(double)<br />

a = +b, b = -a<br />

a = ++b, b = a--<br />

a = !b<br />

a = ~b<br />

a = *b, b = &a<br />

a = char (b)<br />

a = new T, delete a<br />

14 V .*, ->* medlemspointervalg a = T.*b, a = b->*c<br />

13 V *, %, / multiplikative a = b * c<br />

12 V +, - aritmetiske a = b + c<br />

11 V bitvis skift a = b = c<br />

2.5.8 Præcedens 47


9 V ==, != sammenlignende a = b != c<br />

8 V & bitvis AND a = b & c<br />

7 V ~ bitvis XOR a = b ~ c<br />

6 V | bitvis OR a = b | c<br />

5 V && logisk AND a = b && c<br />

4 V || logisk OR a = b || c<br />

3 V ?: aritmetisk betingelse a = b ? c : d<br />

2 H =,+=,*=, osv. tildelingsoperatorer a *= b<br />

1 V , kommaseparator a = 5, b = 7<br />

Tabel 2-8: Operatorernes associativitet (A), prioritet (P) og syntaks.<br />

Den første linie fortæller oversætteren, at funktionen hedder areal, at den skal returnere<br />

data af typen long, samt at den skal kaldes med to parametre, eller argumenter, begge af<br />

typen int. Funktionens krop (mellem de to krøllede parenteser) multiplicerer blot de to<br />

parametre og returnerer resultatet med nøgleordet return.<br />

Funktionen areal kan derefter kaldes andetsteds fra:<br />

f () { // en funktion f()<br />

long b = 5, l = 8; // to long's, b og l<br />

long a = areal (b, l); // a lig areal's returværdi<br />

}<br />

I terminologien skelner vi mellem de to funktioner ved at kalde den ydre for den kaldende<br />

funktion og den indre for den kaldte funktion eller målfunktionen. return kan forekomme<br />

flere steder i en funktion, typisk i forbindelse med betingede kontrolstrukturer (afsnit 2.7), og<br />

behøver derfor ikke at være den sidste sætning i funktionens krop. Funktionen returnerer altid<br />

kontrol til kalderen, når en return-sætning mødes, eller når funktionens skop render ud.<br />

2.6.1 Skop, blokke og levetid<br />

Alle variable har en levetid, som begynder på det sted, hvor de erklæres, og ender i slutningen<br />

af den blok, hvori erklæringen foretages. En blok er indholdet mellem to krøllede parenteser<br />

({...}). Blokke kan indeholde andre blokke, men skal altid være indenfor en funktion. En<br />

variabels skop er defineret som den eller de blokke, hvori den eksisterer og lovligt kan bruges,<br />

starter ved erklæringen og slutter ved udgangen af blokken. På dansk har vi begrebet virkefelt,<br />

der dækker over en del af ordet skop. I appendiks D findes en detaljeret oversigt over danske<br />

oversættelser og begrundelserne for dem.<br />

Variable kan defineres udenfor eller indenfor funktioner. Defineres de udenfor, kaldes de<br />

48 Funktioner 2.6


globale, indenfor er de lokale til den aktuelle funktion. Lokale variable har altså et<br />

indskrænket skop i forhold til resten af programmet. C++'s operator for skop-opløsning kan<br />

hjælpe med lokalisering af en variabel i et andet skop, hvis en navnekonflikt opstår. For<br />

eksempel,<br />

int a = 5; // global a<br />

f () {<br />

int a = 7; // a lokal til f()<br />

int b = a; // b tildeles den lokale a<br />

int c = ::a; // c tildeles den globale a<br />

}<br />

Lokale variable "dør", når blokken udløber. I følgende eksempel forekommer der indlejrede<br />

blokke med hver sine lokale data:<br />

f () {<br />

int a = 5; {<br />

int b = 5; {<br />

int c = 5;<br />

} // her slutter c's skop<br />

} // her slutter b's skop<br />

} // her slutter a's skop<br />

Heltalsvariablen c kendes kun i det inderste niveau. Fordelen med flere blokke er, at en<br />

klump kode kan indsættes i en funktion omsluttet af parenteser med garanti for, at navnekonflikter<br />

ikke opstår. Alle data, der erklæres indenfor blokken vil blive brugt fremfor "ydre"<br />

data af samme navn. Men de fjernes, når blokken ender.<br />

2.6.2 Prototyper<br />

C++ har som beskrevet et stærkt, statisk typesystem. Da en funktion er en afledt type, skal<br />

denne være erklæret før den kan bruges. Som omtalt i indledningen placeres denne erklæring<br />

ofte i en headerfil, som andre programmer derefter kan benytte sig af. Hvis funktionen er helt<br />

lokal, placeres erklæringen i begyndelsen af kildefilen. En funktions-erklæring i C++ ser<br />

således ud:<br />

double Potens (double, double);<br />

Efter denne linie, som også kaldes en fremad-reference eller en prototype, ved overætteren, at<br />

funktionen Potens() returnerer en double og skal have to parametre, begge double'r.<br />

Hvor selve definitionen (med koden) af funktionen ligger er underordnet indtil lænkningstidspunktet.<br />

Alle kald til Potens() vil blive sat korrekt op og type-checket.<br />

2.6.1 Skop, blokke og levetid 49


Det er ikke altid nødvendigt, men dog en god idé, at have prototyper til alle funktioner. Hvis<br />

en funktion ikke bruges, før den defineres, er prototypen ikke nødvendig. I så fald er selve<br />

definitionen på funktionen nok for oversætteren i sikring af typekonsistens. Prototyper findes<br />

i C++, fordi oversætteren læser hele programteksten fra start til slut én gang. Den skal således<br />

have viden om funktioner, der kaldes før deres definition, medmindre de returnerer en int<br />

og har en tom parameterliste. I klassisk C var prototyper ikke påkrævede, men i ANSI C blev<br />

de tvungne og har vist sig at sikre korrekte funktionskald til biblioteker og andre moduler. Se<br />

også afsnit 3.9.5 om mingelering af funktionsnavne i forbindelse med sammenblanding af<br />

flere forskellige prototyper.<br />

2.6.3 void<br />

Hvis en funktion ikke har noget eksplicit output, forventes den ikke at returnere en værdi.<br />

Retur-typen erklæres i så tilfælde som void. Void er engelsk for "tomhed". Det er ydermere<br />

god praksis at skrive void i parameterlisten, hvis funktionen ikke har eksplicit input, altså<br />

ikke modtager parametre fra kalderen. Funktionserklæringen<br />

void f ();<br />

er således identisk med<br />

void f (void);<br />

Det gælder kun for parameterlisten, mens returværdien underforstået returnerer en forekomst<br />

af typen int. Følgende to erklæringer er helt ens:<br />

int f ();<br />

f ();<br />

Det er imidlertid god praksis altid at angive returtypen på en funktion, også selvom den<br />

returnerer en int.<br />

2.6.4 main()<br />

Et C++-program starter sin eksekvering i funktionen main(). Der kan følgelig kun være én<br />

main(), og denne kalder typisk et antal initierings-funktioner, cykler gennem et par<br />

behandlingsfunktioner og rydder til sidst op. Eksekveringen slutter efter main()'s skop,<br />

dvs. når kroppen på funktionen slutter.<br />

Information kan overføres fra loaderen (normalt en del af operativsystemet eller miljøet) til<br />

main() i to variable. Normalt startes et program med en kommandolinie med et filnavn på<br />

det program, der skal udføres, efterfulgt af eventuelle parametre, som i<br />

cp source.doc dest.doc<br />

50 Funktioner 2.6


Funktionen main() i programmet cp modtager information om parametrene i kommandolinien<br />

i to variable, som valgfrit kan specificeres som parametre til funktionen:<br />

void main (int argc, char** argv);<br />

Det første parameter, argc, indeholder antallet af ord i kommandolinien (inklusive<br />

programmets navn, her cp), og argv peger på de individuelle navne. argv er af typen<br />

"pointer til pointer til char", og diskuteres nærmere i afsnit 2.11.1. Hvis main() ikke<br />

behøver information fra miljøet, erklæres den parameterløs.<br />

Hvis main() skal returnere en værdi til miljøet, for eksempel som fejlkode, erklæres den<br />

som<br />

eller<br />

int main ();<br />

int main (int argc, char** argv);<br />

og den ønskede værdi afleveres tilbage til miljøet med return fra main(). Grunden til,<br />

at det er tilladt at erklære main() på forskellige måder er, at den ikke har en prototype.<br />

2.6.5 Funktionskald<br />

En erklæret funktion kan kaldes fra alle andre funktioner, som kender navnet med følgende<br />

syntaks:<br />

double p = potens (3, 7); // 3 i syvende til p<br />

De literale heltalskonstanter 3 og 7 overføres som parametre til funktionen potens(), som<br />

returnerer en værdi, der tildeles til p. Funktionskald i C++ er normalt ved værdi, hvilket vil<br />

sige, at det er indholdet af variablen, ikke selve variablen, der overføres som parameter og<br />

returværdi (benævnes call-by-value). Følgende program<br />

void plus (int a) { // læg 1 til a<br />

a++;<br />

}<br />

void main () {<br />

int a = 1;<br />

plus (a);<br />

cout


vil udskrive<br />

a == 1<br />

og ikke a == 2, som man kunne tro. Kaldet til plus() overfører a's værdi fra<br />

main()'s a til plus()'s a, som oprettes og initieres ved funktionskaldet. Dermed er der<br />

tale om to forskellige variable med samme navn, men med forskelligt skop, som behandles<br />

uafhængigt.<br />

Reglerne for typekonvertering gælder også i funktionskald. Hvis en funktion erklæres som<br />

void seek (long l);<br />

og kaldes med<br />

int i;<br />

seek (i);<br />

vil oversætteren konvertere i til en long, før værdien tildeles l. Dette har ingen betydning<br />

for i. C++ anvender altid call-by-value. De afledte typer kan imidlertid bruges til at<br />

imitere andre sprogs muligheder for andre kaldemetoder, særligt call-by-reference, kald med<br />

reference.<br />

2.6.6 Afledte typer som parametre<br />

Når data overføres som parametre fra en kaldende funktion til en målfunktion, konstrueres<br />

altså en kopi af objekterne som initieres med de værdier, de originale objekter har. Et<br />

parameter i en funktion er derfor det samme som en samtidig erklæring og initiering, blot står<br />

initieringsværdien et andet sted i koden. De opfører som automatiske variable og har deres<br />

skop indenfor hele målfunktionen. Umiddelbart før målfunktionen returnerer, destrueres<br />

parameterobjekterne igen. Det er meget vigtigt at huske disse regler, især når vi i <strong>kapitel</strong> 3<br />

introducerer brugerdefinerede typer, af hvilke vi selv er ansvarlige for konstruktionen og<br />

destruktionen.<br />

Parametre kan imidlertid være af andre typer end blot de fundamentale. Vi kan erklære<br />

parametre som pointere, referencer, vektorer eller vi kan bruge vores egne brugerdefinerede<br />

typer eller datastrukturer. Især pointere og referencer er nyttige som parametre, fordi<br />

funktionen får mulighed for at arbejde på data, der ligger udenfor dens skop. Eksemplet<br />

ovenfor med den fejlagtige funktion plus(), som lægger 1 til parametret, kan skrives om<br />

med pointere, så den virker efter hensigten. Parametere som afledte typer erklæres<br />

fuldstændigt som vi erklærer normale afledte objekter. Følgende omskrevne plus()<br />

modtager en pointer:<br />

void plus (int* a) { // læg 1 til *a<br />

52 Funktioner 2.6


*a++;<br />

}<br />

void main () {<br />

int a = 1;<br />

plus (&a); // kald plus() med adressen på a<br />

cout


double resultat = *kr * *kurs;<br />

cout


void f (long& l) {<br />

l += 42;<br />

}<br />

void main () {<br />

int i = 10;<br />

f (i); // overraskelse: i typekonverteres!<br />

cout


eturneres flere steder i funktionen, for eksempel i forbindelse med en betingelse. Værdierne,<br />

der returneres, kan være forskellige men skal være af samme type eller af typer, der kan<br />

konverteres til samme type som erklæret før funktionens navn. Oversætteren vil normalt give<br />

en advarsel, hvis der ikke specificeres en returværdi et sted i en funktion, der ikke er erklæret<br />

void, og vil i øvrigt sikre, at typen, der returneres, er korrekt.<br />

Returtyper kan ligesom parametertyper være afledte. Funktioner kan returnere pointere,<br />

referencer, vektorer, datastrukturer eller brugerdefinerede objekter. Fordelene ved brug af<br />

pointere eller referencer er de samme som for parametre, blot er de vendt om: den kaldende<br />

funktion får adgang til objekter i den kaldte funktion. Her skal der passes på, at der ikke<br />

returneres pointere eller referencer til automatiske variable i den kaldte funktion - de går ud af<br />

skop, når funktionen ender. Følgende leder til dramatik:<br />

int* f () {<br />

int i = 5;<br />

// ...<br />

return &i; // ikke smart, i kendes kun i f()<br />

}<br />

int& g () {<br />

int j = 5;<br />

// ...<br />

return j; // ikke smart, j kendes kun i g()<br />

}<br />

Oversætteren vil normalt advare om en sådan konstruktion. Når vi returnerer pointere eller<br />

referencer fra funktioner, skal de enten være statisk allokerede objekter i funktionen (afsnit<br />

2.9.1), dynamisk allokerede objekter i funktionen (afsnit 2.9.2), globale variable eller de<br />

parametre, som funktionen modtager, såfremt de er pointere eller referencer.<br />

2.6.8 Rekursion<br />

Rekursion er en teknik, der indebærer, at en funktion kalder sig selv. Dette gøres i situationer,<br />

hvor funktionen har svært ved at arbejde iterativt på data (for eksempel i løkker) men dog<br />

alligevel skal foretage den samme behandling af mange ens data. I C++ kan en rekursiv<br />

funktion for eksempel bruges til at traversere et binært træ:<br />

struct BinaryNode {<br />

void* Data;<br />

BinaryNode* hoejre, *venstre;<br />

};<br />

56 Funktioner 2.6


void inorderTraversal (BinaryNode* n) {<br />

if (n->hoejre) inorderTraversal (n->hoejre);<br />

// ...<br />

if (n->venstre) inorderTraversal (n->venstre);<br />

}<br />

Det udnyttes her, at returadresserne på de kaldte funktioner automatisk placeres på en stak af<br />

maskinen, og lader denne underliggende egenskab være det bærende element i gennemløbningen<br />

af data.<br />

2.6.9 Overstyrede funktionsnavne<br />

I C++ kan flere funktioner dele samme navn, så længe de adskiller sig i parameterlisterne. Det<br />

betyder, at selve funktionsnavnet ikke behøver afspejle parametrene, hvis flere funktioner<br />

med samme natur, men med forskellige interne data, skal bruges. Potens-funktionen overfor<br />

kunne tænkes implementeret for typerne double, long og int. Uden overstyring var vi<br />

tvunget til at skrive de tre funktioner med afvigende navne, som i<br />

double doublePotens (double, double);<br />

long longPotens (long, long);<br />

int intPotens (int, int);<br />

Muligheden for at overstyre funktionsnavnet giver os imidlertid lov til at dele det samme<br />

navn i de tre funktioner † :<br />

double Potens (double, double);<br />

long Potens (long, long);<br />

int Potens (int, int);<br />

Oversætteren udleder udfra syntaksen i selve kaldet til funktionen hvilken af de tre versioner<br />

af Potens(), der skal bruges. Det understreges, at parameterlisterne skal afvige i de<br />

overstyrede funktioner; returtyperne er ikke nok:<br />

int Random (); // et tilfældigt heltal: ok<br />

double Random (); // et tilfældigt kommatal: fejl<br />

Oversætteren vil ikke acceptere den anden funktionserklæring. Det skyldes, at der vil opstå<br />

uløselige tvetydigheder ved kald af funktioner med identiske parameterlister. Hvis<br />

† I tidligere versioner af C++ (før 2.0) var det påkrævet at specificere et overload-nøgleord før første erklæring<br />

eller prototype på en funktion, som var genstand for overstyring. Dette er ikke længere påkrævet, men nøgleordet<br />

tillades stadig for kompatibilitetens skyld, selvom det ignoreres af oversætteren.<br />

2.6.8 Rekursion 57


Random(), var den lovlig, for eksempel kaldtes med<br />

long x = Random ();<br />

ville det ikke være muligt at vide, om det var den ene eller anden overstyrede version af<br />

funktionen, der ønskedes kaldt. Brugen af overstyrede funktioner medfører en særlig intern<br />

mingelering af navnene, så oversætteren og lænkeren kan arbejde unikt med dem, hvilket kan<br />

give problemer i særlige tilfælde. Se afsnit 3.9.5.<br />

2.6.10 Underforståede parametre<br />

Et alternativ til overstyrede funktioner er brugen af underforståede parametre. En funktion<br />

udskrivTal(), som tænkes at udskrive et tal i et bestemt talsystem, kunne se sådan ud:<br />

void udskrivTal (int, int);<br />

hvor de to parametre er selve tallet til udskrivning samt den radix, det skal skrives i.<br />

Forestiller vi os, at vi ønsker et underforstået talsystem (for eksempel 10-talssystemet), kan vi<br />

omskrive funktionen, så det andet parameter vil antage en default-værdi, hvis det ikke<br />

medtages i kaldet:<br />

void udskrivTal (int, int = 10);<br />

Dermed undgås en ekstra overstyret funktion, der blot optræder som klister mellem kalderen<br />

og den egentlige funktion, som den kalder med et ekstra parameter.<br />

Læg mærke til to ting: For det første skal det underforståede parameter skrives i prototypen<br />

(hvis den findes), og ikke i selve definitionen på funktionen. For det andet kan den<br />

underforståede værdi udover at være en konstant, være en global, statisk variabel eller et<br />

andet funktionskald:<br />

void setDato (int = getdate (today)); // prototype<br />

void setDato (int julianDay) { // definition<br />

// julianDay antager default-værdi getDate(today)<br />

// ...<br />

}<br />

2.6.11 Uspecificerede parametre<br />

I visse typer af funktioner er det ikke muligt at specificere alle parametre. For eksempel kan<br />

funktionen printf() i standardbiblioteket, som bruges til formateret output, udskrive data<br />

af vilkårlige fundamentale og afledte typer. printf() har en parametererklæring som ikke<br />

58 Funktioner 2.6


specificerer de faktiske typer samt antallet af dem. Det gøres med den specielle notation (...),<br />

en ellipsis, som fortæller oversætteren, at "der er måske flere parametre".<br />

Vi ved altså ikke på oversættelsestidspunktet, hvordan parameterspecifikationen ser ud, og<br />

bliver derfor nødt til at programmere os ud af det. Standardbiblioteket indeholder et antal<br />

makroer til kontrol af denne type funktioner - kode, som på kørselstidspunktet fortolker<br />

parameterlisterne og initierer datastrukturer, som kan bruges i funktionen. De fleste<br />

funktioner med et uspecificeret antal parametre af vilkårlige typer kommer fra C, og findes i<br />

biblioteker, som blev skrevet, da der ikke var alternativer i selve sproget.<br />

Følgende funktionserklæring<br />

void sendData (int ...);<br />

repræsenterer en parameterliste, som består af mindst en int. De uspecificerede parametre<br />

skal altid komme til sidst i listen. Funktionen kan nu kaldes med for eksempel<br />

sendData (MODEM, "ATZ\r"); // send kommando-streng<br />

sendData (PRINTER, 27); // send et heltal<br />

Funktionen skal bruge information, som ikke er tilgængelig fra oversætteren, og følgelig kan<br />

oversætteren ikke type-checke parameterlister til en sådan funktion. Hvis der for eksempel<br />

ikke tages hensyn til kommatal i sendData(), vil der opstå en fejl under kørslen.<br />

Headerfilen indeholder de nødvendige makroer til initiering og<br />

manipulation af parameterlister. Dog skal parameterlisterne organiseres sådan, at funktionen<br />

har mulighed for at fortolke de forskellige parametres typer. Denne information skal komme<br />

fra kalderens funktion. I C-biblioteket findes for eksempel en funktion<br />

printf(), som formaterer forekomster forskellige fundamentale typer efter et mønster og<br />

skriver dem ud. Denne funktion har prototypen<br />

int printf (char* ...)<br />

Pointeren, som modtages som førsteparameter, peger på en format-streng, som fortolkes på en<br />

måde, der giver funktionen mulighed for at vide hvor mange parametre, der uspecificeret<br />

følger efter. Hvis kalderen gør noget forkert, dvs. hvis format-strengen og de efterfølgende<br />

parametre enten ikke stemmer overens i type eller i antal, så vil der opstå en kørselsfejl -<br />

oversætteren har ikke nogen mulighed for at se sådanne fejl, og teknikken bør derfor helst<br />

undgås. Uspecificerede parametre kan udgås ved brug af overstyrede funktionsnavne eller<br />

underforståede parametre.<br />

2.6.12 inline-funktioner<br />

Ved brug af C++-nøgleordet inline før funktions-erklæringen vil oversætteren indsætte<br />

hele funktionens krop i alle referencer i stedet for at foretage et regulært kald. Fordelen<br />

herved er, at mindre funktioner med mange parametre kan udføres med en hastighedsfordel.<br />

2.6.11 Uspecificerede parametre 59


Prisen er oftest en større objektfil:<br />

inline int max (int a, int b) { return a >= b ? a : b; }<br />

Alle kald til max vil resultere i en indsættelse af kroppen (mellem de krøllede parenteser) i<br />

objektkoden på det sted, hvor kaldet foretages. Parameteroverførsel og kald undgås, koden<br />

eksekveres hurtigere. Generelt er det ikke muligt at erklære funktioner, der indeholder<br />

betingelser eller løkker som inline - prøv at forestille dig hvad der ville ske, hvis en<br />

rekursiv funktion skulle være erklæret sådan.<br />

Vi skal senere se, hvordan inline-funktioner er yderst anvendelige i abstrakte datatyper.<br />

2.6.13 Evalueringsrækkefølge<br />

Parameterlisten i et funktionskald evalueres i vilkårlig rækkefølge. Kommaet, som bruges<br />

mellem parametrene, er ikke en sekvensoperator i den forstand, som den bruges i tildelinger<br />

og udtryk. Evaluering fra venstre mod højre er derfor ikke garanteret, men er udefineret.<br />

Undgå derfor funktionskald som:<br />

f (a (), b ()); // kaldes a() eller b() først?<br />

push (pop () / pop ()); // hvilken pop() kaldes først?<br />

hvis a() skal kaldes før b() eller hvis rækkefølgen af kaldene til pop() har betydning,<br />

hvilket de her har i forbindelse med en division. Det gælder også for manipulationer af data i<br />

parameterlisterne:<br />

f (a++, a);<br />

Da udtrykkene a++ og a skal leveres til f(), og da evalueringsrækkefølgen ikke er<br />

defineret, ved vi ikke, om oversætteren først evaluerer a eller a++. Resultatet er derfor<br />

tvetydigt og kildeteksten er ikke garanteret flytbar.<br />

C++ giver en udtalt frihed i syntaksen, men det er en frihed under ansvar. Det kan se pænt<br />

ud at skabe kryptisk kode med et væld af funktionalitet på meget få linier, men det gavner<br />

sjældent effektiviteten og er en klar ulempe for vedligeholdelsen. Brug af midlertidige<br />

variable skader ikke effektiviteten og gør kildeteksten mere åbenbar og sikker.<br />

2.7 KONTROLSTRUKTURER<br />

Programmets "flow" gennem kørslen kontrolleres af C++'s 9 nøgleord for kontrolstrukturering.<br />

Disse nøgleord baseres på en sand/falsk-situation, hvor dele af programmet springes<br />

over, og andre vælges til videre kørsel.<br />

De 9 nøgleord er if, else, while, do, for, switch, case, goto,<br />

continue, og break.<br />

60 Funktioner 2.6


2.7.1 if/else<br />

Denne kontrolstruktur kendes fra alle proceduralt orienterede sprog, og har i C++ syntaksen:<br />

if (udtryk) sætning;<br />

if (udtryk) sætning; else sætning;<br />

Sætningen udføres, hvis udtrykket evalueres til en værdi, der er forskellig fra nul, og springes<br />

over, hvis udtrykket giver nul. Sætningen kan være sammensat, og skal i så fald omsluttes af<br />

en blok. Med else har vi mulighed for at vælge forgrening på både sandhedsværdien og<br />

usandhedsværdien (den omvendte) i samme struktur. Eksempler på if/else-strukturer i<br />

C++:<br />

// max() returnerer største int af 2<br />

int max (int a, int b) {<br />

if (a >= b)<br />

return a;<br />

else<br />

return b;<br />

}<br />

// max() returnerer største int af 3<br />

int max (int a, int b, int c) {<br />

if (a >= b && a >= c)<br />

return a;<br />

else if (b >= c)<br />

return b;<br />

else<br />

return c;<br />

}<br />

void outputChar (int a) {<br />

if (a == 27) {<br />

sendEscape ();<br />

sendNull ();<br />

}<br />

else sendChar (a);<br />

}<br />

I if/else-strukturer skal man være påpasselig, hvis sætningerne er indlejrede. Spørgsmålet<br />

er hvilken if en bestemt else refererer tilbage til. Overvej følgende eksempel:<br />

2.6.13 Evalueringsrækkefølge 61


void f (int a, int b) {<br />

if (a == 1)<br />

if (b == 2)<br />

g ();<br />

else<br />

h ();<br />

}<br />

Man skulle tro (ikke mindst på grund af indrykningnen), at g() kaldes, såfremt a er 1 og b<br />

er 2, og at h() ellers kaldes. Det forholder sig imidlertid sådan, at en else binder til den<br />

seneste if, så h() vil kun blive kaldt, hvis a er 1, og b ikke er 2. Her må vi tage<br />

blokkene i brug:<br />

void f (int a, int b) {<br />

if (a == 1) {<br />

if (b == 2)<br />

g ();<br />

}<br />

else h ();<br />

}<br />

så oversætteren tvinges til at binde til den tidlige if.<br />

2.7.2 while/do<br />

En anden velkendt kontrolstruktur, while-sætningen, har følgende syntaks:<br />

while (udtryk) sætning;<br />

og udfører sætning sålænge udtryk er sandt, altså giver alt andet end nul. Som i ifsætningen<br />

skal kontrolstrukturens krop omsluttes som blok, hvis den er sammensat af flere<br />

sætninger. En while-struktur kan se ud som følger:<br />

int decCif (int i) { // antal decimale cifre på et tal<br />

int j = 0;<br />

while (i) j++, i /= 10;<br />

return j;<br />

}<br />

char Spejl (char b) { // lav et binært spejlbillede<br />

char t = 0;<br />

int j = 8;<br />

62 Kontrolstrukturer 2.7


while (j--) {<br />

t = 1;<br />

}<br />

return t;<br />

}<br />

while tester på udtrykkets sandhed i starten af løkken. Derved kan løkken helt springes<br />

over. Det er også muligt at teste i slutningen af løkken, for at tillade mindst et gennemløb.<br />

do-while kan sammenlignes med andre proceduralt orienterede sprogs repeat-sætninger.<br />

Dette gøres med do-nøgleordet:<br />

void f (int i, int j) {<br />

do {<br />

i /= j;<br />

} while (i);<br />

}<br />

2.7.3 for<br />

I C++ er kontrolstrukturer med for-sætninger blot en generalisering af while/do. En<br />

for-sætning har følgende syntaks:<br />

for (udtryk1; udtryk2; udtryk3) sætning;<br />

og svarer direkte til følgende kombinerede strukture med while-nøgleordet:<br />

udtryk1;<br />

while (udtryk2) {<br />

sætning;<br />

udtryk3;<br />

}<br />

for-løkker fylder mindre i kildeteksten, og gør den mere naturlig og læsbar. De tre udtryk<br />

skal læses som henholdsvis en initiering, en terminerings-test (afslutningstest) og en<br />

opdatering. Eksempel på en for-struktur:<br />

void f () {<br />

int b = 1;<br />

for (int a = 8; a; a--) b *= 2;<br />

}<br />

2.7.2 while/do 63


Hvis løkkens krop indeholder mere end én sætning, skal den omsluttes af en blok:<br />

for (x = 0; x < 10; x++) {<br />

y += x;<br />

if (y % 100) y++;<br />

}<br />

Det er lovligt at placere kombinerede udtryk i for-strukturen, separeret med kommaer:<br />

for (x = 0, f = 1; x < 20 && y; x++) y = (v [x] != 0);<br />

Erklæringer af variable, der forekommer i for-løkkens er lokale til løkkens skop, og kendes<br />

ikke efter løkkens slutning. Hvis et af de tre udtryk ikke behøves, kan de udelades:<br />

for (; *p; p++) // gentag indtil p peger på '\0'<br />

for (a = 0;;a++) // gentag for evigt<br />

for (;;) // gentag for evigt<br />

2.7.4 break, continue og goto<br />

I løkker har vi undertiden behov for at slutte før tiden, hvis for eksempel en fejl pludselig<br />

opstår. C++ har tre nøgleord for ikke-lineær styring af programmets flow: break, som<br />

bryder ud af den inderste for- eller while-løkke, continue, som går til slutningen af<br />

løkken samt goto, som hopper til et vilkårligt sted indenfor samme funktion.<br />

Følgende funktion benytter break til tidlig afbrydelse. Funktionen konverterer et decimalt<br />

beskrevet tal, der repræsenteres af en streng, til en long:<br />

long atol (char* s) { // konvertér s til long<br />

long j = 0; // returværdi<br />

while (*s != '\0') { // sålænge der er tegn<br />

if (*s > '9' || *s < '0') { // hvis ulovligt ciffer<br />

j = 0; // returværdi nul<br />

break; // bryd ud af while-løkken<br />

}<br />

j = j * 10 + *s % 10; // addér ciffer<br />

s++; // næste ciffer<br />

}<br />

return j; // returnér resultatet<br />

}<br />

I 4. linie analyseres, hvorvidt det aktuelle ciffer er et lovligt decimalt tegn ('0' - '9'). Hvis det<br />

ikke er tilfældet, brydes for-løkken i linie 6, og kontrollen fortsætter i linie 11. break<br />

bruges på denne måde i forbindelse med løkker af for- og while-typen oftest til tidlig<br />

64 Kontrolstrukturer 2.7


afbrydelse af disse, når grunden til afbrydelsen først bliver kendt i midten af løkken og<br />

kræver, at efterfølgende processering ikke bliver udført.<br />

continue ligner break, men forlader ikke løkken. Erstattes break i linie 6 med<br />

continue, vil linie 8 og 9 springes over, og videre kørsel fortsætte fra linie 3. continue<br />

bruges, hvis en situation opstår, som afslutter løkkens funktion, men som ikke kræver, at<br />

løkken forlades:<br />

while (*s != '\0') { // spring mellemrum over<br />

if (*s == ' ') continue;<br />

if (*s > '9' || *s < '0') { // hvis ulovligt ciffer<br />

j = 0; // returværdi nul<br />

break; // bryd ud af while-løkken<br />

}<br />

j = j * 10 + *s % 10; // addér ciffer<br />

s++; // næste ciffer<br />

}<br />

Selv om goto ikke skønnes at have nogen brugbar funktion i moderne programmering, fordi<br />

udnyttelsen af dette nøgleord skaber ustrukturerede og rodede programmer i kraft af det store<br />

tab af abstraktion ved direkte hop, findes den dog alligevel. goto bryder<br />

eksekveringssekvensen og fortsætter efter erklæringen af den etikette, der specificeres ved<br />

goto-nøgleordet:<br />

void f (int a) {<br />

if (!a) goto error; // a må ikke være 0<br />

// ... // forstæt med behandlingen<br />

return; // returnér fra funktionen<br />

error:<br />

// ... // fejlbehandling<br />

}<br />

En etikette er et navn efterfulgt af et kolon (:). Etiketter er lokale til de funktioner, i hvilke de<br />

defineres og kan ikke defineres udenfor funktioner. Både forlæns- og baglæns hop med<br />

goto er tilladt, dog begrænset til den funktion, den befinder sig i samt med det ekstra<br />

forbehold, at der ikke må være erklæringer mellem goto og etiketten, som i:<br />

void f (int i) {<br />

if (i > 0) goto ok; // fejl, kan ikke hoppe over...<br />

char* s = "Error"; // ...denne linie<br />

cout


2.7.5 switch/case<br />

Ofte har et program brug for en flerledet if/else, især i situationer, hvor en<br />

tilstandsvariabel er i fokus. For eksempel,<br />

if (a == 1)<br />

// ... behandl kode for a == 1<br />

else if (a == 2)<br />

// ... behandl kode for a == 2<br />

else if (a == 3)<br />

// ... behandl kode for a == 3<br />

else<br />

// ... hvis a er hverken 1, 2 eller 3<br />

I sådanne tilfælde vil en switch-struktur både være mere læsbar og generere hurtigere kode<br />

gennem specielle tabeller i målkoden:<br />

switch (a) {<br />

case 1: // behandl kode for a == 1<br />

break;<br />

case 2: // behandl kode for a == 2<br />

break;<br />

case 3: // behandl kode for a == 3<br />

break;<br />

default: // hvis a er hverken 1, 2 eller 3<br />

break;<br />

}<br />

Der er et par vigtige forskelle mellem if/else og switch/case:<br />

• Det er nødvendigt at slutte hver case med en break for at forlade switchstrukturen<br />

og fortsætte efter den afsluttende Tuborg-klamme.<br />

• Udtrykket efter en case skal være en konstant. Det er derfor ikke muligt at skrive<br />

case b:, hvis b er en variabel af typen int; i så tilfælde må en flergrenet<br />

if/else benyttes.<br />

• Koden efter default køres hvis ingen af case'erne passer til værdien af den<br />

variabel, der ligger i switch'en. default er således ækvivalent til den sidste<br />

else i en flergrenet if/else-kontrolstruktur.<br />

• To case'r med samme konstant kan ikke indeholdes i samme switch-struktur.<br />

66 Kontrolstrukturer 2.7


• Det er muligt at udføre koden under flere case'r i en switch-sætning, ved<br />

udeladelse af nøgleordet break.<br />

break bruges i switch/case-strukturer til at slutte processeringen for en enkelt case.<br />

Udelades break fortsætter programmet betingelsesløst i den næste case-indgang uden at<br />

foretage nogen sammenligning med dennes værdi. Dette er i visse tilfælde en brugbar detalje,<br />

hvis gensidig udelukkelse af de individuelle case-elementer ikke kræves:<br />

switch (allokering) {<br />

case '3': oprydning3 ();<br />

case '2': oprydning2 ();<br />

case '1': oprydning1 ();<br />

}<br />

Denne switch-sætning tænkes at rydde op i lageret ved endt kørsel af et program.<br />

Variablen allokering indeholder antallet af forskelige lager-allokeringer, der er foretaget<br />

ved opstart. En lineær, revers oprydning i en switch-sætning uden break-sætninger vil<br />

kalde det korrekte antal oprydnings-funktioner. Manglende break-sætninger i slutningen af<br />

case-indgange er typiske kilder til fejl og er derfor et udmærket sted at beynde en<br />

fejlfindingssession.<br />

Det er god praksis også at slutte den sidste case (eller default) med en break.<br />

2.8 DATASTRUKTURER<br />

En datastruktur er en eller flere forekomster af fundamentale eller afledte typer samlet under<br />

et enkelt navn. Fordelen ved en sådan samling er, at en struktur kan behandles som en entitet,<br />

hvorved de enkelte elementer kan ignoreres indtil de skal bruges. Strukturer hjælper også med<br />

organiseringen af data, fordi konceptuelt sammenhængende data kan samles under ét navn og<br />

således er lettere at overskue.<br />

En typisk C++-datastruktur, der definerer objekter vedrørende et koordinat, ser således ud:<br />

struct Koordinat { // ny type hedder Koordinat<br />

int x, y; // og indeholder to int'er<br />

};<br />

Denne definition resulterer ikke i en forekomst af strukturen Koordinat, det er blot en<br />

definition af strukturen, som kan bruges som en ny type. Forekomster af Koordinat kan<br />

specificeres på følgende måde:<br />

Koordinat startPunkt, endePunkt; // to forekomster<br />

C++ vil behandle den ny struktur på samme måde som de indbyggede, fundamentale typer. I<br />

2.7.5 switch/case 67


tildelinger, parameteroverførsler, samt returværdier fra funktioner, hvor variable af typen<br />

koordinat er impliceret, vil oversætteren melde fejl, hvis typerne ikke stemmer overens.<br />

Objekter af forskellige typer kan ikke stå i samme udtryk (medmindre der er defineret<br />

specielle konverteringsregler forinden, se afsnit 3.6):<br />

Koordinat a;<br />

int b = a; // fejl, b er ikke et Koordinat<br />

Indholdet af en ny type kaldes medlemmer af typen. x og y er således medlemmer af typen<br />

Koordinat. Navne på medlemsdata er lokale til de strukturer, de defineres i - deres skop<br />

ligger i de navne, som forekomster af strukturen har. Således kan flere strukturer indeholde<br />

data af same navne:<br />

struct Koordinat2D {<br />

int x, y;<br />

};<br />

struct Koordinat3D {<br />

int x, y, z;<br />

};<br />

uden en navnekonflikt opstår mellem medlemsvariablene. Det er tilladt at erklære en<br />

forekomst af en datastruktur i selve definitionen på strukturen:<br />

struct Koordinat {<br />

int x, y;<br />

} cursorPosition;<br />

er det samme som:<br />

struct Koordinat {<br />

int x, y;<br />

};<br />

Koordinat cursorPosition;<br />

Anonyme datastrukturer, dvs. strukturer, der kun findes som objekt og ikke som type,<br />

behandles i afsnit 2.8.3.<br />

2.8.1 Medlemsreferencer<br />

De enkelte medlemmer i en struktur kan refereres gennem de to medlemsreference-operatorer<br />

. og ->, for henholdsvis forekomster af strukturen og pointere til forekomster. I følgende<br />

68 Datastrukturer 2.8


funktion findes arealet af et rektangel, defineret ved to absolutte punkter:<br />

int Areal (Koordinat a, Koordinat b) {<br />

int laengde = b.x - a.x;<br />

int hoejde = b.y - a.y;<br />

return laengde * hoejde;<br />

}<br />

Strukturerne tager form som brugerdefinerede datatyper, når de bruges som parametre, som<br />

returværdier og i tildelinger:<br />

Koordinat delta (Koordinat a, Koordinat b) {<br />

Koordinat c;<br />

c.x = b.x - a.x, c.y = b.y - a.y;<br />

return c;<br />

}<br />

Pointere til forekomster af strukturer kræver en anden syntaks ved medlemsreferencerne:<br />

Koordinat a; // forekomst af en koordinat<br />

Koordinat* b; // pointer til koordinat<br />

b = &a; // b skal pege på a<br />

b->x = 100, b->y = 50; // referér *b's medlemsdata<br />

Læg mærke til, at b->x er ækvivalent til (*b).x og blot letter syntaksen.<br />

Det er generelt bedre at bruge pointere eller referencer til strukturer under funktionskald, da<br />

langt mindre data skal passere fra kalderen til den kaldte funktion. En stor datastruktur (for<br />

eksempel 1000 bytes) skal kopieres ved hvert funktionskald, hvis ikke pointer-variable<br />

bruges.<br />

2.8.2 Bitfelter og union'er<br />

Datastrukturer har ofte megen spildplads, der opstår, fordi en eller flere medlemsvariable<br />

gensidigt udelukker hinanden, eller fordi båndbredden på en bestemt variabel er mindre end<br />

den mindste fundamentale type. Hertil har C++ to muligheder for at spare på pladsen, nemlig<br />

bit-felter og union'er.<br />

Bit-felter fortæller oversætteren, at en bestemt medlemsvariabel kræver et bestemt antal<br />

bits. Flere bit-felter vil kunne pakkes sammen i en enkelt fundamental type:<br />

struct Flag {<br />

unsigned farve : 3; // 8 forskellige farver<br />

unsigned blink : 1; // blink til/fra<br />

2.8.1 Medlemsreferencer 69


unsigned fed : 1; // fed skrift til/fra<br />

unsigned kursiv : 1; // kursiv skrift til/fra<br />

unsigned unders : 1; // understreg til/fra;<br />

};<br />

Denne struktur definerer en "attribut" på skærmen, nemlig fremtoningen (farve, skriftform<br />

osv.) af teksten. Sammenlagt behøves kun 7 bit, som kan normalt kan indeholdes i en char.<br />

Det skal bemærkes, at brugen af bit-felter er en smule langsommere end brugen af rene<br />

fundamentale typer, fordi de forskellige bits implicit hentes ud af en sådanne (en bit er ikke en<br />

datatype på de gængse processortyper).<br />

En union bruges til at dele en fysisk lagerplads mellem flere forskellige data-elementer.<br />

Oversætteren vil allokere plads til det største af elementerne, og afhængig af referencen vil<br />

den korrekte type blive returneret. Forskellen til en struct er blot, at alle medlemmer har<br />

samme adresse. En union kunne tænkes at repræsentere en landekode, som enten kan<br />

beskrives som en int (45 for Danmark, 1 for USA osv.) eller som to char's (DK for<br />

Danmark, US for USA osv.). Syntaksen er som følger:<br />

union landekode {<br />

int nummer;<br />

char ident [2];<br />

};<br />

En forekomst af landekode vil kunne benyttes således:<br />

landekode land;<br />

land.nummer = 45;<br />

int l = land.nummer; // her bruges land som en int<br />

land.ident [0] = 'D';<br />

land.ident [1] = 'K';<br />

char* m = land.ident; // og her som en char*<br />

Det er programmørens ansvar, at det aktuelle indhold af en union bruges som det skal. Da<br />

samme fysiske lager deles af de forskellige typer, kan problemer let opstå:<br />

land.ident [0] = 'E', land.ident [2] = 'F';<br />

int land.nummer = land.ident; // helt forkert resultat (!)<br />

Indholdet af land er aktuelt et nummer, og en reference til ident vil give et udefineret<br />

resultat. Oversætteren kan ikke checke for fejl i denne sammenhæng, så pas på.<br />

2.8.3 Anonyme strukturer<br />

70 Datastrukturer 2.8


Definitioner af struct og union kan være anonyme, dvs. erklæres uden et typenavn. For<br />

struct'er gælder det, at strukturen skal følges af en erklæring af en forekomst af strukturen,<br />

idet den ellers ikke vil kunne bruges til noget:<br />

struct {<br />

int x, y;<br />

} koordinat;<br />

Den ovenstående datastruktur findes kun som en enkelt forekomst, og kan ikke erklæres igen.<br />

Det er ikke en teknik, der bruges ofte, men kan være en hjælp til at organisere globale data i<br />

moduler, når der kun skal være en enkelt forekomst af disse data. Dermed mindskes risikoen<br />

for navnekonflikter:<br />

struct {<br />

unsigned FPU_status;<br />

int monitor_type;<br />

int no_of_disks;<br />

unsigned heap_size;<br />

unsigned env_size;<br />

} globals;<br />

De forskellige medlemmer af den anonyme struktur refereres på normal vis med medlemsreference-operatoren.<br />

union'er kan også være anonyme, men kan være det uden både typenavn og samtidig<br />

erklæring af en forekomst. Programmet har adgang til medlemmerne i en anonym union<br />

uden brug af medlemsreferencer:<br />

union {<br />

char char_buffer [100];<br />

int int_buffer [100];<br />

long long_buffer [100];<br />

};<br />

De tre medlemmer af denne union vil have samme startadresse i lageret, men vil kunne<br />

bruges uden henvisning via en bestemt forekomst. Det er en god ide at erklære anonyme<br />

union'er, hvis forskellige variable (specielt vektorer) aldrig bruges på samme tid, fordi det<br />

sparer lagerplads. Det er dog tilladt at erklære en eller flere forekomster af en anonym<br />

union efter definitionen, hvorved de samme regler gælder som for anonyme struct'er.<br />

Anonyme struct'er finder også anvendelse i union'er, hvis en union skal indeholde<br />

mere komplicerede blandinger af variable. For eksempel kan vi tænke os en union, som<br />

indeholder en brøk. Brøken kan beskrives som enten en double, to int til tæller og<br />

nævner eller som en long som den reciprokke værdi af tallet:<br />

union Broek {<br />

2.8.3 Anonyme strukturer 71


double v1;<br />

struct { int v2, v3; }; // enkelt medlem af broek<br />

long v4;<br />

};<br />

Opremsninger, enums, kan også være anonyme, efter samme regler som struct'er:<br />

enum { black, red, blue, green = 4 } Palette;<br />

Forekomsten palette er af en unavngiven type, men kan tildeles og sammenlignes med<br />

værdierne black, red, blue og green.<br />

2.8.4 Indlejrede strukturer<br />

Strukturer kan indeholde andre strukturer som medlemmer, hvis abstraktionen søges hævet et<br />

eller flere niveauer:<br />

struct Polygon { // et sæt af koordinater<br />

struct Koordinat {<br />

int x, y;<br />

} k [10]; // Polygon indeholder 10 Koordinater<br />

};<br />

Når indlejrede strukturer skal anvendes, benyttes reference-operatoren i flere gange i samme<br />

sætning:<br />

Polygon p;<br />

p.k [3].x = 5; // det 3. koordinat i p<br />

Det er meget brugbart at "indkapsle" strukturer i andre strukturer, og det giver mening af gøre<br />

de indre strukturer anonyme. Læg imidlertid mærke til, at hvis en struktur, som er indeholdt i<br />

en anden struktur, ikke er anonym, vil typenavnet være kendt også udenfor den ydre struktur<br />

(se afsnit 3.3.9 om objekt-baseret og type-baseret indkapsling). For strukturens typenavn er<br />

det altså ligegyldigt, hvor strukturen er defineret - det er mest et spørgsmål om logisk at samle<br />

associerede strukturer i kildeteksten.<br />

Når vi for alvor blander struct, union og enum sammen, giver det stor fleksibilitet.<br />

Anonyme union'er har stor anvendelighed indeholdt i andre struct'er eller union'er,<br />

idet den ydre struktur kan have forskelligt indhold afhængig af brugen. En struct<br />

indeholdt i en union tillader, at for eksempel en long og to int'er deler lager:<br />

struct Figur {<br />

enum { // anonym opremsning<br />

Cirkel, Firkant, Linie<br />

72 Datastrukturer 2.8


} Slags; // medlemsvariabel<br />

struct Koordinat { // indlejret struktur<br />

int x, y;<br />

} startPunkt; // medlemsvariabel<br />

union { // anonym union<br />

int Radius; // medlemsvariabel<br />

struct { // anonym struct<br />

int Bredde, Hoejde; // to medlemsvariable<br />

} Dimensioner; // medlemsvariabel<br />

Koordinat endePunkt; // medlemsvariabel<br />

};<br />

};<br />

Denne lidt komplicerede struct Figur indeholder en optælling af figurtyper (Slags),<br />

en erklæring af et typenavn (Koordinat), en forekomst af Koordinat og en anonym<br />

union. Den anonyme union indeholder igen en int, to kombinerede int'er i en<br />

anonym struct (Dimensioner) samt endnu en forekomst af Koordinat (hvilket er<br />

grunden til, at denne ikke var anonym). Forekomster af Figur kan repræsentere enten en<br />

cirkel, i hvilket tilfælde den arbejder med en radius, en firkant, i hvilket tilfælde den arbejder<br />

med en vidde og en bredde eller en linie, i hvilket tilfælde den skal bruge et endepunkt. Her er<br />

et par eksempler på brugen af Figur:<br />

// en vindues-figurtype...<br />

Figur Vindue;<br />

Vindue.Slags = Firkant,<br />

Vindue.startPunkt.x = 100,<br />

Vindue.startPunkt.y = 100,<br />

Vindue.Bredde = 64,<br />

Vindue.Hoejde = 32;<br />

// en panel-figurtype...<br />

Figur Panel;<br />

Panel.Slags = Linie,<br />

Panel.startPunkt.x = 100,<br />

Panel.startPunkt.y = 90,<br />

Panel.endePunkt.x = 164,<br />

Panel.endePunkt.y = 90;<br />

// en håndtags-figurtype...<br />

Figur Haandtag;<br />

Haandtag.Slags = Cirkel,<br />

Haandtag.startPunkt.x = 100,<br />

Haandtag.startPunkt.y = 116,<br />

Haandtag.Radius = 4;<br />

2.8.4 Indlejrede strukturer 73


2.8.5 Funktioner som medlemmer<br />

Medlemmer af strukturer kan være både dataobjekter og funktioner. Faktisk er en funktion et<br />

objekt i den forstand, at det er muligt at tage adressen på den. Da C++ udvider konceptet om<br />

medlemmer til også at omfatte funktioner betyder det, at vi kan indkapsle funktioner, der<br />

arbejder på strukturens data i selve strukturen. For eksempel,<br />

struct Cirkel { // en cirkel-type<br />

Koordinat midtPunkt; // center for cirklen<br />

int Radius; // radius<br />

int Diameter () { // diameter-medlemsfunktion<br />

return Radius * 2;<br />

}<br />

};<br />

Funktionen Diameter() er i dette tilfælde en medlemsfunktion under strukturen<br />

Koordinat. Diameter() kaldes udefra med samme syntaks som medlemsvariablene<br />

refereres, nemlig med reference-operatorerne. Diameter() har selv adgang til medlemsvariable<br />

i sin "egen" struktur uden brug af reference-operatorer. Et eksempel på brugen af<br />

Cirkel:<br />

Cirkel c;<br />

c.midtPunkt.x = 100, c.midtPunkt.y = 100, c.Radius = 50;<br />

int d = c.Diameter ();<br />

Den umiddelbare fordel ved medlemsfunktioner er den samme som for medlemsdata,<br />

nemlig at samle alle funktioner, der direkte manipulerer de indre variable under ét og samme<br />

tag. Det giver en bedre anledning til brugen af brugerdefinerede datatyper, at både data og<br />

kode kan samles i typen. Alle referencer sker gennem en forekomst af typen, i stedet for at<br />

bruge forekomsten som parameter i et funktionskald.<br />

Funktioner som medlemmer af datastrukturer er fundamentet for arbejdet med og<br />

implementationen af abstrakte datatyper, og diskuteres i detaljer gennem <strong>kapitel</strong> 3.<br />

2.8.6 Fremad-referencer<br />

Der gælder de samme regler for referencer til datastrukturer som for referencer til funktioner:<br />

de skal erklæres før de kan bruges, fordi C++ oversætter kildetekst til objektkode i et enkelt<br />

gennemløb. Fremad-referencer til datastrukturer er nødvendige, hvis selve definitionen af<br />

strukturen, hvor medlemmerne erklæres, er placeret fysisk senere i kildeteksten end brugen af<br />

74 Datastrukturer 2.8


strukturen. Et eksempel, hvor det er helt nødvendigt at bruge en fremad-reference er i de<br />

tilfælde, hvor to strukturer gensidigt henviser til hinanden:<br />

struct Liste; // fremad-reference til struct Liste<br />

struct Haegte { // en simpel hægte<br />

Haegte* Naeste; // pointer til næste hægte<br />

Liste* Ejer; // pointer til liste som "ejer" hægten<br />

int Data; // hægtens data-element<br />

};<br />

struct Liste { // en simpel hægtet liste<br />

Haegte* Rod; // pointer til første hægte<br />

int Antal; // antallet af hægter i listen<br />

};<br />

Vi ser, at Haegte har en pointer til en forekomst af Liste samt at Liste har en pointer<br />

til en forekomst af Haegte. Derfor skal en af de to strukturer (her Liste, fordi den<br />

defineres sidst) erklæres med en struct Liste før den bruges som type første gang.<br />

2.8.7 Dereference af medlemmer<br />

Operatorerne -> og ., som kort blev remset op i afsnit 2.5.6, kan nu forklares i forbindelse<br />

med datastrukturer. Vi husker, at en forekomst f, erklæret som pointer til type T, kan<br />

derefereres med *f, som giver en forekomst af typen T. Ligeledes, hvis f er erklæret som<br />

pointer til type T i strukturens a's skop, vil *a.f give en forekomst af typen T, fordi<br />

medlemsreference-operatoren har højere prioritet end dereference-operatoren. Dette følger<br />

således de normale regler:<br />

struct T {<br />

char* c;<br />

};<br />

T t;<br />

char d = *t.c; // c i t derefereres<br />

T* tptr = &t;<br />

char e = *t->c; // c i *tptr (også t) derefereres<br />

eller i flere niveauer:<br />

struct T {<br />

2.8.6 Fremad-referencer 75


T* next;<br />

char c;<br />

};<br />

T t1, t2; // to T-objekter<br />

t1.next = t2, t2.next = 0<br />

T* tptr = t1;<br />

t1->next->c = 'a';<br />

Her ses, hvordan reglen om, at udtrykket til venstre for en medlemsreferenceoperator skal<br />

være en struktur eller en pointer til en struktur giver mulighed for mangeledede indgange i<br />

indlejrede strukturer. Det er sjældent, at der er brug for mere end to-tre referencer til<br />

medlemmer i samme udtryk.<br />

Operatorerne .* og ->* benyttes også i forbindelse med datastrukturer. De fungerer på<br />

næsten samme måde som normale pointere til medlemmer, blot med den forskel, at de tager<br />

hensyn til de objekt-orienterede mekanismer som bliver introduceret i <strong>kapitel</strong> 4. De arbejder<br />

således på selve typen og ikke på objekter af typen, et forhold, der er meget væsentligt i<br />

forbindelse med arv og polymorfi. I afsnit 3.7 beskrives disse to operatorers funktion i<br />

detaljer. Her blot et eksempel på syntaksen:<br />

struct Y { // en enkel struktur<br />

char c;<br />

};<br />

char Y::*cptr = Y::c; // cptr peger på en char i Y<br />

// bemærk: ingen objekter endnu<br />

Y yobj; // erklær et Y-objekt<br />

char ch = yobj.*cptr; // c-medlemmet i yobj gennem cptr<br />

Syntaksen Y::*X, som benytter skop-opløsningsoperatoren, betyder, at objektet (her X)<br />

erklæres som pointer til medlem i strukturen Y, ikke objektet Y. Ved at holde generaliteten<br />

på strukturbasis kan objekterne udskiftes efter behag, en arbejdsmodel, der fremmer<br />

udviklingen af genbrugelig kode.<br />

2.9 LAGERADMINISTRATION<br />

Lageradministration handler om organisering, placering og lokalisering af data. I de fleste<br />

sprog, som benytter en separat oversættelse og lænkning, taler man om to typer af data,<br />

nemlig data med intern lænkning og data med ekstern lænkning. Intern lænkning betyder, at<br />

data ikke kan ses af andre objektmoduler end det aktuelle, dvs. at det slet ikke bliver<br />

videregivet til lænkeren. Ekstern lænkning betyder, at navnet på dataelementet står skrevet i<br />

76 Datastrukturer 2.8


objektfilen, og dermed kan ses og bruges af andre moduler.<br />

Princippet for valget af intern og ekstern lænkning er som følger: hvis en funktion eller et<br />

dataelement kun benyttes i den aktuelle kildefil, skal intern lænkning benyttes for at beskytte<br />

mod navnekonflikter i de andre moduler. Kaldes nogle funktioner eller refereres nogle data<br />

fra andre moduler, skal ekstern lænkning benyttes for at identificere de enkelte navne under<br />

lænkning. Hvis der er tale om globale funktioner (funktioner, som ikke er medlemmer af<br />

strukturer eller klasser), er de underforstået erklæret som eksterne, med mindre de erklæres<br />

static. I så tilfælde er de helt lokale til den aktuelle kildefil. Det samme gælder for globale<br />

data (som ikke er erklæret i en funktion eller datastruktur); de er kun lokale, hvis de erklæres<br />

statiske. For eksempel,<br />

// a.cpp<br />

int f () { /* ... */ }<br />

static int g () { /* ... */ }<br />

// b.cpp<br />

extern int f ();<br />

static void g () { int i = f (); }<br />

Når disse to filer oversættes, vil kun funktionen f() fra modulet a.cpp være synlig for<br />

andre objektfiler. Denne funktion kaldes fra b.cpp, som derfor kræver, at funktionen har<br />

ekstern lænkning. Erklæringen af f() som extern funktion i b.cpp betyder, at<br />

funktionen er implementeret i en anden kildefil, og først kan indentificeres ved lænkningen.<br />

Det er altså blot en prototype. Findes funktionen alligevel i den aktuelle kildefil, vil<br />

extern-nøgleordet blive ignoreret, og funktionen vil have extern lænkning. externnøgleordet<br />

er med andre ord underforstået for funktionsprototyper. For data skal nøgleordet<br />

specificeres, hvis dataelementet benyttes i den aktuelle fil, men er erklæret i en anden.<br />

Normalt organiseres kildefilerne, så deres tilsvarende headerfiler indeholder prototyper på<br />

alle funktioner og datastrukturer. Når både kildefilen selv og alle andre moduler, som kalder<br />

funktioner eller benytter datastrukturer fra denne, inkluderer samme headerfil, sikres<br />

konsistensen af funktionskaldene og datamanipulationerne, fordi de samme prototyper gør sig<br />

gældende.<br />

2.9.1 Lagringsklasser<br />

Ser vi bort fra intern og ekstern lænkning og koncentrerer os om en enkelt kildefil, foregår<br />

dataallokeringen på en af tre måder, enten automatisk, statisk eller dynamisk.<br />

• Automatiske variable er variable, som erklæres og har sine skop indenfor enkelte<br />

funktion eller blokke. Funktionen<br />

void f () {<br />

auto int a;<br />

2.9 Lageradministration 77


...<br />

}<br />

vil allokere plads til int'en a på det kørende programs stak, hver gang den kaldes,<br />

altså under programmets kørsel. Det er nødvendigt, da a skal være helt lokal til<br />

f(), især fordi f() kan være en rekursiv funktion (se afsnit 2.6.8). Det er derfor<br />

underforstået, at variable, der erklæres indenfor en blok, er automatiske. Nøgleordet<br />

auto før erklæringen er tilladt for læsbarhedens skyld, men er underforstået og<br />

unødvendigt.<br />

• Statiske variable er variable, som erklæres indenfor en funktion eller en blok, og<br />

bliver allokeret på oversættelsestidspunktet. En statisk erklæring foretages med<br />

nøgleordet static:<br />

void f () {<br />

static int a;<br />

// ...<br />

}<br />

Brugen af static på denne måde vil danne en enkelt int a for funktionen<br />

f(), uanset hvorfra f() kaldes. a vil huske sin værdi fra kald til kald, men er<br />

stadig lokal til f(). Statiske variable i funktioner eller blokke bruges til at holde<br />

værdier (for eksempel en indre status), som ikke må glemmes ved funktionens udløb.<br />

Globale variable, som erklæres udenfor funktioner og blokke, er også statiske<br />

variable. En global variabel vil kunne bruges i alle funktioner i alle moduler, med<br />

mindre variabelnavnet er omdefineret indenfor den pågældende funktion eller blok<br />

og med mindre den eksplicit er erklæret static, jvf. forrige afsnit. Brugen af<br />

globale variable skønnes generelt at være problematisk, fordi alle dele af programmet<br />

har tilgang til dem, og en eventuel fejl derfor er svær at lokalisere.<br />

2.9.2 new og delete<br />

Den sidste metode for lageradministration er dynamisk allokering og deallokering. Ved<br />

dynamisk allokering forstås, at programmet (og programmøren) er ansvarlig for selv at<br />

anmode om lager samt at frigive det til systemet igen, når det ikke længere skal bruges.<br />

Dynamisk allokering bruges, hvis programmet på oversættelsestidspunktet ikke kender til<br />

eksakte størrelser på de anvendte data. C++ har, i modsætning til forgængeren C, indbygget<br />

dynamisk lageradministration. Ved statisk lagerallokation (for eksempel en int tabel<br />

[100]) allokeres pladsen på oversættelsestidspunktet (100 int'er) og placeres i objektfilen.<br />

Hvis programmet på et tidspunkt har brug for 101 elementer, er det grumme svært at gøre<br />

noget ved det.<br />

I standard C findes et antal biblioteksrutiner for dynamisk allokering af lager, familien af<br />

78 Lageradministration 2.9


malloc()-funktioner samt de tilsvarende figivelsesfunktioner, free() og beslægtede.<br />

Disse er funktioner, som eksplicit skal kaldes med en forespørgsel på lagerstørrelsen, og som<br />

returnerer en pointer til det nyallokerede lager. I C++ er allokeringen (eller i hvert fald den<br />

sproglige side af den) som nævnt indbygget i selve sproget, via de to operatorer new og<br />

delete.<br />

new og delete allokerer plads i et særligt område, heap'en, og bruges i forbindelse<br />

med pointere. new skal følges af en type, og returnerer en pointer til den nyallokerede<br />

forekomst af typen. For eksempel,<br />

int* a; // en pointer til en int<br />

a = new int; // anmod systemet om en int<br />

// ...<br />

delete a; // frigiv lagerpladsen igen<br />

new kan følges af en vektor-operator, hvilket fortæller, at en vektor af en bestemt størrelse af<br />

den pågældende type søges allokeret:<br />

int* a; // en pointer til en int<br />

a = new int [100]; // anmod systemet om 100 int'er<br />

// ...<br />

delete a; // frigiv de 100 int'er igen<br />

Der er to faldgruber i forbindelse med new og delete, der er vigtige ikke at glemme. For<br />

det første er det vigtigt at huske at delete allokeret lager igen, ellers frigives det aldrig til<br />

systemet. Afhængig af operativsystemets struktur vil lagerpladsen ikke frigives før<br />

programmet afsluttes (små systemer), før hele systemet nedlukkes (mellemstore systemer)<br />

eller før der foretages en stor oprydning (garbage collection - store systemer). For det andet<br />

skal programmøren være ompærksom på, at den pointer, som adresserede det just frigivede<br />

lager ikke ændrer værdi ved delete. Pointeren er nu meget farlig at bruge indirekte, fordi<br />

den peger på udefineret lager - en såkaldt dangling pointer eller reference. Eftersom<br />

lagerpladsen er lagt tilbage i operativsystemets domæne, kan operativsystemet bruge det<br />

samme fysiske lager til andre formål. Manipuleres området alligevel, kan det lede til<br />

uforudsete resultater - i bedste fald en afslutning af applikationen. På mindre systemer vil hele<br />

maskinen sandsynligvis crashe, mens større systemer vil afbryde programmet med en<br />

reference-fejl, varetaget af operativsystemet.<br />

new og delete kan bruges til dynamisk allokering og frigivelse af alle typer samt<br />

vektorer, også af brugerdefinerede:<br />

Cirkel* a = new Cirkel [100];<br />

I denne sætning allokeres plads til 100 objekter af typen Cirkel.<br />

2.9.2 new og delete 79


Læg mærke til, at data, som er dynamisk allokeret, følger samme regler for intern og ekstern<br />

lænkning som data med andre typer af allokeringer. Er pointervariablen, som tildeles det<br />

allokerede lager, således erklæret som global variabel uden static-specifikationen, vil<br />

dette data via pointeren også kunne nås fra andre moduler.<br />

2.10 PRÆPROCESSERING<br />

Selv om C++-kildetekst normalt gennemløbes én gang af oversætteren, foregår det<br />

konceptuelt i flere faser. Den første fase, præprocesseringen eller forbehandlingen af koden,<br />

foretager forskellige statiske substitutioner, som ikke har syntaktisk eller semantisk relevans,<br />

men som blot letter programmeringsarbejdet. Præprocessoren tager ikke hensyn til skop, så<br />

kommandoer til præprocessoren har effekt fra det sted, de gives, til slutningen af den aktuelle<br />

oversættelse. De skal begynde med et #, kun eventuelt forudgået af mellemrum, og kan<br />

fortsættes på næste linie ved angivelse af en baglæns skråstreg, \.<br />

2.10.1 Inkludering af headerfiler<br />

Det oftest benyttede præprocesseringsdirektiv er det, der benyttes til inkludering af andre<br />

kildefiler, #include. Præprocessoren vil indsætte den angivne fil (normalt en headerfil) på<br />

det aktuelle sted i kildefilen, og gennemløbe denne før den videre oversættelse. Inklusioner<br />

kan foregå i flere niveauer, dvs. en fil, som #include's kan selv #include andre igen<br />

osv., normalt begrænset af filsystemets grænse for antallet af åbne filer. Der er to varianter:<br />

#include <br />

#include "filnavn"<br />

Den første af disse vil søge efter den angivne fil i en udvidet søgesti på filsystemet, oftest i et<br />

bibliotek eller katalog, hvor standard-headerfiler er placeret. Den anden vil blot se i det<br />

aktuelle katalog, dvs. der hvor den aktuelle kildefil befinder sig. Forskellen på de to er, at<br />

mange headerfiler skal deles mellem og bruges i mange applikationer, hvorfor de placeres i et<br />

fælles katalog. Headerfiler, som er specifikke til en bestemt applikation, findes i et lokalt<br />

katalog. Hvis filen med lokal søgning dog ikke findes, forsøges igen med den udvidede<br />

søgesti. Man kan sige, at headerfiler, som inkluderes med citationstegn, er mere implementationsafhængige<br />

end de, der inkluderes med klammer.<br />

2.10.2 Makroekspansioner<br />

En vigtig pointe i softwareudvikling er at sikre konsistens i kildeteksterne. Har man i et<br />

bilforhandler-program for eksempel valgt, at der skal findes 6 biltyper, er det vigtigt, at tallet<br />

6 bruges i al kode. Risikoen for, at man tager fejl eller glemmer dette tal, kan mindskes ved<br />

80 Lageradministration 2.9


ug af makroer:<br />

#define BILTYPER 6<br />

// ...<br />

for (i = 0; i < BILTYPER; i++) // ...<br />

// ...<br />

if (input > BILTYPER) // ...<br />

Og hvis vi pludselig finder ud af, at vi faktisk kun har 5 biltyper, er det blot at ændre tallet ét<br />

sted.<br />

Makroer finder langt mindre anvendelse i C++ end i C, fordi C++ (og ANSI C) har<br />

introduceret symbolske konstanter og inline-funktioner. Til gengæld har makroerne en anden<br />

fordel, nemlig at de bliver præprocesseret i forhold til resten af koden. Præprocessering af<br />

C++ kildetekst fortolker alle linier, som starter med # - det gælder også #include.<br />

Forskellige implementationer af C++-oversættere har forskellige andre præprocessordirektiver.<br />

Makroer defineres generelt således:<br />

#define navn indhold af makroen<br />

Her defineres en makro med navn navn til at indeholde indhold af makro. Makroens<br />

navn er det første ord efter #define, og makroens indhold er resten af linien (til '\n'). Hvis<br />

oversætteren efter makro-definitionen for eksempel støder på<br />

et sødt navn<br />

vil det af præprocessoren automatisk blive erstattet med<br />

et sødt indhold af makroen<br />

før selve oversætteren tager fat. Makroer kan også tage argumenter, som kan indgå i<br />

ekspansionen af indholdet:<br />

#define max(x,y) x > y ? x : y<br />

Makroen max skal have to argumenter, når den bruges i kildeteksten. Det er ikke en<br />

funktion; hele makroens "krop" vil blive indsat i kildeteksten umiddelbart før den oversættes.<br />

max vil ændres fra<br />

til<br />

a = max (i, j);<br />

a = i > j ? i : j;<br />

2.10.2 Makroekspansioner 81


Der er et par farlige fælder i forbindelse med makroer. Da de ikke er genstand for nogen form<br />

for semantisk analyse, kan der opstå mange fejl, der er svære direkte at se i programmet -<br />

netop fordi man "skjuler" den faktiske kode i en makro. Her er et eksempel på en makro, der<br />

ser helt tilforladelig ud, men som kan bruges helt forkert:<br />

#define kubik(i) i * i * i // giv kubiktallet på i<br />

int a = kubik (2); // ok, a bliver 8<br />

int b = kubik (a++); // b bliver sandsyndlgvis 710<br />

// og a ender med at blive 11<br />

Grunden til, at b ikke bliver 512 og a ikke 9 er, at udtrykket i 3. linie erstattes med<br />

int b = a++ * a++ * a++;<br />

Fælden er altså, at argumentet fremstår flere gange i makroen. Et andet eksempel involverer<br />

operator-præcedens:<br />

#define mul(a,b) a * b // multiplicér a med b<br />

int c = mul(5, 7); // ok, giver 35<br />

int d = mul(2 + 3, 4 - 5); // d bliver 9, ikke -5<br />

Her ligger fælden i, at man ikke kan se, hvad makroen erstatter koden med. Den sidste linie<br />

bliver nemlig erstattet med<br />

int d = 2 + 3 * 4 - 5;<br />

hvilket ikke fremgår tydeligt af et kald til mul. Husk derfor altid at omslutte makroargumenter<br />

med parenteser:<br />

#define mul(a,b) (a)*(b)<br />

Problemet med makroer er, at de bliver præprocesseret, og dermed erstatter koden allerede før<br />

C++-oversætteren ser koden. Det kan lede til fejl, som er svære at lokalisere - dels fordi en<br />

makro ikke altid opfører sig på samme måde, og dels, fordi de tillader en omskrivning af<br />

beskyttede navne i C++. Det er nemlig muligt at omdefinere den syntaktiske betydning af for<br />

eksempel int, case og void. En Pascal-fanatiker vil måske glæde sig over følgende<br />

makroer:<br />

#define BEGIN {<br />

#define END }<br />

#define THEN<br />

82 Præprocessering 2.10


hvilket vil gøre C++-programmer skrevet med disse makroer garanteret ulæselige for andre<br />

programmører. Brug makroer med fornuft, og lad være med at bruge dem, hvis det<br />

overhovedet er muligt - der er ofte bedre måder. Eksempler på brugbare makroer er:<br />

#define newline


2.10.3 Betinget oversættelse<br />

Betinget oversættelse betyder, at visse dele af kildeteksten skal springes over af oversætteren,<br />

og dermed helt ignoreres. Til dette formål findes de seks direktiver til præprocessoren if,<br />

ifdef, ifndef, elif, else og endif. De arbejder alle på konstante udtryk, som<br />

enten er definerede konstanter eller makroer.<br />

Et eksempel på brugen af disse direktiver er følgende fragment:<br />

#define DEBUG 1<br />

for (int i = 1; i < 1000; i++) {<br />

#if DEBUG<br />

cout


sandsynlig oversætterfejl i retning af "struktur complex allerede defineret". Med en<br />

betinget oversættelse i filen complex.h kan vi undgå dette problem:<br />

// fil: complex.h<br />

#ifndef COMPLEX_H<br />

#define COMPLEX_H<br />

struct complex { double r, i; };<br />

#endif<br />

hvorved hele headerfilen springes over, hvis navnet COMPLEX_H allerede er defineret,<br />

hvilket det første gennemløb af filen vil gøre. Læg mærke til, at det kun gælder for<br />

oversættelsen af det aktuelle modul. Et alternativ til #ifdef og #ifndef er den binære<br />

operator defined, som returnerer 0, hvis det efterfølgende navn ikke er defineret.<br />

Forskellen til de ovennævnte direktiver er, at denne kan kombineres i mere avancerede<br />

udtryk. For eksempel,<br />

#if defined (UK_VERSION)<br />

#define hello Welcome to PlanKalkül<br />

#elseif defined (DK_VERSION)<br />

#define hello Velkommen til PlanKalkül<br />

#elseif defined (FR_VERSION)<br />

#define hello Bienvenu a PlanKalkül<br />

#else<br />

#error SPROG IKKE DEFINERET<br />

#endif<br />

I dette eksempel ses også brugen af et særligt direktiv, #error, som standser oversætteren<br />

med en fatal fejlmeddelelse, givet efter direktivet.<br />

2.11 MERE OM POINTERE, VEKTORER OG REFERENCER<br />

Det er muligt at udvikle programmer, der ikke benytter de afledte typer *, & og []<br />

(pointer, reference og vektor), men det er hårdt arbejde og resulterer i langsomme<br />

programmer. Derfor er det vigtigt at forstå, hvordan de afledte typer fungerer i relation til de<br />

fundamentale typer samt til hinanden. Husk altid på, at en afledt type ikke i sig selv repræsenterer<br />

en fundamental type, men at den på en eller anden måde refererer en type. En god<br />

huskeregel er, at erklæringen på en afledt type syntaksmæssigt afspejler den senere brug af<br />

typen.<br />

2.11.1 Pointere<br />

2.10.3 Betinget oversættelse 85


Hvis T er en type, vil erklæringen T* skabe en pointer eller en "pegepind" til en T. Sagt på<br />

en anden måde vil en pointer-variabel indeholde adressen på en variabel af fundamental type.<br />

Den dybereliggende grund til at bruge pointere er, at de har en direkte parallel til CPU'ens<br />

adresseregistre - hemmeligheden bag C++'s effektivitet ligger ikke mindst i, at pointerarbejdet<br />

er meget naturligt for en computer. En dreven C++-programmør vil ofte strukturere sit<br />

program med pointere, fordi de er mere effektive i målkoden.<br />

En pointer erklæres således † :<br />

int* t; // t er en pointer til en int<br />

t kan nu bruges som indirekte reference til enhver variabel af typen int. Dette kaldes også<br />

dereference, og gøres med en fortegns-stjerne (*):<br />

int i = 5; // i er en int, initér med 5<br />

int* t = &i; // t er en int-ptr, ininiér med i's adresse<br />

int j = *t; // j er en int, initér med den int, som<br />

// t peger på (samme som i, altså 5)<br />

Af ovenstående tre sætninger ses, at initiering af en pointer skal ske med en adresse. Dette<br />

gøres med den dertil indrettede adresse-på operator (&) eller med tildeling fra andre pointere.<br />

Når en adresse på et objekt tildeles en pointervariabel siger vi, at vi tager adressen på<br />

objektet. Ligeledes ses det, at begrebet "en adresse" ikke skal tages så højtideligt, som<br />

posvæsenet gør det. Den faktiske værdi, som t indeholder (adressen på i), er faktisk<br />

uinteressant. Det interessant er, at den peger på i. Som programmører kan vi faktisk være<br />

ligeglade med, hvor i befinder sig.<br />

En pointer kan også indeholde adressen på andre pointere:<br />

int i = 5; // i er en int, initieres med 5<br />

int* t = &i; // t er en pointer til en int<br />

int** v = &t; // v er en pointer til en pointer til en int<br />

int j = **v; // j initieres med 5<br />

Det ses, hvordan erklæringen og dereferencen af v syntaksmæssigt afspejler hinanden.<br />

Pointere til pointere er et ikke sjældent fænomen, og kan iøvrigt udvides til flere<br />

indirektioner.<br />

2.11.2 Pointeraritmetik<br />

Der er ikke meget sjov ved pointerne, hvis de ikke kan gøres til genstand for manipulation.<br />

Det er tilladt at bruge addition, subtraktion samt op- og nedtællingsoperatorerne i forbindelse<br />

† C-programmører vil her opdage en mindre ændring i de uskrevne kosmetiske regler for kildetekstens udseende.<br />

Pointer-operatoren * binder sig i C++ per definition til typen, og ikke til variablen, hvor det i C er omvendt. Det er<br />

ingenlunde et krav fra C++-standarden, at denne syntaks skal følges, men det er mere klart at selve variabelnavnet<br />

står frit - int* x siger, at x er en int-pointer. Det er selvfølgelig stadig syntaktisk lovligt at skrive int *x.<br />

86 Mere om pointere, vektorer og referencer 2.11


med pointere. Netop dette er en syntaksmæssig faldgrube i C++, fordi det er svært at se, om<br />

det er en direkte eller en indirekte operation, der er tale om.<br />

Hvis p er en pointer til en type T, vil udtrykket<br />

p++;<br />

sætte p lig adressen på det næste element, der følger umiddelbart efter det aktuelle i<br />

maskinens adresserum. Det er programmørens ansvar, at dette element er defineret, hvilket<br />

oftest sikres gennem vektor-erklæringer. Den skalære værdi, der lægges til pointeren,<br />

afhænger kun og helt af størrelsen på T, dvs. den type, p peger på. Det er altså ikke tallet 1,<br />

som adderes i et lineært adresserum, men det antal maskin-ord, som den pågældende type T<br />

optager. Dette antal er maskinafhængigt, fordi C++ som beskrevet i afsnit 2.3 kun garanterer<br />

forholdet mellem de fundamentale typer, og ikke absolutte størrelser. Men programmøren<br />

skal ikke bekymre sig om størrelserne på int, double osv., idet oversætteren altid har den<br />

nødvendige information om maskinens natur, og vil allerede på oversættelsestidspunktet<br />

generere den korrekte værdi for additionen. Det skal dog nævnes, at addition af pointere<br />

normalt ikke er tilladt, fordi den faktiske værdi, en pointer har, i realiteten enten er relativ til<br />

et eller andet punkt i lageret under oversætterens kontrol eller er en absolut adresse i lageret<br />

afhængig af maskinarkitekturen. Udtryk som<br />

char* p, *q;<br />

// ...<br />

char* r = p + q; // fejl: hvad menes med dette?<br />

er derfor ulovlige. Kun heltalskonstanter kan adderes til pointere. Pointeraritmetik benyttes<br />

ofte i forbindelse med vektorer, fordi vektorerne automatisk giver os lineære lister af bestemte<br />

typer, som pointerne kan arbejde i.<br />

2.11.3 Vektorer<br />

Undertiden er det nødvendigt at arbejde med lister af ensartede fundamentale typer. Overvej<br />

følgende problemstilling: 100 måleværdier er opsamlet, og skal bruges i en statistisk beregning.<br />

I stedet for at holde de 100 værdier i 100 separate variable, bruges en vektor af den<br />

pågældende type:<br />

double data [100]; // erklær 100 double'r<br />

Objektet data har her typen "vektor af 100 doubler", og indeholder ligesom en pointer<br />

adressen på det første element i data. Tilgang til de enkelte elementer i vektoren foretages<br />

med følgende syntaks:<br />

a = data [5]; // det 5 (egt. 6.) element i vektoren,<br />

// det samme som a = *(data + 5)<br />

2.11.2 Pointeraritmetik 87


En tilgang til et element i en vektor kaldes en indeksering, og indekset på en vektor af<br />

størrelse n løber fra 0 til n-1. For vektoren data er de lovlige indeks-værdier altså fra 0 til<br />

99 inklusive (100 ialt). Hvis vi vil beregne middelværdien af de opsamlede data, givet ved<br />

m=<br />

n<br />

∑<br />

i=1<br />

kan vi indeksere os gennem vektoren i en løkke:<br />

data<br />

n<br />

const n = 100; // antal elementer<br />

double m = 0; // resultatet initieres med 0<br />

for (i = 0; i < n; i++) // i løber fra 0 til n - 1<br />

m += data [i] / n; // addér elementet over antallet<br />

Vektorer tillader altså ligesom pointere en generalisering af klumper af data. Vektoren data<br />

kaldes en endimensionel liste, fordi den repræsenterer en enkelt række af tal. Det er, som med<br />

pointere, muligt at bruge vektorer af vilkårlige dimensioner:<br />

int matrix [4] [4]; // 4 x 4 int'er<br />

Det er ofte en hjælp at bruge række/søjle-terminologi i to- eller flerdimensionale vektorer.<br />

matrix har her 4 rækker med 4 søjler af int's. Læg mærke til, at de enkelte vektorstørrelser<br />

for hver dimension står i en separat klamme; de skal ikke separeres af et komme<br />

indenfor en enkelt klamme. Kommaet bruges, som beskrevet i afsnit 3.3, til sekvensering.<br />

Indeksering i en flerdimensionel vektor følger samme syntaks som erklæringen af en sådan:<br />

int a = matrix [3] [1]; // 3. række, 1. søjle<br />

Beregningen af adressen på det pågældende element foretages af oversætteren. Den faktiske<br />

fundamentale variabel, som skal udledes af en indeksering, findes ved at gange indekset med<br />

størrelsen på den fundamentale type, og addere dette til adressen på det første element i<br />

vektoren. Resultatet kaldes en effektiv adresse, og udregnes hver gang, en indeksering foretages.<br />

Selvom vektorerne automatisk kan indekseres og allokeres med de rigtige størrelser, har<br />

C++ ingen indbyggede funktioner til manipulation af vektorerne selv. Derfor er det ikke<br />

muligt at skrive<br />

double matrix [4] [4]; // ok, 4 x 4 int's<br />

double kopi [4] [4] = matrix; // fejl, kan ikke tildele<br />

88 Mere om pointere, vektorer og referencer 2.11<br />

i


for at kopiere en vektor til en anden. Her spiller pointere og vektorer sammen på en måde, der<br />

bedst kan forklares ved, at en vektorerklæring i realiteten er en pointer, når den bruges i et<br />

udtryk. Idet en pointervariabel ikke kan tildeles til en vektor er ovenstående udtryk ikke<br />

korrekt. Typen på matrix forstås af oversætteren som en double*[]. Skal vektorer<br />

kopieres, må det enten ske manuelt element for element, som i<br />

for (i = 0; i < 4; i++) // rækkerne<br />

for (j = 0; j < 4; j++) // søjlerne<br />

kopi [i] [j] = matrix [i] [j]; // kopiér (i,j)<br />

eller ved hjælp af en funktion i standardbiblioteket, der kopierer lageret lineært i en mundfuld,<br />

som i sætningen<br />

memcpy (kopi, matrix, sizeof (double) * 4 * 4);<br />

som typisk er implementeret i højoptimeret form. Der henvises til <strong>kapitel</strong> 6, hvor de<br />

væsentligste dele af standardbiblioteket gennemgås, blot er det vigtigt at huske på, at den<br />

fysiske størrelse (i bytes eller maskinord) af en vektor afhænger af den type, den er<br />

komponeret af. En char-vektor af n elementer er således typisk meget mindre end en<br />

float-vektor af n elementer. Her kan sizeof-operatoren, som det sker ovenfor, anvendes<br />

til maskinuafhængig determinering af størrelsen på vektoren - se afsnit 2.5.6.<br />

2.11.4 Initiering af vektorer<br />

Som beskrevet indeholder C++ ingen indbyggede metoder til manipulation af vektorer. Det er<br />

dog muligt at initiere en vektor med start-værdier under erklæringen. Syntaksen varierer en<br />

smule, og afhænger af den fundamentale type, som vektoren er afledt af:<br />

char a [2] = { 'D', 'K' };<br />

int tabel [6] = { 2, 4, 8, 16, 32, 64 };<br />

double piSerie [3] = { 3.1415, 6.2831, 9.4247 };<br />

Det er ikke lovligt at foretage initiering efter en vektor allerede er erklæret:<br />

char a [2];<br />

a = { 'D', 'K' }; // fejl, kan ikke kopiere<br />

For vektorer af typen char er det muligt at initiere med en tekst-streng omsluttet af<br />

anførselstegn. Dette er igen kun lovligt under erklæringen af vektoren, og kan illustreres med<br />

følgende sætninger:<br />

char a [4] = "OOP"; // ok: a -> 'O','O','P','\0'<br />

2.11.3 Vektorer 89


char b [4];<br />

b = "OOP"; // fejl, kan ikke tage adresse<br />

// af literal, ejheller kopiere<br />

Husk, at en streng i C++ altid ender i et "usynligt" nul, altså den faktiske værdi '\0'. Derfor<br />

er vektoren a ovenfor erklæret til længden 4, selvom kun initieres med 3 char. Nulterminerede<br />

strenge kaldes undertiden ASCIIZ-strenge (for ASCII-Zero), og det understreges,<br />

at dette kun er en konvention i sproget. Programmøren kan til alle tider lade strenge slutte<br />

med andre værdier, eller huske længden på den bestemte streng, men standardbiblioteket i<br />

C++ samt den indbyggede initeringsfunktion til char-vektorer forudsætter ASCIIZ. Også på<br />

systemer med andre tegnsæt er nul-terminering mest anvendt.<br />

Det er dog ikke nødvendigt at specificere størrelsen på en vektor, hvis en tildeling kommer<br />

umiddelbart efter. Oversætteren vil selv tælle de pågældende elementer og allokere det<br />

nødvendige lager:<br />

char a [] = "OOP"; // længde 4<br />

int primtal [] = { 1, 2, 3, 5, 7, 11, 13 }; // længde 7<br />

Multidimensionale vektorer initieres med samme syntaks, dog skal for hver ekstra dimension<br />

tilføjes ekstra Tuborg-klammer udenom (læg mærke til hvor der er, og ikke er, kommaer):<br />

char a [4] [4] = { // fire rækker á fire søjler<br />

{ 1, 2, 3, 4 }, // første række<br />

{ 2, 4, 6, 8 }, // anden<br />

{ 3, 6, 9, 12 }, // tredie<br />

{ 4, 8, 12, 16 } // fjerde<br />

};<br />

2.11.5 Vektorer og pointere<br />

Mens man i visse tilfælde kan bruge vektorer uden pointere, er det svært at jonglere med<br />

pointere uden samtidig at arbejde med vektorer. Pointeraritmetik forudsætter defineret lager,<br />

som vektoren tilbyder i erklæringen. Pointere og vektorer spiller sammen på følgende<br />

nøglepunkter:<br />

• En pointererklæring er "tom" i den forstand, at den kun fortæller oversætteren om<br />

den indirekte reference. Vektoren, derimod, tillader allokering af de indirekte data,<br />

som får en plads i lageret.<br />

• Pointerens værdi (adressen på det indirekte element) kan ændres dynamisk i et<br />

programforløb (pointeraritmetik, afsnit 2.11.2), mens en vektor er statisk fra<br />

erklæringsøjeblikket.<br />

90 Mere om pointere, vektorer og referencer 2.11


• Pointeren tillader indeksering vha. []-operatoren i samme stil som vektoren. Uanset<br />

hvor en pointer peger hen, vil en indeksering med n finde det n'te element fra<br />

pointerens værdi.<br />

• I en pointer-erklæring er en initiering tilladt med samme syntaks som i en vektorerklæring:<br />

char* p = "C++"<br />

• En pointer til type T kan uden videre tildeles en vektor af type T, hvorved<br />

pointeren peger på det første element i vektoren. Det omvendte er ikke tilfældet, da<br />

en vektor er statisk.<br />

• Operatorerne new og delete blander pointeren og vektoren i en sætning for<br />

henholdsvis resultat og udtryk.<br />

Vektor-indekseringer erstattes ofte med pointere og udtryk med pointeraritmetik for at øge<br />

effektiviteten af et program. Som beskrevet under i afsnit 2.11.13 vil en indeksering i en<br />

vektor medføre en underforstået beregning af en effektiv adresse. En pointer-variabel<br />

indeholder i sig selv en effektiv adresse, og med pointeraritmetik kan programmøren selv<br />

kontrollere denne adresse dynamisk. Eksemplet med middelværdien kan således omskrives til<br />

const n = 100; // antal elementer<br />

double m = 0; // resultatet initieres med 0<br />

double* p = data; // p peger på vektoren<br />

for (i = 0; i < n; i++) { // i løber fra 0 til n - 1<br />

m += *p / n; // addér elementet over antallet<br />

p++; // næste element<br />

}<br />

For hver iteration gennem løkken bruges de successive elementer i vektoren uden at en<br />

indeksering skal foretages for hvert gennemløb. p++ vil opdatere p til at pege på det næste<br />

element i serien. Det er muligt at blande dereferencer med pointeraritmetik, sålænge der ikke<br />

forekommer tvetydigheder. Den centrale sætning i ovenstående fragment er<br />

m = *p++ / n;<br />

hvor p bliver opdateret efter dereferencen til det aktuelle element i data. Det er altså p,<br />

der er genstand for optællings-operatoren, og ikke den double, der ligger på adressen p.<br />

Operatorernes præcedens placerer ++ over *, så hvis vi vil optælle den af p refererede<br />

double, må vi skrive<br />

2.11.5 Vektorer og pointere 91


(*p)++;<br />

En pointer kan som nævnt tildeles en vektor, hvis de begge er erklæret som afledning af af<br />

samme type. Da en pointer udgør en effektiv adresse, kan den ligeledes tildeles det n'te<br />

element i en vektor ved at tage adressen på dette element. Pointeren peger således ind et sted i<br />

vektoren. For eksempel,<br />

int v [10]; // 10 heltal<br />

int* p = &v [4]; // p peger på index 4 i v (nr. 5)<br />

Negative indeks er tilladt, men sjældent brugt:<br />

int i = p [-2]; // == v [2]<br />

Hvis der er tale om multidimensionale vektorer, følger syntaksen de samme regler. Hvis v er<br />

en todimensionel vektor af typen int, vil en tildeling af en pointer til en int til elementet<br />

på række 0, søjle 0 være:<br />

int* p = &v [0] [0];<br />

altså adressen (&) på dette element. Fordi v allerede er en afledt type "vektor af vektor", som<br />

kan sidestilles med en "pointer til pointer", kan vi fjerne den sidste indeksering samt adressepå<br />

operatoren:<br />

int* p = v [0];<br />

Udtrykket v [0] giver i sig selv adressen på søjle 0. Den indre natur i v er altså, at den<br />

første dimension (rækkerne) indeholder adresserne på start-punkterne i den anden dimension<br />

(søjlerne). I tre- eller flerdimensionale vektorer gentages dette princip. Kun det sidste led i<br />

vektoren specificerer indekset til et faktisk objekt i lageret.<br />

Som beskrevet kan de normale additions- og subtraktionsoperatorer benyttes i forbindelse<br />

med pointere. Hvis adresserne på vektor-elementer er operanter i et udtryk til en pointer,<br />

arbejdes der med antallet af elementer og ikke med maskin-ord:<br />

int v [] = { 1, 2, 3, 4, 5, 6, 7, 8 };<br />

int a = &v [5] - &v [2]; // giver 3<br />

int* p = v + a; // p peger på v [3];<br />

En vigtig detalje i pointer-vektor forholdet er, at [] har højere prioritet end *. Derfor:<br />

char* v [8]; // vektor af pointere<br />

char (*p) [8]; // pointer til vektor<br />

Operatorerne new og delete, som styrer den dynamiske allokering, bruger vektor-<br />

92 Mere om pointere, vektorer og referencer 2.11


operatoren [], hvis mere end ét element skal allokeres. Eksemplerne i vektor-afsnittet<br />

bruger alle statisk allokering, dvs. de initierende data står i objektkoden. Hvis der er tale om<br />

dynamisk allokering ved hjælp af operatorerne new og delete, bruges samme syntaks:<br />

char* p = new char; // bed om en enkelt char<br />

int* a = new int [1000]; // bed om 1000 int'er<br />

float** m = new float [4] [4]; // bed om en 4x4 floatmatrix<br />

Her er altså en mindre, dog ikke uvæsentlig, forskel mellem statisk og dynamisk allokering.<br />

Statisk allokering forudsætter vektoren, fordi den effektive adresse skal kendes på oversættelsestidspunktet,<br />

mens dynamisk allokering kræver en pointer, fordi den effektive adresse<br />

først kendes på kørselstidspunktet. Men da en statisk og dynamisk funktionelt identisk<br />

erklæring og allokering bruges med helt samme syntaks og efter samme semantiske regler, må<br />

pointere og vektorer siges at være næsten ens i brug.<br />

Det vigtigste er altid at huske, hvilken type der arbejdes med. Afledte typer som pointere i<br />

flere led kan være temmelig forvirrende, ikke mindst på grund af syntaksen, men det kan<br />

undertiden hjælpe at gå baglæns i erklæringerne og udtrykkene (og fjerne stjernerne<br />

midlertidigt) indtil en fundamental type eller struktur findes. Der skal være overenssemmelse<br />

mellem antallet af indirektioner på højre og venstre side af en tildeling, ligesom normale<br />

udtryk skal kunne behandles entydigt. Gå derfor altid tilbage til erklæringerne på pointerne og<br />

vektorerne og se, om det aktuelle udtryk i realiteten (gennem dereferencer eller indekseringer)<br />

resulterer i objekter af de samme typer.<br />

2.11.6 Pointere og vektorer i funktioner<br />

Som alle andre typer kan pointere og vektorer leveres til en funktion som et argument eller<br />

parameter:<br />

int strlen (char* p) { // tæl antallet af chars<br />

int i = 0;<br />

while (*p++) i++;<br />

return i;<br />

}<br />

Et kald til strlen() vil overføre værdien af en pointer, altså en effektiv adresse, til p,<br />

som videre bruges i funktionen. Heri ligger en af de væsentligste forskelle mellem vektorer og<br />

pointere, idet en pointer som parameter vil overføre en adresse, mens en vektor som<br />

parameter vil overføre hele indholdet af vektoren. Husk på, at kalde-konventionen i C++ er, at<br />

værdien, ikke selve variablen, overføres til funktionen. I denne forbindelse er det interessant<br />

at huske, at et parameter i en funktion er en egentlig variabelerklæring, så hvis vi erstatter<br />

pointeren med en vektor, som i<br />

int strlen (char p []) { // vektor i stedet for pointer<br />

2.11.5 Vektorer og pointere 93


...<br />

}<br />

vil det ikke have nogen betydning for funktionen. Erklæringen af p som vektor er i denne<br />

forbindelse helt ækvivalent som pointererklæringen. Pointere som parametre er meget<br />

brugbare, fordi funktionerne kan skrives uden forhåndsviden om, hvor data befinder sig og<br />

tillader en generisk behandling af dem.<br />

Ligesom alle andre objekter har en funktion også en adresse. Når et C++-program foretager<br />

et funktionskald, overføres kontrollen (CPU'ens programtæller) til den adresse, hvorpå<br />

funktionen befinder sig. I C++ er det muligt at arbejde med adresser på funktioner, igen for at<br />

opnå genericitet - blot her af kode. En pointer kan erklæres som pegende til en funktion, og<br />

via pointeren kan funktionen så senere kaldes. Dette kan være ekstremt nyttigt i visse<br />

situationer. Syntaksen for erklæring og reference er:<br />

int funk (); // funk er en funktion<br />

int (*p) (); // p er pointer til en parameterløs<br />

// funktion, som returnerer en int<br />

p = funk; // tildel p adressen på funk<br />

int a = (*p) (); // kald funktionen som p peger på<br />

En fordel ved pointere til funktioner er, at generiske funktioner kan bibeholdes, selv om et<br />

program skal udvides. Det kan være nok at skrive en servicefunktion samt udvide en eller<br />

flere datastrukturer. Da pointeren blot indeholder en adresse, kan den flyttes rundt på samme<br />

måde som alle andre pointere. Læg mærke til, at når vi tager adressen på en funktion, skal vi<br />

ikke benytte &-operatoren. Funktionsnavnet er i forvejen en pointer, vi blot bruger med ()operatoren.<br />

typedef void (*PLOT) (int, int); // ptr til plot-funktion<br />

void Circle (int x, int y, int r, PLOT p) {<br />

for (double i = 0; i < 2 * PI; i += PI / r * 24)<br />

(*p) (x + sin (i) * r, y + cos (i) * r);<br />

}<br />

Ovenstående rutine til cirkeltegning modtager udover centrum-koordinat og radius også en<br />

adresse på en plot-funktion. Denne funktion behøver ikke nødvendigvis altid at være den<br />

samme; i nogle tilfælde skal den kunne sætte prikker på en skærm, men i andre tilfælde måske<br />

skrive på en plotter, printer eller på en disk. Det interessant er imidlertid, at funktionen<br />

Circle() ikke behøver ændring, hvis plot-funktionen skal udskiftes. Genericiteten tillader<br />

os altså at blæse på væsentlige dele af programmets logisk separerede dele og simpelthen<br />

abstrahere på deres anvendelse.<br />

2.11.7 Pas på med pointere<br />

94 Mere om pointere, vektorer og referencer 2.11


Pointeren gør livet lettere hvis man mestrer den, men kan også frustrere en, som udfordrer den<br />

for meget. Når et C++-program kører, er der absolut ingen anden end programmøren, som har<br />

ansvaret for, hvad pointerne laver. Oversætteren er fuldstændig ligeglad med, om en pointer<br />

peger på defineret eller udefineret lager, om den har taget fejl af en vektors størrelse eller om<br />

den pludselig har fået trukket for meget fra. Har en pointer først fået en forkert adresse og<br />

derefter bliver derefereret, er katastrofale følger næsten en garanti, fordi lager, som ikke er<br />

defineret, bliver manipuleret af pointeren. Pas derfor på med pointerne - de har en tendens til<br />

at få tingene til at gå galt. Her er et eksempel på, hvad der kan ske med en ukontrollabel<br />

pointer:<br />

void Nulstil (int* p, int laengde) { // nulstil vektor<br />

do { // fortsæt...<br />

*p++ = 0; // nulstil element<br />

if (laengde = 0) break; // ups: tildeling!<br />

laengde--;<br />

}<br />

}<br />

Fejlen i ovenstående funktion Nulstil(), som tænktes at nulstille en vektor af int af en<br />

specificeret længde er, at sammenligningen med laengde, der bruges som kontrol for<br />

afbrydelse af løkken, er fejlagtig, idet der er tale om en tildeling. Det er ikke en syntaksfejl,<br />

men en strukturel fejl som resultat af den typiske sammenblanding af tildelingsoperatoren =<br />

og sammenligningsoperatoren ==. Sandhedsværdien i if-sætningen er den literale konstant<br />

0, hvorfor den aldrig bliver sand og løkken fortsætter dermed for evigt. Evige løkker med<br />

pointeraritmetik og indirekte tildeling er roden til alt ondt i C++, fordi der ikke er kontrol på<br />

kørselstidspunktet af lovligheden af adresseringen. Vær derfor altid meget omhyggelig med<br />

lokale løkker, der benytter pointere indirekte - det er næsten altid her, det går galt.<br />

2.11.8 Pointere og vektorer i datastrukturer<br />

Datastrukturer, som i C++ defineres med nøgleordet struct, har i modsætning til vektorer<br />

mulighed for at indeholde objekter af vilkårlige typer. Derfor benyttes medlemsreferenceoperatorerne<br />

. og -> i forbindelse med henholdsvis forekomster af og pointere til<br />

strukturer. Det tillader at identificere det ønskede medlem i en struktur, uanset hvordan<br />

strukturen iøvrigt er allokeret. Når pointeraritmetik bruges i forbindelse med datastrukturer,<br />

vil oversætteren behandle strukturen som et objekt på samme vilkår som de fundamentale<br />

typer. Addition til en pointer, der peger på en struktur af typen S, vil involvere størrelsen på<br />

en forekomst af strukturen, altså sizeof (S). Vektorer af strukturer erklæres således:<br />

struct Ansat {<br />

char* navn;<br />

2.11.7 Pas på med pointere 95


int afdeling, loenramme, alder;<br />

};<br />

Ansat personale [100]; // 100 statisk allokerede<br />

Ansat* temp = new Ansat [5]; // 5 dynamisk allokerede<br />

og bruges således:<br />

void printAnsat (Ansat* a) {<br />

cout


funktioner. Hvis to strukturer skal kunne referere hinanden, kan vi advare oversætteren om<br />

den ene (sidste) af dem med en prototype:<br />

struct Liste; // vi definerer den senere<br />

struct Haegte {<br />

char* Data; // indhold af hægte<br />

Haegte* Naeste; // pointer til næste hægte<br />

Liste* Rod; // pointer til en liste<br />

};<br />

struct Liste {<br />

Haegte* rod; // første haegte i hægtet liste<br />

};<br />

2.11.9 Pointere og nul<br />

En pointer, som har værdien 0 (nul) peger ikke på lovlige data. Derfor kan en nul-tildeling til<br />

en pointer benyttes for at vise, at pointeren er "inaktiv". At pointeren er lig med nul i<br />

kildeteksten betyder ikke nødvendigvis, at hver bit i pointeren er 0 - pas derfor på med binære<br />

operatorer og pointeraritmetik. Standardbiblioteket indeholder en const, NULL, som er<br />

erklæret som den faktiske værdi 0 som en unsigned long. NULL bør være den eneste<br />

konstant, som en pointer nogensinde sammenlignes med.<br />

2.11.10 Pointere til void<br />

Det er tilladt at erklære en pointer til "uspecificeret lager" med datatypen void:<br />

void* p;<br />

Denne erklæring betyder, at p er en pointer til en adresse i lageret, og intet mere. Fordelen<br />

ved void-pointere er, at man kan jonglere med pointere til lageret i funktioner, som ikke<br />

kender til den endelige natur i et givet lagerområde. Det er ikke tilladt at dereferere p, fordi<br />

oversætteren ikke ved, hvad den skal gøre med data på p's adresse. void* erstatter<br />

char*, som blev brugt som generel pointer i C.<br />

Et eksempel på brugen af void* er en funktion, som allokerer lager. Funktionen kaldes<br />

med en angivelse af, hvor meget lager (i bytes), som søges allokeret, og returnerer en voidpointer<br />

til det allokerede lager:<br />

void* malloc (unsigned long bytecount) {<br />

if (!bytecount) return NULL;<br />

void* lager;<br />

2.11.8 Pointere og vektorer i datastrukturer 97


... allokér lagerplads ved operativsystemkald<br />

return lager;<br />

}<br />

Funktionen malloc() returnerer en void*, som råt og brutalt peger på det allokerede<br />

lager. Hvis pointeren skal benyttes i dereferencer, må den typekonverteres, før den kan<br />

bruges.<br />

2.11.11 Pointere og typekonvertering<br />

Typekonvertering af pointere betyder at dereferencen, ikke selve pointeren, ændrer type. Hvis<br />

tptr er en pointer til en type T og sptr er en pointer til type S, kan tptr tildeles<br />

sptr via en tvungen typekonvertering:<br />

T* tptr;<br />

S* sptr;<br />

tptr = (T*) sptr;<br />

Det er brugbart i programmer, som arbejder med diffuse data, som er struktureret på en måde,<br />

der ikke kendes på oversættelsestidspunktet. Det er dog programmørens ansvar, at de data,<br />

som befinder sig på pointerens adresse er defineret som den type, de bliver konverteret til -<br />

ellers bliver resultatet udefineret. Husk også, at en typekonvertering af en pointer kun er en<br />

semantisk manøvre; hverken de indekserede data eller selve pointeren ændrer sig.<br />

En anden anvendelse er i forbindelse med void-pointere, der som nævnt ikke kan<br />

derefereres. Funktionen alloker() ovenfor returnerer en void* til frit lager, men skal<br />

typekonverteres, før dette lager kan manipuleres:<br />

char* p = (char*) alloker (100 * sizeof (char));<br />

int* q = (int*) alloker (100 * sizeof (int));<br />

ansat* r = (ansat*) alloker (100 * sizeof (ansat));<br />

Det ses altså, at en void-pointer skal konverteres eksplicit, før den kan tildeles en pointer til<br />

en anden type † . Det modsatte er ikke tilfældet; en pointer til en vilkårlig type kan konverteres<br />

til en void-pointer underforstået, hvilket er meget anvendeligt i funktioner, der arbejder<br />

generisk med pointere.<br />

2.11.12 Pointere og const<br />

En pointer kan for eksempel erklæres som const med<br />

†<br />

Dette er en væsentlig forskel mellem ANSI C og C++ - ANSI C tillader underforstået konvertering fra en vilkårlig<br />

pointer til en void-pointer.<br />

98 Mere om pointere, vektorer og referencer 2.11


const char* p; // p er pointer til const char<br />

Denne sætning erklærer p som pointer til en const char. Det er altså ikke pointeren<br />

selv, men dereferencen af pointeren, som er konstant. Dette betyder, at p gerne må ændres,<br />

så den peger på et andet lagerområde, men lagerområdet som p peger på, kan ikke<br />

manipuleres gennem p. Eksempler:<br />

const char data [10]; // 10 konstante char's<br />

const char* p; // p er pointer til const char<br />

p = data; // ok: tildel p data<br />

p++; // ok: næste element i data<br />

char c = *p; // ok: hent element fra data<br />

*p = c; // fejl: p peger på konstant data<br />

char* q = p; // fejl: q og p har ikke samme type<br />

En konstant pointer til en type T har en anden type end en ikke-konstant pointer til en T. Det<br />

er lovligt at tildele en ikke-konstant pointer til en konstant pointer, fordi det ikke overtræder<br />

reglerne for tilgang, men det omvendte kan ikke foretages uden manøvrer. For eksempel,<br />

char data [10];<br />

const char* p = data; // ok: konverteres underforstået<br />

char* q = p; // fejl: kan ikke konvertere<br />

Dette skal ses i lyset af, at parametre i funktioner kan erklæres som konstante pointere<br />

hvorefter funktionerne kan anvendes med både konstante og ikke-konstante pointere i selve<br />

funktionskaldene. Hvis en konstant pointer partout skal konverteres til en ikke-konstant,<br />

bliver vi nødt til at konvertere den tvungent:<br />

char* q = (char*) p; // ok: vi bestemmer selv<br />

Det understreges blot igen, at tvungen typekonvertering sætter sprogets indbyggede<br />

typekontrolsystem ud af kraft og derfor sker på eget ansvar.<br />

Pointere til konstante typer finder stor anvendelighed som parametre i funktioner, hvorved<br />

det gennem funktionen sikres, at der ikke skrives til det lager, som pointeren peger på. Det<br />

gør, at en fejl i forbindelse med en pointer, hvilket er en meget hyppig og svært lokaliserbar<br />

fejl, lettere kan findes, fordi de funktioner, som erklærer den som konstant, ikke kan skrive til<br />

det refererede lager:<br />

void Berserk (const char* p) {<br />

for (;;) *p++ = 0; // fejl: kan ikke skrive til *p<br />

}<br />

Af funktionen Berserk() ses det tydeligt, at en const char* ikke erklærer en pointer,<br />

2.11.12 Pointere og const 99


som har en fast værdi, men derimod en pointer til et lagerområde, som har fast værdi.<br />

Undertiden har man dog brug for det omvendte:<br />

char data [10];<br />

char* const p = data; // konstant pointer til char<br />

Denne sætning erklærer p som konstant pointer til char, dvs. en pointervariabel, som ikke<br />

kan ændre sin værdi og derfor peger fast til det samme lagerområde. Det er altså præcis det<br />

omvendte af en pointer til en konstant type, da det nu er pointeren selv, som er konstant og<br />

lageret som er ikke-konstant. Konstante pointere er for eksempel brugbare i hardwareafhængige<br />

rutiner, hvor en bestemt adresse altid har den samme betydning. Konstante<br />

pointere, ligesom referencer, skal initieres på samme tidspunkt de erklæres.<br />

En konstant pointer til et konstant objekt kan naturligvis også erklæres:<br />

const char* const p; = data;<br />

hvilket forhindrer både tildelinger til p og til *p. Dette kunne være en skrive-beskyttet, fast<br />

adresse i lageret, for eksempel et input-register i en I/O-enhed.<br />

2.11.13 Referencer<br />

Den afledte type en reference er et alias, dvs. et andet navn for et objekt. Referencer findes<br />

ikke i ANSI C, men er nye i C++, og bruges primært i forbindelse med kommunikation til<br />

brugerdefinerede typer. Referencevariable er en mellemting mellem forekomster og pointere,<br />

idet de arbejder med adresser og alligevel bibeholder syntaksen fra normal ikke-indirekte<br />

databehandling. Eller sagt på en anden måde: Ligesom pointere er referencer henvisninger til<br />

andre objekter, og ligesom fundamentale variable kræver de ingen specielle dereferenceoperatorer.<br />

Syntaksen for erklæring af reference-variable er simpel:<br />

int a; // erklær et heltal<br />

int& b = a; // b er et andet navn for a<br />

Dette eksempel erklærer et heltal og en reference, og tildeler referencen adressen på heltallet.<br />

Her er adressen af objektet underforstået, fordi referencetypen arbejder på et niveau, der<br />

automatisk derefererer objektet. a og b er to navne for samme fysiske heltalsvariabel i<br />

lageret, så:<br />

a = 5;<br />

vil også resultere i, at b får færdien 5. Faktisk er de to variable, fordi de peger på samme<br />

lager, den samme variabel. Enhver tildeling til a vil reflekteres i b, og vice versa. Selvom<br />

referencetypen minder meget om pointertypen, er der en væsentlig forskel. Mens en (ikke-<br />

100 Mere om pointere, vektorer og referencer 2.11


konstant) pointer kan ændre værdi vilkårlige steder i koden efter erklæringen, skal en<br />

reference initieres under erklæringen og kan ikke tildeles andre værdier efter denne. Når<br />

referencen er tildelt en forekomst af samme type, bibeholder den altså denne værdi gennem<br />

helt sit skop. Tildelinger til referencevariablen vil automatisk arbejde med en indirektion, så<br />

det er kun data i lageret, der ændres, og ikke referencevariablen selv. En reference kan altså<br />

siges at være en pointer til en konstant adresse i lageret.<br />

Referencernes betydning for grænseflader til objekter diskuteres i detaljer i <strong>kapitel</strong> 3. Der er<br />

imidlertid en anden anvendelse for referencer, som involverer funktioner, der har brug for at<br />

ændre på data i den kaldende funktion. Den gængse kaldemetode i C++ (og i C) hedder callby-value,<br />

altså hvor værdien af variablen sendes fra den kaldende til den kaldte funktion. Har<br />

man brug for at ændre på eksterne data (i forhold til den kaldte funktion), må man overføre<br />

adressen på disse. Dette gøres normalt med pointere, hvor den kaldte funktion modtager en<br />

pointer til et objekt. Problemet er imidlertid, at funktionen skal dereferere pointeren, hver<br />

gang, den bruges. Med referencer er det muligt at skabe et alias for en variabel i den kaldende<br />

funktion, mens man drager fordel af den underforståede dereference, hver gang variablen<br />

bruges. Denne kaldemetode har betegnelsen call-by-reference, altså hvor en reference til et<br />

objekt, og ikke objektets værdi, er det væsentlige i det pågældende parameter.<br />

Der er en vigtig forskel mellem pointere og referencer som parametre i funktioner. En<br />

pointer skal modtage en adresse, og man kan derfor regne med, at den kaldte funktion<br />

sandsynligvis manipulerer data i lageret omkring denne adresse. Med reference-parametre er<br />

det ikke muligt at se, at den kaldende funktion ændrer på eksterne data. Syntaksen for<br />

funktionskaldet er nemlig det samme. Denne forskel mellem pointere og referencer har mest<br />

psykologisk betydning i udviklingen, fordi man kan lade sig snyde af en funktions<br />

bemyndigelser hvad angår rettigheder til tilgang.<br />

Når en funktion modtager et reference-parameter, bliver forekomsten et alias for den<br />

afgivne parameter i den kaldende funktion. Reglerne for samtidig erklæring og tildeling<br />

følges dermed. Følgende funktion<br />

void Kvadrat (double& d) {<br />

d *= d;<br />

}<br />

modtager en reference til en double, som den manipulerer. Kald til denne funktion vil<br />

ændre på det objekt, som bruges i parameterlisten i den kaldende funktion. For eksempel,<br />

double x = 2.5;<br />

Kvadrat (x); // x == 6.25<br />

Referencer kan blive temmelig komplicerede. For eksempel betyder følgende erklæring<br />

void funk (char&*); // prototype...<br />

void (&run) (char&*) = funk;<br />

at variablen run er "en reference til en funktion, som tager en reference til en pointer til en<br />

2.11.13 Referencer 101


char som parameter", samt at den tildeles funktionen funk(). Det giver en mere elegant<br />

syntaks i forhold til pointere, fordi funktionen kan kaldes med en normal syntaks - overvej<br />

forskellen på pointere til funktioner og referencer til funktioner i brug:<br />

void (&r1) (char&*) = funk; // r1 refererer funk<br />

void (*r2) (char&*) = funk; // r2 peger på funk<br />

char* p;<br />

// ...<br />

r1 (p); // kald gennem reference<br />

(*r2) (p); // kald gennem pointer<br />

Referencer kan også bruges som returværdier fra funktioner, igen for at undgå den manuelle<br />

dereference:<br />

int& allokerInt () { // returnerer reference<br />

return *new int; // allokerer forekomst<br />

}<br />

void f () {<br />

int& a = allokerInt (); // alt pr. reference<br />

}<br />

Returværdier af referencetyper er særdeles brugbare, idet selve funktionskaldet kan bruges<br />

med den samme syntaks som normale operationer på den type, den returnerer. Følgende<br />

funktion tildeler en værdi til en intern vektor, som er statisk erklæret:<br />

int& Tildel (int nummer) { // returnerer reference<br />

static int vektor [10];<br />

return vektor [nummer];<br />

}<br />

void f () {<br />

Tildel (5) = 4; // tildel 5. (egt. 6.) element<br />

}<br />

Denne måde at bruge et funktionskald på venstre side af lighedstegnet finder sin største<br />

brugbarhed i forbindelse med operator-overstyring, som beskrives i afsnit 3.5, da det kan<br />

være fordelagtigt at lade en lvalue være en funktion.<br />

Når en funktion returnerer en reference, skal man passe på, at den returnerede værdi ikke<br />

mister sit skop, idet funktionen slutter. I så fald får variablen, der modtager referencen, en<br />

udefineret værdi. Hvis vi for eksempel definerede vektoren vektor i funktionen<br />

Tildel() uden static-nøgleordet (hvorved den bliver en automatisk variabel), vil<br />

lageret, som vektor optager, blive frigivet til systemet lige inden, den returnerer til den<br />

102 Mere om pointere, vektorer og referencer 2.11


kaldende funktion:<br />

int& Tildel (int nummer) {<br />

int vektor [10];<br />

return vektor [nummer]; // ups! vektor mister skop her<br />

}<br />

Oversætteren vil sandsynligvis advare eller fejle ved en sådan returnering. Der er også en<br />

anden faldgrube i arbejdet med referencer som parametre, nemlig at en typekonflikt, der kan<br />

løses med en underforstået konvertering, vil skabe en reference til et midlertidigt objekt, som<br />

bliver genereret af oversætteren. Under normale omstændigheder er det ikke noget problem,<br />

men med referencer kan det få uheldige konsekvenser. For eksempel,<br />

void f (long&);<br />

int i = 1;<br />

f (i); // genererer reference til temporær<br />

Her vil kaldet til f() automatisk generere en midlertidig long som resultat af den<br />

underforståede konvertering, men da funktionen modtager en reference, er det således til<br />

denne temporære variabel, der bliver den faktiske parameter. Oversætteren vil normalt advare<br />

om, at kalderens variabel bliver konverteret og ikke vil kunne manipuleres af funktionen.<br />

Det skal også fremhæves, at referencer egentlig ikke er rigtige objekter. Alle operationer,<br />

der foretages på en reference sker i realiteten på det objekt, der refereres gennem referencen<br />

og ikke på referencen selv. Der er ingen mulighed for at manipulere med eller sammenligne<br />

de fysiske referencer. En konsekvens af dette er, at det ikke er muligt at arbejde med pointere<br />

til referencer og følgelig heller ikke vektorer af referencer:<br />

int x,y;<br />

int& rvek [] = { x, y }; // fejl: kan ikke lagre referencer<br />

2.11.14 Typedefinitioner<br />

Typedefinitioner med typedef minder om makro-definitioner, men udføres ikke af<br />

præprocessoren. typedef tillader introduktion af et nyt navn, som vil ligne en ny type i<br />

sproget. Her er dog kun tale om pseudo-typer, fordi typedef ikke har mange fordele frem<br />

for en generel makro. Metoden bruges igen for at sikre konsistens samt øge læsbarheden<br />

gennem kildefilerne, og har følgende notation:<br />

typedef unsigned char BYTE;<br />

BYTE kan herefter bruges som en unsigned char. Selve typedef-sætningen vil blive<br />

analyseret semantisk af oversætteren, hvilket ikke gælder for makroer, og en fejl i selve<br />

typedef-erklæringen vil blive rapporteret. Der er således større anvendelighed af navne,<br />

2.11.13 Referencer 103


som er erklæret på denne måde, idet komplekse erklæringer kan foretages. For eksempel,<br />

typedef int (*PTF) (double, double);<br />

erklærer PTF som en typedefinition af en pointer til en funktion, som returnerer en int og<br />

som har en parameterliste på to double-objekter. Det gør det nemmere at arbejde med en<br />

kompliceret erklæring, når den er typedef'et, navnligt som parametre. Overvej forskellen<br />

på<br />

og<br />

void DoSomething (int (*p) (double, double), double);<br />

void DoSomething (PTF p, double);<br />

C-programmører udnytter i stor stil typedefinitioner til navngivelse af datastrukturer, så de<br />

ikke behøver skrive struct allesteds i programmet, hvilket er nødvendigt i C. Dette er ikke<br />

nødvendigt i C++, da en strukturerklæring introducerer et egentligt typenavn.<br />

2.12 I/O OG STANDARDBIBLIOTEKET<br />

Sproget C++ har ingen indbyggede metoder for input og output i sproget, men lader disse<br />

opgaver blive løst med sproget. Grunden er, at I/O er meget forskellig fra arkitektur til<br />

arkitektur, for ikke at nævne fra applikation til applikation, og C++ er et sprog, som søget bort<br />

fra hardware-afhængighed. Med sproget selv som værktøj kan metoder til I/O skabes og<br />

tilpasses forholdene.<br />

C++ har imidlertid, ligesom C, et standardbibliotek til I/O. Biblioteket iostream indeholder<br />

definitioner af klasser, funktioner og metoder til varetagelse af I/O †<br />

. Dette afsnit beskriver<br />

anvendelsen af standardbiblioteket; funktionaliteten uddybes i de næste kapitler.<br />

Nøglekonceptet i C++'s standard-I/O er en strøm, som på engelsk hedder en stream. En<br />

strøm er en slags rørledning, i hvilken man kan hælde data eller suge data op. Der findes fire<br />

standard-strømme, én for input, én for output én for fejl-output og en for fuldt bufferet fejloutput.<br />

Applikationen kan skrive og læse fra de tre standard-strømme uden at bekymre sig om<br />

hvor data kommer fra eller er på vej hen. De strøm-erklæringer, der findes i headerfilen<br />

er helliget konvertering fra fundamentale typer til tegn, samt omvendt, så<br />

data kan ind- og udlæses fra og til tastaturet, skærmen, disken og så videre, helt i samme<br />

filosofi som C-biblioteket stdio.h. I dette afsnit beskrives kun funktionaliteten i standardbiblioteket,<br />

mens dets opbygning gennemgås i <strong>kapitel</strong> 6, hvor også udvidelser af databehandlingen<br />

fra fundamentale til bruger-definerede typer forklares - dog skal det nævnes, at det<br />

gamle C-bibliotek udmærket kan anvendes sammen med C++.<br />

† Denne bog beskæftiger sig med AT&T's version 2.0 af stream-biblioteket. Der er et mindre antal afvigelser fra<br />

tidligere versioner af dette bibliotek.<br />

104 Mere om pointere, vektorer og referencer 2.11


En strøm er i mere klare termer en abstraktion af data-flow fra en kilde til en destination. To<br />

generelle strømme er derfor defineret i standard-headerfilen , nemlig<br />

istream (for input) og ostream (for output). Disse er to specielle datatyper, og indgår i<br />

en større sammenhæng i biblioteket. Der er fire standard-definerede forekomster af<br />

istream og ostream, nemlig strømmene<br />

cin standard-input (svarer til stdin)<br />

cout standard-output (svarer til stdout)<br />

cerr standard-error (svarer til stderr)<br />

clog standard-error med buffering (ingen ækvivalens i stdio)<br />

cin, cout og cerr svarer til filbeskrivelserne 0, 1 og 2 på de fleste systemer. I dette<br />

afsnit koncentrerer vi os om de helt centrale anvendelser af disse kanaler, så senere eksempler<br />

kan absorberes, mens den indre virkemåde og mere avancerede og perifere brug beskrives i<br />

<strong>kapitel</strong> 6.<br />

2.12.1 Output<br />

For en C-programmør vil følgende sætning se underlig ud:<br />

cout


en anden operator, nemlig >>, til "hent følgende data". Læse-sætninger ser derfor således ud:<br />

cin >> a;<br />

og uddybes med "læs a fra cin", som er meget tæt på C-funktionskaldet (hvis a er en<br />

int):<br />

scanf ("%d", &a);<br />

Læg igen mærke til, at vi ikke behøver fortælle cin om typen af a, mens vi skal fortælle<br />

scanf() om både typen af og adressen på a.<br />

2.12.3 Filbaseret I/O<br />

Filmanipulation er nært beslægtet med standard-I/O, og startes med en erklæring af en type<br />

ifstream (for input), ofstream for output eller fstream for bidirektionel I/O, alle<br />

defineret i headerfilen . Følgende funktion kopierer en fil:<br />

void Kopier (char* fra, char* til) {<br />

ifstream fra_fil (fra); // en ny input-kanal<br />

ofstream til_fil (til); // en ny output-kanal<br />

char element;<br />

while (fra_fil) { // sålænge der er data<br />

fra_fil >> element; // hent et tegn<br />

til_fil


2. Forklar betydningen af følgende prototyper:<br />

char* f (char a);<br />

char* g (char* (*b)(char d));<br />

char** h (int i ...);<br />

char j (char& k);<br />

char l (char&* m);<br />

3. Skriv strukturer til at beskrive følgende:<br />

a) en hægte i en hægtet liste,<br />

b) en hægte i et binært træ,<br />

c) en hægte i en dobbelt kø (data udtages fra begge sider)<br />

d) en n-vejs hægte i et n-vejs træ.<br />

4. Giv eksempler på, hvornår rekursive funktioner bruges.<br />

5. Hvad er resultatet af følgende udtryk, givet a == 1, b == 2 og c == 3:<br />

a * b - c<br />

a - b * c<br />

a * b++ * c<br />

-a --b --c<br />

--a ---b ---c<br />

a | b & c<br />

c ^ a<br />

++a & --c<br />

6. Skriv funktioner, der gennemløber datastrukturerne fra opgave 2-3. Hvad er<br />

forskellen på de forskellige gennemløb?<br />

7. Forklar naturen af de forskellige lagringsklasser i C++.<br />

8. Forklar begreberne literale konstanter og symbolske konstanter og diskutér<br />

forskellene og anvendelsesområderne. Hvorfor tror du, at den klassiske C-standard<br />

ikke omfattede symbolske konstanter?<br />

9. Skriv erklæringer af følgende objekter:<br />

a. pointer til char,<br />

b. pointer til vektor af char,<br />

c. vektor af pointere til char,<br />

2.12.3 Disk I/O 107


d. pointer til funktion, der returnerer char,<br />

e. reference til pointer til char,<br />

f. reference til parameterløs funktion, der returnerer char.<br />

10. Forklar begrebet implicit typekonvertering og overvej, hvorfor det har betydning i et<br />

sprog som C++. Diskutér også, hvorfor mange andre computersprog ikke har<br />

mulighed for eksplicit typekonvertering.<br />

11. En reference er et andet navn for et objekt. Hvordan tror du, en reference i realiteten<br />

bliver behandlet i programmet?<br />

12. Hvis du fik til opgave at skrive et program, der skulle kunne køre på tre forskellige<br />

maskiner og operativsystemer, hvilke faciliteter i C++ ville du drage nytte af hvad<br />

angår både de maskinuafhængige dele og de maskinafhængige dele?<br />

2.14 REFERENCER OG UDVALGT LITTERATUR<br />

Der findes efterhånden en del gode engelsksprogede bøger om C++. Den endelige definition<br />

på sproget er [Stroustrup 91], en komplet omend svært læselig bog. Bjarnes bog, som netop er<br />

udkommet i anden udgave, læses bedst sammen med en mere pædagogisk anlagt bog, som for<br />

eksempel [Lippman 91], som er en begynderbog. [Berry 88] indeholder mange eksempler på,<br />

hvorfor og hvordan C++ hæver sig over C med mange eksempler på ulemperne ved C. Den<br />

har også beskrivelser af C++ under specifikke miljøer som DOS og UNIX. En ANSIdefinition<br />

af programmeringssproget C findes i [Kernighan 88]. For oversætterdesignere<br />

indeholder [Ellis 90] mange gode eksempler på kodegenerering af de særlige sprogkonstruktioner<br />

i C++.<br />

108 Opgaver til <strong>kapitel</strong> 2 2.13


Dataabstraktion<br />

Som beskrevet i indledningen er et af de store problemer i programudvikling at<br />

få samling på programmerne, så de er lette at overskue, bruge, genbruge og<br />

ikke mindst læse. Ved abstraktion af data forstås den teknik, som abstraherer<br />

eller isolerer alle dele af et program (både kode og data) i moduler, som har<br />

med et bestemt område at gøre samt definerer en passende grænseflade til disse<br />

moduler. I C++ er en sådan isolation ensbetydende med en introduktion af nye<br />

datatyper, der skræddersys til programmerne, som skal bruge dem.<br />

3.1 INTRODUKTION<br />

Alle programmeringssprog indeholder forskellige former for dataabstraktion. Det er en stor del af<br />

ideen bag overhovedet at bruge et generelt programmeringssprog fremfor for eksempel<br />

assembler. Hvis vi adderer to heltal er vi ikke interesserede i, hvordan programmet i sidste ende<br />

rent teknisk foretager denne addition; vi befinder os på et abstraktionsniveau, hvor vi kan være<br />

ligeglade. Adderer vi to decimaltal, er vi ligeledes uinteresserede i, hvordan oversætteren<br />

behandler decimaltal. Typisk vil oversætteren klare både allokering af lager til forekomster af de<br />

indbyggede typer og fortolkninger af betydningerne af de forskellige operatorer, som vi påfører<br />

forekomsterne. Som programmører skal vi ikke fortælle hvad der rent faktisk skal ske med<br />

forekomster af de fundamentale datatyper, det arbejde har oversætter-designeren gjort for os.<br />

C++ tillader os at udvide sproget og introducere nye "abstrakte" datatyper ved at definere,<br />

hvordan de skal administreres og behandles. At en datatype er "abstrakt" betyder ikke, at dens<br />

definition er tvetydig på nogen måde. Termen bruges til at indikere, hvordan man som<br />

programmør kan abstrahere et koncept fra et kompleks af programdetaljer ind i en datatype (hvis<br />

semantik er defineret udenfor oversætteren) samt at understrege forskellen til de indbyggede<br />

datatyper. En abstrakt datatype er, hvis den er ordentligt skrevet, lige så reel som de indbyggede.<br />

Når vi som programmører tænker på datatyper, mener vi for det meste de indbyggede<br />

fundamentale typer, som sproget tilbyder til repræsentation af heltal, strenge, kommatal osv.<br />

Virkeligheden forholder sig imidlertid sådan, at der er mange andre typer end de, vi finder<br />

indbygget i sproget. Ethvert program bygges op i to faser: først defineres alle repræsentationer af<br />

data samt funktioner til manipulation af data, og dernæst skrives selve applikationen, som bruger<br />

disse data og funktioner. Faktisk er den første fase en definition af specialiserede typer, fordi den<br />

handler om de nederste, mest detaljerede lag i en applikation som opererer på et lavt<br />

2.12.3 Disk I/O 109<br />

3


abstraktionsniveau. Anden fase er det øverste, mere brugerorienterede lag, som kalder og bruger<br />

typerne fra et højere abstraktionsniveau. De fleste programmer arbejder altså med en eller anden<br />

form for strukturering af data, som, set helt fra oven, repræsenterer en type - eller klasse - fra det<br />

virkelige liv. Denne klassifikation er grundlaget for arbejdet med dataabstraktion.<br />

Når vi nærlæser et program i et proceduralt orienteret sprog, fremgår disse klasser ikke så klart i<br />

selve koden. C's struct og Pascals record giver os mulighed for at kombinere en<br />

mangfoldighed af data under et samlet navn - en datastruktur - som giver os to umiddelbare<br />

fordele. For det første kan strukturen behandles samlet under dette navn, for eksempel som<br />

parameter i en funktion, og for det andet kan vi arbejde med de individuelle medlemmer af<br />

strukturen, for eksempel i en funktion, som behandler strukturen på en bestemt måde. Afhængigt<br />

af abstraktionsniveauet kan vi altså arbejde enten på eller i datastrukturer, som vi behager og på<br />

den måde afspejler en datastruktur et koncept fra den virkelige verden, som gør dem meget<br />

brugbare i opbygningen af programmoduler.<br />

Desværre, som vi skal se senere, giver datastrukturer kun brugeren mulighed for at abstrahere<br />

data. Programmet skal stadig skrives med både helheder og detaljer i tankerne, når<br />

datastrukturerne behandles. Det udmønter sig i implementations-problemer, fordi den logiske<br />

sammenhæng mellem data og kode bliver eksplicit og derfor svag. Dataabstraktion, som vi finder<br />

i sprog som Ada, Modula-2 og C++, kombinerer datastrukturen med de funktioner, som skal<br />

arbejde i den på en logisk måde. Når typerne defineres i første fase af programmeringen,<br />

repræsenteres de dermed i en klasse med både midler (data) og måder (kode), og giver en bedre<br />

grænseflade til anvendelsen af typen i anden fase.<br />

Denne grænseflade giver os også bedre mulighed for at integrere typerne i underliggende<br />

programbiblioteker. De brugerdefinerede typer i C++ er nemlig skalérbare i den forstand, at de<br />

med en enkelt definition kan benyttes i mange forskellige sammenhænge. Det betyder i praksis, at<br />

komplicerede opgaver som I/O kan integreres med de brugerdefinerede typer på en måde, hvor<br />

både typer og I/O-subsystemet kan skiftes ud, uden at der skal rettes i koden for nogen af delene.<br />

3.1.1 Biblioteker<br />

Alle programmeringsmiljøer har en form for bibliotek, som indeholder ofte brugte datastrukturer<br />

og funktioner til behandling af disse. Bibliotekerne giver os flere fordele: de sparer tid, fordi vi<br />

ikke behøver "genopfinde hjulet" hver gang, vi skriver en ny applikation, og de giver en vis<br />

sikkerhed for, at standard-rutiner er gennemtestede og virker.<br />

Funktionsbiblioteker, som vi kender dem fra C og Pascal, består af en samling af globale navne<br />

på datastrukturer og funktioner med parameterlister, som kan bruges i alle programmer, der<br />

henviser til biblioteket. Vi kalder forfatteren af en biblioteksfunktion en udbyder, og brugeren af<br />

funktionen en klient af biblioteket. Udbyderens funktioner arbejder på de individuelle<br />

medlemmer af strukturerne, og klienten arbejder på strukturerne som helhed. På den måde hæver<br />

klienten sig et abstraktionsniveau over udbyderen og bruger dermed biblioteket modulært.<br />

Hvordan bruges et bibliotek så rent praktisk? Det er hovedsageligt oversætterens<br />

ansvarsområde: klienten angiver i sit program, at en bestemt biblioteksfunktion ønskes benyttet,<br />

og oversætteren sørger for at samle klientens kode med bibliotekets kode. Alt hvad klienten<br />

behøver at vide er syntaksen i referencen (samt selvfølgelig hvad funktionen gør). Et<br />

funktionsbibliotek har imidlertid nogle væsentlige ulemper. Det er meget svært at udvide en<br />

110 <strong>Introduktion</strong> 3.1


eksisterende funktion, hvis den ønskede virkning skal ændres bare en smule. Oftest omskrives<br />

hele funktionen i klientens applikation, hvorved en stor del af fordelen ved biblioteket går tabt. Et<br />

andet problem er, at store biblioteker med mange globale funktioner bliver svære at overskue.<br />

Med over 1000 forskellige navne på funktioner og datastrukturer er det svært at huske navnet på<br />

lige præcis den, man ønsker at benytte. I mere specialiserede systemer med multiprogrammerede<br />

operativsystemer, får funktionerne tillige problemer med at holde rede på statiske lokale data. Det<br />

kræver en semaforstyring, som skal styres fra alle de individuelle biblioteksfunktioner eller fra<br />

ethvert klientprogram.<br />

3.1.2 Grænseflader og informationsskjul<br />

Kernen i problemet viser sig at være grænsefladen mellem klientens program og biblioteket.<br />

Klienten hverken ønsker eller bør have med detaljerne i udbyderens bibliotek at gøre. For det<br />

første tabes abstraktionen, hvorved koden bliver svær at forstå i relation til det problem, det skal<br />

løse, og for det andet er der stor risiko for introduktion af fejl, da klienten ikke nødvendigvis ved<br />

alt om bibliotekets struktur.<br />

Med abstrakte datatyper skjuler vi implementationen fra klienten, som i de fleste tilfælde slet<br />

ikke har adgang til at manipulere typen direkte. Filosofien kaldes informationsskjul (på engelsk<br />

data hiding) og sørger for at ridse en klar grænseflade mellem udbyderdata og klientkode op.<br />

Klienten er tvunget til at bruge udbyderens funktioner til manipulation af typens indre. Dermed<br />

kan en konsistensfejl i en types indre data føres tilbage til udbyderens kode, da kun den har<br />

mulighed for at ændre dem. At lokalisere en fejl bliver dermed et mindre problem, specielt i store<br />

projekter.<br />

I dette <strong>kapitel</strong> skal vi se, hvordan C++'s faciliteter for design af abstrakte datatyper gør livet<br />

lettere for klienten. I <strong>kapitel</strong> 4 beskæftiger vi os med udvidelser af disse typer. Som beskrevet i<br />

afsnit 2.2 ligger grænsefladen mellem klient og udbyder rent praktisk i en header-fil, som gør<br />

oversætteren bekendt med navne, parameterlister osv. Selve bibliotekskoden findes i objekt-filer,<br />

som oversættes separat og lænkes sammen på et senere tidspunkt.<br />

3.2 ET EKSEMPEL-PROBLEM<br />

I erkendelse af, at den eneste rigtige måde at lære et nyt programmeringssprog at kende er at<br />

skrive programmer i det, opstiller vi et eksempel på et problem, som vi vil løse. For klart at<br />

markere forskellene mellem proceduralt orienteret programmering og programmering med<br />

abstrakte datatyper, starter vi med et eksempel i standard C - som nævnt i indledningen af dette<br />

<strong>kapitel</strong> giver de fleste programmeringssprog os allerede en vis form for abstraktion:<br />

/* eksempel 3-1: kommatals-aritmetik */<br />

#include <br />

#include <br />

3.1.1 Biblioteker 111


void main () {<br />

double a, b, c;<br />

a = 5.9;<br />

b = 2.7;<br />

c = a * b - 4.8;<br />

printf ("%lf", c);<br />

}<br />

Dette C-program erklærer 3 double'r, tildeler værdierne 5.9 og 2.7 til a og b, tildeler derefter<br />

resultatet af udtrykket a gange b minus 4.8 til c og kalder derefter C's printf()-funktion<br />

og udskriver c. Programmet vil skrive tallet 11.13. Dette lille program arbejder med den<br />

fundamentale type double, som findes både i C og i C++. Nu antager vi, at vi har brug for at<br />

foretage beregninger med komplekse tal, som ikke findes i sproget som type, hvorfor vi må<br />

simulere den i vores egen software. Kravet er, at en klient skal kunne bruge en behændig syntaks<br />

for manipulation af og beregning med komplekse tal, herunder aritmetik og udlæsning.<br />

Biblioteket, som skal behandle forekomster af komplekse tal på lavt niveau, skal indeholde<br />

funktioner af følgende slags:<br />

• Allokering af et komplekst tal,<br />

• Initiering af et komplekst tal,<br />

• Tildeling af et komplekst tal,<br />

• Addition af to komplekse tal,<br />

• Subtraktion af to komplekse tal,<br />

• Multiplikation af to komplekse tal,<br />

• Udskrift af et komplekst tal og<br />

• Oprydning efter endt brug af et komplekst tal.<br />

Denne eksempel-problemstilling og foreslåede fremgangsmåde er meget typisk i C- og C++miljøer,<br />

idet der lægges meget vægt på modulariteten af den kode, der skal behandle den ny<br />

datastruktur.<br />

3.2.1 En løsning i C<br />

Et klassisk C-funktionsbibliotek indeholder makrodefinitioner (typedef-erklæringer) af<br />

datastrukturer samt funktioner, som arbejder med disse. For typen COMPLEX † skal vi bruge et<br />

antal funktioner, der er vist i tabel 3-1.<br />

Funktionsnavn Beskrivelse<br />

alloc_complex()<br />

assign_complex_a()<br />

Allokér en COMPLEX<br />

Initiér en COMPLEX med reel og imaginær del<br />

† Det er en uskreven konvention i C, at typedef'ede navne skrives med versaler.<br />

112 Et eksempel-problem 3.2


assign_complex_b()<br />

add_complex()<br />

sub_complex()<br />

mul_complex()<br />

print_complex()<br />

free_complex()<br />

Initiér en COMPLEX med en anden COMPLEX<br />

Addér to COMPLEX'e<br />

Subtrahér to COMPLEX'e<br />

Multiplicér to COMPLEX'e<br />

Udlæs en COMPLEX<br />

Deallokér en COMPLEX<br />

Tabel 3-1: Funktionsoversigt i C-løsningen.<br />

Funktionserklæringerne - prototyperne - placeres i en headerfil, complex.h, mens<br />

implementationerne skrives i en kildefil, complex.cpp. Klienten benytter sig kun af<br />

headerfilen og lænker sin kode til den allerede oversatte implementation fra kildefilen,<br />

hvorved der til en vis grænse kan abstraheres fra denne konkrete implementation.<br />

Specifikationen af dette bibliotek, headerfilen, er vist i eksempel 3-2a.<br />

/* complex.h<br />

* eksempel 3-2a: En C-specifikation af komplekse tal<br />

*/<br />

typedef struct { /* datatypen COMPLEX */<br />

double re, im; /* reel og imaginær del */<br />

} COMPLEX;<br />

COMPLEX *alloc_complex ();<br />

void free_complex (COMPLEX*);<br />

void assign_complex_a (COMPLEX*, double, double);<br />

void assign_complex_b (COMPLEX*, COMPLEX*);<br />

void add_complex (COMPLEX*, COMPLEX*, COMPLEX*);<br />

void sub_complex (COMPLEX*, COMPLEX*, COMPLEX*);<br />

void mul_complex (COMPLEX*, COMPLEX*, COMPLEX*);<br />

void print_complex (COMPLEX*);<br />

Header-filen complex.h indeholder en typedef af en COMPLEX, som består af to<br />

double'r samt 6 funktioner til manipulation af denne type. Implementationen af biblioteket<br />

findes i eksempel 3-2b.<br />

/* complex.c<br />

* eksempel 3-2b: en C-implementation af et komplekst tal<br />

*/<br />

#include /* udskrift-funktioner mv. */<br />

#include /* lager-allokering mv. */<br />

#include "complex.h" /* specifikation på COMPLEX */<br />

3.2.1 En løsning i C 113


COMPLEX *alloc_complex () {<br />

COMPLEX *ny = (COMPLEX*) malloc (sizeof (COMPLEX));<br />

ny->re = 0, ny->im = 0;<br />

return ny;<br />

}<br />

free_complex (COMPLEX* a) {<br />

free (a);<br />

}<br />

COMPLEX *assign_complex_a (COMPLEX* a, double r, double i) {<br />

a->re = r, a->im = i;<br />

return a;<br />

}<br />

COMPLEX *assign_complex_b (COMPLEX* a, COMPLEX* b) {<br />

a->re = b->re, a->im = b->im;<br />

}<br />

COMPLEX *add_complex (COMPLEX* a, COMPLEX* b, COMPLEX* c) {<br />

c->re = a->re + b->re, c->im = a->im + b->im;<br />

return c;<br />

}<br />

COMPLEX *sub_complex (COMPLEX* a, COMPLEX* b, COMPLEX* c) {<br />

c->re = a->re - b->re, c->im = a->im - b->im;<br />

return c;<br />

}<br />

COMPLEX *mul_complex (COMPLEX* a, COMPLEX* b, COMPLEX* c) {<br />

c->re = a->re * b->re - a->im * b->im;<br />

c->im = a->re * b->im + a->im * b->re;<br />

return c;<br />

}<br />

void print_complex (COMPLEX* a) {<br />

printf ("(%f,%f)", a->re, a->im);<br />

}<br />

Denne implementation benytter sig i særdeleshed af pointere til strukturer (afsnit 2.11.8).<br />

Funktionen alloc_complex() reserverer plads til en COMPLEX og returnerer en pointer<br />

114 Et eksempel-problem 3.2


til denne mens free_complex() frigiver lageret, som er optaget af en bestemt<br />

COMPLEX til systemet. add_complex(), sub_complex() og mul_complex()<br />

foretager en aritmetisk operation på to COMPLEX'er og returnerer resultatet i en tredie.<br />

print_complex() udskriver en COMPLEX. Som det ses, skal de fleste af funktionerne<br />

have en adresse som parameter.<br />

En klient af complex.c, som benytter sig af funktionerne og typen COMPLEX til en<br />

simpel udregning kan se ud som i eksempel 3-2c.<br />

/* cmpxtest.c<br />

* eksempel 3-2c: test af C-biblioteket med komplekse tal<br />

*/<br />

#include <br />

#include "complex.h"<br />

main ()<br />

{<br />

COMPLEX *a = alloc_complex ();<br />

COMPLEX *b = alloc_complex ();<br />

COMPLEX *c = alloc_complex ();<br />

COMPLEX *d = alloc_complex ();<br />

COMPLEX *temp = alloc_complex ();<br />

assign_complex_a (a, 5.8, 3.7);<br />

assign_complex_a (b, 2.1, 4.4);<br />

assign_complex_a (c, 6.9, 8.7);<br />

print_complex (a); printf (" * ");<br />

print_complex (b); printf (" - ");<br />

print_complex (c); printf (" = ");<br />

mul_complex (a, b, temp); /* dvs. *temp = *a * *b */<br />

sub_complex (temp, c, d); /* dvs. *d = *temp - *c */<br />

print_complex (d);<br />

free_complex (a);<br />

free_complex (b);<br />

free_complex (c);<br />

free_complex (d);<br />

free_complex (temp);<br />

}<br />

Ovenstående C-program producerer følgende korrekte output:<br />

3.2.1 En løsning i C 115


(5.8,3.7) * (2.1,4.4) - (6.9,8.7) = (5.28,7.58)<br />

3.2.2 Gennemgang af C-løsningen<br />

Dette lille bibliotek af C-funktioner udmærker sig ved sin modularitet. Alle funktioner, som<br />

arbejder direkte på de individuelle medlemmer af datastrukturen COMPLEX er indeholdt i<br />

implementationsfilen complex.c, mens klientens program alene arbejder med pointere til<br />

forekomster af strukturen som helhed. Vi ser altså, at C, hvis man overholder visse regler for<br />

struktur, støtter ideen om modulærprogrammering, fordi det tillader os at abstrahere fra alt<br />

andet end headerfilen.<br />

Hvori består så de programmeringstekniske problemer med en sådan implementation? Her<br />

skal fokuseres på to aspekter, som har stor betydning for værdien af funktionsbiblioteket:<br />

• Grænsefladen mellem specifikationen af typen COMPLEX og applikationen, som<br />

arbejder med forekomster af den. Hvilke operationer skal klienten af COMPLEX<br />

benytte, og er disse arrangeret på en fornuftig måde?<br />

• Udvidelsesmulighederne for og vedligeholdelsen af typen COMPLEX. Er det let eller<br />

svært at skrive nye funktioner til manipulation af forekomster? Kan disse integreres i<br />

funktionsbiblioteket? Hvad er risici for introduktion af fejl?<br />

Med disse i baghovedet kan vi opremse et antal konkrete problemer med typen COMPLEX:<br />

• Syntaks. Manipulation af forekomster af COMPLEX gøres gennem specialiserede<br />

globale funktioner, som kaldes fra applikationen, for eksempel kaldet til<br />

mul_complex() i main(). Hvis to forekomster af COMPLEX skal<br />

multipliceres, kan den sædvanlige syntaks, som vi kender fra de fundamentale typer<br />

float, int osv., ikke bruges. Det gælder også erklæringer, og typen har med<br />

andre ord en grænseflade til applikationen, som er helt egenartet. Det gavner mindst<br />

af alt læsbarheden af koden.<br />

• Semantik. Applikationen må i større beregninger selv varetage visse semantiske<br />

opgaver. For eksempel er sprogets præcedens i forbindelse med operatorer sat ud af<br />

spillet, fordi syntaksen er vendt fra de normale operatorer til eksplicitte<br />

funktionskald. I main() i eksempel 3-2 udføres (rent semantisk) operationen d =<br />

a * b + c på de komplekse tal, men multiplikations-operatorens højre prioritet i<br />

forhold til addition må applikationen selv varetage. Funktionskald til manipulatorfunktioner<br />

i complex.c skal med andre ord foretages i en bestemt rækkefølge.<br />

• Lageradministration. Det er op til applikationen at sørge for administrationen af<br />

lageret i tilknytning til forekomster af COMPLEX. Glemmer applikationen kaldet til<br />

alloc_complex(), vil efterfølende manipulationer arbejde med en uinitieret<br />

pointer med sikre katastrofale følger. Sproget giver ingen hjælp i form af advarsler<br />

116 Et eksempel-problem 3.2


om manglende initiering. Ligeledes hvis applikationen ikke frigiver hver eneste<br />

forekomst med kald til free_complex() når pointerens skop render ud, vil<br />

lageret fyldes op med "gamle" data. De fundamentale datatyper understøttes af<br />

sproget og allokeres/frigives automatisk. Det er selvfølgelig muligt at arbejde med<br />

forekomster af COMPLEX i funktionskaldene, hvorved alloc_complex() og<br />

free_complex() bliver unødvendiggjort, da C i så fald selv varetager allokering<br />

og deallokering. Straffen er, at hele datastrukturen kopieres fra den kaldende til den<br />

kaldte funktion. I dette eksempel ville to double'r kopieres, hvilket ikke synes<br />

meget, men hvis vi for eksempel arbejdede med matricer af 6 gange 6 = 36<br />

double'r, ville det tage lang tid at kalde en simpel funktion. Derfor arbejdes<br />

generelt med pointere i C.<br />

• Mellemregninger. Applikationen skal stå for alle eventuelle mellemregninger i<br />

flerledede udtryk med forekomster af typen COMPLEX, hvortil der må bruges<br />

midlertidige eller temporære variable. temp er et eksempel på dette i main().<br />

Udtryk med forekomster af fundamentale typer oversættes med implicitte<br />

midlertidige variable, som genereres og manipuleres af oversætteren. De midlertidige<br />

variable i main() kan ikke indeholdes i funktionsbiblioteket, fordi de skal dele<br />

arbejdsvariablenes skop - og det ligger i applikationen.<br />

• Dokumentation. Idet grænsefladen til complex.c indeholder et antal globale<br />

funktioner, hvis navne og parameterspecifikationer skal kendes af applikationen,<br />

kræver COMPLEX en omfattende dokumentation i anvendelsen af datatypen. Det<br />

kræver ydermere en del disciplin at overholde samme regler for brugen af datatyper i<br />

et funktionsbibliotek, hvis nye typer (for eksempel matricer eller vektorer)<br />

introduceres. Reglerne for funktionernes navne og parametre skal følges af<br />

designeren af biblioteket, og her hjælper sproget ikke spor.<br />

• Udvidelsesmuligheder. En udvidelse af biblioteket kan være svær, i visse tilfælde<br />

umulig, hvis det eksisterende bibliotek ikke lægger den rette platform til grund. Hvis<br />

vi for eksempel vil tilføje en funktion div_complex() til implementationen af<br />

COMPLEX-typen, skal en ny funktion skrives og dokumenteres. C sikrer imidlertid<br />

ikke, at implementationen og specifikationen har konsistens, hvilket gør en sådan<br />

udvidelse genstand for fejl. Det bliver værre endnu, hvis vi får lyst til at udvide<br />

biblioteket med funktioner til operationer mellem forekomster af typen COMPLEX<br />

og den fundamentale type double (en meget relevant udvidelse). Vi må udvide<br />

med en funktion for hver COMPLEX/double-operation, som alle skal have et unikt<br />

navn. Addition mellem en COMPLEX og en double erklæres for eksempel med<br />

navnet add_cmplx_dbl(), multiplikation med mul_cmplx_dbl() osv. Når<br />

vi så også vil kunne operere mellem COMPLEX og int, float og long samt<br />

eventuelle andre typedef'ede typer, får vi hundredevis af forskellige funktioner,<br />

som vi skal huske navnene og parameterindholdet af. Dette er grunden til, at Cbiblioteker<br />

har funktioner med underlige navne.<br />

3.2.2 Gennemgang af C-løsningen 117


• Sikkerhed. Mens fordelen med opdelingen af COMPLEX-typen og de relaterede<br />

funktioner i et separat modul er at skjule implementationen fra klienten, er det<br />

imidlertid et problem at en hvilkensomhelst klient har adgang til de "indre" data, som<br />

han eller hun alligevel aldrig behøver røre ved. Det betyder nemlig, at<br />

biblioteksfunktionerne måske fejler, hvis en forekomst af en type modificeres<br />

udenfor biblioteket. For typen COMPLEX er det ikke et problem, men hvis vi havde<br />

med dynamisk allokering at gøre (hvis en COMPLEX for eksempel indeholdt<br />

pointere), og et klient-program manipulerede med denne pointer, ville biblioteket<br />

givetvis fejle. Beskyttelse af data fra klienten (ikke mindst for dennes egen skyld) er<br />

derfor en stor fordel i forbindelse med sikkerhed i integriteten i de funktioner, der er<br />

helliget dem.<br />

Alle disse problemer er gode, datalogiske emner, der optager sprog- og oversætterdesignerne.<br />

Eksempel 3-2 (a-c) viser, hvordan vi i C kan udnytte mulighederne for modulærprogrammering<br />

ved at separere implementation (udbyderens kode) fra specifikation og<br />

applikation. Men sproget undersøtter ikke selv disse. Det gør dem kun mulige.<br />

C++ kan ikke trylle - der skal stadigvæk skrives kode for manipulation af komplekse tal i et<br />

bibliotek skrevet i C++. Til gengæld understøtter C++ flere af de opremsede problemer på en<br />

elegant måde. Vi gennemgår en implementation af programmet i C++ i flere trin for at se,<br />

hvor de forskellige problemer finder sine løsninger.<br />

3.3 KLASSE-BEGREBET<br />

Som beskrevet giver generelle datastrukturer kun en indholdsmæssig abstraktion -<br />

funktionerne til manipulation af strukturen skal stadig skrives separat. En fuldstændig<br />

abstraktion af et koncept skal derfor, for at kunne skabe en bedre modularitet og grænseflade,<br />

indeholde både<br />

• data (forekomster af datatyper eller afledte typer) og<br />

• metoder (medlemsfunktioner) til at manipulere disse.<br />

I C++ kan vi, som beskrevet i afsnit 2.8.5, definere funktioner som medlemmer af en<br />

datastruktur med den helt naturlige syntaks:<br />

struct min_struktur {<br />

int min_medlems_variabel;<br />

void min_medlems_funktion () {<br />

// ...<br />

}<br />

};<br />

Medlemsfunktionen kaldes med reference til en forekomst af den struktur, som den er<br />

118 Et eksempel-problem 3.2


medlem af, på samme måde som en medlemsvariabel refereres:<br />

min_struktur X;<br />

X.min_medlems_variabel = 5;<br />

X.min_medlems_funktion ();<br />

Begrebsverdenen i C++ definerer en struktur, som både indeholder datamedlemmer og<br />

funktionsmedlemmer som en klasse, da den indeholder al information til repræsentation og<br />

manipulation af et bestemt koncept. Vi klassificerer simpelthen vores kode og data som en<br />

type og samler den i en struktur, som sproget understøtter. Sagt på en anden måde<br />

indskrænker vi funktionernes skop til at ligge indenfor en bestemt navngiven blok, ligesom<br />

vores data. De bliver på den måde en del af et modul, som giver større frihed i<br />

programmeringen, end modul-opbygning i filer (som eksempel 3-2) gør.<br />

Første skridt på vejen i en C++-implementation af vores komplekse tal som abstrakt<br />

datatype er derfor at associere de funktioner, som opererer direkte på typen med selve typen:<br />

struct Complex {<br />

double re, im;<br />

void assign (double = 0, double = 0);<br />

void assign (complex&);<br />

complex add (complex&);<br />

complex sub (complex&);<br />

complex mul (complex&);<br />

void print ();<br />

};<br />

Vi har flyttet funktionerne assign_complex(), add_complex(),<br />

sub_complex(), mul_complex() og print_complex() fra at være globale<br />

funktioner ind i selve definitionen af typen. Funktionernes kvalificerede navne er nu<br />

Complex::assign(), Complex::add(), Complex::sub(),<br />

Complex::mul() og Complex::print(), og vi ser, at skopet på deres navne, ligesom<br />

medlemsvariablene, ligger indenfor strukturen Complex. Hvor vi før kaldte funktionerne<br />

mul_complex(), add_complex() og sub_complex() med pointere til forekomster<br />

af komplekse tal, kalder vi nu funktionerne med underforstået reference til den forekomst, vi<br />

ønsker at operere på. Complex::assign() modtager stadig blot en forekomst. En<br />

erklæring og tildeling af et komplekst tal foretages nu således:<br />

Complex a;<br />

a.assign (0, 0);<br />

Hvad indeholder en forekomst af typen Complex da nu? Med både data- og<br />

funktionsmedlemmer kan det være svært at se, hvordan en Complex egentlig allokeres.<br />

Men i virkeligheden er det en simpel procedure, idet alle funktionerne arbejder på<br />

forekomster af en ensartet struktur, ændrer de sig ikke fra forekomst til forekomst. Derfor vil<br />

3.2.2 Gennemgang af C-løsningen 119


C++ ved erklæringen<br />

Complex a;<br />

allokere plads til data-delen af typen Complex, dvs. to forekomster af double.<br />

Funktionerne findes kun én gang, og arbejder identisk på alle forekomster af Complex:<br />

Complex a, b; // to komplekse tal<br />

a.assign (0, 0); // kald complex::assign() for a<br />

b.assign (0, 0); // samme kald, men for b<br />

For hvert kald til assign() giver C++ ekstra information til funktionen om, hvilken<br />

forekomst af Complex, der er tale om. Der er altså ikke tale om noget overhead i lageret<br />

ved at bruge funktioner som medlemmer af strukturer.<br />

3.3.1 Medlemsfunktioner<br />

Den ovenstående struct Complex indeholder kun erklæringer af funktioner.<br />

Implementationerne af de faktiske funktioner, som i og for sig er identiske med dem, vi så i<br />

eksempel 3-2b, kan skrives på to måder:<br />

• Eksternt definerede medlemsfunktioner skrives udenfor selve klassedefinitionen<br />

(struct-blokken) på et senere tidspunkt i kildeteksten. Sammenhængen med den<br />

specifikke klasse udtrykkes med skop-opløsningsoperatoren (::).<br />

• Internt definerede medlemsfunktioner skrives i selve klassedefinitionen, og står<br />

dermed direkte i struct-blokken.<br />

Forskellen er, om funktionen skal være inline eller ej (afsnit 2.6.12). Hvis funktionens<br />

krop skrives som intern funktion, vil oversætteren erstatte funktionskaldet med den faktiske<br />

kode i funktionen, mens eksternt definerede funktioner kaldes på normal vis.<br />

Reglerne for hvilke funktioner, der skal være inline, er de samme som for normale<br />

funktioner: fylder funktionen meget (har den for eksempel løkker eller andre<br />

kontrolstrukturer) og kaldes den meget ofte, bør den ikke være inline. Hvis funktionen<br />

derimod foretager sig meget lidt (for eksempel et par tildelinger), er det en god idé at skrive<br />

den inline. Samspillet mellem antallet af kald og størrelsen på funktionen afhænger af<br />

applikationen og systemet. Generelt kan det dog slås fast, at jo sjældnere en funktion kaldes,<br />

jo mindre er incitamentet for at skrive den inline, fordi det ikke har væsentlig betydning<br />

for programmets effektivitet.<br />

For vores klasse Complex er assign() en oplagt kandidat for inline-funktion,<br />

fordi den blot tildeler værdier til de to medlemsvariable re og im:<br />

struct Complex {<br />

120 Klasse-begrebet 3.3


double re, im;<br />

void assign (double r, double i) {<br />

re = r, im = i;<br />

}<br />

// ...<br />

};<br />

mens for eksempel print() kan være en normal ikke-inline funktion:<br />

void Complex::print () {<br />

printf ("(%f,%f)", re, im);<br />

}<br />

Det er også muligt at skrive en inline-funktion udenfor struct-blokken, hvilket gøres,<br />

hvis man ønsker at holde selve definitionen på klassen rimelig overskuelig:<br />

inline void Complex::assign (double r, double i) {<br />

re = r, im = i;<br />

}<br />

Denne implementation af Complex::assign() er identisk med den ovenstående<br />

implementation. Forskellen er kun syntaksmæssig.<br />

3.3.2 Underforståede medlemsreferencer<br />

Vi har lige set, hvordan funktionerne Complex::assign() og Complex::print()<br />

henviser til medlemsvariablene re og im uden brug af medlemsreference-operatoren. Det<br />

lader sig gøre, fordi funktionen af oversætteren får den nødvendige information om, hvilken<br />

forekomst af Complex den arbejder på. Når klienten kalder<br />

a.assign (0, 0);<br />

bliver den underforståede reference til medlemmerne i Complex::assign() opfattet<br />

som<br />

a.re = 0, a.im = 0;<br />

Funktionerne ved altså hvilke forekomster, de har med at gøre, da C++ vedligeholder en<br />

underforstået reference til den aktuelle forekomst. Denne reference hedder this og er en<br />

pointer til den aktuelle forekomst - eller med andre ord det underforståede objekt. Når en<br />

medlemsfunktion refererer en medlemsvariabel fortolker C++ denne reference som<br />

underforstået peget på af this:<br />

3.3.1 Medlemsfunktioner 121


inline void Complex::assign (double r, double i) {<br />

re = r, im = i; // dss. this->re = r, this->im = i<br />

}<br />

this kan bruges af medlemsfunktioner, men er unødvendig i reference af forekomster af<br />

medlemsvariable. Den bruges mest som reference til hele forekomsten, for eksempel som<br />

returværdi. Mere om this senere i afsnit 3.5.18.<br />

3.3.3 Overstyrede funktionsnavne<br />

Reglerne for overstyring af funktionsnavne, som de er beskrevet i afsnit 2.6.9 gælder også for<br />

funktioner, som tilhører en klasse. Det tilladt på samme måde at skrive to eller flere<br />

funktioner af samme navn, men med forskellige antal og/eller typer af parametre. Medlemsfunktionen<br />

Complex::assign() er en god kandidat for overstyring:<br />

struct Complex {<br />

double re, im;<br />

void assign (double r, double i) { re = r, im = i; }<br />

void assign () { re = im = 0; }<br />

void assign (Complex& c);<br />

// ...<br />

};<br />

Hermed kan klienten kalde Complex::assign() med eller uden parametre efter behov:<br />

Complex a, b, c;<br />

a.assign (0, 5.25);<br />

b.assign ();<br />

c.assign (b);<br />

I specifikationen af Complex ser vi, at der findes to funktioner assign(). Den ene<br />

tildeler en complex en reel og en imaginær del, den anden tildeler en complex værdien<br />

af en anden Complex.<br />

3.3.4 Underforståede parametre<br />

Funktioner med underforståede parametre (afsnit 2.6.10) er en anden måde at give klienten<br />

flere måder at komme til samme data på. Funktionen complex::assign() kan skrives<br />

med underforståede parametre med samme fordele som en overstyring af funktionen:<br />

122 Klasse-begrebet 3.3


struct Complex {<br />

double re, im;<br />

void assign (double r = 0, double i = 0) {<br />

re = r, im = i;<br />

}<br />

// ...<br />

};<br />

Det giver endda større frihed for klienten, som kan tildele på forskellig vis ved at kalde<br />

assign() på følgende måder:<br />

Complex a, b, c;<br />

a.assign (6, 2.5); // sæt a.re til 6, a.im til 2.5<br />

b.assign (8); // sæt b.re til 8, b.im til 0<br />

c.assign (); // sæt både c.re og c.im til 0<br />

Husk, at kun de sidste parametre kan være underforståede:<br />

void assign (double r, double i = 0); // lovlig<br />

void assign (double r = 0, double i); // ulovlig<br />

3.3.5 Indkapsling<br />

struct-nøgleordet definerer en datastruktur med både funktioner og data, og skjuler dermed<br />

en vigtig del af de indre manipulationer af typen fra klienten. Vi har imidlertid et potentielt<br />

problem: idet medlemsvariablene i en datastruktur er tilgængelige for klienten ved brug af<br />

medlemsreference-operatoren (.), kan en medlemsfunktion ikke være sikker på, hvordan<br />

medlemsvariablene ser ud.<br />

Normalt bæres information fra medlemsfunktion til medlemsfunktion i medlemsvariable.<br />

For eksempel kunne en medlemsvariabel i en klasse for lageradministration specificere<br />

antallet af allokerede lagerenheder:<br />

struct Memory {<br />

void* mem_ptr; // addresse på det allokerede lager<br />

int size; // størrelse på blokken<br />

void alloc (int n); // allokér lager (n bytes)<br />

void free (); // frigiv lageret igen<br />

void init (); // nultil hele lageret<br />

char* where (); // fortæl, hvor lageret ligger<br />

};<br />

3.3.4 Underforståede parametre 123


Medlemsfunktionerne i denne klasse bruger informationen i variablene mem_ptr og size<br />

til at henvise til hvor lageret befinder sig og hvor meget, der er allokeret.<br />

Memory::alloc() sætter de to variable op, og de andre funktioner bruger dem, når de<br />

senere kaldes. Memory::where() returnerer for eksempel adressen på det allokerede<br />

lager og Memory::init() nulstiller hele den blokken.<br />

Klienten skal først erklære en forekomst af Memory og kalde alloc():<br />

Memory diskBuffer;<br />

diskBuffer.alloc (4000);<br />

Hvis klienten nu pludselig tildeler diskBuffer.size en vilkårlig værdi med<br />

diskBuffer.size = 8000; // ups!<br />

og derefter kalder<br />

diskBuffer.init ();<br />

vil klassen skrive til dobbelt så meget lager, som den har allokeret, og med sikkerhed generere<br />

en ulovlig lagerreference og/eller tvinge systemet i knæ. Problemet er, at klienten gør sig<br />

afhængig af implementationen af klassen, og spolerer dermed hele ideen bag at skjule de til<br />

klassen tilhørende variable. Selvom klienten ikke udøver en så fatal gerning, kan det have<br />

andre konsekvenser at gøre sig afhængig af implementationen. For når det kommer til stykket,<br />

kan udbyderen af klassen Memory på et senere tidspunkt bestemme sig for at omdefinere<br />

betydningen af sine funktioner og data - for eksempel kunne size indeholde antallet af 16byte<br />

blokke i stedet for rene bytes. I så fald ville klientens kode, fordi den gør sig<br />

implementations-afhængig, fejle ved en genoversættelse. Dette gælder faktisk alle klasser:<br />

måske ville vi få lyst til at ændre det format, vi internt repræsenterede en Complex på, til<br />

float-typer eller angivelser af brøker. Hvis klientens kode eksplicit refererer til re og im,<br />

vil den ikke kunne bruge en ny version af biblioteket.<br />

Dette er en del af diskussionen om grænseflader. Der er brug for beskyttelse af de intimere<br />

dele af en klasse, så klienten ikke kan komme til dem. Vi er interesserede i at indkapsle data,<br />

som "ejes" af klassen, så ingen andre funktioner, end lige præcis dem vi tillader, kan røre ved<br />

dem. Denne indkapsling kendes delvis fra C, Pascal og andre gængse sprog ved deres<br />

muligheder indenfor modulærprogrammering ved at opdele et program i separate kildefiler,<br />

mens mere moderne sprog som C++, Ada og Modula-2 imidlertid giver os en eksplicit<br />

metode til at beskrive den. Det drejer sig om at beskrive hvilke dele af klassen, som ikke skal<br />

ses udefra - som skal være private.<br />

I C++ er nøgleordene public og private † løsningen på problemet. Med public<br />

kan vi angive, at efterfølgende erklæringer på data og funktioner kan bruges direkte i<br />

referencer fra klienten, mens private betyder, at de kun må bruges i de funktioner, som er<br />

† Et tredie niveau, protected, bruges i forbindelse med objekt-orienteret programmering til definition af<br />

adgangskriterier mellem to klasser og beskrives i <strong>kapitel</strong> 5.<br />

124 Klasse-begrebet 3.3


medlemmer af klassen †† . Complex-klassen kan nu skjule de to indre variable re og im<br />

fra klienten ved at indkapsle dem således:<br />

struct Complex {<br />

private: // privat del - kun tilgængelig fra<br />

double re, im; // medlemsfunktionerne<br />

public:<br />

void assign (double = 0, double = 0);<br />

void assign (Complex&);<br />

complex add (Complex&);<br />

complex sub (Complex&);<br />

complex mul (Complex&);<br />

void print ();<br />

};<br />

En klient af Complex kan nu kun ændre på re og im i en forekomst af typen ved at kalde<br />

en medlemsfunktion. Hvis klientprogrammet indeholder en eksplicit reference til en af<br />

variablene vil det resultere i en oversætterfejl - navnet er simpelthen ikke kendt i klientens<br />

funktion.<br />

De fleste klasser har private medlemmer. C++ har af denne grund en variant af struct,<br />

som fra starten af blokken erklærer medlemmer som private. Varianten hedder class<br />

og opfører sig bortset fra denne detalje akkurat som struct:<br />

class Complex {<br />

double re, im; // private data<br />

public:<br />

void assign (double = 0, double = 0);<br />

void assign (Complex&)<br />

complex add (Complex&);<br />

complex sub (Complex&);<br />

complex mul (Complex&);<br />

void print ();<br />

};<br />

Tilgangs-nøgleordene kan forekomme flere gange i specifikationen af en klasse:<br />

class X {<br />

public:<br />

int x, y; // public data<br />

private:<br />

float f; // private data<br />

†† Vi kan også specifikt give en global funktion rettigheder til de indre variable med friend-nøgleordet.<br />

friend-funktioner beskrives i afsnit 3.5.11 og friend-klasser i næste <strong>kapitel</strong>.<br />

3.3.5 Indkapsling 125


private:<br />

long l; // flere private data<br />

};<br />

Indkapsling giver altså abstrakte datatyper den klare fordel, at vi kan separere<br />

implementationen fra specifikationen i en klasse, som derfor sikrer, at ændringer i klassen<br />

ikke får betydning for klientprogrammet. Det har ydermere den anden fordel, at al kode, som<br />

arbejder på medlemsvariable af en bestemt klasse findes i selve klassen, så lokalisering af fejl<br />

bliver lettere.<br />

3.3.6 struct, class og union<br />

En struct i C++ er altså det samme som en class, men alle medlemmer af strukturen er<br />

public. struct kan bruges, når skjulning af data ikke er betydningsfuld:<br />

struct { // ...<br />

giver samme resultat, og er simpelthen en kortere måde at udtrykke<br />

class { public: // ...<br />

En union er også en klasse i C++. union'er er identiske med struct'er, med den<br />

væsentlige forskel, at alle datamedlemmer i union'en starter på samme adresse. Størrelsen<br />

på en union er altså størrelsen på det største datamedlem. Der er imidlertid nogle<br />

potentielle problemer med union'er, fordi oversætteren ikke kan type-checke brugen af<br />

dem, da de jo per definition er tvetydige (se afsnit 2.8.2).<br />

En union kan indeholde funktioner, som gør det lettere for en klient at komme til de<br />

forskellige medlemmer. For eksempel kan vi definere en union til at indeholde en værdi i et<br />

medlem af en symboltabel - enten en int, en long eller en double:<br />

union value {<br />

int i_value;<br />

long l_value;<br />

double d_value;<br />

void assign (int i) { i_value = i; }<br />

void assign (long l) { l_value = l; }<br />

void assign (double d) { d_value = d; }<br />

};<br />

Funktioner som medlemmer af union'er har kun meget få rigtig brugbare anvendelser - de<br />

hjælper ikke med problemerne med identifikation af den aktuelle type, der ligger i en<br />

126 Klasse-begrebet 3.3


forekomst af en union på et givet tidspunkt. For at identificere den aktuelle status i en<br />

forekomst af en union, dvs. spørgsmålet om hvilken af de indeholdte variable som sidst<br />

blev tildelt en værdi, må denne information være indeholdt i en anden variabel. Det er muligt<br />

at skrive en diskriminerende union, som indkapsles i en klasse sammen med den aktuelle<br />

type-information samt funktioner til korrekt ind- og udlæsning af union'en.<br />

3.3.7 Klasser og skop<br />

Vi har set, hvordan en medlemsfunktion refererer medlemsdata underforstået. I afsnit 2.6.1 så<br />

vi, hvordan globale data af samme navn kan refereres med skop-opløsningsoperatoren, så<br />

tvetydigheder undgås:<br />

int a;<br />

void f () {<br />

int a;<br />

a = 1; // lokal a<br />

::a = 1; // global a<br />

}<br />

Ordentligt skrevne programmer har ikke mange globale variable, og skopopløsningsoperatoren<br />

bruges også mest i forbindelse med klasser. Her kan den bruges i<br />

forbindelse med både data og funktioner, og oftest sidstnævnte:<br />

class X {<br />

void print (); // X::print() prototype<br />

};<br />

void print (); // global print() prototype<br />

void X::print () {<br />

::print (); // kalder global print()<br />

}<br />

Faldgruben i dette er, hvis man glemmer skop-operatoren. Sker dette for eksempel i kaldet til<br />

::print() i X::print(), vil det resultere i en deadlock, fordi ::print() rekursivt<br />

kalder sig selv igen og igen med et uafvendeligt stak-overløb til følge. Evalueringen af en<br />

reference til en funktion eller dataelement i en klasse er: Først undersøges, om funktionen<br />

findes i klassens skop med en præcist passende parameterliste (eventuelt overstyret og/eller<br />

3.3.6 struct, class og union 127


med underforståede parametre) eller type, hvorefter det globale skop søges. Passer ingen af<br />

disse, forsøges andre navne (i samme rækkefølge) med passende underforståede<br />

typekonverteringer.<br />

3.3.8 Private medlemsfunktioner<br />

Medlemsfunktioner kan også erklæres private. En private medlemsfunktion kan ikke<br />

kaldes fra klienten, men kun fra andre medlemsfunktioner. Den umiddelbare grund til at<br />

bruge private medlemsfunktioner er, at en service-funktion, som bruges i flere<br />

medlemsfunktioner, kan skjules helt fra andre dele af programmet. Hvis funktionen - for<br />

eksempel en formateringsrutine - skulle være global, ville en stor del af ideen med<br />

indkapsling forsvinde. Hvis den er public i den aktuelle klasse, ville den godt nok være<br />

indkapslet, men kunne alligevel kaldes fra klienten.<br />

I den offentlige administration har embedsmændende en speciel regel for tilgang til<br />

information, som kaldes "need-to-know". Det betyder, at en embedsmand kun kan stille<br />

spørgsmål, hvis hans arbejdsfunktion kræver svaret - ellers er der ingen grund til at spørge;<br />

det kommer ham simpelthen ikke ved. Den samme regel gælder for medlemsfunktioner i<br />

klasser: hvis en klient ikke har brug for at kalde en bestemt medlemsfunktion, skal denne<br />

være private, på samme "need-to-know"-basis.<br />

3.3.9 Klasse-baseret vs. objekt-baseret indkapsling<br />

Medlemsfunktionerne, som de ses i specifikationen af klassen Complex, indeholder<br />

parameterlister med kun en enkelt reference til en Complex. Reference-operatoren gør det<br />

lettere at kalde funktionen, fordi vi ikke behøver at tage adressen af den Complex, vi ønsker<br />

at kalde funktionen med, i selve funktionskaldet. Hvis vi ønskede at bruge samme<br />

kaldemetode som i eksempel 3-2, skulle implementationen af funktionen arbejde udtrykkeligt<br />

med pointere. Et kald til add() vil se sådan ud:<br />

a.add (&b); // kald med adressen på b<br />

Reference-operatoren giver, som beskrevet i afsnit 2.11.13, en bedre mulighed for automatisk<br />

at referere et pointerobjekt uden at skulle dereferere (tage adressen på) objektet eksplicit.<br />

Grunden til, at funktionen add() kun modtager et enkelt parameter er, at det andet led i<br />

additions-operationen specificeres i kaldet og derfor er underforstået for funktionen. add()<br />

kaldes med eksplicit reference til en forekomst (det ene led) og med en parameter (det andet<br />

led). Men add() skal have adgang til de private variable for begge forekomster for at<br />

foretage en addition.<br />

Det har den også. Indkapsling i C++ er klasse-baseret, fordi en medlemsfunktion har tilgang<br />

til private medlemmer i enhver forekomst af den pågældende klasse, og ikke blot den<br />

specifikke forekomst, som den underforstået kaldes med. Complex::add() kan<br />

manipulere med private data i alle forekomster af complex, sålænge den har forekomster at<br />

128 Klasse-begrebet 3.3


arbejde med.<br />

Implementationen af Complex::add() ser derfor således ud:<br />

// addition af komplekst tal<br />

Complex Complex::add (Complex& c) {<br />

Complex temp; // et midlertidigt komplekst tal<br />

temp.re = re + c.re; // addér de reelle dele<br />

temp.im = im + c.im; // addér de imaginære dele<br />

return temp; // returner resultatet<br />

}<br />

Complex::add() bruger ganske simpelt medlemsreference-operatoren for at komme til de<br />

data, som privat er indeholdt i c. Sprog som Smalltalk er mere restriktive, idet de kun tillader<br />

medlemsfunktioner adgang til den specifikke forekomst, de arbejder på. For at få tilgang, må<br />

funktionen kalde en bestemt adgangsfunktion i samme klasse for den anden forekomst. Vi<br />

siger, at Smalltalk har objekt-baseret indkapsling, mens C++ bruger klasse-baseret eller typebaseret<br />

indkapsling.<br />

3.3.10 Klasser som moduler<br />

Det åbenlyse eksempel på en klasse, som bruges som eksempel her i kapitlet, er en numerisk<br />

datatype. Men klasser kan også bruges til at indkapsle processer i C++. I mange tilfælde kan<br />

vi drage fordel af, at en klasse giver os et begrænset skop for medlemmerne, idet vi kan<br />

anskue en klasse som et "miniature-program":<br />

enum colour_t {<br />

black, blue, red, magenta, green, cyan, yellow, white<br />

};<br />

struct ansi_util {<br />

void cls ();<br />

void home ();<br />

void colour (colour_t);<br />

void clrline ();<br />

void goto (int, int);<br />

};<br />

Denne klasse indeholder kun funktioner, og ingen data. Vi kan se denne klasse som en kapsel<br />

for et antal relaterede funktioner, som her sender ANSI-koder til for eksempel en terminal.<br />

Alle funktionerne kaldes med skop-reference til klassenavnet ansi_util, og gør det<br />

dermed let at se, hvilken gruppe - eller modul - funktionen tilhører. Dette er det tætteste, C++<br />

kommer på regulær modulærprogrammering:<br />

3.3.9 Klasse-baseret vs. objekt-baseret indkapsling 129


ansi_util::cls ();<br />

ansi_util::colour (white);<br />

Vi indkapsler ganske simpelt et antal funktioner i en klasse, men erklærer ikke nogen<br />

forekomster af klassen. Den eneste fordel er, at funktionsnavnene kan gøres rimelig generelle,<br />

og at andre "moduler" - for eksempel en ibm_util - kan skrive med samme<br />

funktionsnavne.<br />

3.3.11 Indkapslede typeerklæringer<br />

Klasser kan erklæres inde i hinanden, og en klasse, der er indkapslet i en anden, kaldes for en<br />

indlejret klasse. Indlejrede klasser er lokale til den klasse, de erklæres i og er skjult fra resten<br />

af programmet. Dette er en afvigelse fra tidligere versioner af C (før 3.0), hvor en indlejret<br />

type altid var global, hvilket skabte problemer, hvis den havde associerede symbolske<br />

konstanter, som, når de er defineret i klasser, er lokale til disse. Den nye standard betyder, at<br />

C++ understøtter lokale skop for navne såvel som for objekter, en facilitet, der har vist sig<br />

uundværlig i objekt-orienteret programmering. Overvej følgende klasseerklæringer:<br />

class X { // en klasse<br />

class X { // en indlejret klasse<br />

int i;<br />

};<br />

public:<br />

class Y { // en anden indlejret klasse<br />

int i;<br />

};<br />

};<br />

X x; // lovlig, X er i det globale navneskop<br />

Y y; // ulovlig, Y er i X's skop<br />

Z z; // ulovlig, Z er i X's skop (og er privat)<br />

For at erklære en forekomst af Y må vi benytte skop-operatoren på samme måde, som når vi<br />

erklærer medlemsfunktioner eksternt:<br />

X::Y y; // ok: X::Y er tilgængelig for klienten<br />

X::Z z; // fejl: X::Z er privat<br />

Hvis der forekommer indlejringer i flere led, som i<br />

struct A {<br />

struct B {<br />

struct C {<br />

130 Klasse-begrebet 3.3


};<br />

};<br />

};<br />

skal skop-operatoren benyttes hele vejen igennem til det kvalificerede navn på den indlejrede<br />

type, der ønskes:<br />

A::B::C c;<br />

Det skal bemærkes, at både den indre og den ydre klasse følger de helt normale regler for<br />

adgang til medlemsvariable i andre klasser. De har således ikke specielle friheder til at røre<br />

ved data i hinanden, uanset at de har et fælles skop. Med andre ord, en indlejret klasse har<br />

ikke noget typeforhold med den ydre klasse, og vice versa, så programmøren må selv sikre<br />

sig, at de har et egentligt objekt at arbejde på af den anden klasse. I de følgende erklæringer<br />

kan det ses, hvilke objekter de indlejrede klasser har adgang til.<br />

int i; // global i<br />

class ydre {<br />

int i; // privat ydre::i<br />

public:<br />

int j; // public ydre::j<br />

static k; // statisk ydre::k<br />

class indre {<br />

int l; // privat ydre::indre::l<br />

void f () {<br />

i = 0; // fejl: hvilken i?<br />

j = 0; // fejl: hvilken j?<br />

k = 0; // ok: ydre::k er nærmest<br />

l = 0; // ok: indre::l findes i denne f()<br />

::i = 0; // ok: global i er i f()'s skop<br />

}<br />

void g (ydre y) {<br />

y.i = 0; // fejl: ydre::i er privat<br />

y.j = 0; // ok: ydre::j er public<br />

}<br />

};<br />

void f () {<br />

l = 0; // fejl: indre::l, har ej objekt<br />

}<br />

};<br />

3.3.11 Indkapslede typeerklæringer 131


I ydre::indre::f() kan vi se, hvordan det ikke er lovligt at forsøge at få adgang til<br />

hverken private eller public dele af den omsluttende klasse ydre. indre har med<br />

andre ord ingen særrettigheder til data (eller funktioner) i ydre selv om den er erklæret i<br />

denne klasse. Kun statisk erklærede data i den (eller de) omsluttende klasser og/eller globale<br />

data kan tilgås fra de indlejrede klasser. Funktioner i indlejrede klasser skal have et konkret<br />

objekt at arbejde på - en indlejret klasse specificerer ikke et typeforhold, blot et leksikalt skop<br />

for klassen. I ydre::indre::g() modtager funktionen en forekomst af ydre, hvilken<br />

den kan arbejde på uden bekymringer så længe reglerne for private data ikke overtrædes. I<br />

samme åndedrag skal det nævnes, at den omsluttende klasse heller ikke har adgang til<br />

medlemmerne i den indlejrede klasse. Der bliver således ikke instansieret et objekt af den<br />

indlejrede klasse, når et objekt af den omsluttende klasse instansieres. Det er grunden til, at<br />

ydre::f() ikke har adgang til ydre::indre::l - funktionen ved simpelthen ikke,<br />

hvilket objekt, der refereres til.<br />

Disse regler for indkapsling af navne gælder også for enum, union og særligt for<br />

typedef.<br />

3.3.12 Anonyme klasser<br />

Undertiden har vi brug for kun en enkelt forekomst af en given klasse, for eksempel en<br />

printer-driver eller en fejl-opsamlings-klasse. Dette gøres med en erklæring af en klasse uden<br />

typenavn, men med en samtidig erklæring af en forekomst. C-programmører vil være bekendt<br />

med dette fra struct-erklæringer så som<br />

struct {<br />

int count; // antal af fejl<br />

int last_error; // sidste fejl<br />

void (*handler) () // pointer til fejl-funktion<br />

} error_handler;<br />

Fremgangsmåden for klasser er den samme som den for struct, enum og union, som<br />

er beskrevet i afsnit 2.7.3. I C++ kan klasser erklæres uden typenavn på præcis samme facon:<br />

class {<br />

int count;<br />

int last_error;<br />

void (*handler) ();<br />

public:<br />

void set_handler ((*fptr) ()) {<br />

handler = fptr;<br />

}<br />

132 Klasse-begrebet 3.3


void handle (int errno) {<br />

count++;<br />

last_error = errno;<br />

(*handler) ();<br />

}<br />

} error;<br />

Der findes kun ét objekt af denne unavngivne type, kaldet error. Klienten kan kalde<br />

funktionerne i error på normal vis, for eksempel med<br />

error.set_error (myhandler);<br />

error.error (12);<br />

men kan ikke skabe nye forekomster af typen. Der er imidlertid en lille ulempe ved denne<br />

måde at skabe enkelt-instanser, nemlig at det er umuligt at skrive konstruktører og<br />

destruktører - automatiske funktioner til opsætning og oprydning - som beskrives nærmere i<br />

afsnit 3.4.<br />

3.3.13 Statiske medlemsvariable<br />

Klasser kan have statiske medlemsvariable, erklæret med static-nøgleordet. Statiske<br />

medlemsvariable opfører sig på den måde, at alle forekomster af klassen deler samme<br />

variabel. Følgende eksempel erklærer to forekomster af en klasse med en statisk og to<br />

normale ikke-statisk variable:<br />

class error {<br />

int last_error;<br />

void (*handler) ();<br />

public:<br />

static count; // int er underforstået her<br />

void set_handler ((*fptr) ()) {<br />

handler = fptr;<br />

}<br />

void handle (int errno) {<br />

count++, last_error = errno;<br />

(*handler) ();<br />

}<br />

};<br />

error disk_error, keyboard_error;<br />

De to forekomster af error, disk_error og keyboard_error har hver sine<br />

last_error og handler medlemmer, men deler count-variablen.<br />

3.3.12 Anonyme klasser 133


Figur 3-1: Ejerforhold for statisk medlem count.<br />

Idet der kun findes en enkelt kopi af statiske medlemsvariable, vil de blive oprettet ved<br />

programmets start. Deres levetid er altså hele programmet igennem, ligesom for statiske<br />

variable i funktioner samt globale variable. Af den grund er det muligt at tildele en statisk<br />

variabel i en klasse en værdi uden at gå igennem en forekomst af klassen, medmindre den er<br />

private:<br />

error::count = 0;<br />

int a = error::count;<br />

Statiske medlemsvariable kan ikke initieres med samme syntaks som andre statiske variable,<br />

dvs. med en samtidig tildeling af en værdi. Initiering af statiske medlemsdata foregår med en<br />

normal erklæring i det globale skop, normalt i implementationsfilen af klassen, for eksempel<br />

som<br />

int error::count = 0;<br />

Denne erklæring og initiering virker både for private- og public-erklærede statiske<br />

medlemsvariable. Det bør i øvrigt bemærkes, at C++ garanterer binær nulstilling af alle<br />

uinitierede statiske variable.<br />

Et alternativ til statiske medlemsdata er normale statiske ikke-medlems-variable med<br />

globalt skop i implementationsfilen for en given klasse. En sådan variabel vil være indkapslet<br />

i denne fil, og vil ikke være kendt i andre filer. Fordelen ved dette er, at det kun kræver en<br />

modifikation af implementationsfilen (.cpp-filen), ikke specifikationsfilen (headerfilen .h),<br />

hvis der skal foretages ændringer ved den statiske variabel. Dette går selvfølgelig ikke, hvis<br />

inline-funktioner, som står i specifikationsfilen bruger den statiske variabel, og at alle<br />

medlemsfunktioner, som refererer den statiske variabel kan placeres i .cpp-filen. Dermed<br />

skal kun implementationsfilen genoversættes - ellers skal alle filer, som er afhængig af<br />

specifikationen af klassen oversættes igen.<br />

3.3.14 Statiske metoder<br />

134 Klasse-begrebet 3.3


En klasse kan også indeholde statiske medlemsfunktioner. Disse har samme privilegier hvad<br />

angår normale funktioner, men med den forskel, at de ikke kan kaldes med reference til en<br />

forekomst af klassen med . eller -> operatorerne. En statisk medlemsfunktion har intet<br />

underforstået objekt og har derfor ingen this-pointer. Den har kun adgang til alle private<br />

dele af klassen, hvis den på en eller anden måde får en konkret forekomst af klassen at arbejde<br />

med.<br />

En statisk medlemsfunktion erklæres med nøgleordet static:<br />

class X {<br />

int i;<br />

public:<br />

static X* readFrom (ostream& input) {<br />

X* xx = new X;<br />

input >> xx->i;<br />

return xx;<br />

}<br />

};<br />

Fordelen ved statiske medlemsfunktioner er, at de kan kaldes uden at der findes en forekomst<br />

af klassen. Ovenstående klasse X har en statisk medlemsfunktion readFrom(), som<br />

indlæser et objekt af typen X. Hvis vi ønsker at indlæse en objekt af typen X uden først at<br />

skulle oprette en forekomst af X, skal vi bruge en funktion vi kan kalde, som har adgang til<br />

de private dele af klassen (husk, at C++ bruger klassebaseret indkapsling), men som ikke<br />

arbejder på en bestemt forekomst af klassen.<br />

Den statiske medlemsfunktion X::readFrom() kan kaldes fra et brugerprogram på<br />

følgende måde:<br />

void main () {<br />

cout


sikrer, at bibliotekets data er konsistente.<br />

struct ansiUtil {<br />

static int cursor_x, cursor_y;<br />

static void cursor_on ();<br />

static void cursor_off ();<br />

static void cls ();<br />

};<br />

ansiUtil::cls ();<br />

ansiUtil::cursor_x = 0, ansiUtil::cursor_y = 0;<br />

ansiUtil::cursor_on ();<br />

Alle data og funktioner udvides syntaksmæssigt med det kvalificerede navn på det "modul",<br />

som de tilhører. Dermed er det en let at se, hvor de hører til og dermed sværere at bruge dem<br />

forkert.<br />

3.3.15 Konstante objekter<br />

Brugbarheden af konstanter blev behandlet i afsnit 2.4.6, hvor det blev gjort klart, at en<br />

const X og en X er to forskellige typer. Konstante forekomster af brugerdefinerede typer<br />

har præcis samme anvendelighed:<br />

class hvadsomhelst { /* ... */ };<br />

void f (const hvadsomhelst& x) {<br />

hvadsomhelst y;<br />

x = y; // fejl: kan ikke tildele konstant<br />

hvadsomhelst t = x; // ok: kan godt kopiere konstant<br />

hvadsomhelst* p = &x; // fejl: kan ikke tage x' adresse<br />

// ...<br />

}<br />

Men hvad sker der, hvis klienten kalder en funktion i et konstant objekt, som ændrer på<br />

objektets indre status? En sådan operation ændrer ikke åbenlyst på objektet, da der ikke<br />

forekommer tildelinger af nogen art. En lille klasse kan illustrere dette:<br />

class intVec { // liste af int<br />

int *rep, antal;<br />

public:<br />

intVec (int i) { rep = new int [antal = i]; }<br />

~intVec () { delete rep; }<br />

void Add (int); // addér til vektor<br />

136 Klasse-begrebet 3.3


void Print ();<br />

// ...<br />

};<br />

void intVec::Add (int j) {<br />

for (int i = 0; i < antal; i++) rep [i] += j, j = rep<br />

[i];<br />

}<br />

void intVec::Print () {<br />

for (int i = 0; i < antal; i++) cout


public:<br />

intVec (int i) { rep = new int [antal = i]; }<br />

~intVec () { delete rep; }<br />

void Add (int) const; // addér til vektor<br />

void Print ();<br />

// ...<br />

};<br />

void intVec::Add (int j) const { // konstant funktion<br />

for (int i = 0; i < antal; i++) rep [i] += j, j = rep<br />

[i];<br />

}<br />

Denne funktion vil imidlertid ikke oversætte, fordi den ændrer i medlemsdata og samtidig er<br />

erklæret som konstant. Det giver også mening, idet en funktion som Add(), der ændrer i<br />

medlemsdata, ikke bør være konstant.<br />

Retningslinierne for brugen af konstante medlemsfunktioner er derfor simple nok: hvis en<br />

metode ikke ændrer i objektets data, bør den erklæres const, for at tillade kald til metoden<br />

for et objekt af klassen, som er erklæret som en konstant. Det giver den størst mulige frihed<br />

for konstanter af brugerdefinerede typer. Her er det vigtigt at huske, at selvom en const<br />

ikke kan typekonverteres hverken underforstået eller tvungent til en ikke-const, er det<br />

omvendte tilladt:<br />

void f (const int x) { /* ... */ }<br />

void g () {<br />

int y = 10;<br />

f (y); // y konverteres til const int<br />

}<br />

Alle metoder, som er erklæret konstante i en klasse kan derfor bruges med reference til både<br />

konstante og ikke-konstante forekomster. intVec-klassen kan for eksempel indeholde en<br />

Hash()-funktion, som returnerer en indeks-værdi til en søgetabel:<br />

unsigned intVec::Hash () const {<br />

unsigned r = 0;<br />

for (int i = 0; i < antal; i++) r ^= (unsigned) rep [i];<br />

return r;<br />

}<br />

Denne metode kan kaldes for både konstante og ikke-konstante forekomster af intVec<br />

uden komplikationer:<br />

138 Klasse-begrebet 3.3


typedef bool int;<br />

bool hashCompare (const intVec& s) {<br />

intVec t;<br />

// ... indlæs t ...<br />

return s.Hash () > // Hash() kaldes for konstant objekt<br />

t.Hash (); // Hash() kaldes for normalt objekt<br />

}<br />

Læg mærke til, at den konstante metode kun har betydning for data, som er medlemmer af<br />

klassen. Parametre, som eventuelt overføres til en konstant metode, er ikke konstante<br />

medmindre de udtrykkeligt erklæres som sådanne. I følgende funktion<br />

bool intVec::Compare (intVec& other) const {<br />

bool r = 0;<br />

for (i = 0; i < antal; i++) r |= rep [i] != other.rep<br />

[i];<br />

return r;<br />

}<br />

bliver rep, altså det underforståede objekts vektor af heltal, til en const int* const<br />

og kan ikke hverken ændre sin egen værdi eller de data, den peger på. other er imidlertid<br />

en ikke-konstant parameter til funktionen Compare(), så other.rep er en ikkekonstant<br />

vektor, da den ikke er et medlem af det aktuelle objekt. Det er nødvendigt at erklære<br />

denne som konstant parameter for også at sikre other som konstant:<br />

bool intVec::Compare (const intVec& other) const {<br />

bool r = 0;<br />

for (i = 0; i < antal; i++) r |= rep [i] != other.rep<br />

[i];<br />

return r;<br />

}<br />

3.3.17 Overstyrede konstante metoder<br />

Der er imidlertid eksempler på, at en metode skal udføre en forskellig operation afhængig af,<br />

om den kaldes for et konstant eller et ikke-konstant objekt. Type-reglerne i C++ skelner<br />

mellem konstante metoder og normale metoder, så en funktion kan overstyres med constnøgleordet<br />

efter samme regler som afvigende parameterlister. Vi kan for eksempel forestille<br />

os, at Hash()-funktionen overfor også gemmer resultatet i en medlemsvariabel.<br />

class intVec {<br />

3.3.17 Konstante metoder 139


int* rep, antal;<br />

unsigned hashValue;<br />

public:<br />

// ...<br />

unsigned Hash () const;<br />

};<br />

unsigned intVec::Hash () const {<br />

unsigned r = 0;<br />

for (int i = 0, i < antal, i++)<br />

r ^= (unsigned) rep [i];<br />

return hashValue = r;<br />

}<br />

Dette går naturligvis ikke for konstante objekter, fordi hashValue bliver tildelt i sidste<br />

linie af funktionen, som således ikke kan være konstant. Oversætteren vil beklage sig over en<br />

tildeling til en medlemsvariabel i en konstant medlemsfunktion. Men det skal stadig være<br />

muligt at hashe konstante objekter. Derfor overstyres funktionen for konstante objekter på<br />

følgende måde:<br />

class intVec {<br />

int* rep, antal;<br />

unsigned hashValue;<br />

public:<br />

// ...<br />

unsigned Hash (); // kaldes for ikke-konstanter<br />

unsigned Hash () const; // kaldes for konstanter<br />

};<br />

unsigned intVec::Hash () {<br />

unsigned r = 0;<br />

for (int i = 0, i < antal, i++)<br />

r ^= (unsigned) rep [i];<br />

return hashValue = r;<br />

}<br />

unsigned intVec::Hash () const {<br />

unsigned r = 0;<br />

for (int i = 0, i < antal, i++)<br />

r ^= (unsigned) rep [i];<br />

return r;<br />

}<br />

Der skelnes nu mellem en Hash()-funktion for forekomster af intVec, som er erklæret<br />

140 Klasse-begrebet 3.3


enten const eller ikke-const, og oversætteren binder kaldet til den funktion, der passer til<br />

erklæringen af objektet: konstante metoder for konstante objekter og ikke-konstante metoder<br />

for ikke-konstante objekter:<br />

bool hashCompare (const intVec& s) {<br />

intVec t;<br />

// ... indlæs t ...<br />

return s.Hash () > // Hash() const kaldes<br />

t.Hash (); // Hash() kaldes<br />

}<br />

På den måde kan udbyderen af klassen styre, hvordan metoder, som har anvendelser for både<br />

konstante og ikke-konstante objekter, men som afviger i detaljer, skal kaldes. Den primære<br />

anvendelse af overstyring med konstante metoder er, når et objekt indeholder en<br />

medlemsfunktion, som returnerer en reference og således kan bruges som et venstre-udtryk i<br />

en tildelingssætning. Da det ikke er lovligt at tildele værdier til konstante objekter vil en<br />

overstyring sikre, at der returneres den rette (kontante/ikke-konstante) variant af referencen,<br />

så fejlen kan rapporteres af oversætteren.<br />

3.3.18 Første løsning i C++<br />

En indkapslet version af Complex-biblioteket i C++ ser nu ud som i eksempel 3-3a og 3-3b<br />

for henholdsvis header og implementation. I eksempel 3-3c ses et eksempel på, hvordan<br />

biblioteket bruges i en enkel udregning af typen d = a * b - c.<br />

// complex.h<br />

// eksempel 3-3a: en C++ specifikation af et komplekst tal<br />

class Complex {<br />

double re, im; // private data<br />

public:<br />

void assign (double r = 0, double i = 0) {<br />

re = r, im = i;<br />

}<br />

void assign (const Complex& c) {<br />

re = c.re, im = c.im;<br />

}<br />

Complex add (const Complex&) const;<br />

Complex sub (const Complex&) const ;<br />

Complex mul (const Complex&) const;<br />

void print ();<br />

};<br />

3.3.18 Overstyrede konstante metoder 141


complex.cpp<br />

// eksempel 3-3b: en C++ implementation af et komplekst tal<br />

#include <br />

#include "complex.h"<br />

Complex Complex::add (const Complex &c) const {<br />

Complex temp;<br />

temp.re = re + c.re;<br />

temp.im = re + c.im;<br />

return temp;<br />

}<br />

Complex Complex::sub (const Complex& c) const {<br />

complex temp;<br />

temp.re = re - c.re;<br />

temp.im = re - c.im;<br />

return temp;<br />

}<br />

Complex Complex::mul (const Complex& c) const {<br />

Complex temp;<br />

temp.re = re * c.re - im * c.im;<br />

temp.im = re * c.im + im * c.re;<br />

return temp;<br />

}<br />

void Complex::print () {<br />

cout


a.print (); // udskriv a<br />

cout


før eventuelle tildelinger eller andre operationer udføres på en forekomst af typen.<br />

I eksemplet med en lagerallokeringstype skal brugeren eksplicit kalde en alloc()funktion<br />

efter at have erklæret en Memory-variabel, hvorefter lageret for eksempel kan<br />

nulstilles:<br />

Memory diskBuffer; // erklær en disk-buffer<br />

diskBuffer.alloc (BUFSIZ); // alloker lager til bufferen<br />

diskBuffer.init (); // nulstil bufferen<br />

Dette er typisk i brugen af ressourcer i næsten alle sprog. Først erklæres ressourcen, dernæst<br />

allokeres den fysiske ressource, og først da kan den bruges for alvor. Programmøren skal altså<br />

huske at kalde visse vigtige funktioner under oprettelsen af en type (under et system som<br />

Microsoft Windows fylder det klassiske Hello World-program flere hundrede linier, fordi<br />

oprettelsen af et vindue kræver talrige allokerings- og definitionskald).<br />

Samme problematik gælder for oprydning i en forekomst. Vi skal udtrykkeligt kalde en<br />

speciel oprydnings-funktion, før objektets skop render ud. Memory-typen har en free()funktion,<br />

som frigiver det allokerede lager til systemet. Hvis vi glemmer at kalde free(),<br />

vil lageret aldrig gives tilbage. Reelt er problemet, at det er klientens ansvar at kalde<br />

funktionerne for opstart og oprydning. For vi kan jo ikke garantere, at klienten husker det!<br />

I C++ kan vi definere specielle medlemsfunktioner til opstart og oprydning i forekomster af<br />

abstrakte datatyper, som kaldes implicit uden klientens indblanding, og som derfor garanterer,<br />

at alt er i orden. Konstruktører (eng. constructors) kaldes ved erklæringen af et objekt, og<br />

destruktører (eng. destructors) kaldes ved slutningen af objektets skop.<br />

3.4.1 Konstruktøren<br />

Konstruktør-funktionen konstruerer bogstaveligt talt objekter. Den kaldes for at allokere lager<br />

til sig selv, tildele værdier til de indre data-medlemmer og foretage andre initieringer for nye<br />

objekter. Næsten alle klasser har en eller flere konstruktører (konstruktør-funktioner kan<br />

overstyres), som kaldes ifølge den måde, en forekomst af klassen erklæres på.<br />

En konstruktør erklæres i den public-delen af en type, og har altid samme navn som<br />

typen selv. Den kan have parametre ligesom andre funktioner, men har ingen returtype, da der<br />

ikke er nogen steder at returnere værdier til fordi funktionen kaldes implicit. Hvis der ikke<br />

findes nogen konstruktører i en klasse, vil oversætteren generere en underforstået konstruktør,<br />

som nulstiller alle data-medlemmer i forekomsten efter en erklæring. Hvis en eller flere<br />

konstruktører defineres, bliver den underforståede konstruktør fjernet og kan ikke kaldes. Det<br />

sikrer, at objekterne initieres rigtigt, eller at fejlbrug af dem fanges på<br />

oversættelsestidspunktet.<br />

Klassen Memory er her defineret med en konstruktør i stedet for en speciel alloc()funktion:<br />

class Memory {<br />

char* mem_ptr; // addresse på det allokerede lager<br />

144 Initiering og nedbrydning af objekter 3.4


int size; // størrelse på blokken<br />

public:<br />

Memory (int n) { // allokér lager (n bytes)<br />

mem_ptr = new char [size = n];<br />

}<br />

// ...<br />

};<br />

Når en klient erklærer en Memory med<br />

Memory diskBuffer;<br />

vil oversætteren beklage sig, da der ikke findes en konstruktør med en tom<br />

parameterspecifikation. Klienten må skrive<br />

Memory diskBuffer (1000);<br />

for at erklære en forekomst af Memory af 1000 enheder. Vi har garanti for, at en klient ikke<br />

kan erklære forekomster af Memory, som han eller hun glemmer at kalde allokeringsfunktion<br />

for, og eliminerer hermed en typisk kilde for programmeringsfejl.<br />

Vær opmærksom på, at anonyme klasser ikke kan have konstruktører. Dette skyldes mere<br />

syntaksen i C++ end reelle tekniske problemer: idet en konstruktør skal have samme navn<br />

som klassen, vil en anonym klasse ikke kunne indeholde en konstruktør, fordi den netop intet<br />

navn har!<br />

3.4.2 Hvornår kaldes en konstruktør<br />

Vi husker fra afsnit 2.9, at der er 3 primære måder at oprette en variabel på, nemlig ved<br />

statisk, automatisk og dynamisk allokering. En konstruktør kaldes, når en forekomst oprettes,<br />

og det gælder alle disse allokeringsformer:<br />

Derfor:<br />

• statiske objekter (også globale og eksterne) kalder konstruktøren når programmet<br />

startes,<br />

• automatiske objekter kalder konstruktøren hver gang, de erklæres, og<br />

• dynamisk allokerede objekter kalder konstruktøren hver gang, operatoren new<br />

efterfølges af den pågældende klasse.<br />

3.4.1 Konstruktøren 145


class X { // et koncept "X"<br />

X (); // konstruktør<br />

// ...<br />

};<br />

X a; // a.X() kaldes ved programstart<br />

void main () {<br />

static X b; // b.X() kaldes ved programstart<br />

X c; // c.X() kaldes her<br />

X* d<br />

d = new X; // d->X() kaldes her<br />

// ...<br />

}<br />

Denne gruppering er lavet udfra hvordan objekterne er allokeret, og tjener til at vise, at det er<br />

den samme funktion, der er tale om, uanset hvor objektet befinder sig. I syntaksmæssig<br />

sammenhæng foretages kald til konstruktører i tre situationer:<br />

...<br />

1. En udtrykkelig oprettelse af et objekt, som i<br />

X a; // X::X() kaldes for a<br />

2. En overførsel af et objekt til en funktion som parameter, overført som værdi, som i:<br />

void print (X a) { /* udskriv a */ }<br />

X b;<br />

print (b); // X::X() kaldes for a (ikke b)<br />

3. En overførsel af et objekt fra en funktion til kalderen som returværdi, som i<br />

X input () {<br />

X a;<br />

// ... indlæs en X i a<br />

return a; // X::X() kaldes for et temporært objekt<br />

}<br />

X b = input (); // ... som kopieres til b her<br />

Disse regler gælder kun for objekter, der overføres til eller fra funktioner som værdier. For<br />

146 Initiering og nedbrydning af objekter 3.4


pointer-argumenter eller referencer bliver konstruktøren ikke kaldt, idet der ikke er tale om et<br />

nyt objekt.<br />

Konstruktørkald kan være en kilde til en pudsig programmeringsfejl, som er svær at opdage,<br />

fordi den ikke umiddelbart ser forkert ud. Givet klassen X overfor, vil erklæringen<br />

X x_obj();<br />

ikke oprette et objekt af typen X som hedder x_obj og kalde konstruktøren X::X() for<br />

dette objekt. Konstruktører kaldes implicit. Der er tale om en prototype på en funktion, som<br />

hedder x_obj, som returnerer en forekomst af X og som har en tom parameterliste!<br />

Konstruktører uden parametre skal altså ikke kaldes med tomme paranteser, men blot skrives<br />

efter den samme syntaks som den, der bruges ved fundamentale typer, nemlig<br />

X x_obj;<br />

Den slags sælsomheder hører med til C++, og kan give anledning til timevis af grublen, fordi<br />

fejlen ikke er åbenlys.<br />

3.4.3 Konstruktører uden parametre<br />

En klasse kan indeholde en konstruktør, som ikke har parametre, eller som har underforståede<br />

værdier for alle parametre. Konstruktører, som ikke kræver parametre hedder underforståede<br />

konstruktører på linie med den, som oversætteren genererer i tilfælde af manglende<br />

konstruktører. Den underforståede konstruktør er brugbare i de tilfælde, hvor et objekt skal<br />

initieres, men hvor denne initiering ikke nødvendigvis kræver indblanding fra klientens side.<br />

Memory-klassen kan for eksempel indeholde en underforstået konstruktør, som allokerer<br />

et fast antal lagerenheder. Konstruktøren skrives som:<br />

Memory::Memory () {<br />

mem_ptr = new char [size = BUFSIZ];<br />

}<br />

Når en klient erklærer en memory med<br />

Memory buffer;<br />

vil oversætteren indsætte et underforstået kald til buffer.Memory() og dermed allokere<br />

BUFSIZ nye char ved brug af new-operatoren. C++ bruger iøvrigt den underforståede<br />

konstruktør når vektorer af abstrakte datatyper erklæres. Hvis du for eksempel skriver<br />

Memory buffer [8];<br />

vil buffer.Memory() blive kaldt 8 gange, for buffer[0], buffer[1] ...<br />

3.4.2 Hvornår kaldes en konstruktør 147


uffer[7]. Der findes ingen andre metoder til at kalde andre konstruktører under en<br />

vektor-erklæring end den underforståede konstruktør.<br />

3.4.4. Konstruktører med ét parameter<br />

Når en konstruktør har et enkelt parameter (eller hvis alle andre parametre har underforståede<br />

værdier), kan en alternativ syntaks bruges under erklæringen af objektet:<br />

Memory buffer = 1000;<br />

Dette er, hvis klassen Memory er defineret med en konstruktør med en enkelt parameter, for<br />

eksempel en int, være identisk med<br />

Memory buffer (1000);<br />

Denne syntaks er mere i stil med den, vi kender fra de kombinerede erklærings/initieringssætninger<br />

for de fundamentale typer.<br />

3.4.5 Konstruktører med flere parametre<br />

Hvis konstruktøren har mere end en parameter, skal disse med i erklæringen af en forekomst<br />

af den pågældende klasse. I vores Complex-klasse vil en konstruktør med både en reel og<br />

en imaginær initierings-værdi være praktisk. Konstruktøren får specifikationen<br />

Complex::Complex (double r, double i);<br />

og bliver kaldt på følgende måde under initieringen:<br />

Complex a (3.4, 8.2);<br />

3.4.6 Klasser med flere konstruktører<br />

Hvis klassen overstyrer konstruktøren, får klienten større frihed i erklæringen af forekomster<br />

af klassen. Klassen Complex, som har to assign()-funktioner, én for tildeling af to<br />

double'r og én for tildeling af en anden Complex. Disse to funktioner kan direkte<br />

omskrives til konstruktør-funktioner, så klienten kalder dem implicit under erklæringen:<br />

class Complex {<br />

double re, im;<br />

148 Initiering og nedbrydning af objekter 3.4


public:<br />

Complex (double r, double i = 0) { re = r, im = i; }<br />

Complex () { re = im = 0; }<br />

// ...<br />

}<br />

De to konstruktører giver klienten mulighed for at oprette Complex-forekomster på<br />

følgende måder:<br />

Complex a = 3; // a.Complex (3, 0) kaldes<br />

Complex b; // b.Complex () kaldes<br />

Complex c (4, 3.33); // c.Complex (4, 3.33) kaldes<br />

Når en klasse har flere konstruktører giver det altså klienten flere indgange til oprettelse af<br />

klassen. Det, vi tilstræber, er en syntaks i erklæringen af abstrakte datatyper, som ligner den<br />

vi kender fra de fundamentale typer. Programmeringsfejl mindskes, fordi syntaksen er kendt.<br />

De fire linier overfor kunne også skrives som:<br />

Complex a = 3, b, c (4, 3.33);<br />

Oversætteren vil binde kaldet fra erklæringen i klientprogrammet til den rette konstruktør på<br />

oversættelsestidspunktet, afhængig af den kontekst, erklæringen har.<br />

3.4.7 Konstruktører med underforståede parametre<br />

Det er naturligvis også tilladt at bruge underforståede parametre i konstruktøren, ligesom for<br />

normale funktioner. Den eneste regel er, at kun de sidste (mod højre) parametre kan være<br />

underforståede. Overvej implementationen af konstruktøren i følgende klasser:<br />

class Complex1 {<br />

double re, im;<br />

public:<br />

Complex1 () { re = im = 0; }<br />

Complex1 (double r) { re = r, im = 0; }<br />

Complex1 (double r, double i) { re = r, im = i; }<br />

// ...<br />

};<br />

class Complex2 {<br />

double re, im;<br />

public:<br />

Complex2 (double r = 0, double i = 0) { re = r, im = i; }<br />

3.4.6 Klasser med flere konstruktører 149


...<br />

};<br />

Disse to implementationer har samme funktionalitet. Det er muligt at erklære forekomster af<br />

klasserne med nul, et eller to parametre, og alle ikke-specificerede parametre vil blive<br />

nulstillet. Hvornår skal man bruge overstyrede konstruktører, og hvornår skal man bruge<br />

underforståede parametre? Spørgsmålet er ikke let, fordi det kommer an på brugen af klassen<br />

fra klientens side. Generelt giver overstyring et større lagerforbrug i kode, fordi der eksisterer<br />

flere funktioner, mens underforståede parametre giver et større lagerforbrug i data, fordi de<br />

underforståede parametre normalt står som skjulte konstanter i objektkoden. Overstyring af<br />

konstruktøren vil være en smule hurtigere, fordi kun det antal parametre, der er brug for i<br />

funktionen, vil blive overført fra klientens erklæring.<br />

Balancegangen må være, at underforståede parametre er at foretrække, hvis to overstyrede<br />

funktioner er identiske, og antallet af underforståede parametre ikke er for højt - for eksempel<br />

ikke overstiger to. Hvis konstruktør-funktionerne har små forskelle, som kræver betingede<br />

kontrolstrukturer, bør de overstyres. Husk på, at konstruktøren repræsenterer et skjult<br />

funktionskald under erklæringer, hvilket kan blive til en hel del i et program, der bruger<br />

mange forekomster. Derfor skal konstruktørens indhold af kontrolstrukturer minimeres.<br />

3.4.8 Initieringslister<br />

Der er et problem med C++'s regler for forskellene mellem tildeling og initiering. Fra afsnit<br />

2.4.7 huskes det, at konstante typer og reference-typer skal initieres ved erklæringen, idet de<br />

ikke er variable i normal forstand. Overvej nu følgende klasse-definition:<br />

class Stof {<br />

const double smeltePunkt, frysePunkt;<br />

double Temperatur;<br />

public:<br />

Stof (double, double);<br />

void setTemp (double t) { Temperatur = t; }<br />

// ...<br />

};<br />

Stof::Stof (int f, int s) {<br />

frysePunkt = f; // fejl: tildeling til konstant<br />

smeltePunkt = s; // ligeså<br />

}<br />

De to medlemsvariable i klassen Stof er konstante, dvs. de kan ikke ændres hverken udefra<br />

(da de i første omgang er private) eller indefra, da de er const. Konstruktøren, som<br />

150 Initiering og nedbrydning af objekter 3.4


tænkes at bruges på følgende måde<br />

Stof H2O (0, 100); // vand, udtrykt i Celcius<br />

H2O.setTemp (16.8); // normaltemperatur i dansk badevand!<br />

vil ikke kunne oversættes, da den tildeler værdier til konstante data. Samme problem opstår<br />

med datamedlemmer, som er erklæret som referencer. Følgende klasse-definition beskriver et<br />

vindue på en skærm, hvor skærmen er indeholdt som reference til en forekomst af en anden<br />

klasse. Således kan flere vinduer dele samme skærm:<br />

class Screen;<br />

class Window {<br />

Screen& display;<br />

int top, left, bottom, right; // vinduets rammer<br />

public:<br />

Window (Screen&, int, int, int, int);<br />

// ...<br />

};<br />

Window::Window (Screen& s, int a, int b, int c, int d) {<br />

top = a, left = b, bottom = c, right = d; // ok<br />

display = s; // fejl: tildeling til reference<br />

}<br />

Problemet er, at både referencer og konstante variable skal initieres på samme tid, som de<br />

erklæres. I klasser er det imidlertid ikke muligt, fordi klassens definition og instantiering er to<br />

forskellige ting. Derfor findes en initieringsliste i C++, som bruges til at initiere sådanne data.<br />

Initieringslister kan kun forekomme i konstruktører, og har følgende syntaks:<br />

klasse::klasse (type T) : /* initieringer her */ {<br />

/* tildelinger her */<br />

}<br />

Konstruktørerne for Stof og Window kommer således til at se ud som følger:<br />

Stof::Stof (int f, int s) :<br />

frysepunkt (f), smeltepunkt (s) { // initieringer<br />

{ } // tildelinger<br />

Window::Window (Screen& s, int a, int b, int c, int d) :<br />

display (s) // initieringer<br />

{<br />

top = a, left = b, // tildelinger<br />

3.4.8 Initieringslister 151


ottom = c, right = d;<br />

}<br />

Initieringslisten udføres mens objektet oprettes, og initierer de medlemsvariable, der er<br />

beskrevet i initieringslisten i den rækkefølge de er erklæret. Det gør det ikke blot muligt at<br />

initiere konstante data og referencevariable, men har også betydnig for forekomster af andre<br />

klasser, som er indkapslede i den aktuelle klasse samt for initiering af base-klasser, som vi<br />

skal se i <strong>kapitel</strong> 4.<br />

Der er også en effektivitetsmæssig fordel ved initieringslister. Idet de er en del af<br />

konstruktørens erklæring, kan oversætteren generere bedre kode for oprettelsen af objekter -<br />

en procedure, som ofte foretages - fordi der skal overføres færre parametre explicit til<br />

konstruktøren. Initieringslisten bliver rent faktisk udført på kalderens side af oprettelsen af<br />

forekomsten, hvilket sparer kopiering af initieringsdata til den dummy-funktion, som en<br />

konstruktør egentlig er. Derfor er det også tilrådeligt at bruge initieringslister for ganske<br />

normale medlemsdata.<br />

Læg mærke til, at rækkefølgen af initieringerne ikke følger initieringslisten, men<br />

erklæringen af medlemsdata i klassen. For eksempel,<br />

class X {<br />

int b, a;<br />

public:<br />

X (int x, int y) : a (x), b (a + y) { }<br />

};<br />

Konstruktøren i denne klasse vil resultere i, at a indeholder værdien af x, og at b er<br />

udefineret, fordi b er erklæret før a i klassen. Når der er tale om initieringer af normale<br />

medlemmer, som kræver aritmetiske behandlinger, er det tilrådeligt enten at lave dem om til<br />

normale tildelinger i konstruktørens krop eller at være meget opmærksom på rækkefølgen af<br />

kaldene. Det er en af de slags fejl, der er svære at finde, fordi det for programmøren ser rigtigt<br />

nok ud, men alligevel ikke er det.<br />

Læg også mærke til, at initieringslisten skal skrives der, hvor konstruktøren implementeres,<br />

ikke hvor den defineres. Hvis den skrives udenfor klassens krop, skal initieringen også skrives<br />

udenfor klassens krop, hvilket er det omvendte af reglerne for underforståede parametre, som<br />

skal stå i definitionen af klassen:<br />

class X {<br />

int x;<br />

public:<br />

X (int y) : x (y); // fejl!<br />

};<br />

X::X (int y) { /* ... */ }<br />

Grunden til dette er, at parameterlisterne skal være identiske i definitionen og i<br />

152 Initiering og nedbrydning af objekter 3.4


implementationen af konstruktøren. Hvis initieringslisten forekommer i definitionen er der<br />

som regel ikke brug for de parametre, som bruges i initieringen i selve konstruktørens krop -<br />

men de skal stadig stå der til ingen verdens nytte. Derfor skal initieringslisten stå sammen<br />

med konstruktørens implementation:<br />

class X {<br />

int x;<br />

public:<br />

X::X (int);<br />

};<br />

X::X (int y) : x (y) { /* ... */ }<br />

3.4.9 Destruktøren<br />

Hvor konstruktøren opretter objekter, er destruktøren ansvarlig for nedbrydningen.<br />

Destruktøren varetager opgaver som deallokering af lager, frigivning af hardware, lukning af<br />

filer - kort sagt alle de funktioner, klienten normalt kalder ved endt brug af en ressource eller<br />

datastruktur. Når et objekts skop render ud (2.6.1) vil destruktøren blive kaldt helt automatisk.<br />

Destruktøren er en medlemsfunktion med samme navn som klassen, men med en tilde (~)<br />

før navnet. En klasse må kun have en enkelt destruktør, idet den kaldes implicit og derfor ikke<br />

kan være afhængig af kaldeformen. Af samme grund kan den ikke have en returværdi.<br />

Her er et eksempel på Memory-klassen med en destruktør:<br />

class Memory {<br />

char* mem_ptr;<br />

int size;<br />

public:<br />

Memory (int i = BUFSIZ) { // konstruktør<br />

mem_ptr = new char [size = n];<br />

}<br />

~Memory () { // destruktør<br />

delete mem_ptr;<br />

}<br />

// ...<br />

};<br />

Oversætteren har en indbygget, underforstået destruktør, som kaldes i alle tilfælde. Den<br />

underforståede destruktør frigiver alt lager, som optages af de variable, der er medlem af<br />

klassen. Derfor frigiver Memory::~Memory() ikke selve pointervariablen mem_ptr og<br />

heltallet size, men kun det lager, som konstruktøren har allokeret.<br />

Den store fordel ved destruktøren er den samme som for konstruktøren: klienten behøver<br />

ikke eksplicit at kalde oprydnings-funktioner, som "gør rent" i lageret eller lignende efter endt<br />

3.4.8 Initieringslister 153


ug af et objekt. Når destruktøren automatisk kaldes, kan klasse-udbyderen være sikker på,<br />

at alt sker, som det skal. Det er ikke mere klientens ansvar at kalde disse funktioner. Dette<br />

koncept er en naturlig følge af indkapsling. Idet vi med klasserne skjuler<br />

implementationsdetaljer fra klienten, så denne kan abstrahere fra de indre detaljer, skal<br />

klienten heller ikke bære ansvaret for at kalde specielle funktioner på specielle tidspunkter.<br />

Det skal gøres automatisk. Når vi bruger en float kalder vi ikke specielle<br />

oprydningsfunktioner, og bør derfor heller ikke gøre det for abstrakte datatyper.<br />

3.4.10 Hvornår kaldes en destruktør<br />

Når en klasse har en destruktør, kaldes denne hver gang en forekomst af den klasse slutter sit<br />

skop (afsnit 2.6.1). For automatiske variable er det slutningen af den blok, hvori den er<br />

defineret, for statiske variable er det slutningen af programmet og for dynamisk allokerede<br />

variable er det når de fjernes af brugeren med delete. For eksempel:<br />

class X { // et koncept "X"<br />

X (); // konstruktør<br />

~X (); // destruktør<br />

// ...<br />

};<br />

static X a; // a.X() kaldes ved programstart<br />

void main () {<br />

static X b; // b.X() kaldes ved programstart<br />

X c; // c.X() kaldes her<br />

X* e;<br />

{<br />

X d; // d.X() kaldes her<br />

e = new X; // e->X() kaldes her<br />

} // d.~X() kaldes her<br />

X f; // f.X() kaldes her<br />

delete e; // e->.~X() kaldes her<br />

} // a.~X(), b.~X(), c.~X() og f.~X()<br />

// kaldes her<br />

Det er muligt at kalde en destruktør eksplicit med<br />

X xobj;<br />

xobj.~X(); // eksplicit destruktion<br />

men det er næppe brugbart under normale omstændigheder - måske kun i situationer, hvor<br />

programmet skal afbrydes og en særlig oprydning skal finde sted, som forbipasserer standard-<br />

154 Initiering og nedbrydning af objekter 3.4


kaldende ved programmets slutning. Et kald til abort(), C-funktionen, der afbryder uden<br />

tøven, er et eksempel på dette.<br />

Hvis konstruktøren og destruktøren til klassen X overfor implementeres således<br />

X::X () { cout


Complex ();<br />

Complex (double, double = 0);<br />

Complex add (Complex&);<br />

Complex sub (Complex&);<br />

Complex mul (Complex&);<br />

void print ();<br />

};<br />

// complex.cpp<br />

// eksempel 3-4b: en C++-implementation af et komplekst tal<br />

#include <br />

#include <br />

// tom konstruktør: nulstiller alt<br />

inline Complex::Complex () {<br />

re = 0, im = 0;<br />

}<br />

// konstruktør med en eller to initierings-variable<br />

inline Complex::Complex (double r, double i) {<br />

re = r, im = i;<br />

}<br />

// addér to komplekse tal.<br />

Complex Complex::add (Complex& c) {<br />

Complex temp;<br />

temp.re = re + c.re;<br />

temp.im = re + c.im;<br />

return temp;<br />

}<br />

// subtrahér to komplekst tal<br />

Complex Complex::sub (Complex& c) {<br />

Complex temp;<br />

temp.re = re - c.re;<br />

temp.im = re - c.im;<br />

return temp;<br />

}<br />

// multiplicér to komplekse tal<br />

Complex Complex::mul (Complex& c) {<br />

Complex temp;<br />

156 Initiering og nedbrydning af objekter 3.4


temp.re = re * c.re - im * c.im;<br />

temp.im = re * c.im + im * c.re;<br />

return temp;<br />

}<br />

// udskriv et komplekst tal<br />

void Complex::print () {<br />

cout


initierer en Complex fra andre typer, hvilket et af kravene til typen også var (afsnit 3.2). Vi<br />

kan tildele en forekomst af Complex til en anden Complex med:<br />

Complex a = 2; // initiér a med (2, 0)<br />

Complex b = a; // initiér b med a, medlemsvist kopieret<br />

Oversætteren kopierer ganske simpelt alle medlemmer af a (de to double'r re og im) én<br />

for én til de tilsvarende pladser i b, og kalder de underforståede konstruktører under<br />

oprettelsen af de nye medlemsdata. Dette er til forskel fra kopiering af objekter i C og i de<br />

allertidligste versioner af C++, hvor det sker bitvist, uden kald til konstruktører af nogen art.<br />

Der er dog klasser, som får problemer med denne simple kopiering. Memory-klassen<br />

indeholder for eksempel en pointer til noget lager, som allokeres i konstruktøren. Hvis vi<br />

foretager følgende tildelinger med Memory, som vi gjorde med Complex:<br />

Memory a (1000); // initiér a med 1000<br />

Memory b = a; // initiér b med a<br />

ser vi, at a først oprettes med 1000 enheder (konstruktøren sætter bla. den private variabel<br />

a.mem_ptr til starten af lageret) og at initieringen af b ikke eksplicit kalder nogen<br />

konstruktør, da den initieres med en tildeling til en anden forekomst af Memory. Når vi så<br />

tildeler b værdien af a, vil pointervariablen b.mem_ptr blive overskrevet med flere<br />

problemer til følge:<br />

• a og b bruger samme lagerplads. Selvom de respektive variable er allokeret separat,<br />

er den ene en pointervariabel, som nu er blevet ændret. Det kan og vil give<br />

problemer i brugen af a og b.<br />

• Medlemsvariablen b.mem_ptr, som pegede på allokeret lager, har nu ændret<br />

værdi som følge af kopieringen. Det allokerede lager er så at sige tabt, fordi vi ikke<br />

ved, hvor det befinder sig. Det betyder også, at<br />

• Når forekomsterne deallokeres senere i en destruktør, vil der opstå problemer, fordi<br />

lageret frigives to gange. Der er kun allokeret lager én gang, men destruktøren kaldes<br />

to gange.<br />

Kernen i problemet er, at en af medlemsvariablene i Memory er en pointertype. Vi har brug<br />

for at fortælle oversætteren, hvad den skal gøre når en bruger forsøger at initiere en forekomst<br />

af Memory med en anden forekomst af samme klasse. Det er her vigtigt at forstå forskellen<br />

mellem initiering og tildeling. Initiering forekommer i tre sammenhænge:<br />

• erklæring med initieringer (som den, vi lige har beskrevet), hvor et objekt erklæres<br />

og initieres i samme sætning,<br />

158 Initiering og nedbrydning af objekter 3.4


• formelle parametre i et funktionskald, dvs. hvor et objekts værdi overførers fra en<br />

funktion til en anden,<br />

• returværdier fra en funktion, hvor et objekt overføres til den kaldende funktion.<br />

Tildelinger forekommer kun i udtryk - stadig ikke i erklæringer - som bruger tildelingsoperatoren<br />

=. Det er altså ikke det samme, når vi skriver<br />

og<br />

Complex a = b; // erklæring og initiering<br />

Complex a; // erklæring<br />

a = b; // tildeling<br />

3.4.13 Kopi-konstruktøren<br />

Det første problem er altså initiering. Når et objekt erklæres med samtidig initiering fra et<br />

andet objekt, vil der normalt forekomme en simpel kopiering, hvilket, som vi har set, kan<br />

skabe problemer. For at løse dette bruger vi en speciel konstruktør med et enkelt parameter,<br />

som er en reference til en forekomst af samme type som klassen selv. En sådan konstruktør<br />

kaldes en kopi-konstruktør, og kopierer ganske simpelt en forekomst. Kopi-konstruktøren<br />

erstatter den indbyggede kopi-funktion i C++. En kopi-konstruktør for Memory-klassen kan<br />

skrives på følgende måde:<br />

Memory::Memory (Memory& m) {<br />

mem_ptr = new char [size = m.size];<br />

memcpy (mem_ptr, p.mem_ptr, m.size);<br />

}<br />

Denne konstruktør kaldes, og kaldes kun, når et objekt af typen Memory erklæres og<br />

initieres med værdien af et andet Memory-objekt i samme sætning. Følgende sætninger<br />

demonstrerer brugen:<br />

Memory a (100); // Memory::Memory (int) kaldes for<br />

// konstruktion af a<br />

Memory b = a; // Memory::Memory (Memory&) kaldes<br />

// for konstruktion af b fra a<br />

Kopi-konstruktøren Memory::Memory (Memory&) allokerer plads til b's interne lager<br />

ved en reference til a's size-variabel, og kopierer også size. Husk at C++ benytter<br />

klasse-baseret indkapsling, hvilket er grunden til, at kopi-konstruktøren kan referere både a's<br />

og b's private medlemsvariable. Dernæst kopierer kopi-konstruktøren lagerpladsen fra a til<br />

b, så b bliver helt identisk med a. Vi kan se, at den opgave, der består i at initiere en<br />

3.4.11 Anden løsning i C++ 159


forekomst af Memory med en anden er noget mere omfattende end blot en medlemsvis<br />

kopiering.<br />

Vi løber ind i et andet problem, når en forekomst af Memory tildeles en anden forekomst<br />

af Memory. Her har vi en anden situation, nemlig en tildeling til et allerede initieret objekt.<br />

Kopi-konstruktøren kan kun bruges, når objektet, der erklæres, endnu ikke er initieret, hvilket<br />

er grunden til, at det hedder en kopi-konstruktør. Når vi blot tildeler et objekt til et andet, som<br />

i<br />

Memory a (100), b (50); // to erklæringer<br />

a = b; // en tildeling<br />

er objektet, som tildeles et andet objekt (her a), allerede initieret. Kopi-konstruktøren, som<br />

allokerer nyt lager, vil derfor ikke kunne bruges.<br />

Dette problem løses med en definition af en tildelings-operator-funktion, som reelt er en<br />

overstyring af lighedstegnet for den aktuelle klasse. Næste afsnit (3.5) beskriver operatoroverstyring,<br />

og kommer også ind på overstyring af tildelings-operatoren for at løse netop<br />

dette problem.<br />

3.4.14 Statiske objekter<br />

Hvis et objekt erklæres med globalt skop (udenfor en funktion) eller erklæres som en statisk<br />

variabel i en funktion (med static-nøgleordet), er objektets levetid defineret fra<br />

programmet start til slut. For statisk erklærede forekomster af brugerdefinerede typer gælder,<br />

at C++ kalder konstruktøren som det allerførste - altså før main() - og destruktøren efter<br />

main(). Hvis vi for eksempel skriver<br />

Complex a (-4, 4.2);<br />

void main () {<br />

static Complex b (3);<br />

// ...<br />

}<br />

vil C++ foretage konstruktørkaldene Complex::Complex(-4, 4.2) for a og<br />

Complex::Complex(3) for b før programmets egentlige start. Ligeledes vil<br />

Complex::~Complex() blive kaldt for begge objekter umiddelbart før programmet<br />

afsluttes.<br />

Konstruktører og destruktører i statiske objekter eller objekter med eksternt skop kaldes<br />

statiske konstruktører og statiske destruktører. Det kræver ingen særlig syntaks at definere<br />

statiske konstruktører og destruktører, og definitionen bruges blot til at beskrive deres brug<br />

samt at markere, at de kun kaldes én gang. Der skal ikke skrives speciel kode for at behandle<br />

statiske forekomster, så enhver konstruktør bliver statisk for en given forekomst, når denne<br />

erklæres statisk. Statiske objekter er brugbare i steder så som biblioteker, som skal initiere<br />

specielle data, før de kan bruges.<br />

160 Initiering og nedbrydning af objekter 3.4


3.4.15 Pointere og referencer til objekter<br />

Reglerne for manipulation af data via pointere og referencer er nøjagtig de samme for<br />

brugerdefinerede typer som for fundamentale typer. Det kan være svært at se, hvordan en<br />

større, kompleks abstrakt datatype kan derefereres med * eller ->, specielt i forbindelse<br />

med returværdier og tildelinger. Det kan derfor hjælpe at foregive, at objektet er fundamentalt<br />

og tænke syntaksen igennem en gang til. Hvis en variabel V er en pointer til en type T vil<br />

udtrykket *V kunne bruges i alle sammenhænge, som en normal forekomst af T kan bruges.<br />

Idet pointere og referencer kun bærer adresse- og typeinformation i erklæringen, har de ikke<br />

nogen forekomst at have med at gøre. Derfor kaldes ingen konstruktører eller destruktører for<br />

pointere og referencer til abstrakte datatyper:<br />

Complex a; // a.Complex() kaldes<br />

Complex& b = a; // ingen konstruktør kaldes<br />

Complex* c = &a; // heller ikke her<br />

Indirekte reference til objekter med fortegns-operatorerne * og & følger igen samme regler<br />

som for de indbyggede typer. Følgende kodefragment er derfor lovligt og ganske rimeligt:<br />

class String { // generel tekst-streng<br />

char* cp;<br />

public:<br />

String () { cp = 0; } // konstruér "tom" streng<br />

String (char*); // konstruér fra C++-streng<br />

String (String&); // konstruér fra String<br />

~String ();<br />

// ...<br />

void f (); // medlemsfunktion<br />

};<br />

void f () {<br />

String navn = "C.S.N.";<br />

String* s = &navn; // String-pointer initiering<br />

s->f(); // kald f i *s<br />

String kopi = *s; // kalder kopi.String (String&<br />

*s);<br />

String& p = navn; // String-reference initiering<br />

}<br />

Det er oftest i forbindelse med funktionskald, at pointer-typer og reference-typer finder størst<br />

anvendelighed:<br />

3.4.14 Statiske objekter 161


*s<br />

void f (String* s, int antal) {<br />

while (antal--) s++ -> f (); // kald f() for alle<br />

}<br />

String& match (String* s, int antal) {<br />

String temp; cin >> temp;<br />

while (antal--)<br />

if (*s++ == temp) return *--s; // returnér hvis ens<br />

return String (); // returnér tom<br />

streng<br />

}<br />

3.4.16 Dynamiske objekter<br />

Abstrakte datatyper kan allokeres dynamisk i lageret med new og delete fuldstændig som<br />

de fundamentale typer:<br />

Complex* a = new Complex;<br />

// ...<br />

delete a;<br />

C++ ser en allokering af en abstrakt datatype som starten på et skop af et nyt objekt, og kalder<br />

en passende konstruktør når objektet allokeres med new samt destruktøren når det<br />

deallokeres med delete. Det er tilladt at give information til konstruktøren under<br />

allokeringen:<br />

Complex* a = new Complex (5, 5);<br />

Læg mærke til, at destruktøren i et dynamisk allokeret objekt, som ikke deallokeres med<br />

delete, aldrig vil blive kaldt. Det er derfor endnu vigtigere at huske dette end for normale<br />

forekomster af fundamentale typer.<br />

Læg også mærke til, at dynamisk allokerede objekter kan arbejde sammen med referencer<br />

og pointere på en meget subtil måde i forbindelse med funktioner. Givet<br />

void pushComplex (Complex&);<br />

som er en (prototype på en) funktion, der tager en reference til en Complex som parameter,<br />

kan vi skrive<br />

pushComplex (*new Complex);<br />

Umiddelbart ser det ud som om, at vi allokerer en forekomst af Complex uden at tildele<br />

162 Initiering og nedbrydning af objekter 3.4


adressen til en pointervariabel. Og hvordan skal vi så kunne delete dette lager igen? I<br />

virkeligheden tildeles den nye forekomst til referencetypen i pushComplex(), som er det<br />

faktiske objekt. Husk, at referencetyper skal initieres når de erklæres, og at dette også gælder<br />

for parametre i funktioner. Parameteren i pushComplex() erklæres, når funktionskaldet<br />

foretages, og initieres med det samme med indholdet af det nyallokerede objekt, på hvilket<br />

tidspunkt konstruktøren kaldes. Initieringer til konstruktøren er selvfølgelig også tilladt:<br />

pushComplex (*new Complex (4));<br />

Grunden til, at der der er en dereference-operator før new i funktionskaldet er, at vi overfører<br />

en faktisk forekomst - ikke en pointer. Men det betyder ikke, at hele objektet føres til<br />

pushComplex(), fordi funktionen er erklæret med en referenceparameter. Så selvom den<br />

nyallokerede forekomst derefereres, er det alligevel kun adresseinformation, som gives til<br />

funktionen.<br />

Det er eksempler som dette, som gør C++ fantastisk kraftfuldt. Men det er vigtigt at forstå,<br />

præcis hvad der foregår. Programmører, som har brugt Pascal, Fortran eller Cobol vil finde<br />

referencer og dereferencer i disse sammenhænge svære at greje. Det "rene" pointerkoncept i C<br />

er i forvejen temmelig avanceret for selv kyndige programmører med erfaring i andre sprog,<br />

og selv erfarne C-programmørers indlæringskurve stejler, når det kommer til referencer og<br />

pointere, som blandes godt og grundigt sammen. Den bedste måde at forstå C++'s faciliteter<br />

på dette punkt er at skrive et par små programmer med abstrakte typer, som indeholder<br />

konstruktører og destruktører med små "pop-up" beskeder. Opgavesamlingen i slutningen af<br />

kapitlet indeholder et par forslag.<br />

Reglerne for destruktion af automatiske variable af vektor-typen (erklæret med lokalt skop i<br />

en funktion eller blok) samt for statiske variable af vektor-typen (erklæret med globalt skop<br />

udenfor funktions-kroppe) følger de normale regler og vil blive destrueret korrekt for alle<br />

medlemmer. Dette sker, fordi oversætteren ved, hvor mange objekter, der er allokeret i<br />

vektoren og kan derfor kalde det korrekte antal destruktører. Ligeledes for dynamisk<br />

allokerede objekter, der er vektorer, foregår destruktionen af vektoren automatisk. Dette er til<br />

forskel fra tidligere versioner (før 2.0) af C++, hvor det var påkrævet at angive vektorens<br />

størrelse efter delete-udtrykket, som i følgende fragment:<br />

int* heltal = new int [100];<br />

// ...<br />

delete [100] heltal;<br />

Denne fremgangsmåde var nødvendig, fordi en pointer som heltal ikke er erklæret som en<br />

vektor, hvorfor en delete uden længdeangivelse blot ville deallokere en enkelt forekomst,<br />

nemlig den, som pointeren refererer. I nyere versioner af C++ er denne semanik indbygget i<br />

pointerne, hvorfor konstruktionen er forældet (men accepteres og ignoreres stadig af de fleste<br />

oversættere). I øvrigt er det en god ide at erklære pointere, der udelukkende bruges i<br />

forbindelse med kald til new som konstante pointere, så de ikke uhensigtsmæssigt ændrer<br />

værdi og går galt i byen i en delete-sætning.<br />

3.4.16 Dynamiske objekter 163


3.4.17 Vektorer af objekter<br />

Brugerdefinerede datatyper kan, som alle andre typer, afledes efter reglerne fra afsnit 2.4.<br />

Klienten kan således oprette vektorer af objekter efter samme syntaksmæssige regler som for<br />

fundamentale datatyper. En vektorerklæring af en brugerdefineret type vil allokere det antal<br />

forekomster, der angives af klienten og initiere dem én for én ved konstruktørkald til de<br />

enkelte elementer.<br />

Klassen String fra afsnit 3.4.14 beskriver en generel streng, dvs. en sekvens af tegn.<br />

Denne type findes som bekendt ikke i C++, og er en god kandidat til indkaplsling i en klasse.<br />

En klient kan udover normale skalære instantieringer af String oprette vektorer efter de<br />

normale regler:<br />

String navne [30]; // 30 strenge<br />

En sådan erklæring vil resultere i 30 allokeringer af String-objekter (30 char-pointere)<br />

og 30 kald til hvert objekts underforståede String::String()-konstruktør:<br />

navne [0].String ();<br />

navne [1].String ();<br />

navne [2].String ();<br />

// ...<br />

navne [29].String ();<br />

Klienten refererer de individuelle objekter på vektoren med medlemsreference-operatoren:<br />

navne [7].f (); // kald f () for 7. element i vektoren<br />

Når vektor-objekter af klasser med destruktører går ud af skop, kaldes destruktøren for alle<br />

objekterne på vektoren i omvendt rækkefølge:<br />

navne [29].~String ();<br />

navne [28].~String ();<br />

navne [27].~String ();<br />

// ...<br />

navne [0].~String ();<br />

Da vektor-erklæringer kalder mange konstruktører og destuktører, er det en god idé at lade<br />

disse funktioner være meget små og eventuelt skrive dem inline for maksimal effektivitet.<br />

De fleste oversættere vil være i stand til at kunne generere en løkke for initiering af alle<br />

objekterne uden det store pladsmæssige forbrug. Dog er store vektor-erklæringer ikke noget,<br />

der bør foregå for ofte i et program, og der henvises i øvrigt til inline-diskussionen i afsnit<br />

2.6.12<br />

Vektoren af String kan naturligvis også initieres på lige fod med normale erklæringer<br />

164 Initiering og nedbrydning af objekter 3.4


med en initieringsliste i en blok:<br />

String navne [3] = { "Jonas", "Laura", "Rikke" };<br />

Her vil de underforståede kald til Strings konstruktør for de tre objekter på navnevektoren<br />

blive gjort til String::String(char*), så de tre objekter bliver initieret med<br />

de respektive C++-strenge i blokken:<br />

navne [0].String ("Jonas");<br />

navne [1].String ("Laura");<br />

navne [2].String ("Rikke");<br />

Hvis angivelsen af vektor-størrelsen er tom (String navne []), vil størrelsen af vektoren<br />

afhænge af antallet af elementer i blokken. Hvis angivelsen af vektor-størrelsen er større end<br />

antallet af elementer i blokken, vil den underforståede konstruktør blive kaldt for de sidste<br />

objekter på vektoren, for hvilke der ikke er eksplicit initierings-data.<br />

Initiering af vektorer af objekter, for hvilke der er konstruktører, som tager to eller flere<br />

paramatre, er syntaksen en smule anderledes. Den numeriske type Complex kan for<br />

eksempel instantieres med to initierings-parametre, og skal derfor oprettes på følgende måde i<br />

skalære erklæringer:<br />

Complex c (10,-1); // Complex c = 10,-1 går ikke<br />

Vektor-erklæringer af typer med den slags konstruktører ser følgelig således ud:<br />

Complex fft_data [4] = { // fire komplekse tal<br />

Complex (3.14, -1), Complex (6.28, -1),<br />

Complex (-3.14, 1), Complex (-6.28, 1)<br />

};<br />

Konstruktør-kaldet udtrykkes dermed eksplicit i stedet for implicit i initieringsblokken. Dette<br />

skyldes simpelthen syntaksen for oprettelse af brugerdefinerede objekter, og hænger til dels<br />

sammen med sprogets syntaksmæssige krav til kompatibilitet med C.<br />

3.5 OVERSTYRING AF OPERATORER<br />

Vi husker fra gennemgangen af C-løsningen på eksempel-problemet med det komplekse tal,<br />

at syntaksen og semantikken i behandlingen af forekomsterne af Complex langtfra var,<br />

hvad vi er vant til med de indbyggede typer. Vi skulle kalde specielle funktioner i bestemte<br />

rækkefølger for at manipulere med objekterne. For at udføre d = a * b - c skulle vi<br />

3.4.17 Vektorer af objekter 165


skrive<br />

mul_Complex (a, b, temp); /* temp1 = a * b */<br />

sub_Complex (temp, c, d); /* d = temp1 - c */<br />

C++ tillader os at redefinere den semantiske mening af næsten alle operatorer for en given<br />

klasse, så for eksempel aritmetiske behandlinger af Complex-forekomster kan foregå mere<br />

naturligt fra en klients synspunkt:<br />

d = a * b - c; // kræver vist ingen forklaring<br />

Faktisk bruger de fleste programmører allerede operator-overstyring, selv om de ikke tænker<br />

over det, fordi næsten alle sprog automatisk overstyrer operatorerne for de indbyggede typer.<br />

I C kan vi for eksempel skrive<br />

int i, j = 2, k = 3;<br />

double f, g = 4.6, h = 8.7;<br />

i = j + k;<br />

f = g + h;<br />

Vi finder det helt naturligt at udtrykket i = j + k adderer to int-typer og tildeler<br />

resultatet en tredie, mens f = g + h gør det samme for tre float-typer. Men<br />

operatorerne = og + har helt forskellige semantik. Den første sætning udfører en heltalsaddition<br />

og heltals-tildeling, mens den anden udfører en kommatals-addition og kommatalstildeling.<br />

Oversætteren behandler heltal og kommatal helt forskelligt som interne<br />

fundamentale typer, og genererer derfor helt forskellig kode for de to sætninger. = og + er<br />

derfor overstyrede operatorer; de har forskellige betydninger i henhold til den kontekst, de<br />

bruges i.<br />

Den store pointe her er, at klienten ikke behøver at bekymre sig om dette. Oversætteren kan,<br />

på grundlag af type-informationen, generere den korrekte kode. Brugerdefineret overstyring<br />

af operatorer for en given C++-klasse er simpelthen en udvidelse af dette koncept: det skal<br />

være muligt at manipulere med forekomster af en hvilken som helst type ved brug af<br />

standard-operatorerne. Dette er muligt, fordi en klasse er en type, som i grunden er ligeså reel<br />

som de fundamentale typer i C++. Oversætteren har al type-information til rådighed, når en<br />

abstrakt datatype udsættes for en operator i en sætning, og kan derfor diskriminere et udtryk<br />

som d = a * b - c for int-typer, double-typer og Complex-typer. Alt, hvad vi<br />

behøver gøre er at at fortælle oversætteren, hvad den skal gøre, når dette sker.<br />

Vi befinder os nu på et grænseland mellem applikations-programmering og<br />

oversætterdesign: at overstyre C++-operatorer er faktisk en udvidelse af oversætterens<br />

semantik. Vi udvider sproget med nye betydninger af samme syntaks. Det kræver naturligvis,<br />

at vi følger visse regler: lad for eksempel være med at overstyre + til at subtrahere. Klienten<br />

skal kunne bruge operatorerne til at abstrahere fra eksplicitte funktionskald og benytte<br />

standard C++-syntaks med samme fordele som de indbyggede operator-overstyringer i<br />

sproget.<br />

166 Overstyring af operatorer 3.5


3.5.1 Operator-funktioner<br />

En operator @ kan overstyres med en funktion til behandling af et udtryk, i hvilken<br />

operatoren @ kombineres med en eller flere operanter af en eller flere bestemte typer. En<br />

funktion erklæres med operator-nøgleordet og operatorens symbol. Skriver vi for<br />

eksempel<br />

Complex a (5, 7), b (2, -3);<br />

Complex c = a - b;<br />

vil oversætteren forsøge at kalde en funktion operator- (operator minus), som tager to<br />

Complex-typer som parametre og har en Complex som returværdi. En sådan funktion kan<br />

implementeres således:<br />

Complex operator- (Complex& a, Complex& b) {<br />

Complex temp (a);<br />

temp.sub (b);<br />

return temp;<br />

}<br />

C++ vil erstatte udtrykket a - b med et funktionskald til operator-() med referencer<br />

til a og b som parametre. operator-() udfører de nødvendige behandlinger af a og<br />

b, og klarer selv brugen af midlertidige variable internt. temp er en midlertidig Complex,<br />

som returneres som resultat af udregningen a - b, og som videre tildeles til c.<br />

Det er altså den samme opgave, der bliver løst, men med en anden og for klienten bedre<br />

syntaks.<br />

3.5.1 Hvad kan overstyres?<br />

Det er tilladt at overstyre følgende operatorer i C++:<br />

[ ] ( ) (type) ++ --<br />

& * + - ~<br />

! / % ><br />

< > = ==<br />

!= ^ | && ||<br />

= *= /= %= +=<br />

-= = &= ^=<br />

3.4.17 Vektorer af objekter 167


|= -> ->* new delete<br />

sizeof<br />

og ulovligt at overstyre<br />

.* . :: ?:<br />

Det er tillige ikke tilladt at definere operator-funktioner for andre symboler end de, som ligger<br />

indbygget i sproget, hvilket gør det ulovligt at erklære en ny operator for for eksempel potensopløftning<br />

(** bruges i flere andre sprog). Det er heller ikke muligt at ændre den indbyggede<br />

præcedens operatorerne imellem. Med andre ord, * vil altid have højere prioritet end + og<br />

-.<br />

Alle binære operatorer, dvs. operatorer, der arbejder på to operanter (for eksempel *), kan<br />

overstyres med operator-funktioner, som tager to parametre, som i ovenstående eksempel<br />

Fortegns-operatorerne, dvs. de, der arbejder på en enkelt operant (for eksempel ! og ~), kan<br />

overstyres med operator-funktioner, som tager et enkelt parameter. Dette gør det muligt at<br />

diskriminere mellem et fortegnsminus og en subtraktion, eller mellem brugen af & som binær<br />

AND og som addresse-på operator:<br />

Complex operator- (Complex&, Complex&); // subtraktion<br />

Complex operator- (Complex&) // fortegnsminus<br />

Complex operator& (Complex&); // addresse-på<br />

Complex operator& (Complex&, Complex&); // bitvis AND<br />

Den eneste operator som tager tre operanter, den triadiske aritmetiske if/else (?:), kan<br />

ikke overstyres.<br />

3.5.3 Overblik<br />

Det er interessant, at lageradministrations-operatorerne new og delete kan overstyres.<br />

Ved at erstatte de indbyggede betydninger af disse operatorer kan en komplet bruger-defineret<br />

lageradministration af en type opnås, især i forbindelse med overstyring af pointer- og<br />

dereference-operatoren * samt reference- og adresse-på operatoren &. Udover new og<br />

delete er indeks-operatoren [] og funktions-kald operatoren () ikke åbenlyse<br />

kandidater, men kan være yderst brugbare.<br />

Det er ikke muligt at overstyre betydninger af operatorer for de fundamentale typer. Med<br />

andre ord, vi kan ikke redefinere meningen af for eksempel + for to int'er. Det er tilladt at<br />

have fundamentale typer som parametre i operator-funktioner, men de må ikke bestå af<br />

udelukkende fundamentale typer:<br />

168 Overstyring af operatorer 3.5


int operator+ (int, int); // ulovlig!<br />

Det er muligt at definere en klasse, som opfører sig akkurat som en indbygget type, og så<br />

overstyre operatorerne for denne klasse. Fordelen ved dette skal ses i sammenhæng til<br />

klassens relationer til andre klasser, et forhold, vi kommer ind på i næste <strong>kapitel</strong>. Men en<br />

erstatning af de fundamentale typer med abstrakte typer giver en væsentlig reduktion i<br />

performance.<br />

Det er tilladt at skrive operator-funktioner som inline-funktioner, hvilket sætter<br />

hastigheden på operationerne drastisk i vejret. Men som for alle inline-funktioner vil du<br />

tabe i kodens størrelse hvad du vinder i dens hastighed. Større funktioner bør ikke inlines,<br />

heller ikke operator-funktioner.<br />

Der er ingen forskel på operatorernes associativitet og præcedens i forhold til hinanden,<br />

men i tidligere versioner af C++ (før 2.1) var det ikke muligt for oversætteren at skelne<br />

mellem en optælling (++) før og efter objektet. Med andre ord, givet en forekomst af en<br />

abstrakt datatype obj, så ville<br />

etObjekt++; // postfix-optælling<br />

++etObjekt; // prefix-optælling<br />

være det samme. I medlemsfunktionen operator++() var det ikke muligt at se, om det<br />

var en prefix eller postfix anvendt operator. I nyere versioner af C++ findes en udvidet<br />

syntaks for netop denne operator (samt naturligvis nedtællingsoperatoren --), som tillader<br />

os at skelne mellem netop disse to anvendelser. Vi udnytter her, at udtrykket a++0 er<br />

syntaktisk ækvivalent til a+0 og lader vores postfix-optællingsfunktion modtage et<br />

parameter, som vi ikke benytter os af på følgende måde:<br />

class X {<br />

public:<br />

operator++ (); // prefix-optælling (++x)<br />

operator++ (int); // postfix-optælling (x++)<br />

operator-- (); // prefix-nedtælling (--x)<br />

operator-- (int); // postfix-nedtælling (x--)<br />

};<br />

void f (X* x) {<br />

++x; // kalder X::operator++ ()<br />

x++; // kalder X::operator++ (int)<br />

--x; // kalder X::operator-- ()<br />

x--; // kalder X::operator-- (int)<br />

}<br />

Heltallet, der overføres til postfix-operatoren er altid 0, medmindre funktionen kaldes<br />

eksplicit (se nedenfor). Denne udvidelse i C++ betyder også, at vi skal være opmærksomme<br />

3.5.3 Overblik 169


på, at de gamle op- og nedtællingsoperatorfunktioner automatisk bliver til prefix-optællinger<br />

hvis de oversættes under nyere systemer. I øvrigt er det ikke tilfældigt, at det er postfixoperatoren,<br />

der modtager et ekstra parameter: alle fortegnsoperatorer har kun ét parameter i<br />

C++ og det har derfor været en nødvendighed at snyde med syntaksen i postfix-operatoren.<br />

Alle operator-funktioner kan i øvrigt kaldes eksplicit. Den ovenstående funktion kan også<br />

kaldes med udtykket<br />

a = operator++ (b);<br />

Eksplicitte funktionskald bruges så godt som aldrig fra klientens programmer, men kan i visse<br />

tilfælde være eneste mulighed fra andre medlemsfunktioner, hvis operanten ikke eksisterer<br />

som egentlig forekomst, eller hvis den er ukendt. Postfix-optællingen kan kaldes med<br />

a = operator++ (b, 0);<br />

eller et andet heltal.<br />

3.5.4 Overstyring af fortegns-operatorer<br />

Fortegns-operatorerne (de modadiske) arbejder alle på en enkelt operant, og omfatter:<br />

& * ~ ! ++<br />

-- (type) sizeof new delete<br />

Disse operatorer kan alle overstyres med en operator-funktion, som tager et enkelt<br />

parameter, og returnerer en forekomst af samme type som parameteren. Der er to undtagelser:<br />

sizeof-operatoren skal returnere en værdi af typen size_t, som er defineret i headerfilen<br />

stddef.h. (type)-operatoren bruges til brugerdefinerede typekonverteringer, og<br />

skal returnere en værdi af samme type, som den, der står i parantesen. Det er meget sjældent,<br />

og ofte farligt, at overstyre sizeof, fordi C++ i visse sammenhænge har skjulte<br />

medlemmer i en klasse. Operator-funktionerne for brugerdefineret typekonvertering samt for<br />

lageradministration (new og delete) beskrives senere i kapitlet.<br />

3.5.5 Overstyring af binære operatorer<br />

De binære (dyadiske) operatorer arbejder alle på to operanter, og omfatter:<br />

%<br />

& * + - /<br />

> ^ | && ||<br />

For binære operatorer gælder det, at operator-funktionen skal modtage to parametre. Disse<br />

170 Overstyring af operatorer 3.5


ehøver ikke nødvendigvis at være af samme type, men er det oftest. Normalt kan operatorfunktioner<br />

med parametre af andre type end returværdien undgås ved hjælp af brugerdefineret<br />

typekonvertering.<br />

Oversætteren har ingen prædefineret idé om, hvilke operationer de forskellige operatorer<br />

gør. Det er en fordel, når vi ønsker en betydning af en operator, som ikke findes i forvejen.<br />

Hvis vi for eksempel ønsker at uddrage "fællesmængden af to mængder", kan vi bruge &operatoren.<br />

Oversætteren er ligeglad med, om & betyder bitvis-AND eller fællesmængde-af.<br />

3.5.6 Overstyring af relationelle operatorer<br />

Alle relationelle operatorer kan overstyres. De relationelle operatorer er:<br />

< > = == !=<br />

Overstyringen af de relationelle operatorer er næsten identisk med overstyringen af de binære.<br />

Forskellen er, at operator-funktionen skal returnere en værdi, der kan testes for værende<br />

"sand" eller "falsk". "Falsk" betyder, at værdien er nul, og "sand", at den er alt andet end nul.<br />

Grunden til, at "sand" og "falsk" står i citationstegn er, at returværdien kan være<br />

hvadsomhelst, blot den kan konverteres til en fundamental type.<br />

Normalt returnerer en relationel operator-funktion en int:<br />

int operator== (Complex& a, Complex& b) {<br />

if (a.isEqual (b)) // sammenlignings-medlemsfunktion<br />

return 1;<br />

else<br />

return 0;<br />

}<br />

Det kan være meget nyttigt at overstyre de relationelle operatoter flere gange - dvs. at<br />

overstyre funktionsnavnene for de overstyrede operatorer - for derved at opnå muligheden for<br />

at kunne sammenligne klasser på kryds og tværs. Dette diskuteres nærmere i afsnit 3.5.16.<br />

3.5.7 Overstyring af tildelings-operatorer<br />

Alle tildelings-operatorer kan overstyres. Disse omfatter:<br />

= *= /= %= += -=<br />

= &= ^= |= !=<br />

Tildelings-operatorer implementeres som de singulære operatorer, dvs. operator-funktioner,<br />

som har en enkelt parameter. Den eneste forskel er, at der ikke er nogen returværdi, samt at<br />

operator-funktionen skal være medlem af en klasse (se afsnit 3.5.10).<br />

3.5.5 Overstyring af binære operatorer 171


Det er normalt ikke nødvendigt at skrive en tildelings-operator, hvis alle medlemmer af<br />

klassen er fundamentale typer. Hvis der imidlertid er medlemmer, som er afledte typer, eller<br />

som er forekomster af andre klasser, er en speciel tildeling nødvendig. Hvis der ikke skrives<br />

en tildelings-funktion, vil et udtryk som<br />

a = b;<br />

simpelthen kopiere indholdet af b til a. Dette skaber de samme problemer som de, vi<br />

diskuterede i afsnit 3.4.11. Kopiering af objekter med tildelings-operatoren diskuteres i afsnit<br />

3.5.12.<br />

De andre tildelings-operatorer, som er hybrider af en aritmetisk eller logisk operator samt en<br />

tildeling, kræver i alle tilfælde en specialskreven operator-funktion. Oversætteren kan ikke<br />

addere to forekomster af brugerdefinerede typer uden hjælp, så der findes ingen<br />

underforståede funktioner til disse tildelings-operatorer.<br />

3.5.8 Overstyring af særlige operatorer<br />

Overstyringer af tilgangsoperatoren til medlemsvariable i datastrukturer eller klasser, ->,<br />

kan overstyres. Et udtryk som k->m fortolkes som (k.operator->())->m. Af den<br />

grund skal en overstyring af denne operator returnere en pointer til et objekt med det<br />

pågældende medlem, eller et for hvilket operatoren også er overstyret. Denne overstyring kan<br />

for eksempel anvendes til at skabe et implicit kald til funktioner under referencer til<br />

medlemmer. På denne måde kan visse medlemmer være public i klassen, og dog være<br />

beskyttet af en overstyret tilgangsoperator. For eksempel,<br />

struct Person {<br />

char* navn;<br />

unsigned alder;<br />

}<br />

class PP { // "abstrakt" pointer til en Person<br />

Person* pers;<br />

public:<br />

PP (Person* p) : pers (p) { }<br />

Person* operator->();<br />

};<br />

// udfør lidt triviel konsistenscheck på den aktuelle Person<br />

Person* PP::operator-> () {<br />

if (pers->navn == 0) {<br />

172 Overstyring af operatorer 3.5


cerr navn<br />

alder ()-<br />

>alder<br />

}<br />

Operatorerne new og delete kan også overstyres, både globalt og for en given klasse<br />

alene. Prototyperne er:<br />

void* operator new (long);<br />

void operator delete (void*);<br />

Ved at overstyre disse to operatorer er det muligt at allokere lager på mere sofistikerede<br />

måder end normalt. For eksempel kan et (objekt-baseret) virtuelt lager administreres af disse<br />

overstyrede operatorfunktioner samt en overstyring af operator*() for forskellige<br />

klasser, så allokeringsrutinen kan vælge en passende metode til udlæsning af objekter for at få<br />

plads til de nye - last used eller least used, for eksempel. Når disse overstyringer blandes<br />

sammen med overstyringer af op- og nedtælling, baner de vejen for semantisk fuldstændige<br />

"kloge pointere".<br />

3.5.9 Retningslinier for operator-overstyring<br />

Selvom principperne bag operator-overstyring er simple, er det ikke en teknik, der bør bruges<br />

uden omhyggelig planlægning og omtanke. Operator-overstyring er så kraftfuld en teknik, at<br />

den kan misbruges på utallige måder, mest med ulæselige programmer til følge. Her er et par<br />

retningslinier for, hvad man bør tilstræbe og hvad man bør undgå:<br />

• Lad så vidt muligt de oprindelige semantiske betydninger af operatorerne stå. Mange<br />

begyndere i C++ bliver så glade for operator-overstyring, at de fuldstændig<br />

forvrænger sproget. C og C++ definerer næsten alle specialtegn i ASCII-sættet til at<br />

have mindst én betydning. Prøv at bibeholde denne betydning - lad for eksempel<br />

være med at redefinere ^ til at stå for potens-opløftning, da den normale betydning<br />

er bitvis exclusive-or, med mindre det er helt nødvendigt.<br />

3.5.8 Overstyring af særlige operatorer 173


• Skriv kun det antal operator-funktioner, som er nødvendige. Det er meget let at<br />

"over-overstyre" en klasse med alle tænkelige operator-funktioner. Kun i klasser, der<br />

definerer numeriske datatyper og andre lignende koncepter er det nødvendigt med et<br />

stort antal operatorer. Normalt er det nok med blot et par stykker.<br />

• Husk at dokumentere alle operatorer fyldigt. Grunden til, at dette er mere nødvendigt<br />

end normalt er, at kaldet til en operator-funktion er underforstået. Hvis klienten ikke<br />

er helt klar over, hvad der foregår, kan operator-overstyring gå hen og blive et<br />

frustrationsmoment af dimensioner.<br />

Dette betyder ikke, at man ikke kan være yderst kreativ i brugen af operator-funktioner, som<br />

vi skal se i afsnittene om indekserings-operatoren og funktionskalds-operatoren.<br />

3.5.10 Operator-funktioner som klassemedlemmer<br />

Operator-funktioner kan, ligesom normale funktioner, være globale, statiske i den aktuelle fil,<br />

eller være en medlemsfunktion af en klasse. Hvis en operator-funktion er medlem af en<br />

klasse, kaldes funktionen med en underforstået første parameter, nemlig den forekomst, der<br />

udgør venstre operant (i binære udtryk) eller den operant, der binder til operatoren (i<br />

singulære udtryk). Fire af operatorerne skal være klassemedlemmer, nemlig tildeling (=),<br />

indeksering ([]), funktionskald (()) samt medlemsreference (->).<br />

Når en operator-funktion kaldes fra et udtryk, skaber oversætteren et underforstået kald med<br />

operanterne som parametre. Hvis typen Complex skal overstyre additions-operatoren, kan<br />

det ske på følgende måde:<br />

Complex operator+ (Complex& a, Complex& b) {<br />

Complex temp (a);<br />

temp.add (b);<br />

return temp;<br />

}<br />

Da operator+(Complex&,Complex&) er en funktion, som arbejder på forekomster af<br />

Complex, men ikke er en medlemsfunktion af klassen Complex, kan den ikke referere de<br />

indre variable, den egentlig har behov for.<br />

Operator-funktionen kan sættes ind i en klasse som medlemsfunktion, hvorved operatorfunktionen<br />

første parameter underforstået bliver den aktuelle forekomst:<br />

class Complex {<br />

double re, im;<br />

public:<br />

Complex operator+ (Complex&) // BINÆR addition!<br />

// ...<br />

174 Overstyring af operatorer 3.5


}<br />

Operator-funktionen Complex::operator+() bliver kaldt i udtryk som:<br />

Complex a, b, c;<br />

a = b + c; // b.operator+ (c) kaldes<br />

Fordelen ved at indsætte operatorer som medlemsfunktioner i en klasse er, at operatorfunktionen<br />

får tilgang til de private data i klassen. operator+ kan nu implementeres<br />

således:<br />

// addér a til det aktuelle objekt<br />

Complex Complex::operator+ (Complex& a) {<br />

Complex temp (re + a.re, im + a.im);<br />

return temp;<br />

}<br />

Husk igen, at C++ bruger klasse-baseret indkapsling, så en medlemsfunktion i Complex har<br />

adgang til private data i alle forekomster af Complex. Derfor er det muligt at konstruere et<br />

midlertidigt objekt med reference til det aktuelle objekts re og im til a's re og im<br />

respektivt. Vi sparer altså at skulle kalde specielle medlemsfunktioner i klassen fra operatorfunktionen<br />

for at komme til de forskellige private data.<br />

3.5.11 friend-funktioner<br />

Desværre er der et problem med indkapslede operator-funktioner, som først kommer til<br />

udtryk, når operatoren bruges i særlige sammenhænge. En medlemsfunktion har et objekt af<br />

sin klasses type som det første underforståede parameter. En operation, som kræver et objekt<br />

af en anden type som venstre led eller første argument skal erklæres som normal funktion.<br />

Overvej følgende klasse:<br />

class X {<br />

int i;<br />

public:<br />

X (int j = 0) { i = j; }<br />

X operator+ (int j) { // for X = X + int<br />

X temp (i + j);<br />

return temp;<br />

}<br />

};<br />

3.5.10 Operator-funktioner som klassemedlemmer 175


Hvis en klient skriver<br />

X a, b (5);<br />

int c = 5;<br />

a = b + c; // a = b.operator+(c)<br />

kaldes operator-funktionen X::operator(int) for b med c som parameter og<br />

returnerer en X med X::i == 10. Hvis klienten ligeledes skiver<br />

X a, b (5);<br />

a = b + 5; // a = b.operator+(5)<br />

vil samme operator-funktion blive kaldt. Problemet opstår, hvis klienten skriver<br />

X a, b (5);<br />

int c = 5;<br />

a = 5 + b; // a = 5.operator+(b) ?<br />

a = c + b; // a = int::operator+(b) ?<br />

C++ kan som sagt ikke overstyre operatorer for de indbyggede typer. Udtrykket 5 + b vil<br />

forsøge at kalde operator+() for en midlertidig int med en parameter af typen X. En<br />

sådan funktion findes ikke. Problemet kan selvfølgelig løses ved at fjerne operator+()<br />

fra klassen X, og gøre den global. Men så har funktionen ikke længere adgang til de private<br />

data i X, som derfor må placeres i public-delen af klassen, som tillader funktionen tilgang<br />

til disse data... men så har alle andre klient-funktioner også adgang til disse data, og vi har<br />

overtrådt en af de fundamentale regler for indkapsling på "need-to-know" basis.<br />

Man kunne overveje, hvorfor oversætteren, når den kommer forbi udtrykket<br />

a = 5 + b;<br />

ikke kan kalde funktionen b.operator+(5) - resultatet ville jo blive det samme.<br />

Desværre er dette ikke muligt, idet faktorernes orden ikke er ligegyldig for alle operatorer.<br />

Hvis vi havde med divisions-operatoren at gøre, ville a.operator/(b) og<br />

b.operator/(a) ikke være det samme.<br />

Der er altså brug for at tildele ikke-medlemsfunktioner adgang til de private dele af en<br />

klasse. Dette gøres med friend-nøgleordet, som giver en ikke-medlemsfunktion samme<br />

privilegier som en medlemsfunktion, selvom den ikke tilhører klassens skop.<br />

En friend-funktion erklæres i klassens specifikation som følger:<br />

class X {<br />

int i;<br />

public:<br />

X (int j = 0) {<br />

i = j;<br />

176 Overstyring af operatorer 3.5


}<br />

friend X operator+ (X&, int);<br />

};<br />

og kan nu implementeres med direkte adgang til de private variable:<br />

X operator+ (X& o, int j)<br />

X temp (o.i + j);<br />

return temp;<br />

}<br />

friend-funktioner er mest brugbare i forbindelse med operator-overstyring. Men det kan<br />

også være nødvendigt at erklære en funktion som friend, hvis den skal have adgang til to<br />

eller flere forskellige klasser. Netop fordi en friend-funktion kan have adgang til flere<br />

klasser har den ingen this-pointer, og har derfor ikke noget underforstået objekt at arbejde<br />

på. Derfor modtager operator+() som ikke-medlems friend-funktion to parametre.<br />

3.5.12 Mere om kopiering af objekter<br />

Selvom erklæringen og brugen af tildelings-operatorer i overstyrede operator-funktioner<br />

minder meget om de, der implementerer andre slags operatorer, er der en væsentlig forskel. I<br />

afsnit 3.4.12 beskrev jeg problemet med at initiere et objekt fra et andet objekt, hvis<br />

objekternes type indholdt pointere eller andre indirekte referencer. Vi husker, at kopikonstruktøren<br />

bruges til at tildele et ikke-initieret objekt værdien af et andet:<br />

Memory a = b; // Memory::Memory (Memory&) kaldes<br />

Hvad nu hvis klienten skrev følgende kode:<br />

Memory a;<br />

a = b; // medlemsvis kopiering, i praksis det<br />

// samme som bitvis kopiering<br />

Hvis vi ikke har defineret en tildelings-operator for klassen Memory, vil b blive kopieret<br />

medlem for medlem til a, med samme problemer til følge, som dem vi beskrev i afsnittet om<br />

kopiering af objekter. Hvis tildelingen a = b skulle kalde kopi-konstruktøren render vi ind<br />

i et andet problem. Kopi-konstruktøren i memory konstruerer jo netop forekomster af<br />

Memory, og allokerer i dette tilfælde nyt lager med mere. Hvis vi kalder kopi-konstruktøren i<br />

en tildeling (husk forskellen mellem initiering og tildeling), er objektet, her a, allerede<br />

initieret, her i sætningen Memory a. Et yderligere kald til en konstruktør (hvadenten det er<br />

kopi-konstruktøren eller en anden konstruktør) vil allokere lageret endnu engang og endda<br />

"glemme" det først allokerede, idet det tidligst allokerede lager ikke deallokeres til systemet.<br />

Tildelings-operatorer i klasser, som har indirekte referencer, skal derfor have en vis<br />

3.5.11 friend-funktioner 177


struktur. For Memory-klassen drejer det sig om, at tildelinen skal rydde op i det aktuelle<br />

objekt, før en reel tildeling kan finde sted. Her er en opdateret version af Memory:<br />

class Memory {<br />

char* mem_ptr;<br />

unsigned int size;<br />

public:<br />

Memory () { // tom initiering<br />

mem_ptr = NULL;<br />

size = 0;<br />

}<br />

Memory (unsigned int i) { // konstruktør<br />

mem_ptr = new char [size = n];<br />

}<br />

Memory (Memory& m) { // kopi-konstruktør<br />

mem_ptr = new char [size = m.size];<br />

memcpy (mem_ptr, m.mem_ptr, size);<br />

}<br />

~Memory () { // destruktør<br />

delete mem_ptr;<br />

}<br />

void operator= (memory&); // tildeling<br />

}<br />

void Memory::operator= (Memory& m) {<br />

if (size != m.size) {<br />

delete mem_ptr;<br />

mem_ptr = new char [size = m.size];<br />

}<br />

memcpy (mem_ptr, m.mem_ptr, size)<br />

}<br />

Alle funktionerne i klassen Memory kaldes underforstået, dvs. uden direkte reference til<br />

funktionsnavnene. Derfor er et eksempel bedst til at beskrive, hvornår og hvordan de kaldes:<br />

void main () {<br />

Memory a (100); // Memory::Memory (unsigned int)<br />

Memory b = a; // Memory::Memory (Memory&)<br />

Memory c; // Memory::Memory ()<br />

c = a; // Memory::operator= (Memory&)<br />

} // 3 gange Memory::~Memory ()<br />

Funktionen Memory::operator=() tester først om de to forekomster har allokeret lager<br />

af samme fysiske størrelse. Hvis det ikke er tilfældet, deallokeres lageret i det aktuelle objekt,<br />

178 Overstyring af operatorer 3.5


og nyt lager allokeres af samme størrelse som det tildelte objekts. Dernæst kopieres selve<br />

lageret.<br />

Forskellen mellem en kopi-konstruktør og en tildelings-operator er i de fleste tilfælde, at<br />

tildelings-operatoren skal foretage en smule lageradministration, fordi objektet allerede er<br />

initieret. Igen er det kun nødvendigt at skrive klasse-specifikke tildelings-operatorfunktioner,<br />

hvis objektet indeholder pointere til forekomster. Hvis alle medlemsvariable er normale<br />

erklæringer af fundamentale eller brugerdefinerede typer, er medlem-per-medlemkopieringen,<br />

som standard-tildelingen udfører, tilstrækkelig.<br />

3.5.13 Midlertidige objekter<br />

I de foregående afsnit har vi set, hvordan et midlertidigt objekt er blevet brugt til for eksempel<br />

at holde en mellemregning i en operator-funktion. Midlertidige objekter kan også oprettes i<br />

sammensatte udtryk:<br />

Complex a = Complex (2.2, 4.4) + Complex (3.3, 7.7);<br />

Denne sætning resulterer i tre nye forekomster af Complex, nemlig objektet a og to<br />

midlertidige, unavngivne objekter. Konstruktøren Complex::Complex(double,<br />

double) bliver kaldt for de to midlertidige objekter som det første. Dernæst kaldes<br />

operator-funktionen operator+(Complex&,Complex&) med de to midlertidige<br />

forekomster, og til sidst kaldes konstruktøren Complex::Complex (Complex&) for a<br />

med resultatet fra operator+. Hvis vi ser på indholdet af operator+(), finder vi, at<br />

denne også opretter et midlertidigt objekt, temp, som bruges i mellemregningen til at holde<br />

summen af de to komplekse tal i udtrykket. temp bliver fjernet, når operator+() slutter,<br />

og de to midlertidige objekter i udtrykket fjernes på et ikke nærmere specificeret tidspunkt,<br />

dog ofte så hurtigt som muligt. Det er imidlertid ikke et problem, som kommer programmøren<br />

ved, fordi oversætteren genererer denne kode automatisk.<br />

Midlertidige objekter kan bruges i næsten alle sammenhænge. Her er medlemsfunktionen<br />

operator+() i en lidt anden udgave:<br />

inline Complex operator+ (Complex& c1, Complex& c2) {<br />

return Complex (c1.re + c2.re, c1.im + c2.im);<br />

}<br />

Her oprettes et lokalt, midlertidigt objekt, som konstrueres (ved et kald til<br />

Complex::Complex(double,double)) med summen af henholdsvis den reelle og den<br />

imaginære del som parametre til konstruktøren. Det er sandsynligvis en lille smule bedre end<br />

at bruge et navngivent objekt, som i denne implementation:<br />

inline Complex operator+ (Complex& c1, Complex& c2) {<br />

Complex temp;<br />

temp.re = c1.re + c2.re;<br />

3.5.12 Mere om kopiering af objekter 179


temp.im = c1.im + c2.im;<br />

return temp;<br />

}<br />

De to versioner udfører nemlig samme opgave. I den første version foretages først additioner<br />

af de medlemsvariablene i de to objekter, og dernæst kaldes en konstruktør for et midlertidigt<br />

objekt, som returneres med det samme. I anden version kaldes default-konstruktøren for det<br />

midlertidige objekt, og dernæst tildeles de to medlemsvariable summen af de to additioner,<br />

hvorpå det midlertidige objekt returneres. Brug derfor unavngivne midlertidige objekter, hvis<br />

det er muligt, og specielt i inline-funktioner. En optimerende oversætter vil kunne<br />

inferere mere om midlertidige objekter, som er udenfor programmørens kontrol, en de, som<br />

behandles direkte af programmet.<br />

Vi ser altså, at C++ kan konstruere og destruere midlertidige objekter, med underforståede<br />

funktionskald til følge. Det er i denne sammenhæng vigtigt at identificere de mest frekvente<br />

underforståede kald til medlemsfunktioner, for derpå at skrive disse kortest muligt og gøre<br />

dem inline. Som sagt bliver objekt-orienterede sprog ofte kritiseret for ikke at være<br />

effektive og at introducere fejl under kørslen. Kun ved at skrive de medlems-funktioner i<br />

klasser på den rigtige måde kan vi sikre, at effektiviteten i objektkoden er så god eller bedre<br />

end hvis vi skulle foretage alle de underforståede kald manuelt. Dette er især vigtigt, når vi i<br />

næste <strong>kapitel</strong> begynder på nedarvnings-begrebet, hvor et enkelt kald (underforstået eller ej)<br />

kan resultere i mange andre skjulte kald til andre klassers medlemsfunktioner.<br />

3.5.14 Overstyring af indekserings-operatoren<br />

Overstyring af de særlige operatorer som for eksempel indeksering ([]) giver<br />

klassedesigneren mulighed for at styre tilgangen til klassen med størst frihed. Brugerdefineret<br />

indeksering betyder, at en klients brug af operatoren medfører et kald til operator[]() i<br />

den pågældende klasse. String-klassen fra forrige afsnit kan udbygges med en<br />

indeksering, som udgør en rimelig syntaks for tilgang til de individuelle enheder i strengen:<br />

class String {<br />

char* cp;<br />

public:<br />

String () { cp = 0; }<br />

String (char*);<br />

String (String&);<br />

~String ();<br />

char operator[] (unsigned);<br />

};<br />

char String::operator[] (unsigned indeks) {<br />

return *(cp + indeks);<br />

}<br />

180 Overstyring af operatorer 3.5


void f () {<br />

String a = "Due";<br />

char b = a [1]; // b == 'u'<br />

}<br />

Medlemsfunktionen, som overstyrer indeksering, kan returnere en hvilkensomhelst type, idet<br />

semantikken bag referencen afhænger af klassens definition. I ovenstående klasse returneres<br />

en forekomst af char, som kan tildeles til andre objekter. Her er en oplagt mulighed for<br />

forbedring, samt at se reference-typens brugbarhed: hvad sker, hvis vi skriver<br />

void f () {<br />

String a = "Due", b = "Martin";<br />

a [2] = b [3]; // char = char ?<br />

}<br />

operator[] kaldes for b med 3, og returnerer en midlertidig forekomst af en char. Det<br />

samme sker for a, hvorved vi har en højreværdi på begge sider af lighedstegnet. Det, vi<br />

forsøger, er at tildele et element i a med en char, hvilket kan realiseres med referencer:<br />

class String {<br />

char* cp;<br />

public:<br />

// ...<br />

char& operator[] (unsigned);<br />

};<br />

char& operator[] (unsigned indeks) {<br />

return *(cp + indeks);<br />

}<br />

void f () {<br />

String a = "Due", b = "Martin";<br />

a [2] = b [3]; // ok: char& = char&<br />

} // a == "Dut"<br />

Ved at returnere en reference, overføres kun type- og adresseinformation, men ingen<br />

midlertidige objekter. Dermed kan udtrykket bruges som både venstre- og højreværdi i et<br />

udtryk, og det bliver tilmed muligt at bruge samme funktion til både ind- og udlæsning via<br />

indeksering. Udtrykket a [2] = b [3] evaluerer først b [3] til værdien 't' og<br />

dernæst a [2] til værdien 'e'. Da begge udtryk er referencer, og da det er tilladt at tildele<br />

en værdi til en reference-type, er semantikken i udtrykket faktisk noget ligenende<br />

char& a = 't';<br />

3.5.14 Overstyring af indekserings-operatoren 181


char& b = 'e';<br />

a = b;<br />

Det er altså reference-typen, som er magien bag at kunne bruge funktioner som disse på begge<br />

sider af lighedstegnet. En indekseringsfunktion som den, der bruges i String vil typisk<br />

indeholde et check på indeks-værdien, så den ikke hopper udenfor størrelsen på vektoren.<br />

En interessant detalje ved overstyring af indekserings-operatoren er, at det ikke<br />

nødvendigvis er et heltal, som skal bruges som indeks. Selvom det kræver en bedre<br />

dokumentering af klassen, at en standard-funktion antager en anden syntaks, er der klare<br />

fordele ved specialiseringer, specielt da operator-funktioner kan overstyres som normale<br />

funktioner som beskrevet i afsnit 3.5.16. Man kan for eksempel overveje en klasse, som<br />

beskriver en symboltabel, dvs. en liste af navne med tilhørende værdier. En indeksering i en<br />

sådan klasse med indeks-operatoren for opslag i navnene for værdierne kunne se således ud:<br />

class SymbolTabel {<br />

// ...<br />

public:<br />

// ...<br />

int operator[] (char*);<br />

};<br />

void f () {<br />

SymbolTabel s;<br />

// ...<br />

a = s ["cur_x"]; // slå værdien af cur_x op<br />

};<br />

eller en klasse, som definerer en bølgeform, og som implementerer [] til indeksering i<br />

bølgeformens y-værdier med kommatal, så en flydende indeksering er mulig ved interpolation<br />

mellem værdierne i bølgeformen:<br />

class Waveform {<br />

double *d;<br />

public:<br />

Waveform (int);<br />

double& operator[] (double&);<br />

};<br />

Waveform w (100); // 100 værdier<br />

void f () {<br />

182 Overstyring af operatorer 3.5


Waveform c (300); // 300 værdier<br />

for (double a = 0; a < 100; a += 1/3)<br />

c [a*3] = w [a]; // tredobbelt længde på bølgeformen<br />

};<br />

Denne frihed bliver bedst udnyttet, hvis klassens grænseflade designes bedst muligt, som vi<br />

skal se nærmere på i <strong>kapitel</strong> 5. Der bør dog ikke herske tvivl om, at en grænseflade, som<br />

designes til klassens natur på en rigtig måde, kan medvirke til en langt bedre brug af klassen<br />

for klienten.<br />

3.5.15 Overstyring af funktionskalds-operator<br />

Overstyringen af funktionskalds-operatoren kan først se lidt overflødig ud, da man jo blot kan<br />

erklære og definere en funktion med et hvilketsomhelst navn. Denne overstyring har også<br />

mindre anvendelighed end de andre, og bruges i mindre grad. En populær anvendelse er<br />

imidlertid en iterator, dvs. et specielt objekt som gennemløber andre objekter. Iterationer i<br />

abstrakte datatyper kan klares med overstyringer af indeks-operatoren, men en alternativ<br />

syntaks kan være at foretrække, jævnfør følgende normale C++-program:<br />

void gets (char* p) { // get-string<br />

char ch;<br />

while ((ch = getchar()) != '\n') *p = ch, p++;<br />

*p = '\0';<br />

}<br />

Her er p en iterator, fordi den gennemløber en tegnsekvens. Funktionen getchar() giver<br />

for hvert kald det næste tegn i input-bufferen. En klasse kan med samme funktionalitet som<br />

mål overstyre funktionskalds-operatoren på følgende måde:<br />

class String {<br />

char* cp;<br />

unsigned len; // længde på strengen<br />

unsigned indeks; // aktuelle indeks<br />

public:<br />

// ...<br />

void start () { indeks = 0; }<br />

char operator() (); // funktionskald<br />

}<br />

char String::operator() () {<br />

if (indeks < len) return cp [indeks++];<br />

3.5.14 Overstyring af indekserings-operatoren 183


eturn indeks = 0;<br />

}<br />

Funktionskalds-operatoren gennemløber strengen ved en underforstået interation og<br />

indeksering med medlemsvariablen indeks. Klienten behøver dermed ikke arbejde med<br />

indekseringer og holde rede på indeksværdier og slutbetingelser, da denne kompleksitet via<br />

funktionskalds-operatoren kan indkapsles i klassen. Sagt på en anden måde ved klassen bedst<br />

selv, hvordan den gennemløbes:<br />

void putString (String& s) {<br />

char ch;<br />

s.start (); // start iteration på s<br />

while ((ch = s ()) != 0)<br />

cout


inline Complex operator+ (Complex& c, double& d) {<br />

return Complex (c.re + d, c.im);<br />

}<br />

// addér en double og en Complex<br />

inline Complex operator+ (double& d, Complex& c) {<br />

return Complex (c.re + d, c.im);<br />

}<br />

Husk, at overstyringer af funktioner er mulige, fordi C++ kan diskriminere mellem de enkelte<br />

funktioner på type-informationen fra parameterlisterne. Vi kan nu underforstået kalde de<br />

forskellige operator+() funktioner i Complex:<br />

Complex a (3, 5);<br />

Complex b (3.2, 7);<br />

double c = 2.5;<br />

Complex d = a + b; // operator+ med to Complex<br />

Complex e = a + c; // operator+ med Complex og double<br />

Complex f = c + a; // operator+ med double og Complex<br />

Complex g = c + c; // konstruktør med enkelt double<br />

Overstyringer af operator-funktioner tillader os altså at kunne blande vores objekter med<br />

objekter af andre typer (også fundamentale) i enkelte udtryk. Der er imidlertid en fare for, at<br />

der skrives alt for mange operator-funktioner, for eksempel en for hver slags addition mellem<br />

alle slags typer i et program og for alle klasser i programmet. Det kan blive til hundredevis af<br />

operator-funktioner for selv et mindre program. Hvis vi ser på, hvad opgaven egentlig består<br />

i, finder vi, at det drejer sig om at konvertere fra en type til en anden. C++ giver os, som vi<br />

skal se i afsnit 3.6, mulighed for at specificere brugerdefinerede typekonverteringer til og fra<br />

enhver klasse. Disse typekonverteringer er under programmørens kontrol, men kaldes<br />

underforstået, og unødvendiggør dermed for mange medlemsfunktioner med samme indhold<br />

for forskellige typer.<br />

3.5.17 Overstyringer i standardbiblioteket<br />

Vi kan nu se sammenhængen i, at standardbiblioteket kan bruge operatorerne > som<br />

input- og output-operatorer. Standard-biblioteket definerer en række klasser, to af hvilke har<br />

navnene istream og ostream for input-strøm og output-strøm. Disse to klasser bliver<br />

instantieret fire gange i header-filen, nemlig som<br />

istream cin; // input-strøm<br />

ostream cout; // output-strøm<br />

ostream cerr; // fejl-strøm<br />

3.5.16 Overstyringer af operator-funktioner 185


ostream clog; // formateret fejl-strøm<br />

I istream-klassen findes en overstyring af operatoren >>, og i ostream findes en<br />

overstyring af operatoren > (istream&, int&);<br />

istream& operator>> (istream&, float&);<br />

//...<br />

ostream& operator


cout. Den vil derfor kunne bruges i alle slags objekter, som er forekomster af ostream,<br />

inklusive fil-objekter og fejl-objekter:<br />

Complex a;<br />

fstream f ("resultat.txt");<br />

f


lægger to komplekse tal sammen med addér-og-tildel operatoren. Den normale semantik af<br />

+= er, at den både foretager en aritmetisk behandling og en tildeling, men også at hele<br />

sætningen er et udtryk, som er resultatet af sætningen, hvilket tillader os at skrive sætninger<br />

som<br />

a = b += c;<br />

hvor a tildeles værdien af b efter den er blevet lagt sammen med c. Hvis vi vil overstyre<br />

denne funktion med standard-semantik, må vi returnere en reference til b i den funktion, der<br />

implementerer overstyringen. Denne reference er netop this. For Complex-klassen<br />

kunne det se således ud:<br />

Complex& Complex::operator+= (Complex& other) {<br />

re += other.re, im += other.im;<br />

return *this;<br />

}<br />

Dette sker i stor udstrækning i standardbibliotekets overstyringer af ind- og<br />

udlæsningsoperatorerne >. Disse medlemsfunktioner returnerer referencer til den<br />

aktuelle strøm, hvilket tillader os at skrive flere objekter efter hinanden, som i<br />

cout


implementerer en hægtet liste.<br />

class Haegte {<br />

Haegte* next;<br />

public:<br />

void Indsaet (Haegte* sted) {<br />

next = sted->next, sted->next = this;<br />

}<br />

// ...<br />

};<br />

Medlemsfunktionen Haegte::Indsaet() modtager en pointer til en anden Haegte og<br />

kæder den aktuelle hægte ind lige efter denne. Til dette formål benyttes this på en elegant<br />

måde.<br />

3.5.19 Tredje løsning i C++<br />

// Complex.h<br />

// eksempel 3-5a: en C++-specifikation af et komplekst tal<br />

class Complex {<br />

double re, im;<br />

public:<br />

Complex ();<br />

Complex (double, double = 0);<br />

Complex (Complex&);<br />

void operator= (Complex&);<br />

friend Complex operator+ (Complex&, Complex&);<br />

friend Complex operator- (Complex&, Complex&);<br />

friend Complex operator* (Complex&, Complex&);<br />

friend ostream& operator


e = 0, im = 0;<br />

}<br />

inline Complex::Complex (double r, double i) {<br />

re = r, im = i;<br />

}<br />

inline Complex::Complex (Complex& c) {<br />

re = c.re, im = c.im;<br />

}<br />

inline void Complex::operator= (Complex& c) {<br />

re = c.re, im = c.im;<br />

}<br />

Complex Complex::operator+ (Complex &a, Complex& b) {<br />

return Complex (a.re + b.re, a.im + b.im);<br />

}<br />

Complex Complex::operator- (Complex &a, Complex& b) {<br />

return Complex (a.re - b.re, a.im - b.im);<br />

}<br />

Complex Complex::operator* (Complex &c) {<br />

return Complex (a.re * b.re - a.im * b.im,<br />

a.re * c.im + a.im * c.re);<br />

}<br />

ostream& operator


cout


Complex + double<br />

Complex operator+ (Complex& c, double& d) {<br />

return Complex (c.re + d, c.im);<br />

}<br />

// Complex + int<br />

Complex operator+ (Complex& c, int i) {<br />

return Complex (c.re + i, c.im);<br />

}<br />

// Complex + long<br />

Complex operator+ (Complex& c, long l) {<br />

return Complex (c.re + l, c.im);<br />

}<br />

// og så videre for de omvendte relationer<br />

Kroppen af operator-funktionerne er alle næsten ens. Løsningen på problemet er derfor<br />

enkelt: vi specificerer en gang for alle, hvordan en type relaterer til en anden type, og lader<br />

denne specifikation hedde en brugerdefineret typekonvertering. Denne typekonvertering kan<br />

og vil bruges i sammensatte udtryk af forskellige typer på samme måde som relationer<br />

mellem de indbyggede typer, som vi kender:<br />

int i = 5;<br />

long l = i; // i konverteres til long(i)<br />

Complex c (3, 7);<br />

double d = c; // c konverteres til double (c)<br />

Det er muligt både at specificere konverteringer fra en type T til andre typer og<br />

konverteringer til en type T fra andre typer. I de næste afsnit skal vi se, hvordan vi skriver<br />

funktioner til konvertering af brugerdefinerede typer, i hvilke sammenhænge de kaldes, og<br />

hvordan de spiller sammen med andre funktioner.<br />

3.6.1 Konverterings-funktioner<br />

I C++ beskrives en konvertering fra type A til type B som en medlemsfunktion i B, som<br />

overstyrer typenavnet A og returnerer en forekomst af A. Hvis vi, som i ovenstående<br />

kodefragment, vil tildele en Complex til en double kan vi realisere dette med følgende:<br />

class Complex {<br />

double re, im;<br />

public:<br />

// ...<br />

192 Brugerdefineret typekonvertering 3.6


operator double () { return re; }<br />

};<br />

Nu vil alle konverteringer fra Complex til double gå gennem funktionen<br />

Complex::operator double():<br />

Complex c (2, 4);<br />

double d = c; // kalder c.operator double ()<br />

Dette kan lade sig gøre, fordi C++ kører statisk type-check på alle forekomster, altså på<br />

oversættertidspunktet. Det væsentlige er her, at C++ vil kalde konverteringsfunktionen<br />

Complex::operator double () hver gang en forekomst af Complex befinder sig,<br />

hvor en double er påkrævet, både i klientkode og i privat klasse-kode:<br />

void f (double);<br />

void g (int);<br />

double h (double d) {<br />

Complex c = -d; // c.Complex (double, double) kaldes<br />

return c; // c konverteres til double før return<br />

}<br />

void main () {<br />

Complex c;<br />

f (c); // c konverteres til double før kaldet<br />

g (c); // fejl, ingen Complex::operator int()<br />

double i = 7;<br />

double j = i + c; // c konv. til double før addition<br />

double k = j * c; // c konv. til double før<br />

multiplikation<br />

i = h (j);<br />

}<br />

Læg mærke til, at en konverteringsfunktion ikke har nogen returtype, dog returnerer en<br />

forekomst af samme type som navnet på funktionen:<br />

Complex::operator double () {<br />

return re;<br />

}<br />

Grunden til dette er, at funktionen kun kan returnere en bestemt type, og derfor tillades en<br />

returtype ikke. Visse C++ oversættere tillader en erklæring af en konverterings-funktion med<br />

returtype-information af samme type som funktionens navn og giver en advarsel om<br />

3.6.1 Konverterings-funktioner 193


syntaksens overflødighed.<br />

3.6.2 Eksplicit konvertering<br />

Programmøren kan kontrollere typekonverteringen for brugerdefinerede typer på samme<br />

måde som de fundamentale ved hjælp af eksplicitte konverteringer eller type-casts. Syntaksen<br />

for dette gør kaldemetoden for de brugerdefinerede konverteringsfunktioner lettere at forstå:<br />

Complex c;<br />

double d = double (c); // eksplicit konvertering<br />

Eksplicit typekonvertering af brugerdefinerede typer er normalt ikke nødvendig, men kan<br />

være uundgåelige, hvis der fremkommer præcisions- eller tvetydighedsproblemer i et udtryk.<br />

Dette behandles i slutningen af afsnittet.<br />

3.6.3 Implicit konvertering<br />

Implicit konvertering fra en type A til en type B forekommer hver gang, en forekomst af B<br />

skal bruges, hvor en A findes. Medlemsfunktionen<br />

A::operator B ();<br />

eller konstruktøren<br />

B::B (A);<br />

foretager denne konvertering. C++ vil på oversættertidspunktet markere en underforstået<br />

typekonvertering, som ikke kan findes som medlemsfunktion, som fejlagtig. Det giver<br />

programmøren en sikkerhed for, at fejl af denne slags ikke bobler igennem til objektkoden,<br />

som for eksempel i sprog som Smalltalk. Vi har set, hvordan implicit typekonvertering<br />

fungerer i tildelinger af en Complex til en double.<br />

3.6.4 Konstruktører og konvertering<br />

Vi husker, at kontruktører kan overstyres som normale funktioner. En anden måde at se på<br />

dette er, at et objekt kan konstrueres på flere måder - fra forskellige typer. En konstruktør kan<br />

altså bruges i typekonverteringer, hvis en konvertering fra en vilkårlig type til den aktuelle<br />

type er nødvendig, specielt i forbindelse med midlertidige forekomster. For klassen<br />

Complex, som har en konstruktør Complex::Complex(double,double=0), er det<br />

muligt at skrive:<br />

194 Brugerdefineret typekonvertering 3.6


Complex a (3, 4);<br />

Complex b = a + 5; // 5 konverteres til en Complex<br />

I udtrykket b = a + 5 ser oversætteren, at udtrykket skal resultere i en Complex,<br />

hvorved konverteringsreglerne (se afsnit 2.4.10) for Complex sættes i kraft. Der konstrueres<br />

et midlertidigt Complex-objekt ved et kald til<br />

Complex::Complex(double&,double&) som adderes med a ved et kald til<br />

operator+(Complex&,Complex&), hvorefter resultatet tildeles b under initieringen i<br />

dennes kopi-konstruktør Complex::Complex (Complex&).<br />

Konstruktører kan altså bruges i konvertering, når en forekomst af en type skal bruges<br />

midlertidigt. Det er med denne metode ikke muligt at konvertere til fundamentale typer, da<br />

disse ikke er klasser, og da konverteringsreglerne for dem ligger fast. Her skal i stedet<br />

anvendes konverteringsfunktioner som beskrevet i 3.6.1, fordi de i modsætning til<br />

konstruktørerne er i stand til at konvertere fra en klasse til en fundamental type samt i øvrigt<br />

at konvertere fra en klasse til en anden uden at ændre i modtagerklassen.<br />

3.6.5 Konverteringer i flere led<br />

Hvis resultatet af et udtryk semantisk skal give et resultat af typen T, men faktisk giver et<br />

resultat af typen S, og hvis der findes en konverteringsmulighed fra S til T, vil<br />

oversætteren indsætte den nødvendige konvertering. Dette gælder for de indbyggede typer,<br />

hvor flere konverteringer underforstået kan forekomme i samme udtryk:<br />

int i = 1;<br />

long l = 2; // l konverteres til int, resultatet<br />

double d = i + l; // til en double<br />

For underforståede typekonverteringer i brugerdefinerede typer gælder det ligeledes, at hvis<br />

en forekomst af type T skal bruges på venstre side af udtrykket, hvis kun en<br />

konverteringsfunktion til type S forefindes, og hvis der findes en konverteringsmetode fra S<br />

til T, vil udtrykket være korrekt. For eksempel:<br />

class Complex {<br />

double re, im;<br />

public:<br />

// ...<br />

operator int () { return re; }<br />

};<br />

void main () {<br />

Complex c;<br />

long l = c;<br />

3.6.4 Konstruktører og konvertering 195


}<br />

Selvom der ikke findes nogen konverteringsfunktion fra en Complex til en long, vil det<br />

stadig være muligt at kunne foretage en sådan tildeling, fordi der kan banes vej via en<br />

konvertering til en int. Complex bliver til int, int bliver til long. Sådanne omveje<br />

er udmærkede for konverteringer, som foretages sjældent i programmet, men hvis de ofte<br />

sker, bør der skrives en specifik konverteringsfunktion, så et minimum af konverteringer sker<br />

(de tager alle tid). Som for alle former for underforstået konvertering er fordelen, at selve<br />

behandlingen overlades til oversætteren og derfor ikke skal skrives eksplicit i kildeteksten.<br />

En anden form for flerledet konvertering viser sig i forbindelse med gentagne kald til<br />

konstruktør-funktioner i klasser:<br />

class A { public: A (int); };<br />

class B { public: B (A); };<br />

class C { public: C (B); };<br />

void f (A); // funktion med en A som parameter<br />

void g (B); // funktion med en B som parameter<br />

void h (C); // funktion med en C som parameter<br />

void main () {<br />

int i = 0;<br />

f (i); // konstruerer en A fra i, kalder f (A)<br />

g (i); // konstruerer en A fra i, konstruerer en<br />

// B fra A og kalder f (B)<br />

h (i); // fejl: kan ikke konstruere en C fra<br />

// en int<br />

}<br />

Vi ser, at når der findes en vej fra en type til en anden, vil der blive udført det antal<br />

konverteringer, der er nødvendige for at opnå et resultat af den pågældende type, dog kun i<br />

højest to led. I kaldet til g() i eksemplet ovenfor bliver der konstrueret et midlertidigt Aobjekt,<br />

selv om hverken kaldet eller parameterlisten henviser til A.<br />

Pas dog på med overstyrede funktioner, fordi det kan lede til tvetydigheder:<br />

class A {<br />

// ...<br />

public:<br />

A (int); // konstruér A fra en int<br />

// ...<br />

};<br />

class B {<br />

196 Brugerdefineret typekonvertering 3.6


...<br />

public:<br />

B (int); // konstruér B fra en int<br />

// ...<br />

};<br />

void f (A); // funktion tager en A<br />

void f (B); // funktion tager en B<br />

void main () {<br />

int i = 0;<br />

f (i); // f(A(i)) eller f(B(i)) ?<br />

}<br />

Problemet med funktionskaldet til f() er, at oversætteren kan konvertere en int til både<br />

en A og til en B. Funktionskaldet til den overstyrede funktion er dermed ikke mere entydigt,<br />

og oversætteren vil ikke kunne binde kaldet korrekt. Derfor er dette kald ulovligt.<br />

3.6.6 Objektets underforståede værdi<br />

Det kan være brugbart at give en brugerdefineret datatype en underforstået værdi, som tillader<br />

klienten at konvertere objektet til en fundamental type. Den underforståede værdi er for<br />

eksempel brugbar når der skal testes for fejl:<br />

ADT x; // x er en Abstrakt DataType<br />

if (x) // er x ok?<br />

Denne konvertering kan for eksempel være en overstyring af operator void*(), da en<br />

pointer med nul-værdi traditionelt er en sikker angivelse af en værdi, der ikke kan bruges til<br />

andet end fejlbehandling. På samme vis kan operator!() overstyres med den modsatte<br />

betydning.<br />

class ADT {<br />

public:<br />

operator void* () { /* ... */ }<br />

// ...<br />

};<br />

I standardbiblioteket iostream (<strong>kapitel</strong> 6) overstyres mange klasser med operatorer for<br />

underforståede værdier, så for eksempel en I/O-fejl nemt kan opdages i programmet.<br />

3.6.5 Konverteringer i flere led 197


3.6.7 Fjerde løsning i C++<br />

Med de nye metoder for brugerdefineret typekonvertering, vi har introduceret i dette afsnit,<br />

kan vi konstruere en mere komplet Complex-klasse, som listes i eksempel 3-6.<br />

// Complex.h<br />

// eksempel 3-6a: en C++-specifikation af et komplekst tal<br />

class Complex {<br />

double re, im;<br />

public:<br />

Complex ();<br />

Complex (double&, double& = 0);<br />

Complex (Complex&);<br />

void operator= (Complex&);<br />

friend Complex operator+ (Complex&, Complex&);<br />

friend Complex operator- (Complex&, Complex&);<br />

friend Complex operator* (Complex&, Complex&);<br />

friend ostream& operator


Complex operator+ (Complex &a, Complex& b) {<br />

return Complex (a.re + b.re, a.im + b.im);<br />

}<br />

Complex operator- (Complex &a, Complex& b) {<br />

return Complex (a.re - b.re, a.im - b.im);<br />

}<br />

Complex operator* (Complex &c) {<br />

return Complex (a.re * b.re - a.im * b.im,<br />

a.re * c.im + a.im * c.re);<br />

}<br />

inline Complex::operator long () {<br />

return long (re);<br />

}<br />

inline Complex::operator double () {<br />

return double (re);<br />

}<br />

ostream& operator


I main() i eksempel 3-6c findes flere typekonverteringer:<br />

• i udtrykket a * b - 3 konverteres heltallet 3 først til en double, og dernæst til<br />

en Complex via konstruktøren Complex::Complex (double&,<br />

double&).<br />

• i udtrykket e + c konverteres e fra en double til en long og c fra en<br />

Complex til en long via konverterings-funktionen Complex::operator<br />

long().<br />

• i tildelingen i = d konverteres d først fra en Complex til en long, og dernæst<br />

til en int.<br />

Klassen Complex kan nu opfylde de fleste af de krav, vi opstillede i starten af kapitlet:<br />

• vi kan allokere og initiere forekomster af Complex på en måde, som minimerer<br />

fejlrisici og tvetydigheder,<br />

• vi kan deallokere forekomster af Complex med sikkerhed for korrekt oprydning,<br />

• vi kan kopiere forekomster af Complex med sikkerhed for korrekt opsætning,<br />

• vi kan blande Complex med forekomster af andre typer i sammensatte udtryk,<br />

• vi kan abstrahere fra implementationen af Complex, ved at bruge standardoperatorer<br />

til manipulation,<br />

• vi kan udskrive forekomster af Complex med samme syntaks som andre typer.<br />

De problemer, vi opremsede i afsnit 3.2.2, kan vi nu gennemgå for eksempel 3-6:<br />

• Syntaks. Manipulation af forekomster gøres gennem medlemsfunktioner, som har et<br />

begrænset, "modulært" skop indenfor den klasse, de tilhører. Ved hjælp af<br />

overstyringer af standard-operatorerne har vi opnået en blød grænseflade fra<br />

klientens kode til klassens funktioner. Typekonverterings-funktionerne sparer os fra<br />

at skrive utallige næsten ens funktioner med en anelse forskelligt indhold.<br />

• Semantik. Selvom vi faktisk udvider sproget C++ til at indeholde en ny type, betyder<br />

det ikke, at der gælder andre regler. Alle henvisninger til og med den ny type vil<br />

blive checket for ækvivalens, og fejl vil blive rapporteret på oversættertidspunktet.<br />

Operator-præcedens i udtryk med komplekse tal gælder som de plejer, og som en<br />

vilkårlig klient ville forvente.<br />

• Lageradministration. Ved hjælp af de specielle medlemsfunktioner, konstruktøren og<br />

200 Brugerdefineret typekonvertering 3.6


destruktøren, kan vi beskrive, hvordan en forekomst af en given klasse skal forholde<br />

sig under initieringen og oprydningen. Klienten skal ikke huske at kalde funktioner,<br />

det gøres automatisk ved start og slut på objektets skop.<br />

• Mellemregninger. Alle mellemregninger er flyttet fra klientens kode til<br />

medlemsfunktionerne i klassen, hvor de ligger som midlertidige variable.<br />

• Dokumentation. Idet klassen har en entydig specifikation af grænsefladen til klienten<br />

(public-delen af klassen), har klienten en bedre direkte forståelse for, hvordan<br />

forekomster af klassen skal manipuleres. Det er i det hele taget et mindre problem,<br />

fordi de mange underforståede kald ikke kræver dokumentation. Derudover kan<br />

selve klassens specifikation bruges i dokumentationsfasen, fordi klassen indkapsler<br />

alle funktioner under samme domæne.<br />

• Udvidelsesmuligheder. En udvidelse med for eksempel en divisionsmetode til<br />

Complex-klassen er langt lettere i eksempel 3-6 end i eksempel 3-2, fordi det blot<br />

indebærer en ny operator/()-funktion. Generelt skal koden stadig testes for fejl<br />

på samme måde som før, men findes en fejl, er den lettere at lokalisere, fordi<br />

grænsefladerne på grund af indkapsling er ridset klart op. Når vi vil blande abstrakte<br />

datatyper med andre abstrakte datatyper samt fundamentale typer, giver<br />

underforstået, brugerdefineret typekonvertering mulighed for "usynligt" at løse<br />

opgaven uden indblanding fra klientens side. Overstyring af funktionsnavne gør det<br />

også lettere at vedligeholde en klasse, fordi ingen nye funktioner med unikke navne<br />

behøves introduceret. Vigtigst er det dog, at indkapslingen sikrer, at klienten ikke<br />

skriver kode, som er afhængig af den indre implementation af en type. Dermed kan<br />

typen ændres og udvides uden at det har betydning for klientens program, som end<br />

ikke behøver genoversættelse.<br />

• Sikkerhed. Indkapslingen af en klasse muliggør, at klassen kan skjule de<br />

implementations-afhængige dele i private-delen af specifikationen. Klienten har,<br />

støttet af sproget, slet ikke adgang til disse data og funktioner, og kan dermed ikke<br />

gøre noget forkert i denne henseende.<br />

3.7 KLASSER OG POINTERE<br />

Pointere til medlemmer i datastrukturer eller klasser følger for indkapslede objekter eller<br />

afledte indkapslede objekter den normale syntaks for pointererklæring og -anvendelse. En<br />

pointer til et objekt af typen T i klassen S' skop har typen pointer til type T. Der er altså ingen<br />

semantisk forskel, blot skal højre-udtrykket under tildelingen til pointeren omfatte objektets<br />

navn:<br />

class S { int i; };<br />

3.6.7 Fjerde løsning i C++ 201


S s;<br />

int* i = &s.i; // ganske normal pointer til int<br />

int& j = s.i; // ganske normal reference til int<br />

Det er helt normal pointermanipulation. Disse pointere peger ind i et objekt af en eller anden<br />

type og er semantisk uden relevans til en klasse. I C++ kan vi imidlertid også arbejde med<br />

pointere til klassemedlemmer, altså pointere, der både bærer information med sig om den<br />

refererede type og også det leksikale skop, som typen befinder sig i.<br />

3.7.1 Pointere til medlemsdata<br />

Er der tale om pointere til datamedlemmer i klasser i stedet for i objekter eller pointere til<br />

statiske variable indkapslet i klasser, skal klassenavnet anvendes i erklæringen i stedet for<br />

objektnavnet:<br />

class S { static int i; };<br />

int* i = &S::i; // i er pointer til int<br />

class T { int j; };<br />

int T::*ptj = &T::j; // ptj er pointer til j i T<br />

Det ses altså, at pointere til statiske klassemedlemmer er af samme type som pointere til<br />

normale objekter. Derimod er pointere til ikke-statiske medlemmer af klasser (ikke objekter)<br />

en beskrivelse af medlemmets position i klassen, et offset til den pågældende variabel. Denne<br />

position kan bruges i forbindelse med objekter af denne, og kun denne, klasse:<br />

T t; // et T-objekt<br />

t.*ptj = 10; // tildel medlem i t værdien 10<br />

Det, der sker i denne tildeling er, at ptj bliver bruges som en indgang i et T-objekt og er<br />

fra klientens side generisk på linie med andre pointere. ptj kunne for så vidt referere<br />

vilkårlige andre objekter i klassen (sålænge disse objekter er af typen int).<br />

3.7.2 Pointere til medlemsfunktioner<br />

Det er også muligt at arbejde med pointere til medlemsfunktioner i klasser. Det omfatter<br />

imidlertid udvidede regler for både erklæring og anvendelse af pointerne. Operatorerne ->* og<br />

.*, som kort blev remset op i afsnit 2.4.6 kan nu forklares i forbindelse med de datastrukturer<br />

og klasser, hvis medlemmer de arbejder på. De er begge binære operatorer, og afviger kun i<br />

en ekstra indirektion af den første operant. Operatoren .* binder to operanter sammen, som<br />

skal være af henholdsvis typerne "forekomst af en klasse T" og "pointer til et medlem i T."<br />

Lad os kort rekapitulere erklæringssyntaksen for en pointer til en normal ikke-<br />

202 Klasser og pointere 3.7


medlemsfunktion:<br />

int f (char*); // global funktion<br />

int (*fptr) (char*) = f; // pointer til global funktion<br />

Disse to objekter har henholdsvis typerne "funktion, der tager en char* som parameter og<br />

returnerer en int" og "pointer til funktion, der tager en char* som parameter og returnerer en<br />

int." Pointeren anvendes således:<br />

int i = (*fptr) ("tekst"); // kald funktion peget på af fptr<br />

Når der arbejdes med medlemsfunktioner, erklæres pointeren specifikt til at pege på et<br />

medlem i en bestemt klasse. Grunden til, at der ikke kan anvendes generelle pointere i<br />

forbindelse med medlemmer er, at der kan forekomme forhold mellem klassen og andre<br />

klasser, der kræver, at oversætteren er bekendt med, hvad klassen hedder - forhold, som har<br />

med arv og virtuelle funktioner at gøre og som beskrives i <strong>kapitel</strong> 4. En pointer til en<br />

medlemsfunktion erklæres med<br />

// erklær en type T med en medlemsfunktion<br />

class T { int f (char*); };<br />

// erklær et objekt af typen T<br />

T t;<br />

// erklær og initiér en pointer til medlemsfunktion i T<br />

int (T::*ptf) (char*) = &T::f;<br />

Læg mærke til, at adresse-af operatoren (&) skal med, når en pointer til en medlemsfunktion<br />

skal findes, hvilket ikke er tilfældet for normale funktioner. Pointere til ikkemedlemsfunktioner<br />

kan tildeles en funktion direkte, fordi der forekommer en underforstået<br />

konvertering fra funktion til adresse. Grunden til denne forskel ligger begravet i, at adressen<br />

på funktionen afhænger af det specifike objekt, der arbejes på.<br />

3.8 PARAMETERISEREDE TYPER<br />

Indtil nu har vi arbejdet med abstrakte datatyper i et design, som på en måde er meget statisk,<br />

idet implementationerne bygger på et fastsat indhold og en formel grænseflade. Klienten, som<br />

benytter de abstrakte typer, har ikke mulighed for at ændre på typernes opførsel men må<br />

anvende dem, som de foreligger. Det kan siges at være en ulempe, fordi det ikke umiddelbart<br />

tillader de abstrakte typer at arbejde sammen med de, som klienten ellers arbejder med eller<br />

selv introducerer i typesystemet. Ulempen manifesterer sig i, at klienten enten er nødt til at<br />

ændre på sine egne typer eller tilpasse dem den abstrakte type eller også selv skrive eller<br />

omskrive den abstrakte type. Det betyder i direkte konsekvens, at de abstrakte datatyper ikke i<br />

3.7.1 Pointere til medlemsdata 203


særlig stor grad fremmer det store mål at genbruge eksisterende kode.<br />

Hvis vi ser på, hvilke egenskaber vi i grunden vil tillæge de statiske abstrakte typer, er det<br />

muligheden for at de internt kan arbejde med objekter af klientfastsatte typer. Med andre ord,<br />

en klasse, som indeholder eller refererer objekter af typen X er ikke meget værd i en<br />

sammenhæng, hvor klienten arbejder med objekter af typen Y. Hvis klienten er i stand til,<br />

uden at begynde at ændre i klassens implementation, at få klassen til at arbejde med Yobjekter<br />

i stedet for, ville der være meget tid sparet. Det, vi leder efter, er en slags<br />

parameterisering af klasser, som tillader os at fortælle en klasse at den skal ændre adfærd<br />

efter behov. Den parameterisering er meget forskellig fra den, vi kender fra parameterlisterne<br />

i normale funktioner, fordi der her er tale om faktiske typer og ikke om objekter.<br />

Funktionerne modtager blot instanser af typer, ikke typerne selv. Parameterisering på<br />

typeniveau er en opgave for oversætteren.<br />

3.8.1 Eksempler på parameterisering<br />

Der er i praksis flere måder at opnå ægte parameterisering af både klasser og funktioner på,<br />

som har været anvendt i både C og C++. Den mest åbenlyse er at benytte præprocessorens<br />

faciliteter for makroekspansioner. Ved at lade klassens indhold afhænge af en makro, som<br />

klienten specificerer indholdet af, kan klassens opførsel kontrolleres eksternt. I praksis er det<br />

mere populært at benytte typedefinitioner i stedet for makroer, fordi makroerne ikke<br />

bliver typechecket. Som eksempel kan vises en simpel implementation af en stak, som<br />

parameteriseres af en typedefinition:<br />

typedef int TYPE; // definér typen - her en int<br />

class Stack {<br />

TYPE** rep; // selve stakken<br />

int sp; // en stak-pointer<br />

public:<br />

Stack (int size) : sp (0) { rep = new TYPE* [size]; }<br />

~Stack () { delete rep; }<br />

void Push (TYPE* t) { rep [sp++] = t; }<br />

TYPE* Pop () { return rep [--sp]; }<br />

};<br />

Stakken Stack manipulerer objekter af typen TYPE, som er typedefineret til en int i<br />

dette tilfælde. Hvis vi blot ændrer i typedef-sætningen, vil stakken kunne arbejde med<br />

objekter af andre typer. I realiteten er det dog kun pointere, der jongleres med - rep er en<br />

pointer til en vektor af pointere - så pas på med at benytte denne stak til lagring af automatisk<br />

allokerede objekter. Der er heller ikke sikkerhed for, at der Push'es over stakkens øvre eller<br />

Pop'es under dens nedre grænse. Denne fremgangsmåde er imidlertid enkel og virker ganske<br />

udmærket i mindre programmer. I en større sammenhæng er der to vanskeligheder, nemlig<br />

dels at kildeteksten til den parameteriserede type (her Stack) skal genoversættes hver gang,<br />

204 Parametiserede typer 3.7


den parameteriseres og dels, at den ikke kan parameteriseres for mere end én type ad gangen i<br />

samme kildefil. Under normale omstændigheder betyder en genoversættelse - uanset, om der<br />

er blevet rettet i kildeteksten eller ej - at der også skal gentestes. Og selv om en enkelt<br />

anvendelse af parameteriseringen godt nok fremmer genbrug, er det ikke en fleksibel og<br />

holdbar løsning.<br />

Alternativt kan vi benytte os af den generiske pointer, der har været ANSI C's store styrke i<br />

forhold til andre sprog, void-pointeren. Som beskrevet i afsnit 2.10.11 er en void-pointer<br />

typeløs og kan konverteres til at pege på reelle typer, mens det omvendte, nemlig en<br />

konvertering fra en void-pointer til en vilkårlig type kræver en tvungen konvertering † . En<br />

implementation af Stack-klassen med void-pointere er ikke væsentlig anderledes end<br />

ovenfor, blot er typedefinitionen fjernet og TYPE erstattet med void:<br />

class Stack {<br />

void** rep;<br />

int sp;<br />

public:<br />

Stack (int size) : sp (0) { rep = new void* [size]; }<br />

~Stack () { delete rep; }<br />

void Push (void* t) { rep [sp++] = t; }<br />

void* Pop () { return rep [--sp]; }<br />

};<br />

En sådan implementation er forholdsvis solid. Den virker med objekter af alle typer og<br />

udnytter en facilitet, som næsten alle C-programmører (og en stor del C++-programmører)<br />

anvender i praksis. Den kan oversættes én gang for alle og afhænger ikke af makroer eller<br />

typedefinitioner. Objektmodulet er direkte lænkbart til klientens kode og kan placeres i et<br />

bibliotek og genbruges om og om igen. Men er klassen i realiteten parameteriseret? Der er<br />

ikke et umiddelbart parameter at se i klassen, og det viser sig da også, at parameteriseringen<br />

sker i klientens kode i næsten alle kald til klassens medlemsfunktioner. Alle kald til Push()<br />

skal indeholde en adresse på et objekt, som automatisk typekonverteres til en void*. For<br />

kald til Pop(), som returnerer en void*, gælder, at returværdien skal typekonverteres<br />

tilbage til at pege på den korrekte type, der blev brugt under kaldet til Push(). Denne<br />

konvertering skal ske tvungent, fordi C++ ikke underforstået kan konvertere fra en void* til<br />

en pointer til en anden type. Det betyder derfor, at klientens kode må indeholde et stort antal<br />

eksplicitte typekonverteringer, som i praksis sætter C++'s typekontrol ud af kraft. Hvis<br />

klienten laver en fejl, er der ingen, der gør opmærksom på det - typisk en konvertering til en<br />

type, som ikke er identisk med den, som den originale pointer peger på. Og selv om<br />

implementationen med void-pointere til gengæld er særdeles dynamisk og fleksibel, fordi<br />

† Det er en af forskellene mellem ANSI C og C++. Under ANSI C er det tilladt at konvertere en void-pointer<br />

implicit til en pointer til en anden vilkårlig type, sandsynligvis for at undgå de trivielle type-casts i<br />

lagerallokeringskald og lignende. Denne praksis blotter imidlertid et gabende hul i typesystemet, fordi en ulovlig<br />

typekonvertering kan foretages implicit. Oversætteren vil ikke kunne type-checke sådanne konverteringer. I C++ er<br />

det ikke nødvendigt at gå på kompromis her, fordi en overstyring af operator new kan gøre det samme job,<br />

endda bedre.<br />

3.7.1 Eksempler på parametisering 205


den samme Stack kan bruges til at holde objekter af forskellige typer, har den et stort<br />

handicap i og med, at den ikke selv kan se, hvad den arbejder med. En klasse bør i realiteten<br />

skrives, så dens indbyggede algoritmer kan drage fordel af semantikken bag de typer, den<br />

manipulerer. En klasse, der udelukkende bruger void-pointere, er derfor meget begrænset i<br />

anvendelse. Overvej for eksempel en medlemsfunktion i Stack, der sorterer elementerne på<br />

stakken. I typedef-versionen er det let nok for oversætteren at sammenligne objekterne på<br />

stakken, fordi den kender til deres type (og kan beklage sig, hvis de ikke har semantik for<br />

sammenligning i form af for eksempel operator-overstyringer), men i implementationen med<br />

void-pointerne er det umuligt, fordi sorteringsfunktionen ikke vil have den fjerneste idé om,<br />

hvordan den skal sortere når den ikke ved, hvad pointerne peger på.<br />

Det, der er brug for, er en blanding af oversætterstøttet typekontrol og en enkel form for<br />

makrolignende parameterisering. Eller med andre ord, en makrofacilitet for funktioner og<br />

klasser, der ligger i oversætterens domæne og ikke i præprocessorens.<br />

3.8.2 Skabeloner<br />

I <strong>kapitel</strong> 4 skal vi se, hvordan disse problemer kan løses med objekt-orienterede teknikker,<br />

som dog kræver meget benarbejde og forudsætter en del mere integritet i programmet end en<br />

simpel abstrakt datatype, som vi her er interesseret i at skrive, gør og er desuden en smule<br />

mindre effektive under kørslen. Der er imidlertid en facilitet i C++ (i implementationer med<br />

officielt versionsnummer over 2.0), som gør det muligt at parameterisere en klasse eller en<br />

funktion uden at gå på kompromis med hverken typesikkerheden eller effektiviteten. Det er en<br />

slags blanding af det bedste fra forslagene i forrige afsnit og kaldes skabeloner (på engelsk<br />

templates). En skabelon skrives for én eller flere generelle typer, hvor det forudsættes, at<br />

typerne har den nødvendige semantik for at den eller de klasser og/eller funktioner, der<br />

arbejder med typen i skabelonen, kan bruge dem. Når klienten instantierer en parameteriseret<br />

klasse eller kalder en parameteriseret funktion, gør han det i forhold til en konkret type, som<br />

angives i klientens kode. Denne angivelse er nok for oversætteren til at vide, hvad der skal<br />

gøres på den anden side af kaldet. En skabelon er således en generel forskrift over måden,<br />

hvorpå en klasse eller en funktion kan parameteriseres. Den definerer en familie af klasser<br />

og/eller funktioner. Det er en metode, der kan sidestilles med en avanceret makroekspansion<br />

med indbygget statisk typekontrol og den største anvendelse ligger indenfor udvikling af<br />

generelle containerklasser (se afsnit 5.6).<br />

Parameterisering er generelt muligt på grund af C++'s muligheder for overstyring. Overvej<br />

følgende overstyrede funktionserklæringer:<br />

int& max (int& x, int& y) { return x > y ? x : y; }<br />

double& max (double& x, double& y) { return x > y ? x : y; }<br />

float& max (float& x, float& y) { return x > y ? x : y; }<br />

myType& max (myType& x, myType& y) { return x > y ? x : y; }<br />

Hvad er forskellen på disse funktioner? Ikke andet end de typer, de arbejder på.<br />

Parameterisering er en metode til at generalisere på typerne i en funktion eller en klasse. Til<br />

206 Parametiserede typer 3.7


funktionen max() kan vi skrive en skabelon, der definerer det fælles i funktionen. Dette<br />

"fælles" er abstraheret i den generiske klasse C (den kan i praksis hedde hvadsomhelst) i det<br />

følgende:<br />

template <br />

C& max (C& x, C& y) { return x > y ? x : y; }<br />

Denne erklæring vil ikke resultere i en instantiering af en funktion; der vil ikke blive<br />

genereret kode. Til gengæld vil enhver brug af skabelonen for en ny type generere kode for<br />

funktionen i relation til denne type. Dér er således igen en fordel over brugen af normale<br />

makroer, som vil instantiere og generere kode for alle typer af funktioner, uanset om de rent<br />

faktisk benyttes. Først når funktionen rent faktisk kaldes, opdager typesystemet, at en ny<br />

forekomst af funktionen skal genereres. Hvis den allerede findes instantieret for den aktuelle<br />

type, bruges naturligvis den eksisterende. For eksempel,<br />

void f () {<br />

int c = max (5, 7); // skaber int& max<br />

(int&,int&)<br />

float f = -5.1, g = 3.2;<br />

float h = max (f, g); // skaber den samme for float<br />

int i = max (c, 13); // bruger den eksisterende<br />

// int& max (int&,int&)<br />

// osv...<br />

}<br />

Idet indholdet af den parameteriserede funktion er konstant og anvendelsen af typer i<br />

funktionen er variabel, er parameterisering en ideel løsning. Blot skal man være opmærksom<br />

på, at enhver brug af funktionen med en ny type resulterer i en helt ny funktion, der optager<br />

plads i lageret. Dog er denne teknik ment som en hjælp for klienten, og denne vil typisk bruge<br />

få eller måske kun en parameterisering. Ideen er, at friheden til at bruge enhver type ligger<br />

hos klienten.<br />

En skabelon for Stack-klassen fra ovenfor kan skrives på følgende måde:<br />

template <br />

class Stack {<br />

T** rep;<br />

int sp;<br />

public:<br />

Stack (int size) : sp (0) { rep = new T* [size]; }<br />

~Stack () { delete rep; }<br />

void Push (T* t) { rep [sp++] = t; }<br />

T* Pop () { return rep [--p]; }<br />

};<br />

3.7.2 Skabeloner 207


Igen resulterer denne erklæring ikke i hverken kode eller data, men er blot en forskrift, der<br />

kan benyttes af klienten. Når klienten erklærer en forekomst af klassen Stack, skal typen<br />

specificeres eksplicit i erklæringen, fordi den ikke kan fortolkes udfra normale parametre eller<br />

andet. For eksempel,<br />

Stack intStack (33); // liste af 33 ints<br />

Denne erklæring vil resultere i en forekomst af Stack for typen int. Klassen Stack er<br />

altså blevet parameteriseret for typen int. Ud over det lagerforbrug, der ligger i de<br />

indkapslede data i Stack (her en int, en int** og 33 stk. int*), bliver alle<br />

medlemsfunktioner i klassen, inklusive destruktører og konstruktører (men ikke friendfunktioner<br />

eller nedarvede funktioner - se næste <strong>kapitel</strong> om arv) instantieret. For meget store<br />

parameteriserede klasser er pladsforbruget for gentagne instantieringer med forskellige<br />

parametre altså en dyr affære i lagerforbrug.<br />

Den store fordel skal ses i lyset af, at enver type kan specificeres som parameter i<br />

instantieringen. Hvis vi for eksempel har brug for stakke af pointere til funktioner, stakke af<br />

typen Complex eller stakke af 3x3-kommatalsmatricer, kan vi erklære<br />

Stack pcStackFrame (0x100);<br />

Stack CalculatorStack (16);<br />

Stack MatrixStack (9);<br />

Forekommer der referencer til den parameteriserede klasse i klassens egne<br />

medlemsfunktioner eller i ikke-medlemsfunktioner, skal parameteret opgives hver eneste<br />

gang lige efter klassenavnet. Det er i virkeligheden slet ikke tilladt at referere til en<br />

parameteriseret klasse uden at bruge et parameter! Overvej for eksempel en ændring af<br />

Stack::Push(), hvor den returnerer en reference til den aktuelle klasse, så flerledede kald<br />

i samme sætning kan udføres:<br />

template <br />

class Stack {<br />

// ...<br />

public:<br />

// ...<br />

Stack & Push (T*);<br />

};<br />

Hvis der skrives Stack uden parameter eller med et parameter, der ikke passer til T, vil<br />

oversætteren brokke sig og nægte at oversætte. Det viser, at typesystemet er særdeles aktivt i<br />

forbindelse med skabeloner. I dette tilfælde er typen på returværdien fra Stack::Push()<br />

blevet parameteriseret af samme type som klassen selv. Når funktionerne skal implementeres<br />

udenfor klassens krop, skal parameterinformationen også med. Her har jeg for øvrigt ændret<br />

parameterets navn for at vise, at navnet er komplet irrelevant, sålænge det benyttes entydigt<br />

indenfor en enkelt funktion eller klasse - ovenstående prototype, der er parameteriseret ved en<br />

208 Parametiserede typer 3.7


type T og nedenstående, der parameteriseres ved X, er den samme funktion. Det tilrådes dog<br />

så vidt muligt at benytte de samme navne i alle funktioner, der er medlem af den samme<br />

klasse eller modul.<br />

template <br />

Stack & Stack ::Push (X* t) {<br />

rep [sp++] = t;<br />

return *this;<br />

}<br />

Denne implementation af Stack::Push() er, ligesom klassen, parameteriseret af den<br />

type, der specificeres efter template-nøgleordet.<br />

Til tider kan det være ønskværdigt eller nødvendigt at håndkode en enkelt implementation<br />

af en funktion for en given type udtrykkeligt i stedet for at lade C++ parameterisere den<br />

automatisk - for eksempel kan den skrives i assembler eller i et andet sprog. Dette kan gøres<br />

af klienten for den type, der arbejdes på i en given sammenhæng, på følgende måde:<br />

Stack & Stack ::Push (int* iptr) {<br />

*(rep + sp) = iptr, sp++;<br />

return *this;<br />

}<br />

Undertiden kan de parameteriserede funktioner og klasser blive syntaksmæssigt forvirrende at<br />

arbejde med, hvis de skal gentages mange gange i et program. Af den grund er det en god ide<br />

at give dem generelle navne ved hjælp af typedef, så de i højere grad ligner normale typer<br />

i systemet. For eksempel,<br />

typedef Stack funPtrStack;<br />

typedef Stack complexStack;<br />

typedef Stack matrixStack;<br />

// ...<br />

funPtrStack returnAdresses = 0x100;<br />

complexStack fourierSeries = 16;<br />

matrixStack inversionMatrices = 9;<br />

Alle funktioner, der benytter parameterisering, skal efterstilles en templatespecifikation,<br />

så oversætteren er i stand til at skabe de korrekte forbindelser til de anvendte<br />

typer. Når en klasse omsluttes af en skabelon, bliver alle medlemsfunktioner automatisk<br />

parameteriseret. Funktioner, der erklæres udenfor klassens krop, skal som nævnt eksplicit<br />

parameteriseres, ligesom friend-funktioner, der jo er ikke medlemmer af klasser. Følgende<br />

fragment viser en overstyring af + for Stack, implementeret i en friend-funktion, som<br />

adderer to stakke:<br />

3.7.2 Skabeloner 209


template <br />

class Stack {<br />

// ...<br />

public:<br />

// ...<br />

friend Stack operator+ (Stack &, Stack &);<br />

};<br />

template <br />

Stack operator+ (Stack & x, Stack & y) {<br />

Stack result;<br />

int c = x.sp < y.sp ? x.sp : y.sp;<br />

while (c) {<br />

result.Push (x.sp [c] + y.sp [c]);<br />

c--;<br />

}<br />

return result;<br />

}<br />

Det ses, at en sådan funktion skal have parametre med alle steder, hvor der henvises til den<br />

type, der er parameteriseret. Det er ikke muligt at overstyre et parameteriseret klassenavn<br />

yderligere på manuel vis. I stedet må medlemsfunktionerne eksplicit skrives til den type, der<br />

er brug for.<br />

Statiske medlemmer i parameteriserede klasser har kun en enkelt forekomst for hver<br />

parameterisering af klassen:<br />

template <br />

class X {<br />

static C x_medlem;<br />

};<br />

X iX1, iX2; // skaber en int* x_medlem<br />

X dX1, dX2; // skaber en double* x_medlem<br />

Her får objekterne iX1 og iX2 en fælles statisk medlemsvariabel af typen int*, mens<br />

objekterne dX1 og dX2 får en af typen double, altså to ialt. Det skal nævnes, at det ikke<br />

er tilladt at parameterisere i flere led i selve erklæringerne, så følgende er ulovligt:<br />

Stack stakAfIntStakke; // ulovlig<br />

Skabeloner er klart den bedste facilitet for genbrug af kode i C++. I afsnit X.X.X skal vi se,<br />

hvordan vi yderligere kan blande dem sammen med objekt-orienterede teknikker som arv<br />

samt også, hvordan de farlige tvungne typekonverteringer i forbindelse med void-pointere<br />

210 Parametiserede typer 3.7


kan indkapsles i klasser, så de er sikrere og mere robuste. Ved at blande skabelon-klasser<br />

sammen med void-pointere kan vi nemlig spare på instantieringerne af funktionerne i<br />

skabelon-klasserne. I afsnit 3.10.2 findes et eksempel på en associativ liste, der er<br />

implementeret ved hjælp af skabeloner.<br />

3.9 DESIGN AF ABSTRAKTE DATATYPER<br />

Den abstrakte type Complex er af en art, som normalt klassificeres som en numerisk<br />

datatype. Numeriske datatyper passer godt ind i det konventionelle<br />

programmeringsparadigme, der oftest centraliserer databehandlingen omkring tal. Der er<br />

imidlertid mange andre anvendelser for klasser i C++, fordi metoden er generel. Indkapsling<br />

og kompleksitets-skjulning er faciliteter, som også gør programudviklingen lettere, når der<br />

skal arbejdes modulært med generelle datastrukturer. I afsnit 3.10 gennemgås et antal<br />

anderledes applikationer for abstrakte datatyper, som arbejder med klasse-begrebet udfra<br />

andre synsvinkler. Inden da skal vi se på de egenskaber ved C++-klasser, som sammenblandet<br />

med nogle af de generelle sprogstrukturer fra <strong>kapitel</strong> 2 giver anledning til designspørgsmål.<br />

For mange er dette et punkt, hvor der sker to ting. Når anatomien bag klasser er forstået, og<br />

de derefter bruges på måder, som ikke falder naturligt, er det svært at se, præcis hvad der sker.<br />

Dernæst, når også dette er forstået, får man en mere gennemført opfattelse af klassen som<br />

redskab. At programmere med klasser bliver helt naturligt, og mange finder det faktisk<br />

umuligt at revertere til de tidligere principper for programopbygning. Målet er at kunne<br />

visualisere et design opbygget efter princippet om indkapsling, uden først at skulle gennemgå<br />

en analyse efter traditionelle principper og senere konvertere det til abstrakte datatyper.<br />

3.9.1 Samspil mellem klasser<br />

Selvom klassernes reelle anvendelighed ligger i, at de indkapsler og skjuler kompleksitet fra<br />

klienten, kan kompleksiteten i selve klassen blive stor, faktisk så stor, at designeren af klassen<br />

render ind i de samme problemer som dem, der blev beskrevet i starten af kapitlet. Derfor kan<br />

man drage fordel af, at klasserne kan arbejde sammen og udover at skjule implementationsdetaljer<br />

fra brugeren også gøre arbejdet med udviklingen af selve programmet, der<br />

implementerer klasserne, lettere.<br />

Lad os tage et eksempel fra dette <strong>kapitel</strong> og udbygge det. Eksemplet fra afsnit 3.4.15 med<br />

dynamiske strenge af tegn kan udmærket gå hen og blive meget komplekst, når vi tager<br />

samme design-krav i betragtning som for Complex-typen. Vi starter med et program, der<br />

arbejder med strenge. Programmet består af en funktion, som søger i en streng efter en<br />

substreng og erstatter denne, hvis den findes, med en anden substreng - en meget generel<br />

funktion, der er brugbar i bla. tekstbehandlere. For ekempel er vi interesserede i følgende:<br />

Original streng: "Danmarks Radio"<br />

Søgestreng: "Radio"<br />

Erstatningsstreng: "Akvarium"<br />

Resultatsstreng: "Danmarks Akvarium"<br />

3.7.2 Skabeloner 211


Funktionen, som skal foretage denne udskiftning, skal ikke være en del af String-klassen,<br />

da den er alt for specifik. I stedet er det en normal funktion, som er klient af String:<br />

// dstrtest.cpp<br />

// eksempel 3-7a: test af dynamiske strenge<br />

#include "dstring.h" // erklæring af String<br />

#include // standard-I/O<br />

String erstat (const String& orig, // original streng<br />

const String& soeg, // søgestreng<br />

const String& erst) { // erstatningsstreng<br />

if (!soeg.len ()) return orig; // ingen søgestreng<br />

String r; // resultatet<br />

int i = 0, h = 0, k = soeg.len ();<br />

while (k + i


}<br />

Funktionen erstat() modtager en original streng (orig), en søgestreng (soeg) samt en<br />

erstatningsstreng (erst), og erstatter alle forekomster af soeg i orig med erst. I<br />

main() kaldes erstat() tre gange med forskellige data, og følgende udskrives:<br />

Original: The Paths of Glory lead only to the Grave<br />

Erstat: only<br />

Med: but<br />

Resultat: The Paths of Glory lead but to the Grave<br />

Original: Holland er europamestre i fodbold<br />

Erstat: Holland<br />

Med: Danmark<br />

Resultat: Danmark er europamestre i fodbold<br />

Original: xyzxxyyzzxxxyyyzzz<br />

Erstat: xx<br />

Med: AA<br />

Resultat: xyzAAyyzzAAxyyyzzz<br />

Algoritmen for erstat() er simpel: variablen i holder den position i den originale<br />

streng, der søges på for øjeblikket, og kører dermed fra 0 til længden af orig, mens h<br />

husker den sidste position, hvor en funden forekomst af søgestrengen befandt sig i den<br />

orig. while-løkken foretager en sammenligning mellem den aktuelle substreng i orig<br />

og søgestrengen og erstatter derefter denne, hvis den fandtes. Resultatstrengen r opbygges<br />

inkrementalt og ryddes til sidst op efter løkkens slutning.<br />

Hvis vi går i detaljer med funktionen erstat() ser vi en hel del egenskaber ved<br />

behandlingen af forekomster af String-klassen, som giver grund til forklaring. Funktionen<br />

lægger for det første strenge sammen som om, de var numeriske datatyper. Her gælder, at<br />

addition af forekomster af String betyder sammenlægning (på engelsk concatenation) af<br />

strenge, dvs. at<br />

"abc" + "def" == "abcdef"<br />

Dernæst finder vi, at funktions-operatoren () er overstyret for String, idet der<br />

forekommer udtryk som<br />

orig (i, k);<br />

flere steder i teksten. Denne overstyring returnerer substrengen i orig fra det i'ende<br />

element, k elementer fremad. Der findes også overstyringer af tildel-og-adder og<br />

sammenlignings-operatoren. Metoderne i den abstrakte datatype String arbejder således<br />

med substrenge i flere af operationerne, hvilket giver anledning til at opdele String-<br />

3.8.1 Samspil mellem klasser 213


klassen i to: en generel String og en Substring, som arbejder på dele af strenge for<br />

derigennem at simplificere designet.<br />

// dstring.h<br />

// eksempel 3-7b: specifikation af en streng-klasse<br />

#include // C's strengbibliotek<br />

class String; // fremad-reference<br />

// specifikation af substreng-klassen<br />

class Substring {<br />

friend String; // giv adgang til String<br />

char* ssp; // substrengen selv<br />

unsigned sslen; // længden på substrengen<br />

Substring (const String&, unsigned, unsigned);<br />

Substring (const Substring&);<br />

public:<br />

void operator= (const Substring&);<br />

int operator== (const String&) const;<br />

String operator+ (const String&) const;<br />

};<br />

// specifikation af streng-klassen<br />

class String {<br />

friend Substring;<br />

char* sp; // strengen selv<br />

unsigned slen; // længden på strengen<br />

public:<br />

String (); // underforstået konstruktør<br />

String (const char*); // konstruér fra C++-streng<br />

String (const String&); // kopi-konstruktør<br />

String (const Substring&); // konstruér fra Substring<br />

~String () { if (slen) delete sp; }<br />

unsigned len () const { return slen; }<br />

operator const char*() const { return sp; }<br />

String operator+ (const String&) const;<br />

String& operator+= (const String&);<br />

void operator= (const String&);<br />

int operator== (const String&) const;<br />

Substring operator() (unsigned, unsigned);<br />

Substring operator() (unsigned, unsigned) const;<br />

214 Design af abstrakte datatyper 3.8


char& operator[] (unsigned i) { return sp [i]; }<br />

char operator[] (unsigned i) const { return sp [i]; }<br />

};<br />

// dstring.cpp<br />

#include "dstring.h"<br />

// konstruér en Substring fra en del af en String<br />

inline Substring::Substring (const String& s,<br />

unsigned i, unsigned j) {<br />

ssp = &((String&)s).sp [i], sslen = j;<br />

}<br />

// konstruér en Substring fra en anden Substring<br />

inline Substring::Substring (const Substring& s) {<br />

ssp = s.ssp, sslen = s.sslen;<br />

}<br />

// tildeling af en Substring fra en anden Substring<br />

inline void Substring::operator= (const Substring& s) {<br />

strncpy (ssp, s.ssp, s.sslen);<br />

}<br />

// sammenlign en Substring og en String<br />

inline int Substring::operator== (const String& s) const {<br />

return !strncmp (ssp, s.sp, sslen);<br />

}<br />

// sammenlæg en Substring med en String<br />

String Substring::operator+ (String& s) const {<br />

String r (*this);<br />

return r + s;<br />

}<br />

// konstruér en "tom" String<br />

inline String::String () : sp (NULL), slen (0) { }<br />

// konstruér en String fra en C++-tegnstreng<br />

String::String (const char* s) {<br />

slen = strlen (s), sp = new char [slen + 1];<br />

strcpy (sp, s);<br />

}<br />

3.8.1 Samspil mellem klasser 215


konstruér en String fra en anden String<br />

String::String (const String& s) {<br />

slen = s.slen, sp = new char [slen + 1];<br />

strcpy (sp, s.sp);<br />

}<br />

// konstruér en String fra en Substring<br />

String::String (const Substring& s) {<br />

slen = s.sslen, sp = new char [slen + 1];<br />

strncpy (sp, s.ssp, slen);<br />

sp [slen] = '\0';<br />

}<br />

// sammenlægning og tildeling af to strenge<br />

String& String::operator+= (const String& s) {<br />

char* temp = new char [slen + s.slen + 1];<br />

strncpy (temp, sp, slen);<br />

temp [slen] = 0;<br />

strncat (temp, s.sp, s.slen);<br />

temp [slen + s.slen] = 0;<br />

delete sp;<br />

sp = temp;<br />

slen += s.slen;<br />

return *this;<br />

}<br />

// sammenlægning af to strenge<br />

String String::operator+ (const String& s) const {<br />

String temp (*this);<br />

return temp += s;<br />

}<br />

// tildeling af en String fra en anden String<br />

void String::operator= (const String& s) {<br />

if (slen) delete sp;<br />

slen = s.slen, sp = new char [slen + 1];<br />

strcpy (sp, s.sp);<br />

}<br />

// sammenligning af en String med en anden String<br />

inline int String::operator== (const String& s) const {<br />

return !strcmp (sp, s.sp);<br />

}<br />

216 Design af abstrakte datatyper 3.8


udtræk en Substring fra en String<br />

Substring String::operator() (unsigned p, unsigned l) {<br />

return Substring (*this, p, l);<br />

}<br />

// udtræk en Substring fra en konstant String<br />

const Substring String::operator() (unsigned p,<br />

unsigned l) const {<br />

return Substring (*this, p, l);<br />

}<br />

Hvad repræsenterer klasserne Substring og String? Rent faktisk er de to klasser<br />

meget ens, hvilket kan ses, hvis man ser på deres datamedlemmer:<br />

class String {<br />

char* sp;<br />

unsigned slen;<br />

// ...<br />

};<br />

class Substring {<br />

char* ssp;<br />

unsigned sslen;<br />

// ...<br />

};<br />

Den generelle forskel ligger i, at String er skabt med en pæn grænseflade til klienten,<br />

mens Substring er en meget tynd klasse med kun få metoder. Og så er der to meget<br />

væsentlige forskelle: mens String-objekter indeholder tegn, der i vanlig C++-stil slutter i<br />

'\0', så de kan skrives ud og behandles som normale tegnsekvenser, indeholder<br />

Substring tegnstrenge, som alene beskrives ved deres længde (sslen). Rent faktisk<br />

allokerer Substring aldrig lager til sine indre variable, men peger altid ind i en String.<br />

Den anden store forskel er, at Substring ikke kan instantieres af klienten! Idet<br />

Substring's konstruktører er i den private del af klassen, kan de ikke ses af brugeren,<br />

som vil få en fejl fra oversætteren. Også kopi-konstruktøren fra String til Substring er<br />

privat, for at undgå, at klienten opretter en Substring med<br />

String s = "Hello";<br />

Substring ss (s, 2, 4); // fejl: konstruktør privat<br />

Substring sss = s; // fejl: kopi-konstruktør privat<br />

Når en klasse som Substring ikke kan bruges af klienten, kaldes den en privat klasse. I<br />

både String og Substring erklæres den anden klasse som friend, hvilket gør alle<br />

3.8.1 Samspil mellem klasser 217


medlemmer i klassen tilgængelige i metoder i den anden klasse. Forekomster af String kan<br />

således arbejde på data i forekomster af Substring og vice versa. Substring's rolle er<br />

således at abstrahere en del af kompleksiteten fra String, hvilket har resulteret i meget små<br />

og letforståelige metoder i begge klasser.<br />

Substring har tre public metoder, en for tildeling fra en String, en for<br />

sammenligning med en String og en for addition af en Substring med en String.<br />

Disse metoder er public, fordi klienten skal kunne bruge dem. Finessen er, at klienten<br />

(dvs. eksempel 3-7a) ikke er klar over, at den til tider arbejder med midlertidige forekomster<br />

af Substring og til tider med forekomster af String. Når klienten for eksempel skriver<br />

String a = "en streng"<br />

String b = a (3, 3); // giver "str"<br />

sker der flere ting. Først kaldes operator() for a, hvilket returnerer en Substring,<br />

som reelt er en pointer-reference og en længdeerklæring ind i a. String::operator()<br />

opretter et midlertidigt Substring-objekt ved kald til klassens konstruktør (hvilket den har<br />

lov til som friend-klasse) som den umiddelbart returnerer. Konstruktøren i Substring<br />

foretager blot en tildeling af en pointer og en længdeerklæring. Når Substring-objektet<br />

når tildelingen til b kaldes konstruktøren String::String(Substring&) for b, som<br />

opretter og initierer b. Klienten opdager således aldrig, at der er Substrings inde i<br />

billedet.<br />

Substring-konstruktøren, som opretter et objekt fra en String, et indeks og en<br />

længdeerklæring indeholder en interessant sprogkonstruktion:<br />

ssp = &((String&)s).sp [i];<br />

Konstruktøren modtager en const String reference, som det er nødvendigt at ændre til<br />

en String-reference. Udtrykket &((String&)s) typekonverterer s tvungent fra en<br />

konstant til en ikke-konstant. Dette er nødvendigt, da konstruktøren ellers tildeler en ikkekonstant<br />

(ssp, en char*) med adressen på en konstant (&s.sp) i en sætning som<br />

ssp = &s.sp [i]; // adressen af det i'ende element i s.sp<br />

Denne typekonvertering er potentielt meget farlig, fordi vi standser oversætterens forsøg på at<br />

forbyde ændringer i konstanter. Hvis det var muligt for en klient at lave forekomster af<br />

Substring, kunne det lade sig gøre at ændre på en konstant Substring gennem et kald<br />

til denne funktion. Men det er kun klassen String, som kan kalde denne funktion, og i<br />

andet led er det kun de to operator()-overstyringer, som kalder denne konstruktør for at<br />

finde en substreng i en streng. Den ene operator()-metode returnerer en konstant<br />

Substring og kan derfor ikke lede til bekymringer, mens den anden arbejder på ikkekonstante<br />

String-objekter, som gerne må ændres af klienten. Denne måde at styre<br />

konstante og ikke-konstante kald til metoder i klassen er derfor meget brugbar i klasser som<br />

String, hvilket også kan ses i de to lignende overstyringer af operator[]:<br />

218 Design af abstrakte datatyper 3.8


String a = "en normal streng";<br />

const String b = "en konstant streng";<br />

a [0] = 'E'; // ok<br />

b [0] = 'E'; // fejl<br />

Tildelingen til a[0] kalder den ikke-konstante metode, som returnerer en reference til det<br />

første tegn i strengen a, fordi a ikke er konstant. Den samme sætning for b giver derimod<br />

en oversætterfejl, fordi der ikke findes en metode, som returnerer en reference til en char,<br />

men blot værdien af denne. Denne værdi kan naturligvis ikke bruges på venstre side af<br />

lighedstegnet (den er ikke en rvalue) hvilket fanges af oversætteren. Til gengæld er det muligt<br />

at benytte den på venstre side (som lvalue) i sætninger som<br />

a [1] = b [7];<br />

da dette ændrer på b.<br />

Overstyringen af den underforståede konvertering fra String til char* er til nytte for<br />

brugeren, som tillades at blande forekomster af String med normale C++-strenge:<br />

String a = "en streng";<br />

char buf [256];<br />

strcpy (buf, a); // a konverteres til const char*<br />

cout


afbrydelse af programmet ikke rydder op i klientens data, som derfor ikke frigives til<br />

systemet.<br />

Fejlbehandling er et af de områder, hvor C++ kommer til kort. Der er ingen<br />

sprogkonstruktioner, der kan hjælpe os med at fange fejl, der pludselig opstår på grund af<br />

forkert brug fra klientens side eller indskydelser fra operativsystemet † . Vi er nødt til at<br />

programmere os ud af problemet, og kan eventuelt bruge den konvention, der kendes fra for<br />

eksempel C, hvor en bestemt værdi er defineret som en fejl. Når vi i C kalder en funktion for<br />

at læse det næste tegn sekventielt i en fil, returnerer funktionen værdien -1 (et ulovligt tegn),<br />

hvis der var fejl under læsningen. En global konstant bliver af læse-funktionen initieret med<br />

en fejlkode, som kan læses af klienten, som kan arbejde videre derfra. For eksempel:<br />

int i = 0;<br />

int h = open ("filnavn", O_RDONLY);<br />

if (h == 0) fejl ("Filen kunne ikke åbnes!");<br />

while (i != EOF) {<br />

if (i == -1) fejl ("Fejl under læsning");<br />

i = getc (h);<br />

}<br />

Denne metode kan også bruges i C++, men vil lede til trivielle sammenligninger med<br />

returværdier fra alle kald til metoder i klasserne. Bruger klassen overstyring af operatorer,<br />

skal de ellers så naturlige udtryk bagefter checkes for fejl, hvilket gør kildeteksten ulæselig.<br />

En anden mulighed er at fortælle klassen én gang for alle, hvad den skal gøre i tilfælde af<br />

pludseligt opståede fejl. Ved at indkapsle en pointer til en fejlbehandlende funktion i klassen<br />

kan klassen til enhver tid kalde klientens prædefinerede fejlrutine. For String kan en sådan<br />

udbygning se således ud:<br />

enum err { MEM_ALLOC, USER_ERROR, /* ... */ };<br />

typedef void (*funptr) (err); // pointer til funktion<br />

class String {<br />

static funptr errorHandler;<br />

// ...<br />

public:<br />

// ...<br />

String operator+= (const String&);<br />

};<br />

// sammenlægning og tildeling af to strenge<br />

† Dette gælder for AT&T C++ version 2.0. Efter version 2.1 forventes fejlbehandling at være en integreret del af<br />

oversætteren, med mulighed for automatisk opfangning af fejl. Se eventuelt appendiks A eller [Stroustrup 87] for<br />

detaljer. ANSI C++ ventes dog ikke at indeholde disse faciliteter.<br />

220 Design af abstrakte datatyper 3.8


String& String::operator+= (const String& s) {<br />

char* temp = new char [slen + s.slen + 1];<br />

if (temp == NULL) (*errorHandler) (MEM_ALLOC);<br />

strncpy (temp, sp, slen);<br />

temp [slen] = 0;<br />

strncat (temp, s.sp, s.slen);<br />

temp [slen + s.slen] = 0;<br />

delete sp;<br />

sp = temp;<br />

slen += s.slen;<br />

return *this;<br />

}<br />

Typen funptr er en pointer til en funktion, som modtager en fejlkode. Den er erklæret<br />

static i klassen, og forekommer således kun én gang. Klienten skriver en fejlbehandlende<br />

rutine, som rydder op i egne data, specielt dynamisk allokerede data, og vælger en passende<br />

aktion - afslutning, genstart eller hop til en hovedløkke i programmet:<br />

void errhdl (err num) {<br />

if (num == MEM_ALLOC) {<br />

cout


Som beskrevet i afsnit 3.5.8 kan de særlige operatorer new og delete overstyres og<br />

gives nye definitioner under programmørens kontrol. Konsekvensen af dette er, at vi kan<br />

modellere specielle dynamiske lageradministrationer mens vi bibeholder syntaksen for normal<br />

allokering og destruktion af objekter. Overstyringen foretages med en erklæring af<br />

operatorerne efter de samme regler, som gør sig gældende under normal operator-overstyring:<br />

void* operator new (size_t);<br />

void operator delete (void*);<br />

Disse to globale metoder skal sørge for henholdsvis at allokere det nødvendige lager og<br />

returnere en pointer til det allokerede data samt at fjerne det allokerede lager igen. De kaldes,<br />

når der foretages et eksplicit kald til new og delete. new-overstyringen modtager en<br />

variabel af typen size_t (erklæret i header-filen ), som normalt er lig en<br />

unsigned long, og skal returnere en void* til det lager, den allokerer. delete<br />

modtager en pointer til det lager, som ønskes deallokeret, og skal ikke returnere en værdi.<br />

Det er sjældent, at der er brug for at omdefinere den indbyggede lageradministration i C++,<br />

men en eventuel applikation kan være en enkel opbygning af et virtuelt lager, organiseret på<br />

en objekt-orienteret måde. Hvis de brugerdefinerede lageradministrationsfunktioner ikke kan<br />

allokere interne lager ved en forespørgsel, kan de skrive dele af det aktuelle lager ud på et<br />

sekundært lager (eksempelvis en disk) og erstatte dette med det nyallokerede lager. Med<br />

overstyringer af både new og delete samt af pointer-operatoren kan funktionerne<br />

kontrollere, hvilke af de allokerede objekter, der har ligget ubrugte hen. På den måde kan<br />

programmet efter forskellige retningslinier (tidligst brugt, round-robin etc.) vælge hvilke det<br />

objekt, der skal swappes til disk.<br />

En simpel implementation af de to lageradministrationsfunktioner er blot en enkel<br />

grænseflade til det eksisterende C-biblioteks funktioner malloc() og free(). Her<br />

overlades administrationen af de blokke, der er allokeret til disse funktioner, som normalt<br />

igen blot lader operativsystemet om det:<br />

void* operator new (size_t size) {<br />

return malloc (size); // "dum" allokering<br />

}<br />

void operator delete (void* pointer) {<br />

free (pointer); // "dum" deallokering<br />

}<br />

Disse overstyringer af de globale lagerallokeringsoperatorer er især brugbare under systemer,<br />

hvor der ikke findes et underliggende operativsystem (for eksempel embeddede systemer),<br />

fordi det gør koden flytbar. Det er også overordentligt anvendeligt under systemer, som<br />

allokerer lager i blokke af faste størrelser. For eksempel vil et kald til new under OS/2 2.0<br />

altid medføre et minimumsforbrug på 4096 bytes. Operativsystemet indeholder specielle<br />

funktioner til sub-allokering af dette lager, men det er en triviel og kedelig opgave. Med en<br />

enkel overstyring kan sub-allokeringen foregå transparent i forhold til applikationen.<br />

222 Design af abstrakte datatyper 3.8


Det er temmeligt vigtigt at overstyre begge fuktioner, da den underforståede anden funktion<br />

ellers vil blive brugt med garanterede uønskede virkninger. Den indbyggede operator<br />

new() vil normalt returnere 0, hvis det adspurgte lager ikke kunne allokeres. Dog vil<br />

funktionen først forsøge at kalde en fejlbehandlingsfunktion, som normalt kaldes<br />

new_handler(), hvis en sådan er defineret. En sådan funktion kan defineres af<br />

programmøren og indsættes i systemet, således at en eventuel fejl opsamles og kan forsøges<br />

reddet. Et kald til set_new_handler() modtager en pointer til en funktion, som<br />

modtager en void* og returnerer void. Kaldet vil indsætte denne funktion som global<br />

fejlbehandlingsfunktion og vil iøvrigt returnere adressen af den aktuelle funktion:<br />

void MyErrorHandler (void* pointer) {<br />

cout


void operator delete (void* pointer) {<br />

count--;<br />

delete pointer;<br />

}<br />

};<br />

I forbindelse med klasse-specifikke overstyringer af disse operatorer er der to vigtige punkter<br />

at tage notits af. Eventuelle afledte klasser (ved brug af arv, se afsnit 4.3) fra en klasse, der<br />

indeholder overstyringer af new og delete vil overtage metoderne, og de skal derfor<br />

skrives med dette i tankerne. Det er også vigtigt at være klar over, at oversætteren selv finder<br />

ud af størrelsen på det objekt, der skal allokeres. Den værdi, der overføres til operator<br />

new() er antallet af maskinord ( == sizeof (char) ) og skal ikke multipliceres med<br />

størrelsen på det aktuelle objekt. Under arv er dette meget vigtigt at huske på - disse<br />

funktioner nedarves nemlig, men kan ikke være virtuelle (afsnit 4.4.3), da de er statiske. De<br />

fundamentale typers allokeringer kan ikke overstyres enkeltvis, de globale new- og<br />

delete-operatorer gælder for dem alle.<br />

Når en vektor af objekter af en klasse allokeres med new, kaldes den globale operator<br />

new() og når vektoren fjernes igen, kaldes operator delete() i det globale<br />

navnerum. Husk at antallet af kaldte konstruktører er proportionalt med størrelsen af vektoren<br />

i erklæringen.<br />

I det næste eksempel vises, hvordan en klasse kan anvende new og delete til at foretage<br />

intern administration af de objekter, der allokeres. Klassen, som repræsenterer simplificeret<br />

streng, kæder alle dynamisk allokerede objekter sammen i en hægtet liste. Ligegyldigt hvor i<br />

programmet, en String allokeres med new vil den indgå i denne liste. Det tillader os at<br />

allokere forekomster af String uden at tildele returværdien fra new-kaldet til en<br />

pointervariabel samt at gennemgå listen og sikre konsistensen i alle objekterne. Det betyder<br />

også, at alle String-objekter kan deallokeres én gang for alle eller måske udlæses på et<br />

sekundærlager i forbindelse med oprydning efter en uventet hændelse som for eksempel en<br />

allokeringsfejl.<br />

class String {<br />

char* rep;<br />

String* next; // næste streng i listen<br />

String* prev; // forrige streng i listen<br />

static String* first; // første streng i listen<br />

static String* last; // sidste streng i listen<br />

public:<br />

String () { }<br />

String (const char* s) {<br />

rep = new char [strlen (s) + 1];<br />

strcpy (rep, s);<br />

}<br />

~String () { delete rep; }<br />

void* operator new (size_t);<br />

224 Design af abstrakte datatyper 3.8


void operator delete (void*);<br />

static void Walk (ostream&);<br />

static void delete_all ();<br />

friend ostream& operatornext = s;<br />

s->prev = last,<br />

last = s, s->next = 0;<br />

return s;<br />

}<br />

void String::operator delete (void* ptr) {<br />

String* s = (String*) ptr;<br />

if (s->prev) s->prev->next = s->next;<br />

else first = s->next;<br />

if (s->next) s->next->prev = s->prev;<br />

else last = s->prev;<br />

::delete s; // kald global delete<br />

}<br />

void String::Walk (ostream& os) {<br />

for (String* s = first; s; s = s->next)<br />

os


new String ("fjerde");<br />

new String ("femte");<br />

String::Walk (cout); // udskriv dem alle<br />

String::delete_all (); // fjern dem alle<br />

}<br />

Læg mærke til, at disse operatorer følger de normale regler for tilgang i klasser. Det<br />

betyder, at en privat operator new vil forbyde klienter at allokere forekomster af den<br />

pågældende klasse dynamisk. Læg også mærke til, at disse eksempler viser forskellen på<br />

instantiering og initiering. Instantieringen er den proces, der allokerer ressourcer for et objekt,<br />

mens initieringen konfigurerer objektet. Det er operator new der instantierer og<br />

konstruktøren, der initierer. Det betyder også, at et eksplicit kald til en destruktør i et<br />

dynamisk allokeret objekt ikke vil kalde operator delete.<br />

operator new og operator delete kan overstyres efter normale regler, så de<br />

modtager ekstra information fra klienten i form af parametre. For eksempel,<br />

class X {<br />

public:<br />

void* operator new (size_t size, int i) {<br />

// ...<br />

}<br />

void operator delete (int j) {<br />

// ...<br />

}<br />

};<br />

X* xp = new (3) X;<br />

// ...<br />

delete xp (3);<br />

Parametre til new og delete er sjældent anvendelige, men er tænkt som information til<br />

allokatoren og deallokatoren, som skal være på plads før objektet konstrueres. Et forslag i et<br />

distribueret system kunne for eksempel være den egentlige maskine, som objektet skal<br />

allokeres på, men man kan også forestille sig, at klienten kan angive præallokeret lager i<br />

kaldet til new, som ved overstyringen<br />

void* operator new (size_t, void* ptr) { return ptr; }<br />

blot accepterer denne placering af lageret. Nu kan klienten skrive<br />

char mem [0x100]; // 256 bytes præallokeret<br />

lager<br />

String* s = new String (mem); // en String i dette område<br />

226 Design af abstrakte datatyper 3.8


og ganske enkelt styre, hvor objekterne bliver allokeret. En interessant fordel ved at overtage<br />

selve lageret er, at de dynamisk allokerede objekter (med virtuelle funktioner og alting - se<br />

afsnit 4.4) kan inspiceres og udlæses af klienten selv. Pas imidlertid på ikke at delete<br />

lager, der er allokeret på denne måde, fordi lageret rent faktisk ikke skal frigives. Det rejser et<br />

problem, nemlig at vi ønsker at kalde objektets destruktør, men ikke at deallokere det. Så<br />

derfor kalder vi blot destruktøren eksplicit:<br />

s->String::~String (); // destruér, men ej deallokér<br />

I versioner af C++ før 2.0 omfattede overtagelsen af lageradministrationen en vis akrobatik i<br />

forbindelse med tildelinger til this-pointeren. Det er beskrevet i afsnit 3.5.18 og er nu en<br />

forældet teknik, som de fleste oversættere ikke tillader. I afsnit 5.5.6 vises et eksempel på,<br />

hvordan arv spiller sammen med brugerdefineret lageradministration.<br />

3.9.4 Objekt-I/O<br />

Programmer, der ikke er i stand til at ind- og udlæse data, er der ikke meget ved. Funktionsorienterede<br />

programmer skrevet efter strukturerede metoder bruger specielle funktioner til<br />

denne form for dataudveksling. Når der arbejdes med abstrakte datatyper, er det imidlertid<br />

ofte mere hensigtsmæssigt at indkapsle I/O-funktioner i klasser, så de enkelte objekter selv er<br />

i stand til at lagre og hente deres indhold. Ved objekt-I/O skal forstås det enkelte objekts -<br />

ikke klasses - evne til at læse og skrive data. Anvendeligheden af dette skal ses i forbindelse<br />

med mange applikationers nødvendighed for at kunne dele data mellem flere processer i et<br />

system. I distribuerede systemer skal et objekt for eksempel være tilgængeligt flere steder på<br />

samme tid. Visse objekt-orienterede systemer, især databasesystemer, indeholder begrebet<br />

bestandige objekter, dvs. objekter, hvis eksistens ikke er bundet til eksekveringen af et<br />

bestemt program. Objekterne "lever" altid et eller andet sted i systemet, uanset om et program<br />

arbejder på dem til et givent tidspunkt.<br />

I dette afsnit beskrives en simpel form for objekt-I/O, som arbejder på en enkelt abstrakt<br />

type, og som giver typen en vis form for bestandighed. I det større perspektiv kræver objekt-<br />

I/O, at der tages stilling til en mængde problemer med datarepræsentation i objekt-orienterede<br />

systemer, da begreber som arv, polymorfi og statiske data gør objektindholdet meget<br />

kompliceret. I <strong>kapitel</strong> 5 diskuteres teknikker for bestandige objekter i objekt-hierarkier.<br />

Den enkleste måde at udvide en type på, så den er i stand til at ind- og udlæse objekter af<br />

typen, er at anvende standardbibliotekets stream-klasser. Gennem disse kan vores typer<br />

skrives til og læses fra en fil, kommunikeres til andre samkørende processer, over parallelle<br />

eller serielle porte osv. Fremgangsmåden er den samme for alle ADT'er, og består af to<br />

metoder til henholdsvis læsning og skrivning. For at udbygge klassen Complex med objekt-<br />

I/O tilføjes en statisk metode readFrom(), som læser en Complex fra en navngiven<br />

strøm og returnerer denne, mens den normale metode dumpOn() skriver objektet på en<br />

strøm:<br />

3.8.3 Brugerdefineret lageradministration 227


class Complex {<br />

double re, im;<br />

public:<br />

// ...<br />

static Complex& readFrom (istream& = cin);<br />

void dumpOn (ostream& = cout);<br />

};<br />

Complex& Complex::readFrom (istream& input) {<br />

Complex* temp = new Complex;<br />

input >> temp->re >> temp->im;<br />

return *temp;<br />

}<br />

void Complex::dumpOn (ostream& output) {<br />

output


}<br />

3.9.5 Samarbejde med andre sprog<br />

I mange sprog er der ingen direkte faciliteter for lænkning af objektkode oversat af andre<br />

oversættere. For eksempel er der et problem med samarbejdet mellem C og Pascal, fordi de<br />

har hver deres metode at arrangere data på stakken under parameteroverførsler til funktioner<br />

og fordi Pascal-overættere normalt udskriver navne i objektfilen med versaler mens C tillader<br />

både store og små bogstaver. For at komme dette problem til livs, kan der mange oversættere<br />

specificeres særlige nøgleord til parameteroverførselskontrol. I Borland C++ kan vi skrive<br />

pascal int write (char*, int); // pascal kaldekonvention<br />

cdecl int open (char*, int); // C kaldekonvention<br />

for at kontrollere, om returadressen skal stakkes først eller sidst ved kaldet til funktionen.<br />

Nøgleord som pascal er ikke en del af ANSI-standarden, og bliver det sandsynligvis ikke.<br />

Når der skrives C++, er der et andet forhold, der problematiserer tingene yderligere. Navne<br />

på funktioner, der er overstyret, skal i objektfilen have et unikt idenfificérbart navn for at<br />

kunne lænke ordentligt. Disse navne bliver genereret af oversætteren, der normalt skaber et<br />

navn, der har relevans til parametre eller klasser. For eksempel vil<br />

int write (int);<br />

int write (char);<br />

int write (int, char*);<br />

give navne af følgende art til funktionerne, når de skrives ud som objektfil fra oversætteren:<br />

__write@si __write@sc __write@sicP<br />

Oversætteren differentierer på den måde funktioner med samme navn ved at påføre ekstra<br />

tegn afhængig af parametrenes typer. Der er et umiddelbart problem med denne teknik, der<br />

går under betegnelsen name mangling, "navnemingelering". De fleste lænkere har nemlig en<br />

maksimumlængde for navne, hvilket er cirka 32 tegn. Hvis navnemingeleringen<br />

afstedkommer, at et navn bliver længere end 32 på grund af de ekstra påførte tegn, vil to<br />

navne enten være ens (lænkerfejl) eller der må bruges en hashing-teknik for at ændre ét af<br />

navnene. Helt sikkert bliver det dog aldrig, så det tilrådes ikke at benytte alt for lange navne i<br />

overstyrede funktioner.<br />

Men problemet bliver mere tydeligt, hvis koden skal lænkes med andre biblioteker. Hvis vi<br />

for eksempel bruger en standard-funktion med prototypen<br />

double sin (double);<br />

3.8.4 Objekt-I/O 229


og andetsteds i programmet selv introducerer en overstyring af dette navn som<br />

Complex sin (Complex);<br />

vil oversætteren typisk mingelere de to navne med følgende resultat. Klitentens program, der<br />

bliver oversat separat, vil producere et objektmodul, der indeholder de mingelerede navne<br />

inklusive kaldet til den funktion, der findes i standard-biblioteket, som jo allerede er oversat<br />

og findes i en anden objekt- eller biblioteksfil. Når de to objektmoduler lænkes, kan kaldet til<br />

den ene funktion ikke lokaliseres, fordi oversætteren uden at blinke har ændret navnet i selve<br />

koden: Hvor det oversatte bibliotek indeholder en funktion med et navn som _sin kalder<br />

klientens kode noget i retning af _sin@dd, og lænkeren er ikke i stand til at producere et<br />

køreklart program.<br />

Løsningen er at benytte, hvad der i C++ hedder type-sikker lænkning, som er meddelelser til<br />

oversætteren om, hvad der må og ikke må mingeleres. Hvis prototypen på den første sin()funktionen<br />

får starten<br />

extern "C" double sin (double);<br />

vil oversætteren under alle omstændigheder lade dette navn være. Denne meddelelse fortæller<br />

også, at der skal benyttes C-konventioner, for eksempel parameterrækkefølge. For mange på<br />

hinanden følgende udtryk kan en blok benyttes:<br />

extern "C" {<br />

double sin (double);<br />

double cos (double);<br />

double tan (double);<br />

}<br />

Det skal bemærkes, at selve konventionen er maskinafhængig, og i visse tilfælde også<br />

oversætterafhængig. Pointen er blot, at C++ skal kunne arbejde sammen med biblioteker<br />

udviklet i andre sprog. Det er let at forestille sig type-sikker lænkning til biblioteker oversat<br />

med andre sprog end C. Zortech C++ til MS-DOS og OS/2 tillader for eksempel Pascalkonventioner<br />

med<br />

extern "Pascal" ...<br />

så det varer sikkert ikke længe for vi ser type-sikker lænkning muliggjort til Fortran- eller<br />

COBOL-biblioteker.<br />

For programmører, der skal bruge eksisterende C-biblioteker eller skriver understøttende<br />

kode i assembler, kan det være en fordel at sætte C-konvention om hele den inkluderede<br />

headerfil. Det tillader, at headeren kan bruges i både C++ og i C. Oversætteren vil behandle<br />

alle erklæringer, definitioner og prototyper i traditionel C-stil uden at mingelere navne eller<br />

lignende. For eksempel,<br />

230 Design af abstrakte datatyper 3.8


extern "C" {<br />

#include <br />

}<br />

En anden mulighed er at teste for den indbyggede makro __cplusplus, som er defineret,<br />

hvis der er tale om en C++-oversættelse, og udefineret, hvis der er tale om en anden<br />

oversættelse (normalt C). Denne makro kan testes for eksistens, og via betinget oversættelse<br />

kan mingelering forbydes for C++-oversættelser:<br />

#if defined (__cplusplus)<br />

extern "C" {<br />

#endif<br />

// ... erklæringer og prototyper ...<br />

#if defined (__cplusplus)<br />

}<br />

#endif<br />

3.10 ANVENDELSER FOR ABSTRAKTE DATATYPER<br />

I dette afsnit beskrives fire reelle anvendelser for abstrakte datatyper, som har forskellige<br />

anvendelsesområder. Eksemplerne bliver benyttet og videreudviklet i senere kapitler, og<br />

indgår også i klasebiblioteket i den sidste del af bogen. Der henvises i forbindelse med dette<br />

afsnit især til [Horowitz 78] og [Sedgewick 88].<br />

3.10.1 Klassen LinkedList<br />

En meget ofte benyttet datastruktur er den hægtede liste. Hægtede lister (undertiden også<br />

kaldet kædede lister) er ikke til at komme udenom, når data skal organiseres og relateres uden<br />

forhåndkendskab til, hvor mange elementer der bliver brug for under kørslen. Hvis<br />

programmøren på forhånd ved, hvor mange elementer der skal bruges, kan disse blot<br />

allokeres i en vektor med new, men hvis antallet er afhængigt af kørslen af programmet, skal<br />

der en anden teknik til.<br />

Den hægtede liste arbejder med klumper af data, som peger på hinanden med<br />

pointervariable. De enkelte klumper kaldes hægter eller nodes, og den hægtede liste består<br />

blot af en pointer til den første hægte. Skal en ny hægte påføres listen er det blot et spørsmål<br />

om dynamisk at allokere en ny hægte og lade den indgå i listen, så den kan findes frem igen.<br />

En hægtet liste kan grafisk repræsenteres som i figur 3-2. Den hægtede liste har en pointer<br />

til det første element, som igen peger på det andet og så videre indtil den sidste hægtes pointer<br />

har værdien 0. At finde en hægte i listen bliver dermed et spørgsmål om at traversere listen,<br />

dvs. iterativt gennemgå listen mens pointeren derefereres, indtil den ønskede hægte bliver<br />

3.8.5 Samarbejde med andre sprog 231


aktuel. Den hægtede liste i dette afsnit har en meget simpel implementation, som kun<br />

indeholder fremadrettede referencer (en singulært hægtet liste), og som blot har en pointer til<br />

den første hægte. Andre implementationer kan indeholde bagudreferencer til de forrige<br />

hægter (en dobbelthægtet liste), kan have både pointere til den første og sidste hægte i listen<br />

og kan eventuelt have en præallokeret headnode, som sparer en smule databehandling i<br />

indsættelsen af nye hægter.<br />

Figur 3-2: Grafisk repræsentation af en hægtet liste.<br />

I denne implementationen brugers en hægte, som har et prædefineret indhold, nemlig et<br />

streng-objekt fra afsnit 3.9.1:<br />

// eksempel 3-8<br />

// en hægtet liste (klasserne Node og List)<br />

typedef int bool;<br />

// Node er en privat klasse<br />

class Node {<br />

friend LinkedList;<br />

Node* next; // peger på den næste hægte<br />

String* data; // peger på hægtens data<br />

Node (String* s, Node* n) :<br />

data (s), next (n) { }<br />

};<br />

// LinkedList definerer en singulært hægtet liste<br />

class LinkedList {<br />

Node* head; // peger på første hægte<br />

public:<br />

LinkedList () { head = 0; }<br />

void Insert (String*); // indsæt en streng i listen<br />

String* Retrieve (); // hent en streng fra listen<br />

bool isEmpty () { return !head; } // er listen tom?<br />

};<br />

232 Anvendelser for abstrakte datatyper 3.10


metode til indsættelse af data i listen<br />

void LinkedList::Insert (String* s) {<br />

head = new Node (s, head);<br />

}<br />

// metode til udlæsning af data fra listen<br />

String* LinkedList::Retrieve () {<br />

String* s = head->data;<br />

Node* n = head->next;<br />

delete head; head = n;<br />

return s;<br />

}<br />

Hægtede lister bruges oftere end de fleste tror, så selv om de er meget simple at implementere<br />

er det ingen grund til ikke at indkapsle dem i klasser. Ovenstående eksempel bruger et statisk<br />

typefelt - en String - hvilket egentlig burde give anledning til at kalde listen for en<br />

StringLinkedList. I næste <strong>kapitel</strong> udvides klassen, hvor LinkedLister ved hjælp af<br />

arv og polymorfi får generelle egenskaber og kan bruges i mange forskellige sammenhænge.<br />

LinkedLists virkemåde demonstreres ved følgende eksempel:<br />

Output:<br />

void Print (LinkedList& list) {<br />

while (!list.isEmpty ()) {<br />

cout data);<br />

delete s;<br />

}<br />

}<br />

void main () {<br />

LinkedList elever;<br />

elever.Insert (new String ("Gurli"));<br />

elever.Insert (new String ("Johanne"));<br />

elever.Insert (new String ("Henriette"));<br />

Print (elever);<br />

}<br />

Gurli<br />

Johanne<br />

Henriette<br />

Bemærk, at listen udlæses i omvendt rækkefølge, fordi der altid arbejdes på den hægtede liste<br />

fra head-pointeren, både under indsættelse af hægter (Insert()) og udlæsning af hægter<br />

(Retrieve()). I denne implementation er klienten iøvrigt ansvarlig for allokering og<br />

3.10.1 Klassen LinkedList 233


deallokering af hægternes indhold. De String-objekter, som klienten opretter, skal klienten<br />

også selv fjerne, mens klassen LinkedList tager sig af lageradministrationen af Nodeobjekter.<br />

Faktisk ser klienten aldrig noget til Node, hvorfor denne klasse er en såkaldt privat<br />

klasse. Alle medlemmer er erklæret private, og klassen ikke har en underforstået<br />

konstruktør, men derimod en eksplicit, som er privat, kan kun friend-klasser og -<br />

funktioner instantiere objekter af Node. LinkedList er således friend til Node.<br />

I næste <strong>kapitel</strong> udbygges denne hægtede liste til også at omfatte sortering, indeksering og<br />

andre operationer, der er relevante for denne datastruktur.<br />

3.10.2 Klassen AssocArray<br />

Vektorer i C++ er så generelle, som de overhovedet kan blive. Mange sprog indeholder<br />

faciliteter for sikring af lovlige referencer i vektorer, mens andre kan behandle elementerne<br />

associativt (indholds-relateret) i modsætning til sekventielt. C++'s vektorer er simple, fordi<br />

sprogets filosofi er simpel - hvis der er brug for specielle vektorer, kan programmøren selv<br />

skrive dem i en abstrakt datatype. Der er ingen grund til at udvide selve sproget med en<br />

facilitet, som kun i visse sammenhænge er nødvendig. Klassen i dette afsnit er en avanceret<br />

skabelon-vektortype, som under kørslen vil checke for ulovlige opslag samt tillade associative<br />

opslag. Den erklæres med det ny typenavn, og bruges med få undtagelser på samme måde<br />

som normale vektorer, takket være overstyring af indeks-operatoren:<br />

AssocArray a (10); // 10 elementer i en int-vektor<br />

a [4] = 0; // tildeling til 4. element<br />

a [20] = 0; // fejl: indeks er for stort<br />

Såvidt den omfangssikrende del af klassen. Normale opslag i en vektor benyttes, når indekset<br />

haves, og indholdet ønskes. Associative opslag er omvendte: indholdet af et element kendes,<br />

og indekset ønskes. Hertil bruges funktionskalds-operatoren, og associtative opslag får<br />

følgende syntaks:<br />

AssocArray a (10);<br />

a [5] = 33; // 5. element indeholder 33<br />

int i = a (33); // i bliver 5<br />

Ulovlige opslag ved for store indeks-værdier eller indhold, som ikke findes i vektoren<br />

resulterer i en fejlmeddelelse og en "sikker" returværdi, som er en statisk medlemsvariabel i<br />

klassen, og som blot skal sikre, at klienten ikke skriver til udefinerede data.<br />

// eksempel 3-9<br />

// en associativ liste (skabelonklassen AssocArray)<br />

template <br />

class AssocArray {<br />

234 Anvendelser for abstrakte datatyper 3.10


TYPE* array; // den indkapslede vektor<br />

unsigned size; // antal elementer i vektoren<br />

public:<br />

AssocArray (const int = 0);<br />

AssocArray (const AssocArray&);<br />

~AssocArray ();<br />

AssocArray& operator= (const AssocArray&);<br />

AssocArray operator+ (const AssocArray&) const;<br />

TYPE& operator[] (const unsigned);<br />

unsigned operator() (const TYPE&);<br />

friend ostream& operator


array = new TYPE [size];<br />

memmove (array, from.array, size * sizeof (TYPE));<br />

}<br />

return *this;<br />

}<br />

// addition af to AssocArray'er<br />

template <br />

AssocArray AssocArray::operator+<br />

(const AssocArray& other) const {<br />

AssocArray temp (size + other.size);<br />

if (temp.size) {<br />

memmove (temp.array, array, size * sizeof (TYPE));<br />

memmove (&temp.array [size], other.aray,<br />

other.size * sizeof (TYPE));<br />

}<br />

return temp;<br />

}<br />

// sekventielt opslag i en AssocArray<br />

template <br />

TYPE& AssocArray::operator[] (const unsigned idx) {<br />

if (idx < size) return array [idx];<br />

cerr


for (int i = 0; i < arr.size - 1; i++)<br />

strm


Ved at ændre på erklæringen af vektoren i starten, så vektorens parameterisering er en anden<br />

end en int, kan en AssocArray repræsentere vektorer af andre typer. For eksempel:<br />

Output:<br />

void main () {<br />

AssocArray a (4), b (4);<br />

for (int i = 0; i < 4; i++)<br />

a [i] = i * 1.3, b [i] = -i * 1.3;<br />

// ...<br />

}<br />

a = { 0, 1.3, 2.6, 3.9 }<br />

b = { 0, -1.3, -2.6, -3.9 }<br />

c = { 0, 1.3, 0.91, 2.21, -6.76, -8.06, -17.81, -19.11 }<br />

AssocArray: index out of bounds<br />

c [32] = 0<br />

3.10.3 Klassen Random<br />

Tilfældige tal er nødvendige i mange videnskabelige applikationer. I et normalt<br />

funktionsbibliotek findes funktioner, som ved gentagne kald returnerer en serie af tilfældige †<br />

tal efter en given forskrift, ofte initieret med en begyndelsesværdi. En tilfældighedsgenerator<br />

kan indkapsles i en abstrakt datatype med fordele over et normalt funktionsbibliotek, fordi<br />

klienten kan arbejde med en datatype "et tilfældigt tal".<br />

Implementationen af klassen Random har ydermere en fordel over standard-funktionen i<br />

C++ (og C), da den bruger sin egen algoritme [Kirkpatrick 81] til generering af serien af tal.<br />

Hvis klienten således instantierer flere Random-objekter, vil de arbejde fuldstændigt<br />

uafhængigt af hinanden, et faktum, der ikke holder for funktionsbiblioteket, som bruger den<br />

samme globale funktion og dermed status. Hvis klienten bruger tilfældige tal i en<br />

hændelsesdrevet applikation, vil brugen af uafhængige generatorer sikre, at de samme serier<br />

kan genskabes ved brug af samme startværdier.<br />

Random er også et eksempel på, hvordan funktionalitet fra standardbiblioteker kan<br />

indkapsles i en abstrakt datatype, så en bedre grænseflade til klienten opnås. Objekter<br />

instantieres med angivelse af en seed (en startværdi for serien) og et loft for de tilfældige tal,<br />

så klienten kan anmode om tal i et bestemt interval. Klassens konstruktør initierer en liste af<br />

† Eftersom en digital computer er deterministisk, vil den ikke kunne generere totalt tilfældige tal, hvilket er grunden<br />

til, at de oftest benævnse pseudo-tilfældige.<br />

238 Anvendelser for abstrakte datatyper 3.10


tal som udgør den grundlæggende del af algoritmen og en underforstået<br />

konverteringsfunktion laver Random-objekter om til positive heltal. Det tillader klienten at<br />

arbejde med tilfældige tal på følgende måde:<br />

Random r; // r er et tilfældigt tal<br />

unsigned serie [100]; // serie er en liste af heltal<br />

for (i = 0; i < 100; i++)<br />

serie [i] = r; // fyld listen op med tilfældige tal<br />

Klassens implementation ser således ud:<br />

static const rndbufsiz = 250;<br />

class Random {<br />

unsigned* buffer;<br />

int index, ceiling;<br />

public:<br />

Random (int = rand (), int = 0);<br />

~Random () { delete buffer; }<br />

operator unsigned ();<br />

};<br />

// konstruktør: initiér buffer med tilfældige tal<br />

Random::Random (int seed, int ceil) : ceiling (ceil) {<br />

buffer = new unsigned [rndbufsiz];<br />

srand (seed);<br />

index = 0;<br />

int msb = 0x8000, mask = 0xFFFF;<br />

for (int i = 0; i < rndbufsiz; i++) buffer [i] = rand ();<br />

for (i = 0; i < rndbufsiz; i++)<br />

if (rand () > 0x4000) buffer [i] |= msb;<br />

for (i = 0; i < 16; i++) {<br />

int k = i * 11 + 3;<br />

buffer [k] &= mask;<br />

buffer [k] |= msb;<br />

mask >>= 1, msb >>= 1;<br />

}<br />

}<br />

// konverteringsmetode: returnerer 16-bit tilfældigt tal<br />

Random::operator unsigned () {<br />

int i = (index >= 147) ? index - 147 : index + 103;<br />

unsigned next = buffer [index] ^ buffer [i];<br />

buffer [index] = next;<br />

3.10.3 Klassen Random 239


index = (index >= rndbufsiz - 1) ? 0 : index + 1;<br />

return ceiling ? next % ceiling : next;<br />

}<br />

3.10.4 Klassen Set<br />

En meget brugbar datatype, som ikke findes i hverken C eller C++, men derimod i Pascal og<br />

Modula-2, er en mængde. Ved en mængde forstås en samling af værdier, som alle kun kan<br />

være indeholdt i mængden én gang. Operationer på mængder omfatter fællesmængder mellem<br />

to mængder, foreningsmængder og indsætning/udtagning af de enkelte værdier i mængden.<br />

Klassen Set er en abstrakt datatype, som beskriver en mængde, hvor værdiernes maxima<br />

fra starten er fastlagt. Set repræsenterer hvert tal i mængden med én bit, som beskriver om<br />

et givet tal er til stede i mængden eller ej. Da et tal kun kan forekomme én gang i mængden,<br />

er en enkelt bit nok til at repræsentere tallet - sålænge denne bit står på en bestemt plads.<br />

Udregningen af denne plads i lageret er da også, hvad Sets metoder næsten alle handler om.<br />

Set er et eksempel på en numerisk type, som internt arbejder med en datatype, som ikke<br />

findes normalt i C++. En bit er ikke en type i C++, og derfor må Set skjule<br />

implementationen fuldstændig af vejen. Det er ikke muligt for eksempel at returnere<br />

referencer til elementer, sådan som AssocArray gør (afsnit 3.10.2).<br />

typedef int bool;<br />

class Set {<br />

unsigned char* data;<br />

unsigned long bits;<br />

unsigned long bytes () const { return (bits + 7) >> 3; }<br />

Set& largest (const Set& s) const {<br />

return bits > s.bits ? *this : s;<br />

}<br />

public:<br />

Set (const unsigned long);<br />

Set (const Set&);<br />

~Set () { delete data; }<br />

Set& operator= (const Set&);<br />

Set operator+ (const unsigned long) const;<br />

Set operator- (const unsigned long) const;<br />

Set& operator+= (const unsigned long);<br />

Set& operator-= (const unsigned long);<br />

bool operator[] (const unsigned long) const;<br />

Set operator| (const Set&) const;<br />

Set operator& (const Set&) const;<br />

Set& operator|= (const Set&);<br />

Set& operator&= (const Set&);<br />

240 Anvendelser for abstrakte datatyper 3.10


ool operator== (const Set&) const;<br />

bool operator!= (const Set& s) const {<br />

return !(*this == s);<br />

}<br />

void off () { memset (data, 0x00, bytes ()); }<br />

void on () { memset (data, 0xFF, bytes ()); }<br />

friend ostream& operator


indsættelse af en værdi i den aktuelle mængde<br />

Set& Set::operator+= (const unsigned long bit) {<br />

if (bit < bits) data [bit >> 3] |= (1 > 3] &= ~(1 > 3] & (1


eturn *this;<br />

}<br />

// sammenligning af to mængder<br />

bool Set::operator== (const Set& o) const {<br />

for (unsigned long bit = 0; bit < largest (o).bits;<br />

bit++)<br />

if (operator[] (bit) != o [bit]) return 0;<br />

return 1;<br />

}<br />

// udskrivning af mængde på standard-strøm<br />

ostream& operator


eturnerer en reference til det aktuelle objekt, og er ikke konstante, da de har<br />

indflydelse på det aktuelle objekts data. Klienten kan således skrive<br />

a = b += c;<br />

så a tildeles b efter c er blevet adderet til b. Returværdien fra operator+=()<br />

kan være en reference, da der ikke er grund til at oprette en midlertidig forekomst for<br />

at returnere det aktuelle objekt.<br />

• Klassens metoder benytter hinanden med stor fordel, hvorved det demonstreres, at en<br />

implementation med små, isolerede metoder har gavn af disses begrænsede<br />

funktionalitet. Sammenlignings-operatoren er et godt eksempel. I forbindelse med<br />

sammenligning af to mængder skal det også bemærkes, at størrelsen på de to<br />

mængder ikke behøver være den samme. Selv om den ene mængde har flere antal<br />

mulige værdier end den anden, kan de to mængder stadig indeholde samme aktuelle<br />

værdier, hvorved de bliver ens.<br />

Ud over selve klassens interessante interne detaljer viser Set også eksempler på, hvordan<br />

visse operator-overstyringer kan give mindre problemer i brug. Overvej følgende eksempel på<br />

brugen af Set:<br />

Output:<br />

void main () {<br />

Set s1 (10), s2 (10);<br />

for (int i = 0; i < 5; i++)<br />

s1 += i * 2, s2 += i * 2 + 1;<br />

cout


s3 = { 0 2 4 8 }<br />

{ 0 2 4 8 } == { 0 2 4 8 }<br />

I main() fyldes to mængder på med henholdsvis ulige og lige tal i intervallet fra 0 til 9, og<br />

klasserne samt deres forenings- og fællesmængder udskrives. I cout-sætningen<br />

demonstreres, hvordan sprogets indbyggede præcedens i evalueringen af subudtrykkene<br />

bliver et problem for forekomster af Set. Hvis klienten skriver<br />

cout


4. Hvad er forskellen på type-baseret indkapsling og objekt-baseret indkapsling, og<br />

hvilken metode bruges i C++? Diskutér anvendeligheden af den anden metode.<br />

5. Du har skrevet et bibliotek til behandling af strenge, som indeholder følgende<br />

funktioner:<br />

void strcpy (char*, const char*);<br />

void strcmp (const char*, const char*);<br />

unsigned strlen (const char*);<br />

Hvilke metoder kan tages i brug for at forbedre dette bibliotek?<br />

6. Design en klasse til manipulation af matricer. Matricer er todimensionale vektorer af<br />

kommatal. Hvordan bør de private data arrangeres? Hvilke konstruktører bør bruges?<br />

Hvilke operatorer bør overstyres? Hvilke typekonverteringer har klassen brug for?<br />

Hvilke medlemsfunktioner skal bruges?<br />

7. Udvid standardbiblioteket til at omfatte ind- og udlæsning af matricer (opgave 3.6).<br />

8. I C++ er der en udtrykkelig forskel på initiering og tildeling. Hvad består denne<br />

forskel i? Hvorfor er det interessant?<br />

9. Hvad er forskellen på indkapslede og ikke-indkapslede operator-funktioner? Hvornår<br />

bruges hvilken type?<br />

10. Hvad er en friend-funktion? Giv et eksempel på, hvornår det er nødvendigt at<br />

bruge friend-funktioner.<br />

11. Hvad er forskellen på en fundamental og en abstrakt datatype? Hvad er forskellen fra<br />

en klients synspunkt?<br />

12. C++ er et objekt-orienteret sprog med en funktions-orienteret syntaks. Det giver<br />

anledning til tvedtydigheder i mange sammenhænge. Giv nogle eksempler.<br />

13. Modificér eksempel 3-6 i afsnit 3.6.7, så ingen medlemsfunktioner er inline. Mål<br />

forskellen i eksekveringstider for forskellige anvendelser af klassen, for eksempel<br />

operator-kald, vektor-erklæringer og dynamiske allokeringer.<br />

14. Givet klassen i eksempel 3-5, hvilke medlemsfunktioner kaldes underforstået i<br />

følgende sætning:<br />

Complex a = Complex (5,6) + Complex (3,3) + 2;<br />

246 Opgaver til <strong>kapitel</strong> 3 3.11


15. Forklar begrebet brugerdefineret typekonvertering. Hvorfor og hvornår bør dette<br />

bruges?<br />

16. Givet klassen i eksempel 3-6, hvilke medlemsfunktioner kaldes underforstået i<br />

følgende sætning:<br />

double d = (Complex (3, 7) * 4) + Complex (2);<br />

17. Givet følgende prototype:<br />

void f (G&);<br />

hvilken information overføres til funktionen f() ved dette kald:<br />

f (*new G);<br />

Hvad indebærer denne form for allokering, når objektet, der allokeres med new<br />

senere skal destueres? Hvem bør holde rede på objektet: den kaldende eller den<br />

kaldte metode?<br />

18. Hvad er forskellen på statiske medlemsvariable og normale medlemsvariable? Giv et<br />

eksempel på brugen af statiske medlemsvariable.<br />

19. Hvad er forskellen på statiske medlsmsfunktioner og normale (ikke-statiske)<br />

medlemsfunktioner? Giv et eksempel på brugen af statiske medlemsfunktioner.<br />

20. Hvad er en privat klasse? Diskutér anvendeligheden af private klasser, og i hvilke<br />

sammenhænge, de kan bruges.<br />

21. Nogle objekt-orienterede sprog indeholder ikke faciliteter til at forbyde klienter<br />

adgang til objekters data. Diskutér ulemperne ved dette forhold fra et udviklingsmæssigt<br />

synspunkt.<br />

22. Skriv en skabelon til en generel sorteringsfunktion. Funktionen skal kunne sortere<br />

objekter af alle typer i lineære lister af vilkårlige typer hvor det forudsættes, at<br />

typerne har en operator>()-overstyring. Du må selv vælge sorteringsalgoritme.<br />

3.12 REFERENCER OG UDVALGT LITTERATUR<br />

[Stroustrup 87a] og [Stroustrup 87b] fortæller noget om udviklingen af C++ fra det<br />

procedurale C til et sprog med abstrakte typer og modularitet. Om sprogets sammenhæng med<br />

tidligere forskning og i forbindelse med andre sprog henvises til [Lippmann 88]. I [Coplien<br />

3.11 Opgaver til <strong>kapitel</strong> 3 247


92] findes en oversigt over de mange idiomer, som en C++-programmør skal være bekendt<br />

med.<br />

248 <strong>Introduktion</strong> 4.1


Objekt-Orienteret Programmering<br />

Forrige <strong>kapitel</strong> handler om klasser. I terminologien bruges begreberne klasser<br />

og abstrakte datatyper i flæng, hvilket desværre er en smule hæmmende for en<br />

bred forståelse af sprogets muligheder, for dataabstraktion er kun én af<br />

ingredienserne i et objekt-orienteret program. I dette <strong>kapitel</strong> skal vi se på,<br />

hvordan vi kan udnytte C++'s muligheder for at understøtte udvikling af<br />

objekt-orienterede programmer til fulde. Men først skal nogle principper for<br />

traditionel programdesign "aflæres"; en frokost er aldrig gratis.<br />

4.1 INTRODUKTION<br />

Overgangen fra et proceduralt orienteret sprog som C til et objekt-orienteret sprog som C++ er<br />

lettest i den fase, der handler om abstrakte datatyper. Indkapsling og dataskjul, som er beskrevet i<br />

de forrige kapitler, er let at lære for en programmør, som har brugt de gængse metoder, fordi<br />

metoden bygger på allerede kendte principper, der i vid udstrækning også bruges under for<br />

eksempel C.<br />

Det er muligt at bruge et sprog som C++ udelukkende med henblik på manipulation af abstrakte<br />

datatyper. Et program, som før bestod af funktionsbiblioteker og globale datastrukturer, kan<br />

omskrives til at indeholde isolerede klasser med indbyggede metoder til manipulation af<br />

tilhørende data. Problemerne, som løses med dataabstraktion, er af programmeringsteknisk<br />

karakter. De objekt-orienterede principper adresserer løsning af design- og<br />

konstruktionsproblemer i softwareudvikling.<br />

Når vi anvender et bestemt sprog og en bestemt udviklingsmetode til løsning af et<br />

programmeringsproblem, bruger vi et sæt af regler for at strukturere programmets opbygning. Et<br />

ofte brugt begreb for et sådant sæt regler, som tilsammen udgør udviklingsmetoden og sprogets<br />

støtte til denne, er et paradigme. Det proceduralt orienterede paradigme handler om at nedbryde<br />

det overordnede problem i delproblemer, og implementere disse som miniature-programmer. Det<br />

er altså et spørgsmål om funktionelt at isolere opgaverne fra toppen nedefter, og abstrahere de<br />

enkelte opgaver i mindre blokke - for eksempel funktioner eller filer.<br />

Som beskrevet i <strong>kapitel</strong> 1 er det sjældent, at et sprog direkte understøtter en given<br />

programmeringsmetode. Nogle sprog giver mulighed for programmering efter mange forskellige<br />

paradigmer, mens andre kun tillader ét bestemt, og andre igen direkte understøtter programmøren<br />

3.11 Opgaver til <strong>kapitel</strong> 3 249<br />

4


i en bestemt metode. I C er det for eksempel muligt at skrive både objekt-orienterede<br />

programmer, proceduralt orienterede programmer og selv udøve logikprogrammering, i Modula-<br />

2 [Wirth 82] er dataabstraktion og modulærprogrammering næsten uundgåelig, mens Smalltalk<br />

[Goldberg 83a] helt påtvinger programmøren det objekt-orienterede paradigme. At bruge en<br />

bestemt programmeringsmetode i et sprog, som ikke direkte understøtter et givent paradigme<br />

bliver derfor et spørgsmål om disciplin fra programmørens side. Desværre er programmører et<br />

sært folkefærd, som alle arbejder forskelligt, og som alle har deres egne kæpheste.<br />

C++ er en slags hybrid i denne sammenhæng. Sproget tvinger ikke programmøren til at bruge<br />

et bestemt paradigme, men understøtter i videst muligt omfang det paradigme, der vælges. Da<br />

C++ er kildetekst-kompatibelt med C, er dette næsten en forudsætning. De overbygninger, der<br />

findes over C i C++ er, når det kommer til stykket, meget få og simple. Men når de bruges<br />

korrekt, har de kolossal effekt.<br />

4.1.1 Det objekt-orienterede paradigme<br />

De fleste programmører er bekendt med struktureret programmering og design, som handler om<br />

at konkretisere data, relationer mellem data og behandling af data, set udfra et programeksekveringsmæssigt<br />

synspunkt. Man tegner data-flow diagrammer, hvor data forbindes med<br />

logisk separerede dele af programmet, og man tegner flow-charts, hvor programmet arbejder på<br />

de tildelte data. Derefter skriver man subprogrammerne efter flow-chartet.<br />

Objekt-orienteret programmering bygger videre på klasse-begrebet, og beskriver relationer<br />

mellem klasser. Idet klasser består af både data og af metoder til bearbejdning af disse, er<br />

relationerne mellem klasserne hvad vi mangler for at kunne konstruere mere komplekse<br />

programmer. I afsnit 3.9.1 så vi, hvordan klasser kan arbejde sammen i et eksempel med<br />

afgrænsede typer. I det eksempel var det nødvendigt udtrykkeligt at beskrive forskellene mellem<br />

de to klasser ved underforståede brugerdefinerede typekonverteringer, som skulle skrives<br />

eksplicit i begge klasser. Objekt-orienteret programmering giver en bedre og renere metode til<br />

beskrivelse af klasse-relationer.<br />

Der er tre nøglepunkter, der kendetegner et objekt-orienteret sprog:<br />

• Indkapsling, som beskriver grænsefladen mellem en klasse og en klient af denne,<br />

• Arv, som bruges til at beskrive sammenhænge, relationer og afhængigheder klasser<br />

imellem, samt til at lette udviklingen af generisk kode,<br />

• Dynamisk binding, som skjuler forskellene mellem klasserne fra klienten (udtrykt ved<br />

arv).<br />

Indkapsling, som er beskrevet i <strong>kapitel</strong> 3, tvinger klienten til at bruge et defineret sæt af regler<br />

og operationer for manipulation af en forekomst af en klasse (klassens specifikation), og sikrer<br />

konsistens i dens data. Klienten isoleres bogstaveligt talt fra forekomstens data og fra de<br />

algoritmer, der bruges til at arbejde på dem (klassens implementation). Dermed kan en klasse<br />

ændres med garanti for, at et klientprogram ikke fejler, og fejlfinding gøres ligeledes lettere, idet<br />

kode, som arbejder på bestemte data i en klasse, er isoleret indenfor samme klasse.<br />

250 <strong>Introduktion</strong> 4.1


Arv er en teknik, som tillader en klasse at overtage data og metoder fra en eksisterende klasse,<br />

og som dermed simplificerer udviklingen af nye klasser. Ideen er, at vi kan nøjes med at beskrive<br />

forskellene mellem de to klasser i stedet for at omskrive en hel klasse. Pointen er, at hjulet<br />

allerede er opfundet. Hvis du opfinder et nyt hjul, og det er bedre end det, der findes i forvejen, så<br />

har du opnået noget. Ellers har du spildt din tid. Arv gør det mere favorabelt at skrive generiske<br />

programmer, altså programmer, som udfører generelle opgaver, og isolere disse i klasser. Andre,<br />

mere specifikke opgaver, kan nedarve fra de generelle klasser og bygge de mere specifikke dele<br />

ovenpå. Genbrug af kode er et af de vigtigste emner i fremtidens softwarekonstruktion. De<br />

kæmpe-opgaver, der skal løses, tillader ikke enhver at genopfinde hjulet efter behov, med alle de<br />

fejl og abnormaliteter samt ikke mindst tidsforbrug dette måtte medføre. For programmører, der<br />

hidtil har arbejdet med proceduralt orienterede sprog, kan beskrivelsen af nedarvninger være<br />

svære at se pointen i. Men husk altid, at de ikke har det fjerneste med data-flow eller control-flow<br />

diagrammer at gøre. De beskriver ikke algorimer eller datamodeller, men udgør snarere en<br />

nedbrydning af et program i reelle koncepter - på samme abstraktionsniveau, som man kan<br />

bevæge sig på under hele udviklingsfasen. Det giver en stor frihed at kunne tale om<br />

afgrænsninger i et program som isolerede koncepter helt fra designfasen og ned i kildeteksten.<br />

Når der ikke foretages kontekst-skift, får man en bredere og mere homogen forståelse for<br />

programmets logik.<br />

Dynamisk binding, eller sen binding, gør klientprogrammerne mere generiske ved at skjule<br />

forskellene mellem relaterede klasser. Via dynamisk binding kan forskellige klasser have<br />

forskellige implementationer af den samme funktion. Herefter kan en klient kalde denne funktion<br />

uden at kende præcis hvilken klasse, der arbejdes på. Hvad er fordelen i det? For eksempel kan et<br />

klientprogram, som udskriver formularer, være den samme uanset typen af formular.<br />

Klientprogrammet kalder blot en metode i klassen, og dynamisk binding sikrer, at det er den<br />

rigtige metode for den specifikke klasse af en forekomst, der kaldes. Men hvordan kan klienten<br />

kalde en funktion i en klasse uden at kende karakteren af denne klasse? Det er blot en af<br />

fordelene ved arv. Som vi skal se, tillader arv os at behandle forskellige klasser med mindre indre<br />

afvigelser på samme måde for derved at opnå en generalisering af den kode, der behandler alle<br />

klasse-forekomster af samme karakter. Dynamisk binding virker ved at forsinke<br />

bindingstidspunktet til programmet rent faktisk kører, og beskrives nærmere i afsnit 4.4.2.<br />

4.1.2 Terminologi<br />

Med objekt-orienteret programmering medfølger en del nye begreber, som kræver en forklaring,<br />

før vi for alvor kan tage fat:<br />

Indkapsling skjuler implementationsdetaljer ved at samle data og kode i klasser, som beskrevet<br />

i <strong>kapitel</strong> 3. Når en klasse arver fra en anden, kaldes den nedarvede klasse for baseklassen eller<br />

den overordnede klasse og den nedarvende for den afledte eller underordnede klasse. Visse bøger<br />

kalder bruger også termerne nedarvede og nedarvende klasser, men de leder efter min mening til<br />

begrebsforvirringer (hvad er hvad?) og er alt for ens (kun ét bogstav til forskel). I denne bog<br />

holder jeg mig til de førstnævnte baseklasser og afledte klasser.<br />

4.1.1 Det objekt-orienterede paradigme 251


Figur 4-1: Lineær arv (a), multipel arv (b) og hierarki af klasser (c).<br />

Det er muligt igen at nedarve fra en afledt klasse, ligesom baseklassen selv kan være en afledt<br />

klasse. For at få hold på dette bruges base og afledt relativt fra den aktuelle klasse, der tales om.<br />

Der kan nedarves flere gange fra samme baseklasse til forskellige afledte klasser og en afledt<br />

klasse kan nedarve fra flere forskellige baseklasser, hvilket bærer betegnelsen flersidet eller<br />

multipel arv. Et system af baseklasser og afledte klasser kaldes et klasse-hierarki. Der findes<br />

mange systemer til at afbilde klasse-hierarkier, men de mest simple viser blot de enkelte klasser<br />

som kasser og nedarvningerne som streger imellem dem, hvor afledningen foregår vertikalt<br />

nedefter og hvor pilene går i retning af baseklasserne. Her er det så muligt at specificere de<br />

enkelte klassers data og metoder inde i kasserne, så det klart fremgår, hvad der nedarves (figur 4-<br />

1). Der findes mange forskellige udvidelser til denne repræsentation, for eksempel har [Booch<br />

91] en hel række af ikoniserede pile til at symbolisere blandt andet arv, instantiering, indkapsling<br />

som medlem og andre typeforhold samt tilsvarende symboler for klasseforhold så som leksikalt<br />

skop, parametre, felter og synkrone/asynkrone kald. En anden model til beskrivelse af klassehierarkier<br />

er en linie-baseret liste af klassenavne, indrykket efter afhængigheder af baseklasser, så<br />

klassenavnene mod venstre er baseklasser og navnene mod højre er de afledte klasser:<br />

class_a<br />

class_b<br />

class_c<br />

class_d // d arver fra c, b og a<br />

class_e<br />

class_f<br />

class_g<br />

252 <strong>Introduktion</strong> 4.1


class_h // h arver fra g og a<br />

class_i<br />

Denne model er simplere og fylder meget mindre, men skaber problemer, når den skal beskrive<br />

komplekse klassehierarkier (med multipel arv, for eksempel, som beskrives i afsnit 4.5). Den<br />

bruges imidlertid i opremsningerne af eksempelklasserne i <strong>kapitel</strong> 7.<br />

De egenskaber, sproget får fra dynamisk binding af metoder i klasser, kaldes polymorfe. Dette<br />

begreb, som er græsk og betyder mangeformet egenskab, dækker over det princip, at en given<br />

metode eller datastruktur kan bruges på mange forskellige måder, uden konkret at skulle<br />

specificere måden for den enkelte metode eller datastruktur. Det er ikke helt let at begribe dette<br />

koncept, men det er meget centralt i objekt-orienteret programmering, så lad os tage et eksempel:<br />

du skriver en funktion, som i en løkke tegner et antal grafiske objekter på skærmen. Disse<br />

objekter er alle forekomster af forskellige klasser som beskriver forskellige geometriske former.<br />

Under normale omstændigheder skal du i løkken for hver iteration kalde en specifik metode i<br />

hver klasse afhængig af, hvilken geometrisk form den har (da det jo er forskellige metoder, der<br />

tegner forskellige former). Dette klares for eksempel med en switch-kontrolstruktur i løkken.<br />

Pointen er nu, at de polymorfe metoder tillader dig at være ligeglad med, hvilken klasse det<br />

aktuelle objekt tilhører, sålænge alle klasserne har den samme indkapslede tegne-metode. Den<br />

sene binding vil så at sige foretage valget helt underforstået. Funktionen bliver dermed meget<br />

simpel og letlæselig, og det bliver endvidere, som vi skal se, meget let at udvide funktionens<br />

virkemåde uden at ændre en linie eller sågar at genoversætte den! Det er således polymorfiens<br />

funktion både at organisere programmerne bedre og at gøre dem mere generiske.<br />

Polymorfi realiseres delvist gennem en programkonstruktion i C++, som hedder virtuelle<br />

funktioner eller metoder. Virtuelle metoder har polymorfe egenskaber i den forstand, at de<br />

repræsenterer de "usynlige" forskelle mellem klasserne. De manifesterer dermed den flertydighed,<br />

som et typehierarki kan have. Grunden til, at de er virtuelle er, at de har egenskaberne<br />

af metoder, men ikke nødvendigvis implementerer disse rent fysisk. De kan med andre ord<br />

erstattes af andre funktioner uden videre. Virtualitet kendes på mange måder i datalogien, for<br />

eksempel indenfor lageradministration og maskinarkitekturer.<br />

Når en baseklasse konstrueres med virtuelle metoder, er det normalt ikke meningen, at den skal<br />

instantieres, dvs. at en klient opretter et objekt af klassen, fordi den kun definerer en fællesnævner<br />

for afledte klasser. Baseklasser med denne kvalitet kaldes ofte for abstrakte klasser, fordi det ikke<br />

er muligt at skabe et objekt af klassen. Det abstrakte ligger i, at klassen ikke kan bruges af<br />

klienten, men kun som baseklasse for andre klasser.<br />

4.2 ET EKSEMPEL-PROBLEM<br />

Som i sidste <strong>kapitel</strong> starter vi med en problemstilling, som vi løser per absurdum, dvs. med en<br />

metode, vi ved er "forkert", for derefter bedre at kunne se fordelen i at bruge en mere "rigtig"<br />

metode - i henhold til objekt-orienterede pricipper. Programmet, der skal skrives, er et grafisk<br />

system, der kan bruges til opstilling af forskellige geometriske former på skærmen. Først løses<br />

problemet i C++ med abstrakte datatyper og derefter forbedres det trinvist, som flere objektorienterede<br />

faciliteter kommer for dagen.<br />

4.1.2 Terminologi 253


Kravet til programmet, som skal kunne fungere som bibliotek for klienter, der skal bruge<br />

forskellige figurer på skærmen, er, at figurtyperne punkt, linie og rektangel skal kunne behandles<br />

og det skal være muligt dynamisk at ændre på figurerne. De skal ydermere kunne tegnes på en<br />

skærm af vilkårlige dimensioner, som defineres af klienten. En sådan skærm kaldes en logisk<br />

eller virtuel skærm, fordi den har alle egenskaber en rigtig skærm har, men er rent faktisk kun en<br />

software-repræsentation af en skærm. Programmet skal derfor kunne oversætte brugerens<br />

koordinatsystem til de dimensioner, som den aktuelle hardware har. En sådan oversættelse kaldes<br />

en transformation, og involverer en skalering af koordinater fra logiske (virtuelle) koordinater til<br />

fysiske (reelle) kordinater. Grunden til, at en transformation af koordinater er interessant er, at det<br />

gør de geometriske klasser uafhængige af en bestemt opløsning på skærmen eller den enhed, man<br />

nu ønsker at vise sit billede på. Ved at ændre transformationen kan det samme geometiske<br />

program tegne figurer på et andet koordinatsystem uden at skulle omskrives. Den umiddelbare<br />

fordel er, at grafikprogrammet kan flyttes til anden hardware, dvs. en anden skærmtype - for<br />

eksempel med større opløsning eller flere farver. Men også indenfor samme skærm er det muligt<br />

at forestille sig vinduer, dvs. indrammede, logiske subskærme indenfor den fysiske skærm. Igen<br />

er det selve de geometriske objekter uvedkommende; de er blot interesserede i at repræsentere og<br />

behandle sig selv, og ikke i absolutte, fysiske skærm-orienterede konstanter.<br />

4.2.1 En løsning med abstrakte datatyper<br />

Vi angriber problemet med de metoder, vi gennemgik i <strong>kapitel</strong> 3. Programmet skrives ud fra et<br />

design, som indkapsler alle gensidigt uafhængige data og funktioner i klasser, som kan<br />

instantieres og bruges af klienten. Vi er interesserede i at arbejde med forskellige geometriske<br />

former, samt at udskrive disse på en output-enhed. Programmet skal derfor kunne arbejde med<br />

datatyper som Firkant og Linie. Først erklærer vi to andre klasser, som arbejder sammen<br />

med de geometriske klasser og som går igen mange gange i geometriske programmer. De to<br />

klasser hedder Koordinat og Transformation, og behandler henholdsvis koordinater på<br />

en todimensionel flade samt transformationer fra logiske til fysiske koordinatsystemer.<br />

4.2.2 Klassen Koordinat<br />

Koordinater bruges i næsten alle manipulationer af grafiske figurer, deriblandt beskrivelser af<br />

punkter i et koordinatsystem, selve koordinatsystemets dimensioner samt transformationer<br />

mellem koordinatsystemer og er derfor en oplagt kandidat til indkapsling i en brugerdefineret<br />

datatype. Klassen Koordinat's definition ser sådan ud:<br />

// eksempel 4-1a: en koordinat-ADT<br />

enum bool { nej, ja };<br />

class Koordinat {<br />

double x, y; // det faktiske koordinat<br />

public:<br />

254 Et eksempel-problem 4.2


Koordinat () : x (0), y (0) { };<br />

Koordinat (double& xi, double& yi) : x (xi), y (yi) { }<br />

Koordinat (const Koordinat& k) : x (k.x), y (k.y) { }<br />

Koordinat operator+ (const Koordinat&) const;<br />

Koordinat operator- (const Koordinat&) const;<br />

Koordinat operator* (const Koordinat&) const;<br />

Koordinat operator/ (const Koordinat&) const;<br />

Koordinat& operator+= (const Koordinat&);<br />

Koordinat& operator-= (const Koordinat&);<br />

Koordinat& operator*= (const Koordinat&);<br />

Koordinat& operator/= (const Koordinat&);<br />

friend ostream& operator


}<br />

De to andre aritmetiske overstyringer, operator+= og operator-=, tildeler begge et<br />

Koordinat summen af to Koordinater, hvoraf den ene er det aktuelle objekt. Derudover<br />

returnerer de også resultatet (som reference-type), så sætninger som<br />

a += b += c; // c adderes til b, dernæst b til a<br />

kan oversættes:<br />

Koordinat& Koordinat::operator+= (const Koordinat& k) {<br />

x += k.x, y += k.y;<br />

return *this;<br />

}<br />

Koordinat& Koordinat::operator-= (const Koordinat& k) {<br />

x -= k.x, y -= k.y;<br />

return *this;<br />

}<br />

De fire medlemsfunktioner operator*(), operator*=(), operator/() og<br />

operator/=() multiplicerer og dividerer objekter af Koordinat. At multiplicere og<br />

dividere koordinater er brugbart i transformationer af figurers placeringer på skærmen, dvs. når<br />

der skal oversættes fra virtuelle til fysiske koordinater.<br />

Koordinat Koordinat::operator* (const Koordinat& k) const {<br />

return Koordinat (x * k.x, y * k.y);<br />

}<br />

Koordinat& Koordinat::operator*= (const Koordinat& k) {<br />

x *= k.x, y *= k.y;<br />

return *this;<br />

}<br />

Koordinat Koordinat::operator/ (const Koordinat& k) const {<br />

return Koordinat (x / k.x, y / k.y);<br />

}<br />

Koordinat& Koordinat::operator/= (const Koordinat& k) {<br />

x /= k.x, y /= k.y;<br />

return *this;<br />

}<br />

256 Et eksempel-problem 4.2


Overstyringen af standard-output-operatoren


Figur 4-2: Grafisk transformation.<br />

Klienten kan til enhver tid bestemme sig for at ændre på de fysiske koordinater ved at fortælle<br />

transformations-klassen om de nye koordinater. Derved vil efterfølgende brug af de geometriske<br />

klasser automatisk omstille sig til det aktuelle koordinatsystem. I dette eksempel benytter vi ikke<br />

en faktisk fysisk skærm af to årsager: for det første er det ikke interessant for eksemplet, hvordan<br />

man rent algoritmisk tegner linier og streger på en bestemt enhed, og for det andet er det svært at<br />

vise sekventielt grafisk output i en bog. Derfor nøjes vi med bot at skrive operationen som en<br />

grafikkommando. I en reel situation ville vi have to transformationer, først en fra brugerens<br />

selvvalgte koordinatsystem til normaliserede koordinater, dvs. et altid ensartet logisk<br />

koordinatsystem (for eksempel afgrænset mellem 0 og 1), og dernæst fra de normaliserede<br />

koordinater til fysiske koordinater. I vores eksempel er vi ikke interesserede i at gøre os<br />

hardware-afhængige på nogen måde, og springer derfor dette led over.<br />

Transformation-klassen indeholder derfor blot den transformation, som oversætter fra<br />

logiske til fysiske koordinater:<br />

258 Et eksempel-problem 4.2


eksempel 4-1b: en todimensionel transformationsklasse<br />

class Transformation {<br />

Koordinat transfunkt;<br />

public:<br />

Transformation () : transfunkt (1, 1) { };<br />

void setFysisk (const Koordinat& fysisk) {<br />

transfunkt *= fysisk;<br />

}<br />

void setLogisk (const Koordinat& logisk) {<br />

transfunkt /= logisk;<br />

}<br />

Koordinat operator () (const Koordinat& k) const {<br />

return transfunkt * k;<br />

}<br />

};<br />

Klassen Transformation indeholder et Koordinat-medlem, som hedder<br />

transfunkt og som beskriver selve transformationen. Når en Transformation<br />

instantieres, sættes transfunkt til (1,1), dvs. et forhold mellem de logiske og de fysiske<br />

koordinater på 1:1 for både x- og y-koordinater. To medlemsfunktioner styrer henholdsvis det<br />

logiske og det fysiske koordinatsystem og har reciprokke virkninger for transfunkts<br />

indhold.<br />

setFysisk(), som tillader brugeren at ændre på den fysiske skærm, modtager<br />

dimensionerne på det fysiske koordinatsystem i et Koordinat, som ganges med transformationsfunktionen.<br />

Jo større, det fysiske koordinatsystem bliver, des mindre bliver tælleren i<br />

forholdet mellem det logiske og det fysiske koordinatsystem. Hvis setFysisk() kaldes med<br />

en Koordinat(1280,1024), bliver tranformationsfunktionen 1280:1 for x-aksen og 1024:1<br />

for y-aksen, dvs. at hvis et koordinat skal oversættes skal det ganges med 1280 for x og 1024 for<br />

y. setLogisk() arbejder lige omvendt, idet den dividerer transformationsfunktionen med<br />

angivelsen af det logiske koordinatsystem. Hvis klienten angiver det fysiske koordinatsystem til<br />

(1280,1024) og det logiske til (640,256), vil forholdet blive 2:1 for x og 4:1 for y, dvs. den<br />

logiske skærm er ca. otte gange mindre end den fysiske. Det omvendte forhold, dvs. et kald til<br />

setLogisk() med for eksempel (10000,10000), vil resultere i et forhold på ca. 1:8 for x og<br />

1:10 for y.<br />

De geometriske klasser bruger kald til forekomster af Transformation via den<br />

overstyrede operator(), som returnerer det fysiske koordinatsæt for et givet logisk<br />

koordinatsæt. Transformation ganger blot de logiske koordinater, som kommer fra den<br />

geometriske klasse, med den aktuelle transformations-funktion og returnerer dette som et<br />

Koordinat. Hvis det fysiske koordinatsystem er sat op til (1280,1024) og det logiske til<br />

(640,512), vil transfunkt have værdien (2,2). Et kald til operator() med et vilkårligt<br />

koordinatsæt vil returnere den korrekte oversættelse:<br />

Transformation t;<br />

4.2.3 Klassen Transformation 259


Output:<br />

t.setFysisk (1280,1024);<br />

t.setLogisk (640,512);<br />

cout


const Koordinat IBM_8514 (1280,1024); // fysisk skærm<br />

const Koordinat IBM_CGA (320,200); // logisk skærm<br />

void main () {<br />

trans.setFysisk (IBM_8514);<br />

trans.setLogisk (IBM_CGA);<br />

Koordinat punkt (100,100);<br />

cout


Da Figur er en privat klasse, kan den ikke instantieres af klienter udenfor medlemsfunktioner i<br />

de klasser, som er erklæret friend til Figur. Læg mærke til, at det ikke er nødvendigt at<br />

prototype de tre friend-erklæringer. Figur beskriver blot et generelt grafisk objekt med et<br />

startpunkt, en farve og et kan-ses-flag.<br />

4.2.5 Klasserne Punkt, Linie og Firkant<br />

Nu er det tid til at definere de faktiske geometriske klasser, som er friend til Figur:<br />

Punkt, Linie og Firkant. Punkt er et enkelt billedelement på skærmen (en pixel), mens<br />

en Linie og et Firkant har et startpunkt og et endepunkt. Alle tre klasser har en forekomst<br />

af en Figur som privat medlemsvariabel.<br />

// eksempel 4-1d: en konkret figur-klasse<br />

class Punkt {<br />

Figur fig;<br />

public:<br />

Punkt () : fig () { }<br />

Punkt (const Koordinat& k) : fig (k) { }<br />

void Tegn ();<br />

void Slet ();<br />

void Flyt (const Koordinat&);<br />

};<br />

void Punkt::Tegn () {<br />

fig.vises = ja;<br />

cout


Punkt har to konstruktører: en underforstået konstruktør, som blot opretter et en forekomst af et<br />

Punkt og nulstiller alle variable samt en konstruktør, som initierer et Punkt med et<br />

koordinatsæt. Der er ingen destruktør, da et Punkt kun indeholder medlemsvariable som<br />

forekomster og ikke som indirekte pointere til forekomster. Den underforståede destruktør er<br />

derfor nok til at destruere Punkter (man kunne dog selvfølgelig forestille sig, at destruktøren<br />

slettede punktet fra skærmen eller noget lignende - det behøver ikke altid være de udtrykkelige<br />

regler, der bestemmer, hvad en destruktør skal bruges til). Ligeledes er de underforståede kopikonstruktører<br />

og tildelings-operatorer tilstrækkelige.<br />

Udover konstruktørerne indeholder klassen tre public metoder til behandling af punkter:<br />

• Tegn() tegner et Punkt på skærmen,<br />

• Slet() sletter Punktet på skærmen,<br />

• Flyt() flytter et Punkt til et nyt koordinat, relativt fra den aktuelle position.<br />

Tegn() skriver således blot koordinaterne ud på skærmen, mens Slet() fortæller, at punktet<br />

på koordinaterne nu er slettede. Flyt() ser først, om punktet er aktuelt (vises på skærmen),<br />

hvorefter det i bekræftende fald fjernes. De nye koordinater gøres aktuelle og punktet vises igen,<br />

hvis det tidligere var vist på skærmen.<br />

Når et Punkt instantieres med et Koordinat, videregives dette til Figur-forekomsten.<br />

Der er ingen andre datamedlemmer, da indholdet af Figur er nok til at beskrive et Punkt. De<br />

tre normale metoder i Punkt benytter forekomsten af den globale transformationsklasse<br />

trans til beregning af fysiske koordinater. Punkter kan nu bruges som følger:<br />

void main () {<br />

Koordinat Normal (1,1);<br />

trans.setLogisk (Normal); // 1:1-oversættelse<br />

trans.setFysisk (Normal); // logisk == fyisk<br />

Punkt a (Koordinat (10,10)), c;<br />

c = a;<br />

a.Flyt (Koordinat (12,6));<br />

a.Tegn (); c.Tegn ();<br />

c.Flyt (Koordinat (3,-3));<br />

a.Slet ();<br />

}<br />

Dette program udskriver:<br />

Punkt tegnet på (22,16)<br />

Punkt tegnet på (10,10)<br />

Punkt slettet på (10,10)<br />

Punkt tegnet på (13,7)<br />

Punkt slettet på (22,16)<br />

4.2.5 Klasserne Punkt, Linie og Firkant 263


Vi skal også have linier og rektangler:<br />

// eksempel 4-1e: flere konkrete figur-klasser<br />

class Linie {<br />

Figur fig;<br />

Koordinat endePunkt;<br />

public:<br />

Linie () : fig (), endepunkt () { }<br />

Linie (const Koordinat& k, const Koordinat& e) :<br />

fig (k), endePunkt (e) { }<br />

void Tegn ();<br />

void Slet ();<br />

void Flyt (const Koordinat&);<br />

};<br />

void Linie::Tegn () {<br />

fig.vises = ja;<br />

cout


void Flyt (const Koordinat&);<br />

};<br />

void Firkant::Tegn () {<br />

fig.vises = ja;<br />

cout


Flyt() og Slet() fordi de er underlagt hvert deres skop - de respektive klasser.<br />

4.2.6 Gennemgang af ADT-løsningen<br />

Vi har implementeret de tre klasser Punkt, Linie og Firkant som abstrakte datatyper,<br />

fordi de repræsenterer tre isolerede koncepter i en grafisk applikation. Dataabstraktion handler<br />

netop om at isolere konceptuelt forskellige dele af programmet i indkapslede moduler, som kan<br />

skjule implementationen og kompleksiteten fra brugeren.<br />

Men koncepter er aldrig helt isolerede. Selvom en linie ikke ligner et rektangel, er de dog begge<br />

geometriske former, og selvom et punkt er noget helt andet end en linie, deler det dog mange<br />

egenskaber med linien. Det kan vi endda se helt klart: selvom de tre klasser er helt isolerede, er<br />

en stor del af koden helt identisk fra klasse til klasse.<br />

Problemet her består altså ikke kun af programmeringstekniske overvejelser om grænseflader til<br />

klienten og skjulning af implementationer. Komplekse programmer (og de er flest) indeholder<br />

mange relaterede og dog divergerende koncepter, der skrives som specialiserede dele af det.<br />

Hvordan kan disse afvigelser beskrives med et minimum af dublet-kode? I anden omgang ser vi,<br />

at brugen af relaterede klasser bliver svær fra klientens side. Når flere klasser deler en del af deres<br />

anvendelighed, men er forskellige typer, får klienten svært ved at arbejde på dem samlet.<br />

4.2.7 En forbedret ADT?<br />

Måske er abstrakte datatyper slet ikke gode til at repræsentere mere komplicerede og<br />

sammenhængende koncepter. En løsning, der ligger tættere på de funktionsorienterede sprog,<br />

ville måske være bedre til opgaver som denne. For eksempel kan vi omskrive den generelle<br />

klasse Figur, til at repræsentere både punkter, rektangler og linier, og som deler<br />

medlemsfunktionerne for alle figurer. Prisen for at kunne dele disse funktioner er, at vi selv skal<br />

overtage typekontrollen på de forskellige geometriske former.<br />

// eksempel 4-2: en kombineret figur-klasse<br />

enum figurType { Punkt, Linie, Firkant; }<br />

class Figur {<br />

figurType slags;<br />

bool vises;<br />

Farve f;<br />

union { // anonym union<br />

Koordinat p; // for punkter<br />

struct { Koordinat ls, le; } // for linier<br />

struct { Koordinat rx, re; } // for firkanter<br />

};<br />

public:<br />

266 Et eksempel-problem 4.2


Figur (figurType t, Koordinat& k) :<br />

p (k) { // konstruktion af punkt<br />

f = hvid, slags = t, vises = nej;<br />

}<br />

Figur (figurType t, Koordinat& k1, Koordinat& k2) :<br />

ls (k1), le (k2) { // konstruktion af linie<br />

f = hvid, slags = t, vises = nej; // eller firkant<br />

}<br />

void Tegn ();<br />

void Slet ();<br />

void Flyt ();<br />

};<br />

void Figur::Tegn () {<br />

switch (slags) {<br />

case Punkt:<br />

cout


vises = nej;<br />

}<br />

void Figur::Flyt (Koordinat& k) {<br />

bool vistes = vises;<br />

if (vises) Slet ();<br />

switch (slags) {<br />

case Punkt:<br />

p += k; break;<br />

case Linie:<br />

ls += k, le += k; break;<br />

case Firkant:<br />

rs += k, re += k; break;<br />

}<br />

if (vistes) Tegn ();<br />

}<br />

Nu har vi indkapslet alle figurtyperne i en enkelt klasse, men det synes ikke at have mindsket<br />

kompleksiteten. Der er tvetydigheder i konstruktørerne, som må benytte sig af forskellige<br />

parameterlister og strukturen i unions for at initiere objekterne korrekt til den rette figurtype.<br />

De tre specielle klient-metoder Tegn(), Slet() og Flyt() er faktisk slet ikke så delt<br />

mellem figurtyperne, som de ser ud til. Selvom de tre metoder er fælles for alle figurer, skal de<br />

foretage et valg af kode for den aktuelle figur for hvert kald. Det er selvfølgelig nydeligere at<br />

indkapsle dette valg i stedet for at lade det være op til klienten, men koden skal alligevel udføres<br />

for hver operation.<br />

Eksempel 4-2 introducerer en ny opremsning, en figurType, som indeholder navnene på de<br />

forskellige figurer, der kan indeholdes i klassen Figur. En indkapslet, anonym union i<br />

Figur består af de tre forskellige datastrukturer, som de tre figurtyper behøver (henholdsvis et<br />

koordinat, to koordinater samt to koordinater). Medlemsfunktionerne tester variablen slags,<br />

som beskriver figur-typen (Punkt, Linie eller Firkant), og vælger en bestemt rute<br />

afhængig af denne; de forskellige figurer skal jo i sidste ende tegnes forskelligt.<br />

Og det er netop dette, der er kernen i problemet. De fleste programmer indeholder et større antal<br />

datastrukturer, som er næsten ens. De behandles alle på samme måde, men da de ikke er helt ens,<br />

må der skrives behandlende kode (metoder) til hver enkelt af dem. Det giver en masse kode, som<br />

næsten ser redundant ud, men som simpelthen behandler en specialiseret klasse eller datastruktur.<br />

Struktureret design, for eksempel, resulterer ofte i programmoduler, som indholder næsten<br />

identiske funktioner - ofte blot med typenavne eller variabelnavne erstattet. Det er normalt<br />

umuligt at skrive generiske funktioner til fælles behandling af disse næsten ens strukturer, fordi<br />

de netop ikke er helt ens.<br />

Dette kan illustreres med et forslag til et klientprogram, der benytter klasserne i eksempel 4-1<br />

og 4-2. Det ville for eksempel være nyttigt at kunne holde et antal figurer i en liste (vektor, hægtet<br />

liste, stak eller andet) og skrive dem ud eller flytte dem rundt med reference til elementerne i<br />

268 Et eksempel-problem 4.2


listen. Men da figurerne er forskellige, må der skrives forskellige funktioner til at behandle de<br />

forskellige typer - det er ikke muligt at generalisere: Selvom de har meget til fælles, kan sproget<br />

ikke se lighederne. En cirkel-tegnefunktion er fra oversætterens synspunkt noget helt andet end<br />

en linie-tegnefunktion. Med eksempel 4-1 som bibliotek må et klientprogram eksplicit foretage<br />

dette type-check i koden og kalde den ønskede metode i det ønskede objekt. Hvis eksempel 4-2<br />

ligger til grund, skal selve klassen skrives om, hvis en ændring ønskes.<br />

Det, der er brug for, er en mere formel metode til udtrykkeligt at beskrive forskellene mellem<br />

klasser, som har noget til fælles. Denne metode hedder arv, og er hjørnestenen i det objektorienterede<br />

paradigme og dets faciliteter for genericitet og genbrug af kode.<br />

4.3 ARV<br />

Vi husker fra <strong>kapitel</strong> 3, at klasser og typer har samme betydning i objekt-orienteret terminologi.<br />

Når et program består af flere forskellige klasser med mindre afvigelser, er det en fordel, hvis<br />

sprogets typesystem hjælper programmøren med at undgå så mange eksplicitte (programkontrollerede)<br />

type-checks som muligt. Jo bedre type-ækvivalens sproget selv kan beskrive, des<br />

mindre triviel kode skal skrives dertil.<br />

Typesystemet i C++ udvider, som beskrevet i <strong>kapitel</strong> 3, begrebet fra de indbyggede, skalære<br />

typer til brugerdefinerede, abstrakte typer. Den store gevinst er i første omgang, at de<br />

brugerdefinerede typer kan blandes i sproget uden tab af abstraktion og i anden omgang, når de<br />

objekt-orienterede faciliteter tages i brug, at typerne kan relateres til hinanden og smelte sammen.<br />

Typesystemet udvides altså ikke blot til at omfatte indkapslinger og pæne grænseflader, men også<br />

til at beskrive forhold typerne imellem.<br />

For at forstå begrebet "et forhold mellem typer" må vi se på programudvikling og -design på en<br />

ny måde. For mange betyder dette, at de velkendte metoder og paradigmer skal lægges på hylden,<br />

for objekt-orienterede programmer er fundamentalt anderledes end andre programmer. Dog er<br />

paradigmet heldigvis naturligt forekommende.<br />

4.3.1 Type-ækvivalens<br />

Vi har det med at klassificere verden. Vi ordner virkeligheden efter klasser og opstiller systemer<br />

for afhængigheder og relationer. Grundprincippet i det objekt-orienterede paradigme er præcis<br />

det samme. Programmer bør designes og opbygges med de "logiske byggesten", verden er lavet<br />

af.<br />

For eksempel klassificerer vi verdens fauna meget stringent. Vi taler om forskellige dyrearter,<br />

som igen opdeles i racer og racerne manifesterer sig undertiden som faktiske dyr med<br />

individuelle karakterer og egenskaber. Dumbo er en elefant † , som har et pattedyrs egenskaber.<br />

Insekter er et andet godt eksempel, hvor indviklede systemer kategoriserer de forskellige dyr.<br />

Ligeledes for planteverdenen. Litteratur og musik. Astronomi og mytologi. Vi inddeler vores<br />

hverdag i et hierarki af klasser for at holde rede på, hvad tingene repræsenterer, hedder og kan<br />

bruges til.<br />

† Dumbo er faktisk en forekomst af en elefant (vistnok indisk).<br />

4.2.7 En forbedret ADT? 269


Det interessante er nu, at kategoriseringen indebærer en naturlig nedarvning nedefter i<br />

hierarkiet: det, som gælder for et pattedyr, gælder for både hunde, katte, marsvin og spidsmus.<br />

Det, som gælder for hunde gælder både for Pluto og Nuser. Det er ikke en tilfældighed, at vi<br />

inddeler verden på denne måde. Hvis det hele var en stor pærevælling af isolerede samlinger af<br />

egenskaber og adfærd ville vi simpelthen ikke kunne overskue noget. Sådan har det til gengæld<br />

været med software i lang tid.<br />

Tilbage til C++. Når vi taler om nedarvning mellem klasser, betyder det, at én klasse kan<br />

"overtage" kode og data fra andre klasser efter bestemte regler og udbygge eller modificere dem<br />

efter behov. Således kan en overordnet baseklasse beskrive alt, hvad der vedrører pattedyr (for<br />

eksempel gennemsnitlig levetid, kropstemperatur og drægtighedstid), hvorefter en underordnet<br />

afledt klasse kan overtage alt, hvad baseklassen indeholder og udbygge med flere data og<br />

metoder for at specialisere klassen, for eksempel for hunde med pelstype og opdrætningsforhold.<br />

En anden klasse kan nedarve for aber og udvide med sprogkundskaber og klatrefærdighed.<br />

Ligesom i det konceptuelle hierarki bliver det faktisk implementerede hierarki i C++ en samling<br />

af klasser, hvor de overordnede er generelle og de underordnede er mere specifikke.<br />

Figur 4-3: Et associativt opbygget hierarki.<br />

270 Arv 4.3


Der er flere fortrin ved en sådan konceptuel opdeling. Ikke nok med, at klasserne bliver lette at<br />

overskue og at eksisterende kode bliver let at genbruge på grund af den direkte nedarvning fra<br />

andre klasser, så kan et baseobjekt også bruges som basis-reference til mange forskellige afledte<br />

objekter. Hvis en type har arvinger er det logisk ækvivalent til dem alle, og kan derfor bruges til<br />

at samordne mange forskellige afledte objekter i en enkelt databehandling. Eller med andre ord:<br />

en baseklasse har type-ækvivalens til afledte klasser. Dette giver en utrolig frihed i<br />

programmeringen, fordi man ikke behøver kende den faktiske afledte klasse, men kan arbejde på<br />

baseklassen alene. For eksempel kan en hundekennel holde sine internerede hunde i en liste af<br />

hunde, som indeholder referencer til en baseklasse. Hver reference peger egentlig på en bestemt<br />

forekomst af en klasse, som er nedarvet fra hunde-klassen, for eksempel puddel, gravhund<br />

og collie. Sålænge en klasse nedarver fra hunde (eller fra en anden klasse, som nedarver<br />

fra hunde osv.) kan den bruges som "generel hund". Hvis en metode i klassen, som er fælles<br />

for alle hunde (for eksempel logre()), skal kaldes for alle hunde, behøver vi ikke kende den<br />

faktiske afledte klasse for at kalde funktionen. Det er nok at kende til baseklassen.<br />

Denne overlagte ligegyldighed gennemføres ved at forsinke bindingstidspunktet mellem kalder<br />

og modtager fra oversættertidspunktet til kørselstidspunktet, det, der kaldes sen binding eller<br />

dynamisk binding. Når en baseklasse har disse mange forskellige skjulte egenskaber, kaldes den<br />

en polymorf klasse. Polymorfe klasser og magien bag dynamisk binding behandles i afsnit 4.4.<br />

4.3.2 Lineær arv<br />

Lad os gå tilbage til problemstillingen i eksemplerne 4-1 og 4-2. Hvad er det mest rigtige design<br />

af geometri-klasserne? Eksempel 4-1 har en indkapslet funktionalitet, som skjuler<br />

implementationsdetaljerne fra klienten, men som kræver en hel del dublet-kode i alle klasserne,<br />

fordi de er meget ens. Eksempel 4-2 udnytter derimod den samme kode lidt bedre, men kræver, at<br />

klassen skrives om for hver ændring. Hvis vi er interesserede i at tegne en trekant, skal næsten<br />

alle metoder i Figur-klassen skrives om - man skal med andre ord ind og pille ved kode, som<br />

er meget sensitiv, og som man eventuelt ved virker. Derefter skal metoderne testes igen for alle<br />

varianter, selv dem, der allerede virkede og var kendte. Lineær arv udtrykker en<br />

typesammenhæng, som indgår i C++'s typesystem som en forskel mellem to typer. Løsningen er<br />

en mellemting mellem eksempel 4-1 og 4-2, idet der er brug for at dele visse funktioner klasserne<br />

imellem og separere andre funktioner fra hinanden. Vi erklærer derfor en generel Figur-klasse,<br />

som er meget lig samme klasse fra eksempel 4-1:<br />

class Figur {<br />

public:<br />

Farve f;<br />

Koordinat p;<br />

bool vises;<br />

Figur () : p () { f = hvid, vises = nej; }<br />

Figur (Koordinat& k) : p (k) { f = hvid, vises = nej; }<br />

};<br />

4.3.1 Type-ækvivalens 271


Figur er en generalisering af de tre geometriske klasser. Alle klasserne har en x/y-koordinat og<br />

et flag, der fortæller, om figuren vises eller ej. Nu ændrer vi erklæringen af de tre klasser fra<br />

eksempel 4-1:<br />

class Punkt : public Figur { // et punkt er en figur<br />

public:<br />

void Tegn (); // kun punkter ved, hvordan de<br />

void Slet (); // selv skal tegnes & slettes<br />

};<br />

class Linie : public Figur { // en linie er også en figur<br />

Koordinat endepunkt; // men udbygger med et punkt<br />

public:<br />

Linie (); // og har derfor en konstruktør<br />

void Tegn (); // kun linier ved, hvordan de<br />

void Slet (); // selv skal tegnes.<br />

};<br />

class Firkant : public Figur {<br />

Koordinat endepunkt; // firkanter er også figurer<br />

public:<br />

Firkant (); // og har derfor en konstruktør<br />

void Tegn (); // metoder til tegn og slet<br />

void Slet ();<br />

};<br />

I erklæringsdelen af klassen, efter class navn, skrives et kolon og et andet klassenavn.<br />

Betydningen af dette er, at klassen skal arve data og metoder fra klassen efter kolonet. Sætningen<br />

class A : public B { // ...<br />

kan læses som "klasse A er en slags B...". public betyder her, at A får privat adgang til B's<br />

offentlige data og metoder, og beskrives nærmere i afsnit 4.3.5. Vi har altså abstraheret de fælles<br />

egenskaber i de tre geometriske klasser ind i en ny, overordnet klasse, som de tre klasser nedarver<br />

fra. Punkt, Linie og Firkant er afledt fra Figur, de er subtyper af denne type. Ikke<br />

blot i vores begrebsverden, men rent semantisk i C++ er de tre klasser Figurer.<br />

Læg mærke til forskellene på de tre klasse-erklæringer. Punkt har ingen data, koordinatet p<br />

samt vises-flaget og farven er nok til at beskrive et punkt. Linie udbygger med et nyt<br />

koordinat, et endepunkt, som fortæller, hvor linien ender. Firkant udvider ligeledes med et<br />

andet Koordinat, som her beskriver det andet hjørne.<br />

272 Arv 4.3


Figur 4-4: Nedarvning fra Figur.<br />

Denne abstraktion beskriver forskellen mellem de tre klasser på en meget elegant måde. Idet både<br />

Punkt, Linie og Firkant er Figurer, kan de bruges som sådanne. Det er nu muligt for<br />

eksempel at skrive en hægte af Figurer, som kan indeholde en hvilken som helst klasse,<br />

sålænge den er nedarvet fra figur:<br />

class FigurHaegte {<br />

Figur& fig;<br />

FigurHaegte* naeste;<br />

public:<br />

FigurHaegte (Figur& f) : fig (f) { }<br />

void HaegtPaa (FigurHaegte& fh) { naeste = &fh; }<br />

};<br />

Konstruktøren i hægten initierer nye forekomster af FigurHaegte med en Figur eller en<br />

arving af Figur. Her ses, hvordan tre forskellige klasser kan lægges i samme liste:<br />

4.3.2 Lineær arv 273


void f () {<br />

Punkt a;<br />

Linie b;<br />

Cirkel c;<br />

FigurHaegte h1 (a), h2 (b), h3 (c);<br />

h1.HaegtPaa (h2);<br />

h2.HaegtPaa (h3);<br />

h3.HaegtPaa (0);<br />

}<br />

Type-ækvivalensen kan også forstås i form af konverteringer fra forekomster af afledte klasser til<br />

forekomster af baseklasser. Et objekt, der er en forekomst af en baseklasse kan tildeles et andet<br />

objekt, der er en forekomst af en afledt klasse af baseklassen, fordi det afledte objekt indeholder<br />

samtlige data og metoder, som findes i baseklassen. Det omvendte er ikke tilfældet, et afledt<br />

objekt kan ikke tildeles et baseobjekt. Følgende kode-fragment viser en sådan tildeling:<br />

class base_klasse {<br />

// ...<br />

};<br />

class afledt_klasse : base_klasse {<br />

// ...<br />

};<br />

void f () {<br />

base_klasse b;<br />

afledt_klasse a;<br />

b = a;<br />

}<br />

Nedarvningen foregår, som navnet siger, nedefter. Baseklassen har derfor ikke adgang til data og<br />

metoder i afledte klasser, fordi nedarvningen foregår rettet: baseklassen kan ikke "vide", hvem<br />

der nedarver. Det ville iøvrigt ødelægge ideen med generalitet.<br />

unions kan ikke bruges i nedarvning, hverken som baseklasser eller som afledte klasser,<br />

dels fordi de ikke kan indeholde tilgangs-specifikationer og dels, fordi der er tvetydigheder i<br />

unions. Derfor skal en union indeholdes i andre klasser som medlemmer.<br />

4.3.3 Tilgangs-specifikationer<br />

En afledt klasse har tilgang til data og metoder i baseklassen efter særlige regler. Overvej<br />

følgende arvefølge:<br />

274 Arv 4.3


class A {<br />

public:<br />

int i;<br />

void set_i (int j) { i = j; }<br />

};<br />

class B : A {<br />

public:<br />

int k;<br />

void set_k (int);<br />

};<br />

void B::set_k (int l) {<br />

k = l;<br />

};<br />

B nedarver datamedlemmet i og metoden set_i() fra A og udvider endvidere med<br />

variablen k og metoden set_k(). i og set_i() er derfor reelt også medlemmer af B, og<br />

kan derfor bruges af B. Et objekt af typen B har også data-medlemmet i og metoden<br />

set_i(). Derfor kan en metode i B "se" de to medlemmer i A og bruge dem som om, de var<br />

erklæret direkte i B:<br />

eller<br />

void B::set_k (int l) {<br />

k = l;<br />

i = l; // refererer A::i<br />

}<br />

void B::set_k (int l) {<br />

set_i (k = l); // kalder A::set_i ()<br />

}<br />

Det er ikke nødvendigt, medmindre der er tvetydigheder, at foretage en eksplicit reference til<br />

baseklassens data og metoder. Det underforståede objekt (3.3.2) udvider sit skop til baseklassen,<br />

og this bliver den underforståede medlemsreference for både den afledte klasse og for<br />

baseklassen.<br />

Tvetydigheder kan opstå, og kun opstå, hvis baseklassen og den afledte klasse indeholder data<br />

eller metoder af samme navn. I så tilfælde skal der gives en eksplicit reference med skoptilgangs-operatoren<br />

:: til den klasse, man ønsker at arbejde på:<br />

class A {<br />

4.3.3 Tilgangs-specifikationer 275


public:<br />

int i;<br />

void set_i (int);<br />

};<br />

void A::set_i (int j) {<br />

i = j; // A::i tildeles<br />

}<br />

class B : A {<br />

public:<br />

int i;<br />

void set_i (int);<br />

};<br />

void B::set_i (int j) {<br />

i = j; // B::i tildeles<br />

}<br />

Når metoden set_i() kaldes for objekter af klassen B, er det B::set_i(), der er aktuel,<br />

og når set_i() kaldes for objekter af A, er det A::set_i(). Reglerne følger helt de<br />

eksisterende for medlemsfunktioner, der refererer data eller metoder, som er tvetydige:<br />

eller<br />

int i;<br />

void B::set_i (int j) {<br />

i = j; // B::i tildeles<br />

A::i = j; // A::i tildeles<br />

::i = j; // global i tildeles<br />

}<br />

void set_i (int); // global funktion<br />

void B::set_i (int j) {<br />

A::set_i (i = j); // kalder baseklassens set_i()<br />

::set_i (j); // kalder global set_i()<br />

}<br />

Dette ligger meget tæt på den syntaks, der bruges til kald af metoder i objekter, som er<br />

medlemmer af klassen i stedet for at være baseklasser. Her bruges blot medlemsreferenceoperatoren<br />

i stedet, da objektet netop er medlem af klassen:<br />

276 Arv 4.3


class X {<br />

public:<br />

void f ();<br />

// ...<br />

};<br />

class Y {<br />

X x;<br />

void f () { x.f (); }<br />

// ...<br />

};<br />

Den afledte klasse arver altså både data og metoder fra baseklassen, og må eksplicit tilkendegive<br />

baseklassens navn, hvis data eller metoder erstattes med andre af samme navn. En baseklasse kan<br />

for eksempel indeholde en Udskriv()-funktion, som udskriver detaljer om denne. Den afledte<br />

klasse kan også indeholde en Udskriv(), som først udskriver indholdet af sig selv, og dernæst<br />

kalder baseklassens Udskriv() for at få dette med. Dermed kan alle baseklassens metoder<br />

relateres til data i denne, og behøver ikke skrives om (endsige genoversættes) for eventuelle<br />

afledte klasser. Hvis den afledte klasse ikke redefinerer data eller metoder, vil en reference til<br />

disse (hvis de er public), resultere i nærmeste baseklasse med definitionerne.<br />

4.3.4 Tilgang fra klient-kode<br />

En klasse kan have private medlemmer (3.3.8), for at skjule disse fra klienten af klassen. Når<br />

en klasse B nedarver fra en klasse A, vil B ikke have adgang til de private dele i A:<br />

class A {<br />

int i;<br />

public:<br />

void set_i (int l) { i = l; }<br />

};<br />

class B : A {<br />

int j;<br />

public:<br />

void set_j (int);<br />

};<br />

void B::set_j (int k) {<br />

j = k; // ok, B har tilgang til egen j<br />

i = k; // fejl, B har ingen tilgang til A's i<br />

set_i (k); // ok, B har tilgang til A's set_i()<br />

};<br />

4.3.3 Tilgangs-specifikationer 277


Man kan spørge, hvad ideen er i dette. Hvorfor kan en klasse ikke nedarve en eksakt kopi af<br />

tilgangs-mønstret i baseklassen? Svaret ligger i, at de private dele af en klasse søges skjult fra en<br />

klient på to grundlag: dels, at klienten skal kunne abstrahere fra disse data og dels, at den kode,<br />

som arbejder på de private data, skal kunne isoleres for at få hold på konsistensen. Hvis metoder i<br />

en afledt klasse har tilgang til private date i en baseklasse, er det blot et spørgsmål om at nedarve<br />

fra en given klasse for at få adgang til de private dele. Dermed er grundlaget for indkapslingen<br />

fjernet, da alle dele af programmet ved en mindre manøvre vil kunne få tilgang, og kilden til en<br />

fejl vil være svær at finde.<br />

Arv kan derfor foregå på to måder: private og public, hvor private er underforstået.<br />

I de ovenstående eksempler nedarves data og metoder derfor private. Det betyder, at klienter<br />

udefra ikke har tilgang til baseklassens data eller metoder i den afledte klasse. Det kan være en<br />

smule kompliceret at se, hvem der har adgang til hvad i disse tilfælde, så vi tager et eksempel til,<br />

hvor vi nedarver to gange fra A:<br />

class A {<br />

int i; // i er private datamedlem i A<br />

public:<br />

void set_i (int); // set_i() er public metode i A<br />

};<br />

class B : private A { // B nedarver private fra A<br />

int j; // j er private datamedlem i B<br />

public:<br />

void set_j (int); // set_j() er public metode i B<br />

};<br />

class C : public A { // C nedarver public fra A<br />

public:<br />

int k; // k er public datamedlem i C<br />

void set_k (int); // set_k() er public metode i C<br />

};<br />

void A::set_i (int l) {<br />

i = l; // ok, A har tilgang til A::i<br />

}<br />

void B::set_j (int l) {<br />

j = l; // ok, B har tilgang til B::j<br />

i = l; // fejl, B har ej tilgang til A::i<br />

set_i (l); // ok, B har tilgang til A::set_i()<br />

}<br />

void C::set_k (int l) {<br />

278 Arv 4.3


k = l; // ok, C har tilgang til C::k<br />

i = l; // fejl, C har ej tilgang til A::i<br />

set_i (l); // ok, C har tilgang til A::set_i()<br />

}<br />

Det ses, at der ikke er forskel i tilgangs-rettighederne for klasserne B og C i A's data og<br />

metoder. Tilgangs-specifikationen i erklæringen af klasserne har kun betydning for klienter, som<br />

bruger B og C:<br />

void f () {<br />

A a;<br />

a.i = 0; // fejl, A::i er private<br />

a.set_i (0); // ok, A::i er public<br />

B b;<br />

b.i = 0; // fejl, A::i er private<br />

b.j = 0; // fejl, B::j er private<br />

b.set_i (0); // fejl, B::set_i() er private<br />

b.set_j (0); // ok, B::set_j() er public<br />

C c;<br />

c.i = 0; // fejl, C::i er private<br />

c.k = 0; // ok, C::k er public<br />

c.set_i (0); // ok, C::set_i() er public<br />

c.set_k (0); // ok, C::set_k() er public<br />

}<br />

Klienter, som bruger objekter af B har ingen tilgang til hverken private eller public<br />

medlemmer af A som nedarves af B, fordi B nedarver med private tilgangs-specifikation<br />

fra A. For C, som nedarver med public tilgangs-specifikation fra A, har klienten tilgang til<br />

public medlemmer af A, som nedarves i C, og spejler altså A direkte. Nedarves public er<br />

tilgangen til medlemmer i baseklassen for en klient de samme som tilgangen for et objekt af<br />

baseklassen.<br />

Et ord om nedarvning af klasser i en struct: Når en struct nedarver fra en anden<br />

struct eller class, foregår nedarvningen underforstået public, og ikke private. Dette<br />

er en af de specielle forskelle mellem struct og class:<br />

struct B : A { // ...<br />

er identisk med<br />

class B : public A { public: // ...<br />

Der er visse muligheder for at ændre på disse regler om tilgang. Hvis en klasse B nedarver<br />

4.3.4 Tilgang fra klient-kode 279


private fra en klasse A, vil alle medlemmer (data og metoder) i A være skjult fra klienten.<br />

Det er dog muligt for B udtrykkeligt at beskrive, at public medlemmer i A også skal være<br />

public i B:<br />

class A {<br />

// ...<br />

public:<br />

int i; // public medlem i A<br />

};<br />

class B : A { // private arv, i bliver private for<br />

// ... // klienter af B, men tilgængelig i B<br />

public:<br />

A::i; // men her ændres det eksplicit, så i<br />

}; // bliver public fra B's synsfelt<br />

Dette gør A::i til et public datamedlem i B, og klienter af B har tilgang til denne variabel.<br />

A::i i B er ikke en erklæring i normal forstand, og afsætter ikke plads til en ny variabel. Det er<br />

blot en tilkendegivelse af, at adgangsbetingelserne for denne variabel skal ændres. Det samme<br />

gælder den anden vej:<br />

class B : public A {<br />

A::i; // i bliver private fra B's synsfelt<br />

// ...<br />

};<br />

Dette vil ændre A::i's tilgangs fra public til private for klienter af B. Det skal dog<br />

nævnes, at denne eksplicitte ændring af tilgangsrettigheder på en medlem-for-medlemsbasis kun<br />

kan bruges til at genskabe den rettighed, som medlemmet har i baseklassen. Et public medlem<br />

i baseklassen kan ikke gøres private i den afledte klasse, ejheller kan et private<br />

baseklassemedlem ændres til et public medlem i afledningen. Derfor opstår fejl i følgende<br />

kodefragment:<br />

class A {<br />

int i;<br />

public:<br />

int j;<br />

};<br />

class B : public A {<br />

A::j; // fejl: kan ikke ændre tilgang til<br />

private<br />

public:<br />

A::i; // fejl: kan ikke ændre tilgang til public<br />

280 Arv 4.3


};<br />

Det er også værd at nævne, at metoder opfører sig som data fra klientens side hvad angår<br />

tilgang. Derfor vil et kald til en metode i et afledt objekt, hvor metoden faktisk er defineret i<br />

baseobjektet rent faktisk kalde denne funktion i baseobjektet:<br />

class A {<br />

int a;<br />

public:<br />

void print ();<br />

};<br />

class B : public A {<br />

int b;<br />

public:<br />

};<br />

void f () {<br />

B b;<br />

b.print (); // kalder A::print ()<br />

}<br />

Dette følger ideen om, at metoder, som arbejder på baseklassens data, ikke skal skrives om i den<br />

afledte klasse. Fra klientens side eksisterer metoden som om den var skrevet i den afledte klasse.<br />

Igen er den afledte klasse blot en overbygning på baseklassen, så når en nedarvning specificeres<br />

med : overtages altså alle data og metoder, dog kan ikke alle ses fra klientens kode.<br />

Det er muligt eksplicit at referere baseklassen i en reference til public nedarvede data eller<br />

metoder i den afledte klasse. Dette gøres med en specifikation af baseklassens navn umiddelbart<br />

før medlemmets navn:<br />

struct A { int i; void f (); };<br />

struct B : A { };<br />

void g () {<br />

B b;<br />

b.i = 0; // implicit reference til baseklassen<br />

b.A::i = 0; // eksplicit reference til baseklassen<br />

b.f (); // implicit reference til baseklassen<br />

b.A::f (); // eksplicit reference til baseklassen<br />

}<br />

Under lineær arv er eksplicitte referencer til baseklassen unødvendige, fordi oversætteren selv<br />

4.3.4 Tilgang fra klient-kode 281


kan finde det rigtige navn. I afsnit 4.5 skal vi imidlertid se eksempler på, at referencer til<br />

baseklassen kan være nødvendig i forbindelse med tvetydigheder i særlige situationer.<br />

4.3.5 protected tilgang<br />

Klasser har to grænseflader: en mod klienten og en mod andre klasser, som nedarver<br />

egenskaberne. Den ene side er bruger-orienteret og kræver en god definition af klassens<br />

virkemåde og koncept. Den anden har med udvidelser at gøre, og kræver, at klassen er så generel<br />

som mulig.<br />

Der er imidlertid et problem. Hvis en klasse A har et private medlem, som ønskes skjult<br />

for klienten, kan dette ikke ses af den afledte klasse, som måske kan have fordel af at kunne se<br />

dette medlem:<br />

class A {<br />

int i; // private medlem i A<br />

public:<br />

// ...<br />

};<br />

class B : public A {<br />

void f () {<br />

i = 0; // fejl, i er ikke tilgængelig<br />

}<br />

// ...<br />

};<br />

void f () {<br />

A a;<br />

a.i = 0; // fejl, A::i er ikke public<br />

B b;<br />

b.i = 0; // fejl, B::i er ikke public<br />

}<br />

Der er to umiddelbare løsninger på dette. Enten kan i placeres i public-delen af A, hvorved<br />

B nedarver den som private medlem i B, og dermed har tilgang. Eller også kan der skrives<br />

en speciel tilgangs-funktion i A, som læser og skriver i. Den første metode er uinteressant, fordi<br />

i, udover at være tilgængelig fra B, også vil kunne manipuleres af en klient af A. Den anden<br />

metode ligger nærmere OOP-filosofien, idet data bør læses og skrives af kode i klassen, så<br />

klienten gør sig implementations-uafhængig. Men B er strengt taget ikke en klient. Metoder i<br />

klasser bør være så effektive som muligt, fordi de vil blive kaldt ofte. En afledt klasse bør derfor<br />

282 Arv 4.3


ikke kalde metoder i baseklassen til manipulation af data, som den afledte klasse selv kunne<br />

udføre, medmindre baseklassens metode er god nok.<br />

Af denne grund findes en tredje tilgangs-metode i klasser udover public og private,<br />

nemlig protected (beskyttet). protected tilgang betyder, at klienter ser medlemmerne<br />

som private og dermed ikke har tilgang samt at afledte klasser ser dem som public og<br />

dermed har tilgang.<br />

Arvemetode i<br />

den afledte<br />

klasse<br />

private arv<br />

public arv<br />

Original tilgangsspecifikation i baseklassen<br />

public protected private<br />

private<br />

public<br />

ingen<br />

private<br />

Tabel 4-1: Den afledte klasses tilgang ved arv med forskellige tilgange.<br />

class A {<br />

// ... // private medlemmer<br />

protected: // protected medlemmer<br />

int i; // i er private for klienter<br />

public: // public medlemmer<br />

// ...<br />

};<br />

ingen<br />

ingen<br />

Med andre ord: medlemmer af en klasses private del kan kun og altid kun læses og skrives<br />

fra metoder i klassen. Hverken klienter eller afledte klasser har mulighed for at manipulere disse<br />

data direkte. protected medlemmer af klassen kan ikke manipuleres fra klienter, men kan ses<br />

fra afledte klasser, som nedarver offentligt (class B : public A { ...). public<br />

medlemmer i klassen kan ses af både afledte klasser, men kun af klienter, hvis der arves<br />

private. Klienten har med andre ord kun adgang til baseklassens medlemmer, hvis de er<br />

erklæret public i baseklassen og hvis den afledte klasse arver public.<br />

Kun ét spørgsmål mangler besvarelse, nemlig klientens tilgang til data i en afledt klasse, hvor<br />

data oprindelig er defineret i baseklassen. Hvis data er erklæret public i baseklassen, og hvis<br />

de nedarves public i den afledte klasse (class B : public A { ...), kan de ses af<br />

klienten. I alle andre tilfælde er de skjult, dvs. hvis data er enten protected eller private i<br />

baseklassen, eller hvis den afledte klasse nedarver private (class B : private A {<br />

...).<br />

Her følger eksemplet overfor igen med protected tilgang for i:<br />

class B : public A {<br />

void f () {<br />

i = 0; // ok, B har tilgang til i<br />

4.3.5 protected tilgang 283


};<br />

// ...<br />

};<br />

void f () {<br />

A a;<br />

a.i = 0; // fejl, i er ikke public<br />

B b;<br />

b.i = 0; // fejl, i er ikke public<br />

}<br />

Nu har B::f() retteligt adgang til A::i, hvorimod f() ikke kan oversættes. Læg mærke til,<br />

at tilgangen til protected medlemmer følger samme regler, når der er tale om friendklasser.<br />

protected tilgang har også betydning for abstrakte klasser, idet en protected<br />

konstruktør umuliggør instantiering af klassen, fordi den ikke befinder sig i klientens skop.<br />

4.3.6 Tvetydigheder ved lineær arv<br />

Der kan opstå navnekonflikter i forbindelse med arv, hvis den afledte klasse definerer et navn,<br />

som baseklassen allerede indeholder. For både data og funktioner gælder, at objekterne i den<br />

afledte klasse har prioritet i forhold til baseklassens. Overvej:<br />

struct A {<br />

int i;<br />

};<br />

struct B : A {<br />

int i;<br />

void f () {<br />

i = 0; // B::i<br />

}<br />

};<br />

void f (B b) {<br />

b.i = 0; // B::i<br />

}<br />

Hvis der er brug for manipulation af eller tilgang til baseklassens data eller funktioner af samme<br />

navn som den afledtes, må der gives eksplicit reference til baseklassens typenavn. Det tilrådes<br />

ikke at underbygge et design på dette, fordi klienten helst skulle være ligeglad med eventuelle<br />

baseklasser, når der arbejdes på afledte klasser alene. Syntaksen er:<br />

284 Arv 4.3


struct B : A {<br />

int i;<br />

void f () {<br />

i = 0; // denne klasses i-medlem<br />

A::i = 0; // baseklassens i-medlem<br />

}<br />

};<br />

void f (B b) {<br />

b.i = 0; // B-klassens i-medlem<br />

b.A::i = 0; // A-klassens i-medlem<br />

}<br />

De sædvanlige regler for tilgang gælder naturligvis også her, så private og protected<br />

medlemmer i baseklassen kan ikke ses fra klienten, mens private medlemmer i baseklassen<br />

ikke kan ses af den afledte klasse.<br />

4.3.7 Konstruktion af afledte objekter<br />

Når et objekt af en afledt klasse oprettes, vil eventuelt definerede konstruktører kaldes opefter i<br />

hierarkiet. Hvis A er baseklasse til B, og hvis begge har konstruktører, vil en instantiering af B<br />

resultere i et kald til A::A() og dernæst til B::B(). Heraf følger, at konstruktører ikke<br />

nedarves, men kaldes automatisk for alle klasser i arvefølgen. Normale metodekald resulterer i et<br />

og kun et kald, mens konstruktørkald til afledte klasser vil kalde alle konstruktører<br />

(underforståede eller eksplicitte) opefter.<br />

Hvis baseklassen har en konstruktør, som kræver parametre (dvs. hvis der ikke findes en<br />

underforstået konstruktør), skal der defineres en konstruktør for den afledte klasse, som initierer<br />

baseklassen. Eller mere specifikt: Hvis en baseklasse har en konstruktør, skal den afledte klasse<br />

også have en. For klasser med underforståede konstruktører kaldes først baseklassens<br />

konstruktør, dernæst den afledte klasses konstruktør. For eksempel,<br />

class A {<br />

public:<br />

A () { cout


B () { cout


Figur (const Koordinat& k) : p (k) {<br />

vises = 0;<br />

}<br />

// ...<br />

};<br />

Konstruktøren Figur::Figur() initierer startpunktets Koordinater. Når nu vi nedarver<br />

fra Figur, må vi eksplicit fortælle, hvordan information skal flyde fra konstruktør til<br />

konstruktør opefter i klasse-hierarkiet:<br />

class Linie : public Figur {<br />

Koordinat endepunkt;<br />

public:<br />

Linie (Koordinat& st, Koordinat& en) :<br />

Figur (st), endepunkt (endepunkt) { }<br />

// ...<br />

};<br />

Klassen Linie udbygger Figur med et endepunkt. Linies konstruktør indeholder derfor et<br />

ekstra Koordinat, som initierer dette private datamedlem. Men Linie skal også videregive<br />

informationen om startpunktet til baseklassen Figur. Dette gøres i initieringslisten med samme<br />

syntaks som for medlemsdata, blot med navnet på baseklassens typenavn, hvilket er det samme<br />

som at kalde baseklassens konstruktør eksplicit. Dette skyldes, at Linie ikke har en konkret<br />

Figur at arbejde med, idet den kun nedarver fra klassen.<br />

Her kommer initieringslisterne igen til berettigelse. Hvis den afledte klasses konstruktør<br />

eksplicit skulle kalde baseklassens konstruktør, måtte det afledte objekt nødvendigvis oprettes før<br />

baseobjektet. Men det nytter ikke noget, fordi baseklassen skal oprettes før den afledte klasse for<br />

at opsætte hierarkiet ordentligt. Initieringslisten kommer før konstruktørens krop, og udføres<br />

dermed før denne. Det er også et spørgsmål om effektivitet. Når oversætteren ser et kald til en<br />

baseklasses konstruktør i en initieringsliste i en afledt klasse, vil dette helt forbigå den afledte<br />

klasses konstruktør i målkoden. Kaldet til baseklassen "bobler op" til dennes konstruktør, og<br />

bliver faktisk til et direkte kald fra instantieringen i klientkoden til baseklassen. Den afledte<br />

klasse indeholder ikke reel kode til behandling af disse informations-overførsler. Hvis der var tale<br />

om et eksplicit funktionskald, ville der blive genereret triviel kode til konstruktion af<br />

baseobjekter, og det ville være op til oversætteren at optimere sig ud af dette bagefter, en svær og<br />

langsommelig opgave.<br />

Nu er det klart, at hvis en baseklasse A har en underforstået konstruktør, så vil en vilkårlig<br />

konstruktør i en afledt klasse B, som skrives<br />

B::B (/* ... */) { // ...<br />

være identisk med<br />

B::B (/* ... */) : A () { // ...<br />

4.3.7 Konstruktion af afledte objekter 287


fordi der ikke skal overføres information. Men konstruktøren kaldes under alle omstændigheder,<br />

fordi den kan udføre opgaver, som er uafhængige af udefrakommende data.<br />

Det er vigtigt at være opmærksom på den rækkefølge, C++ intitierer baseklasser på. For en<br />

given klasse som instantieres, foregår initieringen efter følgende regler:<br />

1. Baseobjektet initieres, hvis det findes.<br />

2. Eventuelle datamedlemmer i klassen initieres i den rækkefølge, de er erklæret. Hvis en<br />

klasse indeholder brugerdefinerede typer, bliver deres konstruktører (og eventuelle<br />

baseklassers konstruktører) kaldt.<br />

3. Det aktuelle objekt initieres ved eksekvering af konstruktørens krop.<br />

C++ anvender disse regler rekursivt, hvilket vil sige, at hvis en klasse har en baseklasse, som selv<br />

er afledt, vil dennes baseklasse blive initieret først eller hvis der forekommer baseklasser eller<br />

brugerdefinerede typer i de datamedlemmer, som klassen måtte indeholde. Overvej følgende<br />

eksempel:<br />

class A { // A er en isoleret klasse<br />

public:<br />

A () { cout


Ovenstående program vil udskrive<br />

A::A()<br />

B::B()<br />

A::A()<br />

C::C()<br />

A::A()<br />

B::B()<br />

D::D()<br />

Kig eksemplet igennem og sammenlign med reglerne for initiering på forrige side.<br />

4.3.8 Destruktion af afledte objekter<br />

Destruktører, der som bekendt ikke har parameterlister, og derfor ikke behøver information fra<br />

klienter, skal ikke behandles specielt fra afledte klasser. Destruktører nedarves ikke, men kaldes<br />

automatisk i baseklassen. De kaldes i omvendt rækkefølge end konstruktørerne, så afledte<br />

objekter først destrueres, dernæst baseobjekter:<br />

Output:<br />

class A {<br />

public:<br />

A() { cout


A::A()<br />

A::A()<br />

B::B()<br />

A::A()<br />

B::B()<br />

C::C()<br />

C::~C()<br />

B::~B()<br />

A::~A()<br />

B::~B()<br />

A::~A()<br />

A::~A()<br />

Igen giver det mening, at den afledte klasses destruktør kaldes først, idet denne typisk vil rydde<br />

op i data, som relateres til baseobjektet. Derfor er det ufikst, hvis baseobjektet ikke eksisterer.<br />

Destruktører nedarves ikke. Der genereres imidlertid en underforstået destruktør, hvis ingen<br />

erklæres, og den kalder baseobjektets (objekternes) destruktører automatisk efter det aktuelle<br />

objekt er blevet destrueret. Automatisk genererede destruktører er altid public.<br />

4.3.9 Typekonvertering under arv<br />

Visse regler gælder for konvertering mellem objekter af klasser, som har et base/afledt-forhold:<br />

• Det er kun muligt at konvertere til og fra objekter, som er nedarvet public fra<br />

baseklassen.<br />

• Et forekomst af en afledt klasse kan konverteres til en forekomst af en public<br />

baseklasse, som i<br />

class base { // ...<br />

class afledt : public base { // ...<br />

afledt a;<br />

base b = a; // a konverteres til base<br />

• En pointer eller reference til en forekomst af en afledt klasse kan konverteres til en<br />

pointer eller reference til en forekomst af en baseklasse, som i<br />

afledt* a;<br />

base* b = a; // a konverteres til base*<br />

afledt& c = *a; // ingen konvertering<br />

base& d = c; // c konverteres til base&<br />

• En pointer eller reference til et medlem af en baseklasse kan konverteres til en pointer<br />

290 Arv 4.3


eller reference til et medlem af en afledt klasse (behandles i detaljer i afsnit 3.7), som i<br />

class base { public: int i; // ...<br />

class afledt : public base { public: // ...<br />

int base::*bp = &base::i; // bp peger på base::i<br />

int afledt::*ap = bp; // ap peger på afledt::i<br />

Disse konverteringer skal ses i sammenhæng med, hvordan strukturen af afledte objekter ser ud.<br />

Nedarvningen overtager jvf. afsnit 4.3.2 data og metoder i baseklassen (alt overtages, blot kan<br />

dele af baseklassen eventuelt ikke ses fra den afledte klasse), så den afledte klasse repræsenterer<br />

summen af baseklassen og sig selv. Diagrammet i figur 4-4 kan derfor opdeles på en mere reel<br />

måde med henblik på indholdet af de afledte klasser.<br />

Figur 4-5: Det afledte objekts reelle indhold.<br />

I figur 4-5 fremstår det klart, hvordan en forekomst af Linie kan konverteres til en forekomst<br />

af Figur, og derefter behandles som en sådan, fordi alle medlemmer i baseklassen også tilhører<br />

den afledte klasse. Konvertering af forekomster vil derfor tabe den information, som findes i den<br />

afledte klasse udover det nedarvede materiale. Men baseklassens information bibeholdes, hvilket<br />

gør, at en pointer til en baseklasse, som tildeles adressen på en afledt klasse kan arbejde på, og<br />

kun på, de nedarvede elementer. Derfor kan en pointer til et medlem i en baseklasse konverteres<br />

til en pointer til et medlem i en afledt klasse, hvilket muliggør funktioner, som arbejder på de<br />

generelle dele af de afledte klasser uden at have viden om de medlemmer, som de afledte klasser<br />

bygger til.<br />

4.3.10 En løsning med nedarvning<br />

Nu omskriver vi eksempel 4-1 med klasserne Punkt, Linie og Firkant som nedarvede<br />

klasser.<br />

Først erklærer vi baseklassen Figur, som indeholder et Koordinat og et flag, vises,<br />

som fortæller om figuren er synlig eller ej. Disse data er fælles for alle Figurer: et startpunkt<br />

og et synligt-eller-ej flag. Vi giver også figuren et navn, så vi kan se, hvad den forestiller.<br />

Baseklassen har fire medlemsfunktioner, som også er fælles for alle Figurer. En konstruktør<br />

opretter Figurer med et startpunkt og et navn mens en destruktør fjerner Figurer. De<br />

normale metoder giver navnet på figuren og flytter den til et nyt punkt. Vi opdeler nu klassen i en<br />

4.3.9 Typekonvertering under arv 291


privat del, en offentlig del og en beskyttet del og placerer datamedlemmer og medlemsfunktioner<br />

i de tre afdelinger afhængig af klient- og arvingbehov.<br />

// eksempel 4-3: en generel figur<br />

class Figur {<br />

char* navn;<br />

protected:<br />

Farve f;<br />

bool vises;<br />

Koordinat p;<br />

public:<br />

Figur (Koordinat&, char*); // konstruktør<br />

Figur (Figur&); // kopi-konstruktør<br />

~Figur (); // destruktør<br />

void operator= (Figur&); // tildelings-operator<br />

const char* figurNavn ();<br />

void Flyt (Koordinat&);<br />

};<br />

Konstruktøren Figur::Figur() initierer Koordinatet p med startpunktet i<br />

initieringslisten og allokerer plads til navnet på det frie lager. Standard-funktionen strlen<br />

returnerer længden af strengen (antallet af tegn op til det afsluttende '\0', men uden dette) og<br />

lader navn pege på dette lager. Derefter kopieres navnet i s til det nyallokerede lager. Vi kan<br />

ikke blot tildele navn til s, da s kan være en midlertidig variabel.<br />

Figur::Figur (Koordinat& k, char* s)<br />

: p (x, y), f (hvid), vises (ja) {<br />

navn = new char [strlen (s) + 1];<br />

strcpy (navn, s);<br />

cout


Figur::~Figur () {<br />

delete navn;<br />

}<br />

Af samme grund som kopi-konstruktørens tilstedeværelse må vi også have en tildelings-funktion,<br />

så forekomster af Figurer kan tildeles hinanden:<br />

void Figur::operator= (Figur& f) {<br />

p = f.p, f = f.f, vises = f.vises;<br />

if (navn != NULL && strlen (navn) != strlen (f.navn)) {<br />

delete navn;<br />

navn = new char [strlen (f.navn) + 1];<br />

}<br />

strcpy (navn, f.navn);<br />

}<br />

figurNavn() returnerer blot navnet som en char-pointer, så klienten kan bruge denne til<br />

udskrivning eller andet. Det er en const char*, som returneres, for klienten skal ikke kunne<br />

ændre i navnet uden tilladelse - hvis han gjorde noget forkert, ville den indre orden i Figur<br />

være ødelagt af kode udenfor Figur, et af de nøglepunkter, dataabstraktion hjælper med at<br />

undgå. En tvungen konvertering (type-cast) er nødvendig hvis oversætteren ikke skal advare om<br />

en ikke-const-til-const underforstået konvertering.<br />

const char* Figur::figurNavn () {<br />

return navn;<br />

}<br />

Medlemmet Flyt() adderer blot et nyt koordinatsæt til startpunktet p. Flyt() arbejder<br />

altså relativt fra det aktuelle startpunkt. En midlertidig forekomst af et Koordinat oprettes,<br />

fordi grænsefladen til Koordinat-klassen kun tillader operator-kald med andre<br />

Koordinater.<br />

void Figur::Flyt (Koordinat& k) {<br />

p += k;<br />

cout


public:<br />

Punkt (const Koordinat& k, char* n) : Figur (k, n) { }<br />

void Tegn ();<br />

void Slet ();<br />

};<br />

Punkt udbygger ikke med datamedlemmer, men indeholder derimod to nye metoder til at tegne<br />

Punkter og slette punkter (Tegn() og Slet()). Strengt taget er det ikke nødvendigt med<br />

en konstruktør, når der ikke er data, der skal initieres, men Figur har ingen underforstået<br />

konstruktør og bliver derfor eksplicit kaldt fra Punkt::Punkt(). Der er ingen destruktør;<br />

Figur::~Figur() kaldes således blot fra den underforståede destruktør.<br />

Når et Punkt skal flyttes, er det blot et spørgsmål om at ændre det Koordinat, som<br />

beskriver punktet. Da Punkter ikke udbygger Figurer med data, som har betydning for deres<br />

placeringer i koordinatsystemet, kan Flyt()-metoden i Figur-klassen nedarves direkte, og<br />

dermed genbruges helt. Metoderne til at tegne og slette Punkter kan ikke nedarves fra Figur,<br />

da Figurer naturligvis ikke kan vide noget om Punkter:<br />

void Punkt::Tegn () {<br />

cout


Linie::Linie (const Koordinat& s, const Koordinat& e,<br />

char* n) : Figur (s, n), endePunkt (e) { }<br />

Parameteren s er startpunktet mens e er slutpunktet for linien. Igen har klassen ingen indirekte<br />

data, og behøver ingen destruktør. At flytte en Linie indebærer en modifikation til både<br />

baseklassen Figurs Koordinat og til Linies Koordinat. Da alle figurer flyttes<br />

relativt, opfattes parametrene til Flyt() som en delta-værdi, positiv eller negativ, som adderes<br />

til de to Koordinater:<br />

void Linie::Flyt (Koordinat& k) {<br />

Figur::Flyt (k); // flyt startpunkt<br />

endePunkt += k; // flyt slutpunkt<br />

}<br />

Læg mærke til, hvordan baseklassens Flyt()-metode genbruges i den afledte klasses<br />

Flyt(). Her er der tale om, at Linier og Figurer ikke flyttes på samme måde, men at der<br />

dog er et forhold, som kan beskrives med en smule ekstra kode, som - og det er det vigtigste -<br />

ikke kræver, at den originale Flyt()-metode i baseklassen skal ændres.<br />

At tegne og slette Linier er trivielt, og er derfor blot skitseret:<br />

void Linie::Tegn () {<br />

cout


void Slet ();<br />

};<br />

Firkant::Firkant (const Koordinat& s, const Koordinat& e,<br />

char* n) : Figur (s, n), endePunkt (e) {<br />

}<br />

void Firkant::Tegn () {<br />

cout


void Firkant::Slet () {<br />

cout


Der er altså fra klientens side i princippet ingen forskel på brugen af klasser, som er helt<br />

isolerede og klasser, som er nedarvet fra den samme baseklasse. Forskellen ligger kun i designet,<br />

og kommer bedst til udtryk, når klasser skal modificeres eller nye klasser skal indlægges. Klassen<br />

Firkant er et bevis på, at generaliserings-princippet holder. Jo højere vi kommer op i<br />

hierarkiet, jo mere generelle er vores klasser, og jo længere ned, vi kommer, jo mere specifikke er<br />

de. Deraf følger, at hvis to klasser har mere til fælles, end de sammenlagt har til fælles med<br />

baseklassen, er det mere naturligt at lade dem nedarve fra hinanden eller fra en ny klasse, som<br />

skubbes ind i midten af baseklassen og de to relaterede klasser.<br />

Figur 4-6: Ny acyklisk orienteret<br />

graf (dag), hvor Firkant nedarver<br />

fra Linie.<br />

4.3.11 Problemer med lineær arv<br />

Princippet i nedarvning mellem klasser er subtilt. Når vi indfører begrebet klasser og relationerne<br />

mellem dem udtrykt ved arv, kan vi ikke undgå at skabe nye problemer. Det værste ved disse nye<br />

problemer er, at de er udtryk for det objekt-orienterede paradigmes begrænsninger, samtidig med,<br />

at de er af en anden karakter, end vi normalt har med at gøre. figur-hierarkiet i forrige afsnit<br />

løser en hel del problemer i form af implementeringsmæssige regler for generisk kode. Men hvis<br />

vi går i detaljer med klasserne ser vi, at der er blevet skåret hjørner af for at præsentere klasserne<br />

så pæne som muligt.<br />

Det viser sig, at baseklassens statiske egenskaber, som kommer til udtryk ved, at de ikke kan<br />

kalde metoder i de afledte klasser er et væsentligt problem. Overvej for eksempel de forskellige<br />

implementationer af Flyt() for klasser, som udbygger med egne medlemsvariable. Et fælles<br />

træk for alle metoder, som flytter geometriske figurer er, at der skal ske tre ting i rækkefølge:<br />

1. Først skal figuren slettes, hvis den er synlig, dvs. der skal foretages et kald til den<br />

specifikke klasses Slet()-funktion hvis vises == ja.<br />

2. Dernæst skal figuren flyttes, dvs. der skal foretages to ting, et kald til baseklassens<br />

298 Arv 4.3


Flyt()-funktion og en eventuel ændring i den afledte klasses medlemsdata.<br />

3. Sidst skal figuren tegnes igen på de nye koordinater, altså et kald til den specifikke<br />

klasses Tegn()-funktion, hvis det originale vises-flag var ja.<br />

De fælles træk for alle Flyt()-metoder er altså et kald til Slet(), en opdatering af data, og<br />

et kald til Tegn(). Siden kaldene til Slet() og Tegn() skal foretages i et bestemt objekt,<br />

kan Flyt() ikke nedarves af klasserne, men skal skrives for hver enkelt klasse. Her er for<br />

eksempel implementationen af to Flyt()-metoder for to vilkårlige klasser, Cirkelbue og<br />

Trekant:<br />

void Cirkelbue::Flyt (int x, int y) {<br />

bool husk = vises; // husk om figuren vises<br />

if (vises) slet (); // slet hvis vist<br />

figur::Flyt (x, y); // flyt startpunkt<br />

e += koordinat (x, y); // flyt endepunkt (e)<br />

if (husk) Tegn (); // hvis vist, tegn igen<br />

}<br />

void Trekant::Flyt (int x, int y) {<br />

bool husk = vises; // husk om figuren vises<br />

if (vises) slet (); // slet hvis vist<br />

figur::Flyt (x, y); // flyt startpunkt<br />

e1 += koordinat (x, y); // flyt endepunkt 1 (e1)<br />

e2 += koordinat (x, y); // flyt endepunkt 2 (e2)<br />

if (husk) Tegn (); // hvis vist, tegn igen<br />

}<br />

Selvom det er et meget banalt eksempel, viser det klart, at visse metoder i et klasse-hierarki<br />

nødvendigvis må indeholde mange linier (eller tit skærmfulde) af identisk kode, fordi en enkelt<br />

linie i midten af metoden afviger.<br />

Et andet problem er, at selvom de afledte klasser er logisk ækvivalente til baseklasserne, dvs. at<br />

baseklasserne kan tildeles de afledte klasser som i nedenstående kode-fragment, så kan de ikke<br />

umidelbart erstatte hinanden i programmet. Idet afledte klasser overtager alle data og alle<br />

public metoder fra baseklassen til fri afbenyttelse i klientens kode, kan vi skrive kode som:<br />

class base { public: funktion (); // ...<br />

class afledt : base { // ...<br />

void f () {<br />

base b;<br />

afledt a;<br />

b = a; // baseobjekt tildeles afledt objekt<br />

b.funktion (); // kalder funktion i baseobjektet<br />

4.3.11 Problemer med lineær arv 299


ase* bp; // pointer til baseobjekt<br />

bp = &a; // tildeles afledt objekt<br />

bp->funktion (); // kalder funktion i baseobjektet<br />

}<br />

Dette lader sig gøre, fordi funktioner i baseklassen også er funktioner i den afledte klasse. Når<br />

basefunktionen kaldes gennem en forekomst, som er typekonverteret fra en afledt klasse til en<br />

baseklasse, er det kun baseklassens data, der kan arbejdes på, og den information, der er gået tabt<br />

i konverteringen har ingen betydning i den henseende.<br />

Men det faktum, at information går tabt, sammenholdt med, at baseklasser ikke direkte kan<br />

kalde funktioner i afledte objekter (med mindre de indeholder eksplicitte referencer til disse) gør,<br />

at der er mange operationer, der ikke kan udføres gennem baseklassen. Dette kan illustreres ved<br />

et eksempel på en vektor af Figurer. Lad os antage, at vi vil holde en tegning af et antal<br />

forskellige objekter, alle nedarvet fra Figur. I princippet gør lineær arv, som den er beskrevet<br />

her i afsnittet, intet for at hjælpe programmøren med at abstrahere fra forskellene mellem de<br />

enkelte afledte klasser.<br />

// eksempel 4-9: vektor af pointere til baseklasse<br />

void f () {<br />

Koordinat k1 (100,100), k2 (200,200);<br />

const antal = 3;<br />

figur* tegning [antal]; // vektor af figur-pointere<br />

tegning [0] = new Punkt (k1, "A");<br />

tegning [1] = new Linie (k1, k2, "B");<br />

tegning [2] = new Firkant (k2, k2 + k1, "C");<br />

for (int i = 0; i < antal; i++)<br />

tegning [i]->Flyt (30,20); // kalder Figur::Flyt()<br />

delete tegning [0]; // fatal !<br />

delete tegning [1]; // fatal !<br />

delete tegning [2]; // fatal !<br />

}<br />

Det viser sig, at C++ ikke med lineær arv er i stand til at blande afledte klasser med hinanden.<br />

Problemets kerne er, at selvom der er type-ækvivalens mellem en baseklasse og en afledt klasse,<br />

er det, at information går tabt i konverteringen, uhensigtsmæssigt. Hvis vi antager, at alle<br />

klasserne i Figur-hierarkiet har destruktører, vil de tre delete-sætninger i eksemplet overfor<br />

alle kalde Figur::~Figur og ikke de respektive destruktører for Punkt, Linie og<br />

Firkant. Derfor må vi udtrykkeligt typekonvertere hvert medlem af tegning til de rigtige<br />

klasser før deallokering.<br />

En anden måde at udtrykke dette på er, at en liste af objekter af forskellige afledte klasser vil<br />

under disse omstændigheder alle opføre sig som baseklassen, idet informationen om den afledte<br />

300 Arv 4.3


klasse gik tabt i konverteringen. Det er altså ikke muligt, uden eksplicit at konvertere det enkelte<br />

objekt tilbage igen, at kalde en metode i den afledte klasse. De tre kald til Flyt() vil ikke blive<br />

ledt til de tre afledte klasser, men gå direkte til baseklassen, med mindre vi skriver<br />

((Punkt*)tegning [0])->Flyt (30,20);<br />

((Linie*)tegning [1])->Flyt (30,20);<br />

((Firkant*)tegning [2])->Flyt (30,20);<br />

Det blev nævnt i indledningen til dette <strong>kapitel</strong>, at objekt-orienteret programmering er et<br />

spørgsmål om design rettere end teknik. Derfor viser de første problemer sig først fra klientkodens<br />

side, som det ses af ovenstående eksemplers syntaks. Det er nu de tvungne<br />

typekonverteringer, som klienten bliver nødt til at bruge, som er problemet. Tvungne typekonverteringer<br />

skal så vidt muligt begrænses til et minimum, fordi de er farlige i den forstand, at<br />

en lille fejl kan forsage stor skade og være svær at lokalisere. Oversætteren beklager sig nemlig<br />

ikke. Når der bruges tvungne konverteringer er ansvaret programmørens. Alt håb er imidlertid<br />

ikke ude.<br />

4.4 POLYMORFE KLASSER<br />

Jo dybere man graver sig ned i et nyt programmeringsparadigme, jo mere spidsfindige bliver de<br />

problemer, man støder på. De metoder, der er beskrevet i <strong>kapitel</strong> 3 og i de første afsnit i dette<br />

<strong>kapitel</strong> er for de fleste, som har erfaring med andre programmeringssprog og metoder for design,<br />

overkommelige at forstå umiddelbart. Dette afsnit indeholder nogle af de faciliteter i objektorienteret<br />

programmering, hvor mange synes, det ændrer sig radikalt. Derfor starter vi varsomt.<br />

4.4.1 En isotrop opfattelse af klasser<br />

Hvis vi for et øjeblik forlader programmeringens verden og tænker på en "klasse" som et generelt<br />

begreb, finder vi, at vi ofte generaliserer mere på klassernes faktiske egenskaber end på deres<br />

indhold. Faktisk er det klassernes "opførsel" vi i det hele taget kan relatere os selv til. Måden, vi<br />

grupperer klasser på, er derfor i høj grad afhængig af, hvordan klasserne opfører sig. Selvom en<br />

flaske vin og en sodavand har meget få ting til fælles, er de begge en del af samme kategori, som<br />

har at gøre med drikkevarer. Egentlig grupperer vi på den måde for at kunne bruge de forskellige<br />

klasser. På den måde har klassernes repræsentation større betydning i vores begrebsverden end<br />

deres indre opbygning. Vores viden om, hvordan man drikker vin og hvordan man drikker<br />

sodavand er (stort set) den samme - vores indbyggede metode til at udføre denne opgave følger<br />

samme mønster.<br />

Det interessante ved denne iagttagelse er, at vi i mange henseender har samme metode, i hvert<br />

fald i teorien, til at klare forskellige opgaver, selvom de indebærer forskellige resultater. De fleste<br />

af os ved for eksempel hvordan man kører bil, knallert og cykel. Det er bestemte metoder, der<br />

bruges til at starte en bil, vende den rundt og til at accellerere og bremse. Men det spiller ingen<br />

rolle i selve handlingen om det er en Opel eller en Citroën, vi kører i. Vi er faktisk komplet<br />

4.3.11 Problemer med lineær arv 301


ligeglade; fabrikanten af bilen har sørget for, at der er en ensartet grænseflade til bilen, så vi<br />

behøver normalt ikke at bruge et nyt sæt regler, når vi skifter bil. På samme måde med knallerter<br />

og cykler i trafikken. De er forskellige transportmidler, men reglerne for, hvordan de skal styres<br />

gennem gaderne er de samme - vi har lært dem en gang for alle, og bruger de samme<br />

"kommandoer" for begge.<br />

Denne ensartethed i anvendelse af klasser i den virkelige verden er også, hvad objekt-orienteret<br />

programmering gør for klasser. Sagen er, at hvis vi arbejder med flere forskellige klasser, som har<br />

en ensartet grænseflade, kan vi i bestemte situationer være ligeglade med den faktiske klasse, når<br />

vi blot ved, hvordan den opfører sig. Det kan umiddelbart være svært at se, at man kan bruge et<br />

objekt uden at kende navnet på den klasse, som den er en forekomst af, men prøv at tænke på den<br />

naturlige definition ovenfor, når vi nu tager et konkret eksempel fra C++-verdenen.<br />

Vi tænker os, at vi skal skrive et system til administration af udlån i et bibliotek. Biblioteket<br />

indeholder publikationer af mange forskellige typer, som alle er relaterede, og dog har mindre<br />

afvigelser. Alle publikationer har en titel, et primært forfatternavn, et forlagsnummer samt<br />

antallet af bøger på hylden og udlånte eksemplarer. I første omgang er der to forskellige typer af<br />

publikationer, nemlig bøger og tidsskrifter. Alle bøger har et ISBN-nummer, mens tidsskrifter har<br />

et ISSN-nummer og en liste af artikler inde i bladet. Bøger kan igen opdeles i skøn- og<br />

faglitteratur, med hver deres genre-kode.<br />

Når vi låner en publikation ud, er vi interesserede i at udføre en metode, som er ens for alle<br />

publikationer, nemlig at nedtælle antallet af indeholdte eksemplarer, optælle antallet af udlånte<br />

eksemplarer og registrere låneren. På det tidspunkt, altså når vi kalder metoden udlaan(), kan<br />

vi tillade os at abstrahere fra, hvorvidt publikationen er en skønlitterær bog eller et<br />

sejlsportsmagasin. Vi vil bare låne den ud. Ligeledes når der skal gøres status over<br />

publikationerne for at udlæse antallet af udlånte bøger. Her er vi interesserede i at gennemløbe<br />

alle forekomster af alle typer af publikationer og summere udlåns-indikationen. Men det er helt<br />

uinteressant hvad publikationen hedder eller hvilken specifik klasse den tilhører. Vi vil blot kalde<br />

en metode, som fortæller os antallet af udlånte eksemplarer. Principperne for lineær arv tillader os<br />

at skrive en metode i en baseklasse, som arbejder på de fælles data for alle publikationer, her<br />

udlånsinformation.<br />

Men vi har også brug for en metode til udlæsning af publikationens art, navn, forfatter, indhold<br />

og andre data, der er specifikke for de enkelte slags publikationer. En sådan metode skal skrives i<br />

den afledte klasse, idet baseklassen ikke har den nødvendige information til at foretage en<br />

udlæsning af disse data. Nu står vi så med det interessante problem, at vi stadig ønsker at være<br />

uafhængige af, hvorvidt publikationen er af type A eller B (vi vil blot skrive indholdet ud), men<br />

kan samtidig ikke kalde en metode i baseklassen. Det er tydeligt, at disse operationer er<br />

klientorienterede. Fra klientens side er den eneste umiddelbare løsning at foretage en tvungen<br />

type-konvertering eller en reference til det aktuelle klassenavn, i kaldet til det objekt, der skal<br />

arbejdes på. Men, som beskrevet i afsnit 4.3.9, giver det en bunke af nye problemer, som er<br />

endnu mere uløselige. Det er altså grænsefladen til klasserne, som skal være mere gennemsigtig.<br />

Indtil nu har grænsefladen til klienten været begrænset til den enkelte klasse, men objektorienteret<br />

programmering giver os også mulighed for at beskrive en mere flydende grænseflade<br />

klasserne imellem.<br />

302 Polymorfe klasser 4.4


4.4.2 Bindingstidspunkter<br />

Når en metode i en klasse kaldes (eller for den sags skyld en normal funktion), foretages en<br />

indentifikation af metodens endelige adresse, dvs. dens placering i lageret, hvorefter programmet<br />

hopper til denne adresse. Denne identifikation sker normalt under oversættelsen † , hvor metodens<br />

adresse er unik og kendt, og skrives direkte i klientens kode. Dette kan ikke ændres, med mindre<br />

klienten foretager tvungne type-konverteringer af de objekter, som indeholder metoderne. Af<br />

denne grund kaldes denne proces for statisk binding eller tidlig binding med henvisning til, at<br />

bindingen mellem klient og metode sker før programmet startes.<br />

C++ har et alternativ. Ved at forsinke bindingstidspunktet til programmet rent faktisk kører, er<br />

det muligt for programmet selv at determinere det faktiske objekt og foretage bindingen<br />

umiddelbart før kaldet. Denne bindingsform kaldes for dynamisk binding eller sen binding, fordi<br />

det først sker, efter programmet er sat i gang. For denne type kald er det klart, at oversætteren<br />

ikke kender metodens endelige adresse. Programmet indeholder selv det nødvendige "klister" til<br />

at udføre denne opgave, og derfor er der et mindre overhead i kald til metoder, som kræver<br />

dynamisk binding. Dette forhold diskuteres nærmere i afsnit 4.4.5.<br />

C++ kan, som et af de eneste objekt-orienterede sprog, foretage både statisk og dynamisk<br />

binding ud fra den filosofi, at programmerne skal være effektive. Statisk binding er hurtigere, og<br />

skal derfor bruges, hvis det kan.<br />

4.4.3 Virtuelle funktioner<br />

Hvis en baseklasse kun tilbød medlemsfunktioner af den slags, der er beskrevet i afsnit 4.3, ville<br />

nedarvning som sådan ikke være særlig interessant, fordi en baseklasse ikke ville kunne tilbyde<br />

særlig mange interessante metoder. Da baseklasser er meget generelle, er normale metoder i dem<br />

også meget generelle. Normale metoder skal implementeres i de klasser, som erklærer dem, og<br />

skal følgelig kaldes fra klienten med reference til samme klasser. Som tidligere beskrevet har<br />

baseklasser ifølge klasse-hierarkiets natur ikke adgang til afledte klassers data og metoder, så det<br />

er ikke muligt at konstruere generelle metoder i baseklassen, som kalder de mere specifikke<br />

metoder i de afledte klasser. Dermed bliver den mest indlysende fordel ved lineær arv en<br />

behændig metode til genbrug af kode. Vi skal imidlertid senere (i afsnit 5.9) se eksempler på, at<br />

arv ikke bør anvendes som generel genbrugsmekanisme, men som generel<br />

abstraktionsmekanisme. Fra klientens synspunkt kan det nemlig være ligegyldigt, om en klasse<br />

nedarver fra en anden klasse eller om den er isoleret, sålænge der arbejdes med lineær arv. Men<br />

behandlingen af typer kræver, at forskellige klasser skal kunne behandles ens og med samme<br />

syntaks, ligesom vi kender den indbyggede polymorfi fra de gængse sprogs fundamentale typer.<br />

For at skabe en blød grænseflade for alle klasser i et hierarki tages et nøgleredskab i objektorienteret<br />

programmering i brug. Den egalitet mellem medlemsfunktioner i afledte klasser, som<br />

er eksemplificeret i afsnit 4.4.1, implementeres i C++ ved hjælp af virtuelle funktioner. En virtuel<br />

funktion i en baseklasse har den egenskab, at den kan erstattes på kaldetidspunktet med en<br />

metode i en afledt klasse. På den måde repræsenterer en virtuel funktion en "rød tråd" gennem et<br />

†<br />

Normalt sker selve samlingen af lænker-værktøjet (compile-time binding), men kan også foregå ved indlæsningen<br />

af programmet (load-time binding) eller ved brug af specielle biblioteker, som kaldes dynamic link libraries. Fælles<br />

for dem alle er dog, at metodernes positioner i lageret skrives statisk ind i klientkoden.<br />

4.4.2 Bindingstidspunkter 303


klassehierarki, og udgør en vertikal ensartet grænseflade til klienten. Klienten kan således arbejde<br />

på en række af afledte klasser uden at bekymre sig om deres type.<br />

En virtuel funktion erklæres med nøgleordet virtual før funktionserklæringen. Dermed<br />

fortælles oversætteren, at den givne funktion kan erstattes af en funktion i en afledt klasse. Men<br />

hvordan og hvorfor sker denne erstatning? Hvordan ved oversætteren, at det skal være en afledt<br />

klasse, som skal modtage kaldet? Svarene på disse spørgsmål ligger gemt i pointermanipulation<br />

med objekter. Når der ikke arbejdes med pointere, vil et metodekald i en klasse altid henvise til<br />

en metode i den aktuelle forekomst. Med pointere til objekter kan det ifølge reglerne for<br />

typekonvertering gennem nedarvede klasser være muligt at have en pointer til et baseobjekt, som<br />

egentlig peger på et afledt objekt, som beskrevet i afsnit 4.3.8. Når en virtuel metode kaldes via<br />

en pointer til et objekt, vil kaldet dynamisk binde til en funktion i det objekt, som egentlig<br />

refereres af pointeren, hvis metoden er erklæret i den klasse, som objektet er en forekomst af.<br />

Ifølge afsnit 4.3.1 vil en pointer til et baseobjekt, som tildeles adressen på et afledt objekt ikke<br />

miste typeinformationen fra det afledte objekt. Det er grunden til, at oversætteren kan identificere<br />

den virtuelle funktion og kalde den korrekte version for det objekt, pointeren refererer.<br />

For eksempel,<br />

class X { public: virtual int f(); };<br />

class Y : public X { public: virtual int f(); };<br />

X a; // a er en forekomst af X<br />

Y b; // b er en forekomst af Y<br />

a.f(); // X::f() kaldes for objektet a<br />

b.f(); // Y::f() kaldes for objektet b<br />

X* xp1 = &a; // xp1 er en pointer til en X<br />

X* xp2 = &b; // b er afledt af X, adressen kan derfor<br />

// tildeles en X*<br />

xp1->f(); // kalder X::f() for objektet a<br />

xp2->f(); // kalder Y::f() for objektet b !<br />

Her ses, hvordan den virtuelle metode f() kaldes med forskellige forudsætninger. Først kaldes<br />

den for forekomster af henholdsvis X og Y, hvorved den opfører sig som normal metode.<br />

Dernæst tildeles adressen på de to forekomster til to pointer-variable. De efterfølgende kald til<br />

f() med reference til disse to pointere ser helt ens ud, men resulterer i kald til X::f() og<br />

Y::f() for henholdsvis xp1 og xp2, fordi pointerne bærer typeinformation med sig. Kaldet<br />

til xp2->f(), binder dynamisk til et Y-objekt selvom xp2 er en pointer til en X.<br />

Da pointerens store anvendelse ligger i dens generalitet, kan virtuelle funktioner og pointere på<br />

ovenstående måde bruges til at skrive funktioner, som abstraherer fuldstændig fra de datatyper,<br />

de arbejder på. Hvis en funktion f() tager en pointer eller reference til en type T som<br />

parameter, og hvis T indeholder en public virtuel metode v(), vil et kald til<br />

parameterobjektets v() binde til den type, som f() kaldes med. Dermed er f() helt<br />

generisk, da det er muligt at nedarve en ny type fra T og kalde f() med en reference til denne.<br />

De problemer, der opstod i løsningen af Figur-problemet med lineær arv, var alle relateret til<br />

kode, som var ikke-generisk og som derfor skulle modificeres, hvis programmet skulle udvides.<br />

304 Polymorfe klasser 4.4


Overvej følgende ændringer i klasserne (forkortet for læsbarhedens skyld):<br />

// eksempel 4-10: figurklasser med virtuelle funktioner<br />

class Figur {<br />

// ...<br />

public:<br />

virtual void Tegn ();<br />

virtual void Slet ();<br />

void Flyt (Koordinat&);<br />

// ...<br />

};<br />

class Linie : public Figur {<br />

// ...<br />

public:<br />

virtual void Tegn (); // sådan tegnes en linie<br />

virtual void Slet (); // sådan slettes en linie<br />

// ...<br />

};<br />

class Punkt : public Figur {<br />

// ...<br />

public:<br />

virtual void Tegn (); // sådan tegnes et punkt<br />

virtual void Slet (); // sådan slettes et punkt<br />

// ...<br />

};<br />

Den eneste principielle forskel på eksempel 4-3 og 4-10 er, at de metoder, som er specifikke i<br />

funktion for den givne klasse, men som er fælles i abstraktion for alle klasserne, er lavet om til<br />

virtuelle funktioner. En global funktion Tegn() kan nu skrives uden forhåndskendskab til<br />

Linie og Punkt, men blot arbejde på Figurer:<br />

void Tegn (Figur& f) {<br />

f.Tegn ();<br />

}<br />

På samme måde kan funktionen Flyt(), som nedarves fra Figur i de to afledte klasser,<br />

kalde virtuelle metoder for sit eget underforståede objekt. Dermed kan kompleksiteten for mindre<br />

dele af funktioner, som har næsten samme funktion (afsnit 4.3.10), abstraheres og lægges i en<br />

virtuel funktion. Funktionen Figur::Flyt() kan have følgende implementation:<br />

void Figur::flyt (Koordinat& k) {<br />

4.4.3 Virtuelle metoder 305


ool husk = vises;<br />

if (vises) Slet (); // virtuelt metodekald<br />

p += k; // flyt startpunktet<br />

if (husk) Tegn (); // virtuelt metodekald<br />

}<br />

Ved et kald til Figur::Flyt(), som direkte nedarves fra Figur, bliver de efterfølgende<br />

kald til Tegn() og Slet() foretaget for det underforståede objekt, og afhænger dermed af<br />

typen, som Flyt() blev kaldt for. Med virtuelle funktioner kan metoder i afledte klasser altså<br />

kaldes fra metoder i baseklasser.<br />

4.4.4 Dynamisk Binding som indkapsling<br />

I 1968 skrev Edsgar Dijkstra en artikel med titlen GO TO Statement Considered Harmful - GO<br />

TO anses for skadelig [Dijkstra 68]. Hans argument var, at den blinde hoppen rundt i progammet<br />

efter regler, som i abstraktion lå meget lavt, gør koden ulæselig og umulig at genbruge på længere<br />

sigt. Artiklen var startskuddet til et gennemgribende paradigmeskift, som resulterede i<br />

principperne for struktureret programmering.<br />

Et af hovedelementerne i struktureret programmering er symbolik. I en situation, hvor der kan<br />

være tale om flere grenvalg, er det i dag skik at anvende en switch-struktur (2.7.5) fordi den<br />

gør koden læsbar, en trods alt vigtig faktor. I de to følgende eksempler foretages sådanne<br />

grenvalg på en abstrakt datatype, som indeholder egen subtype-information, taget fra afsnit 4.2.7<br />

(eksempel 4-2):<br />

// forstør en figur med en given faktor<br />

void Forstoer (Figur& fig, double factor) {<br />

switch (fig.slags) {<br />

case Punkt:<br />

// et punkt kan ikke forstørres!<br />

break;<br />

case Linie:<br />

// ... kode til at forstørre en linie ...<br />

break;<br />

case Firkant:<br />

// ... kode til at forstørre et rektangel ...<br />

break;<br />

}<br />

}<br />

// rotér en figur om en given koordinat med en given grad<br />

void Roter (Figur& f, Koordinat& cent, double ang) {<br />

switch (fig.slags) {<br />

case Punkt:<br />

306 Polymorfe klasser 4.4


}<br />

// et punkt kan ikke roteres!<br />

break;<br />

case Linie:<br />

// ... kode til at rotere en linie ...<br />

break;<br />

case Firkant:<br />

// ... kode til at rotere et rektangel ...<br />

break;<br />

}<br />

De to funktioner Forstoer() og Roter() er skrevet efter strukturerede principper og<br />

udviser en høj grad af funktionel abstraktion. Der er dog to problemer, som det strukturerede<br />

paradigme ikke løser:<br />

• den del af kildeteksten, som behandler en given Figur-type, er spredt rundt i<br />

programmet. Det er ikke muligt at isolere kode, der relaterer sig til en bestemt type i<br />

samme modul eller oversættelsesenhed. Hvis der opstår en fejl i behandlingen af for<br />

eksempel et rektangel, kan den være svært at lokalisere.<br />

• hvis der skal foretages udvidelser i programmet, for eksempel en ny Figur-type<br />

Cirkel, skal der ændres i alle dele af programmet, som behandler Figurer. De dele<br />

viser sig oven i købet at være de mest kritiske, nemlig selve definitionen på typen samt<br />

funktionerne til behandling af alle former for typer. Netop fordi koden for de enkelte<br />

subtyper ikke er isoleret, skal det meste af programmet genoversættes og gentestes for<br />

eventuelle fejl. For hvem kan garantere, at en programmør ikke pludselig finder<br />

grundlag for en mindre forbedring i koden for at forstørre en Firkant mens han<br />

udvider med en Cirkel?<br />

Programmer, der skrives på denne måde er alstå ikke bare implementations-afhængige, fordi<br />

programmøren varetager type-kontrollen over en given polymorf datastruktur, men er også<br />

rodede, fordi kildeteksten ikke kan organiseres på en - efter subtyperne - naturlig måde.<br />

Pointen her er, at typesystemet i C++ skal overtage så megen kontrol som muligt. De enkelte<br />

switch-strukturer i funktionerne skal erstattes med nedarvede typer, som indeholder virtuelle<br />

metoder. De afledte klasser repræsenterer hver type-variant, en virtuel metode erstatter hver<br />

switch-sætning og hver case-sætning bliver til en metode i den afledte klasse, som bindes<br />

dynamisk. Hermed lokaliseres den kode, som arbejder på en enkelt subtype af Figur til en<br />

enkelt kildefil, og opståede fejl med denne type vil kun kunne findes der. Af samme grund<br />

oversættes koden for hver subtype separat, og det er derfor ikke nødvendigt at genteste koden for<br />

andre subtyper i systemet, hver gang en rettelse eller udvidelse foretages.<br />

Under det objekt-orienterede paradigme kan vi ændre Dijkstras frase til switch Statement<br />

Considered Harmful, fordi programkonstruktioner, som bygger grenvalg op på denne måde<br />

organiserer kildeteksten i akkurat samme "spaghetti", som den fordums GO TO gjorde, blot efter<br />

4.4.4 Dynamisk Binding som indkapsling 307


et andet paradigme †<br />

. Et program med switch-sætninger er stort og uoverskueligt, fordi<br />

semantikken simpelthen går tabt i de enkelte grenvalg. Dynamisk binding giver os imidlertid<br />

mulighed for en elegant opdeling af programmerne, som lokaliserer relateret kode i isolerede<br />

stumper ved hjælp af et sikkert type-check på kørselstidspunktet. Her er forrige eksempel<br />

omskrevet efter det objekt-orienterede paradigme:<br />

// eksempel 4-11: typeuafhængighed for Roter() og Forstoer()<br />

class Figur {<br />

Koordinat p;<br />

public:<br />

virtual void Forstoer (double);<br />

virtual void Roter (Koordinat&, double);<br />

};<br />

class Punkt : public Figur {<br />

public:<br />

virtual void Forstoer (double);<br />

virtual void Roter (Koordinat&, double);<br />

};<br />

class Linie : public Figur {<br />

Koordinat e;<br />

public:<br />

virtual void Forstoer (double);<br />

virtual void Roter (Koordinat&, double);<br />

};<br />

void Forstoer (Figur& fig, double factor) {<br />

fig.Forstoer (factor); // polymorf!<br />

}<br />

void Roter (Figur& fig, Koordinat& cent, double ang) {<br />

fig.Roter (cent, ang); // polymorf!<br />

}<br />

De globale funktioner Forstoer() og Roter() er nu fuldstændig uafhængige af<br />

subtyperne af Figur, og vi kan uden videre aflede en ny klasse fra denne uden at røre ved en<br />

eneste linie eksisterende kode. Det eneste, funktionerne skal vide er, at der findes virtuelle<br />

funktioner i arvefølgen under Figur - den dynamiske binding sørger for selve identifikationen<br />

af typen. Klasser med virtuelle funktioner kaldes polymorfe klasser.<br />

4.4.5 Virtuelle metoder i detaljer<br />

†<br />

Windows eller OS/2 PM programmører kender de op mod 1000 liniers typiske switch-sætninger.<br />

308 Polymorfe klasser 4.4


Man kan stille det relevante spørgsmål om, hvordan C++ identificerer et afledt objekt, når kaldet<br />

i sig selv ikke på oversættelsestidspunktet er entydigt. Svaret ligger i, at et objekt med virtuelle<br />

metoder indeholder ekstra skjulte datamedlemmer, som peger på de specifikke virtuelle metoder.<br />

Det betyder, at de tre klasser Firkant, Punkt og Linie fra sidste afsnit alle har to<br />

usynlige datamedlemmer, nemlig pointere til de to funktioner Forstoer() og Roter().<br />

Oversætteren bruger den skjulte pointer, når der skal foretages et kald til en virtuel funktion. Da<br />

en virtuel funktion skal være erklæret i baseklasssen, kan oversætteren generere et indeks til<br />

pointeren til den virtuelle funktion, som er ens for alle klasser, som nedarver fra denne<br />

baseklassen. Dermed er kompleksiteten i dynamisk binding ikke større end en ganske normal<br />

indirektion, hvor den effektive adresse er resultatet af en dereference af en pointer. De skjulte<br />

medlemmer i de polymorfe klasser er blot pointere til funktioner. Den ekstra databehandling, som<br />

ligger i denne indirektion, bliver selvfølgelig påført programmets effektivitet i forhold til statisk<br />

bundne kald. Dog er der ikke tale om et større nedslag, da en indirektion ofte kan foretages i en<br />

eller to enkle instruktioner på assembler-niveau, og er på visse processorer endda implementeret<br />

direkte i instruktionssættet.<br />

I Figur 4-7 ses, hvordan de forskellige objekters skjulte virtuelle tabeller refererer de faktiske<br />

funktioner. Den virtuelle tabel, som indeholder pointerne til de virtuelle metoder, forekommer<br />

dog kun en gang for hver polymorf klasse, ikke for hver forekomst af klassen. Forekomsten<br />

indeholder en pointer til den virtuelle tabel, så en polymorf klasse vil højest indeholde et skjult<br />

overhead svarende til en pointerforekomst (multipelt afledte klasser indeholder dog flere pointere,<br />

mere om det i afsnit 4.5).<br />

4.4.5 Virtuelle metoder i detaljer 309


Figur 4-7: Skjulte datamedlemmer i polymorfe klasser (konceptuel model).<br />

310 Polymorfe klasser 4.4


Figur 4-8: Et kald til en virtuel funktion. Her ses den reelle tabel til de virtuelle funktioner.<br />

Figur 4-8 viser, hvordan to forskellige forekomster af klassen Punkt gennem en pointer til<br />

den virtuelle tabel gennemfører et virtuelt funktionskald, der svarer til, at to uafhængige pointere<br />

til Figurer, som reelt peger på Punkt-objekter, kalder Forstoer()-funktionen for disse<br />

pointere.<br />

4.4.6 Polymorfe metoder<br />

Virtuelle metoder er faktisk en form for overstyring, da der er tale om, at en identisk syntaks<br />

medfører forskellige resultater. Forskellen fra generelt overstyrede funktionsnavne og operatorer<br />

er, at de virtuelle metoder bindes dynamisk. C++ opstiller visse krav til virtuelle metoder, som<br />

skal følges for at få magien til at virke:<br />

• Først og fremmest skal en virtuel metode erklæres i baseklassen, dvs. den klasse, hvis<br />

type bruges i reference til de virtuelle kald. Metodernes prototyper skal være helt<br />

identiske i baseklassen og i de afledte klasser, fordi syntaksen er ens i kaldet uanset<br />

hvilken type, der er tale om. Udover funktionsnavnet skal både parameterlisten og<br />

returværdien i den afledte klasse være mage til baseklassens. Oversætteren kan sikre<br />

dette, og vil normalt fejle, hvis det ikke overholdes. Virtuelle metoder må godt<br />

overstyres efter de normale regler, hvorved klassen blot får to eller flere virtuelle<br />

metoder at arbejde med.<br />

• Da virtuelle funktioner arbejder i et klassehierarki med reference til den klasse, som de<br />

er erklæret i, har de altid et underforstået objekt, for hvilket den specifikke polymorfe<br />

metode kaldes. Det forbyder virtuelle friend-funktioner, fordi en sådan ikke har et<br />

underforstået objekt, da den kan være friend til mere end én klasse. Det tillader<br />

imidlertid en virtuel funktion i én klasse at være erklæret som friend i en anden<br />

klasse, da funktionen i så fald har et underforstået objekt (den første klasse).<br />

• Det er ikke nødvendigt at specificere virtual-nøgleordet i erklæringen af virtuelle<br />

metoder i de afledte klasser. Det er nok at fortælle baseklassen, at metoden er virtuel,<br />

hvorefter alle afledte klasser også opfatter den som sådan. Det er dog tilrådeligt at skrive<br />

virtual før en virtuel metode, fordi det gør det helt klart, hvilken type metode, der er<br />

tale om. virtual-nøgleordet skal kun skrives i klassens krop, ikke i en eventuel<br />

ekstern implementation af metoden.<br />

• Det er ikke påkrævet, at en afledt klasse redefinerer en virtuel metode, sålænge den har<br />

en erklæring i baseklassen. Hvis en virtuel metode ikke findes i den afledte klasse, arves<br />

den fra den nærmeste baseklasse, som implementerer den (med mindre den er ren, se<br />

afsnit 4.4.10).<br />

En metode bør erklæres virtuel, hvis designeren af baseklassen mener, at den eventuelt får en<br />

4.4.5 Virtuelle metoder i detaljer 311


anden betydning i en afledt klasse. Forskellen på at erklære metoden virtuel og lade den stå som<br />

normal funktion er fra dette synspunkt, at baseklassen kan bruges som fællesnævner for de<br />

afledte klasser i klientens programmer. Det gør klientens kode langt mere generisk og<br />

genbrugelig.<br />

Læg mærke til, at en virtuel metode kun er polymorf, sålænge der er tale om kald gennem<br />

pointere eller referencer til den klasse (eller baseklasse), som metoden er medlem af. Selv om en<br />

metode er virtuel, vil den binde statisk til kalderen, hvis den kaldes med henvisning til en<br />

forekomst af klassen:<br />

void f (Figur& fig, Punkt pkt) {<br />

fig.Forstoer (1.30); // dynamisk binding<br />

pkt.Forstoer (1.30); // statisk binding<br />

}<br />

Det sidste kald til Forstoer() binder statisk, fordi oversætteren identificerer kaldet til et<br />

Punkt på oversættertidspunktet. Der er nemlig ikke noget at være i tvivl om, når der arbejdes<br />

med forekomster af klasser.<br />

Hvis den virtuelle metode i den afledte klasse afviger fra den originale prototype i baseklassen,<br />

får den ikke den tilsigtede funktion. Det er vigtigt at sikre ækvivalensen, for oversætteren vil ikke<br />

advare om fejl:<br />

class Firkant : public Linie {<br />

// ...<br />

public:<br />

void Forstoer (float);<br />

virtual Forstoer (double);<br />

// ...<br />

};<br />

Den virtuelle metode i Firkant kan ikke arbejde sammen med den nedarvede metode<br />

Forstoer() fra Linie, fordi den afviger i specifikation. Den første metode i den afledte<br />

klasse er blot en normal metode, mens den anden bliver en virtuel metode nedefter i hierarkiet,<br />

men ikke opefter, da den returnerer en int. Klassen Firkant indeholder derfor hele tre<br />

funktioner Forstoer(), da den også nedarver den virtuelle funktion fra baseklassen:<br />

void Firkant::Forstoer (float); // normal metode<br />

int Firkant::Forstoer (double); // virtuel metode<br />

void Firkant::Forstoer (double); // virtuel metode (arvet)<br />

Selvom en afledt klasse ikke redefinerer en virtuel metode, og dermed arver baseklassens, kan en<br />

ny afledning redefinere metoden:<br />

312 Polymorfe klasser 4.4


class Base {<br />

public:<br />

virtual void f ();<br />

};<br />

class Afledt : public Base {<br />

public:<br />

};<br />

class Afledt2 : public Afledt {<br />

public:<br />

virtual void f ();<br />

};<br />

Selv om klassen Afledt ikke redefinerer den virtuelle metode f(), og dermed overtager den<br />

fra Base, så får metoden en anden betydning i Afledt2, som erklærer en ny af samme navn<br />

og dermed ikke arver den oprindelige.<br />

Der er en mulighed for tvetydighed her. Hvis en baseklasse erklærer en virtuel metode i<br />

public-delen af klassen, og en nedarvet klasse reerklærer metoden i en private eller<br />

protected sektion, hvad sker så? Det er ikke ulovligt at foretage en sådan ændring i adgang til<br />

en given virtuel funktion, fordi grænsefladen til den afledte klasse kan kræve det. Overvej<br />

class Base { public: virtual void f (); };<br />

class Afledt : public Base { protected: virtual void f (); };<br />

void foo (Base& b) {<br />

b.f();<br />

}<br />

Den virtuelle funktion f(), som kaldes fra foo() er public, og giver derfor funktionen<br />

adgang. Hvad sker, hvis funktionen kaldes med<br />

void main () {<br />

Afledt p;<br />

foo (p); // kalder indirekte den protected'e<br />

} // virtuelle metode Afledt::f() !<br />

hvorved foo() vil kalde en metode, der reelt er protected og for hvilken den er forment<br />

adgang? Svaret er, at en virtuel funktion altid har samme visibilitet som den klasse, den kaldes<br />

med reference til. Funktionen foo() kalder en virtuel metode, som er erklæret public i den<br />

klasse, som metoden kaldes for. Dermed vil alle fra Base afledte klasser have samme<br />

adgangsbetingelser, nemlig public. Hvis funktionen i stedet arbejder på en reference til en<br />

Afledt, vil alle kald til den virtuelle funktion være protected, fordi den fra Afledts<br />

synspunkt og nedefter har dette adgangskriterium. Følgende vil derfor ikke kunne oversættes:<br />

4.4.6 Polymorfe metoder 313


void foo (Afledt& a) {<br />

a.f(); // fejl: f() er protected i Afledt<br />

}<br />

Eller helt konkret:<br />

Afledt a; Base& bp = a;<br />

Afledt& ap = a;<br />

bp.f(); // ok, kalder Afledt::f()<br />

ap.f(); // fejl, ingen adgang til f()<br />

Der skal også lægges vægt på, at de virtuelle funktioner har præcis de samme prototyper i både<br />

baseklasser og afledte klasser. Hvis den afledte klasses reerklærede virtuelle funktion afviger i for<br />

eksempel et parameter, introducerer den en ny virtuel funktion! Husk derfor altid at<br />

dobbeltchecke konstante funktioner, parametre og returværdier samt parametertyper.<br />

4.4.7 Polymorfe datastrukturer<br />

Hvis en klasse K indeholder referencer eller pointere til andre polymorfe klasser, er disses<br />

indhold afhængig af de faktiske klasser, som K peger på. Dermed er K en polymorf klasse i en<br />

anden forstand, nemlig set fra datasiden, og kaldes derfor en polymorf datastruktur. Dette<br />

koncept kan eksemplificeres med en ny klasse Billede, som indeholder et antal Figurer:<br />

// eksempel 4-12a: en klasse, der både er klient og arving<br />

class Billede : public Figur {<br />

Figur** liste;<br />

int antal;<br />

public:<br />

Billede (int = 100);<br />

~Billede ();<br />

Billede& Indsaet (Figur*);<br />

virtual void Tegn ();<br />

virtual void Slet ();<br />

virtual void Forstoer (double);<br />

virtual void Roter (Koordinat&, double);<br />

};<br />

Billede::Billede (int kapacitet) {<br />

liste = new Figur* [kapacitet];<br />

antal = 0;<br />

}<br />

314 Polymorfe klasser 4.4


Billede::~Billede () {<br />

for (int i = 0; i < antal; i++;) delete liste [i];<br />

delete liste;<br />

}<br />

Billede& Billede::Indsaet (Figur* fig) {<br />

liste [antal++] = fig;<br />

return *this;<br />

}<br />

void Billede::Tegn () {<br />

for (int i = 0; i < antal; i++) liste [i]->Tegn ();<br />

}<br />

void Billede::Slet () {<br />

for (int i = 0; i < antal; i++) liste [i]->Slet ();<br />

}<br />

void Billede::Forstoer (double factor) {<br />

for (int i=0; i < antal; i++) liste [i]->Forstoer<br />

(factor);<br />

}<br />

void Billede::Roter (Koordinat& k, double ang) {<br />

for (int i = 0; i < antal; i++) liste [i]->Roter (k, ang);<br />

}<br />

Klassen Billede indeholder en liste af pointere til Figurer, som reelt kan pege på ethvert<br />

objekt, som er en forekomst af en af Figur afledt klasse. Dermed kan Billede bruges til at<br />

beskrive en tegning af mange forskellige slags Figurer, og med et indhold, som kan ændre sig<br />

dynamisk. Den egenskab, at en klasse indeholder forekomster af eller pointere til andre klasser<br />

gør den til en såkaldt container-klasse, hvilket beskrives nærmere i <strong>kapitel</strong> 5. Generelt er<br />

Billede en kollektion af Figurer, som kan bruges på følgende måde:<br />

// eksempel 4-12b: polymorfe datastrukturerer i anvendelse<br />

void main () {<br />

Billede& stjerner = *new Billede (3);<br />

Billede& kometer = *new Billede (2);<br />

stjerner // definér stjernerne<br />

.Indsaet (new Punkt (Koordinat (100,100)))<br />

.Indsaet (new Punkt (Koordinat (300,50)))<br />

.Indsaet (new Punkt (Koordinat (200,300)));<br />

4.4.7 Polymorfe datastrukturer 315


kometer // definér kometerne<br />

.Indsaet (new Linie (Koordinat(20,20),Koordinat(30,400)))<br />

.Indsaet (new Linie (Koordinat(80,200),Koordinat(300,30));<br />

Billede nathimmel (2);<br />

nathimmel.Indsaet (&stjerner).Indsaet (&kometer);<br />

nathimmel.Tegn ();<br />

delete *stjerner;<br />

delete *kometer;<br />

}<br />

Dette eksempel illustrerer, hvordan polymorfe datastrukturer kan acceptere alle former for afledte<br />

klasser som medlemsdata. I eksemplet erklæres to Billeder, stjerner og kometer, som<br />

fyldes med henholdsvis Punkter og Linier. Så erklæres et nyt Billede, nathimmel,<br />

som får indsat stjernerne og kometerne, og dernæst skrives ud:<br />

Punkt på (100,100)<br />

Punkt på (300,50)<br />

Punkt på (200,300)<br />

Linie fra (20,20) til (30,400)<br />

Linie fra (80,200) til (300,30)<br />

Det spændende ved dette eksempel er, at Billede både arver fra Figur og har Figur<br />

som medlemsvariabel. Det tillader, at vi indsætter Billeder i Billeder, og dermed kan<br />

anskue stjerner og kometer som subbilleder uden at skulle erklære dem som sådanne.<br />

Klassen Billede udvider dermed sin polymorfi til at omfatte sig selv og viser, hvor stor<br />

polymorfiens styrke er.<br />

4.4.8 Initiering af polymorfe klasser<br />

Konstruktører nedarves ikke, og kan følgelig ikke være virtuelle. En konstruktør har til opgave at<br />

initiere en forekomst af en given klasse, og har intet at gøre i de afledte klasser. Der skal derfor<br />

ikke tages specielle hensyn i forhold til baseklasserne, når der skrives konstruktører i en afledt<br />

klasse.<br />

Det omvendte er derimod tilfældet. Initieringsrækkefølgen i et afledt objekt under konstruktion<br />

siger, at baseklassen initieres før den afledte klasse (afsnit 4.3.7). Når baseklassens konstruktør<br />

bliver kaldt, er objektet derfor stadig under konstruktion, hvilket har betydning for de virtuelle<br />

funktioner:<br />

// eksempel 4-13: virtuelt kald fra konstruktør<br />

class X {<br />

public:<br />

virtual void f () { cout


};<br />

class Y : public X { // Y arver fra X<br />

public:<br />

Y () { f (); } // kons. kalder virtuel funktion<br />

};<br />

class Z : public Y { // Z arver fra Y<br />

public:<br />

Z () { f (); } // kons. kalder virtuel funktion<br />

virtual void f () { cout


~X();<br />

}<br />

class Y : public X {<br />

public:<br />

Y();<br />

~Y();<br />

}<br />

void f () {<br />

X* a = new Y; // kalder konstruktøren Y::Y()<br />

delete a; // kalder destruktøren X::~X()<br />

}<br />

Problemet er, at det underforståede kald til destruktøren i sætningen delete a binder statisk<br />

til X::~X(), selvom a rent faktisk er en forekomst af Y. For at undgå dette erklæres<br />

destruktøren virtuel, hvorved kaldet binder dynamisk til den rigtige destruktør og bobler opad i<br />

hierarkiet på den sædvanlige måde. Destruktører er ikke virtuelle som standard, fordi der er<br />

mange anvendelser af nedarvede klasser, hvor polymorfi ikke er nødvendig, og hvor det blot ville<br />

skabe et unødvendigt overhead i både kode- og datastørrelse.<br />

En god regel er, at hvis en klasse er afledt, og skal bruges polymorft (med indirekte reference til<br />

baseklassen), og især, hvis den indeholder virtuelle metoder, så skal destruktøren også være<br />

virtuel. Da destruktører ikke nedarves, overtages en eventuel virtuel destruktør ikke fra<br />

baseklassen, og en manglende definition resulterer i et ikke-virtuelt, underforstået destruktørkald.<br />

4.4.10 Abstrakte klasser<br />

Undertiden arbejder vi med koncepter, som ikke har en fysisk form eller anvendelighed, men må<br />

klassificeres som abstrakte uden en egentlig manifestation i virkeligheden. I eksemplet i afsnit<br />

4.3.1 (figur 4-3) arbejdes med forskellige dyrearter, som nedad i hierarkiet bliver mere og mere<br />

specifikke. En vilkårlig klasse Elefant har for eksempel en klar betydning og manifestation,<br />

da det er muligt at forestille sig en forekomst af dette dyr, ligesom det er muligt at skabe<br />

Svaner, Rokker og Myg. Alle disse klasser er konkrete; det er muligt at forestille sig en<br />

forekomst af dem.<br />

Det gælder ikke for Elefants nærmeste baseklasse, Pattedyr. Selv om vi kan arbejde<br />

med Pattedyr på et generaliserende og abstrakt niveau, kan vi ikke forestille os en forekomst<br />

af et pattedyr. Vist er en elefant et pattedyr, men det er mere end det. Der findes ikke en skabning,<br />

som blot er et pattedyr, hvilket gør ordet til et begreb uden fysisk repræsentation i virkeligheden.<br />

Vi har en stor abstrakt begrebsverden, tænk blot på ordene - eller klasserne - køretøj, bog, pige,<br />

væske og computerspil. Umiddelbart tænker vi, at vi da har en bil, som er en 2CV, en bog som<br />

hedder Princpia Mathematica, en pige, som hedder Charlotte osv., men de er mere end blot en<br />

bil, en bog og en pige. Grunden til, at vi overhovedet har disse abstrakte begreber er, at vi skal<br />

318 Polymorfe klasser 4.4


uge dem, når vi ikke kender den konkrete klasse, men blot én af de mere generelle nærmeste<br />

klasser, som den tilhører. Uden at kende til indholdet i eller titlen på en bog ved vi godt, hvordan<br />

den skal tages ud af bogreolen, bladres i, læses osv.<br />

Principperne for nedarvning i C++ tilstræber en generalitet opefter og en specialisering<br />

nedefter. De øverste klasser i et hierarki bliver derfor de mest generelle, for hvilke der ikke er<br />

berettigelse for egentlige instantierede objekter. Overvej<br />

Figur f;<br />

Hvad menes med denne erklæring? Den siger, at f er en forekomst af Figur, men hvad er en<br />

Figur, når det kommer til stykket? Det er kun de afledte klasser, som har en egentlig betydning,<br />

og det er slet ikke meningen, at baseklassen Figur skal instantieres som her. Det giver<br />

anledning til et par væsentlige spørgsmål:<br />

• hvordan skal de virtuelle funktioner implementeres i en klasse, hvis koncept ikke egner<br />

sig til instantiering? Hvordan skal en Figurs Tegn()-metode for eksempel se ud?<br />

• hvad sker der, hvis programmøren glemmer at erklære og definere en virtuel funktion i<br />

en afledt klasse, så den dermed nedarves fra baseklassen med en uønsket virkning?<br />

Da der ingen idé er i at instantiere Figur-klassen, fordi den repræsenterer et abstrakt begreb,<br />

har de virtuelle metoder i den ingen reel funktion. De kunne eksempelvis se ud som følger:<br />

void Figur::Tegn () {<br />

cerr


...<br />

public:<br />

virtual void Tegn () = 0; // ren virtuel metode<br />

virtual void Slet () = 0; // ren virtuel metode<br />

// ...<br />

};<br />

De to metoder i Figur er en slags spøgelsesmetoder, som blot erklærer de virtuelle metoder<br />

uden at definere dem. Dette har to konsekvenser:<br />

• den klasse, som indeholder rene virtuelle metoder, kan ikke instantieres, da klassen er<br />

funktionel inkomplet. En erklæring som<br />

Figur f;<br />

vil generere en oversætterfejl, fordi et kald til f.Tegn() ikke kan binde til en egentlig<br />

funktion. Dermed repræsenterer en klasse med rene virtuelle funktioner det abstrakte<br />

begreb ovenfor, og kaldes derfor for en abstrakt klasse. Abstrakte klasser bruges kun<br />

som baseklasser for andre klasser, og det er umuligt at skabe reelle objekter fra dem.<br />

• den klasse, der er nedarvet fra en klasse emd en eller flere rene virtuelle metoder vil<br />

automatisk overtage disse metoder. De skal enten erklæres og implementeres i den<br />

afledte klasse for at gøre klassen ikke-abstrakt, ellers vil de rene virtuelle metoder arves<br />

direkte i den afledte klasse og gøre denne abstrakt. I tidligere versioner af C++ (før 3.0)<br />

arvedes de rene virtuelle metoder ikke, hvorved programmøren var tvunget til enten at<br />

generklære dem som rene eller rent faktisk at implementere dem. Det har imidlertid vist<br />

sig, at de fleste afledte klasser af abstrakte baseklasser selv var abstrakte klasser, hvorfor<br />

funktioner af denne art nu arves direkte. Følgende kode var ikke tilladt i C++ 2.1:<br />

class X { public: virtual f (); };<br />

class Y : public X { };<br />

fordi Y ikke enten redefinerede f() som ren eller implementerede funktionen. I ANSI<br />

C++ og AT&T C++ 3.0 er disse erklæringer lovlige; klassen Y vil være abstrakt og<br />

indeholde den rene virtuelle funktion f().<br />

En abstrakt klasse repræsenterer dermed virkelighedens abstrakte begreber, altså koncepter, der<br />

ikke har en fysisk form. Det betyder ikke, at vi ikke kan arbejde med pointere til abstrakte<br />

datatyper, ganske som i den rigtige verden. Selvom en klasse er abstrakt, kan vi sagtens erklære<br />

en pointer til en forekomst af denne klasse. Når pointeren skal tildeles adressen på et objekt, skal<br />

det blot være en forekomst af en konkret klasse, nedarvet fra den abstrakte klasse.<br />

Hvis en afledt klasse ikke implementerer den rene virtuelle metode, den arver, bliver metoden<br />

en ren virtuel metode i den afledte klasse. Derved kan mange klasser nedefter i hierarkiet være<br />

abstrakte, hvilket finder mange paralleller i virkeligheden: Fugl nedarver fra Dyr,<br />

320 Polymorfe klasser 4.4


Sangfugl nedarver fra Fugl, men de er alle abstrakte klasser. Først, for eksempel, når<br />

Solsort arver fra Sangfugl har vi en konkret klasse.<br />

4.4.11 En løsning med virtuelle metoder<br />

Klasserne i Figur-hierarkiet kan nu omskrives med en abstrakt baseklasse, som indeholder<br />

virtuelle metoder, og som derved løser problemerne fra afsnit 4.3.10.<br />

// eksempel 4-14: en abstrakt figur-klasse<br />

class Figur {<br />

char* navn;<br />

protected:<br />

Farve f;<br />

bool vises;<br />

Koordinat k;<br />

public:<br />

Figur (const Koordinat&, const char*);<br />

Figur (const Figur&);<br />

virtual ~Figur () { delete navn; }<br />

void operator= (const Figur&);<br />

const char* figurNavn () { return navn; }<br />

const Koordinat& startPunkt () { return k; }<br />

virtual void Flyt (const Koordinat&);<br />

virtual void Tegn () = 0;<br />

virtual void Slet () = 0;<br />

};<br />

class Punkt : public Figur {<br />

public:<br />

Punkt (const Koordinat& k, const char* s)<br />

: Figur (k, s) { }<br />

virtual void Tegn ();<br />

virtual void Slet ();<br />

};<br />

class Linie : public Figur {<br />

Koordinat endepunkt;<br />

public:<br />

Linie (const Koordinat& a, const Koordinat& b,<br />

const char* s) : Figur (a, s) { endepunkt = b; }<br />

virtual void Flyt (const Koordinat&);<br />

virtual void Tegn ();<br />

4.4.10 Abstrakte klasser 321


virtual void Slet ();<br />

};<br />

Implementationen af metoderne i Figur er ækvivalente til de fra eksempel 4-3. De to afledte<br />

klasser erklærer de rene virtuelle funktioner Tegn() og Slet(), men indeholder ikke<br />

tildelings- eller kopi-konstruktør, da den underforståede kan bruges for den afledte klasse: der er<br />

ingen indirekte data. Flyt()-metoden nedarves fra Figur til Punkt, mens den omskrives i<br />

Linie, der også skal flytte sit endePunkt. Metoderne i Punkt og Linie er mage til<br />

eksempel 4-4 og 4-5. Lad os igen udvide med et Billede:<br />

// eksempel 4-15: virtualiseret version af eksempel 4-12<br />

class Billede : public Figur {<br />

Figur** liste;<br />

int antal; // antal Figurer i Billedet<br />

int kapacitet; // maksimum-kapacitet<br />

public:<br />

Billede (int a = 100) : Figur (Koordinat(0,0), "Billede")<br />

{ liste = 0, antal = 0, kapacitet = a; }<br />

Billede (const Koordinat& k, const char* s, int a = 100)<br />

: Figur (k, s) { liste = 0, antal = 0, kapacitet = a; }<br />

Billede (Billede&);<br />

virtual ~Billede ();<br />

virtual void Flyt (const Koordinat&);<br />

virtual void Slet ();<br />

virtual void Tegn ();<br />

Billede& Indsaet (Figur*);<br />

};<br />

Billede::~Billede () {<br />

for (int i = 0; i < antal; i++) delete liste [i];<br />

delete liste;<br />

}<br />

void Billede::Flyt (const Koordinat& k) {<br />

Figur::Flyt (k);<br />

for (int i = 0; i < antal; i++) liste [i]->Flyt (k);<br />

}<br />

void Billede::Tegn () {<br />

for (int i = 0; i < antal; i++) liste [i]->Tegn ();<br />

}<br />

322 Polymorfe klasser 4.4


void Billede::Slet () {<br />

for (int i = 0; i < antal; i++) liste [i]->Slet ();<br />

}<br />

void Billede::Indsaet (Figur* fig) {<br />

if (antal < kapacitet) liste [antal++] = fig;<br />

return *this;<br />

}<br />

Metoderne i Billede er ligefremme: de kalder iterativt alle indeholdte objekter med samme<br />

metodenavn. Et Billede med 10 Punkter, som modtager et kald til Tegn(), vil kalde<br />

Tegn()-metoden i alle 10 punkter. Den virtuelle destruktør sikrer, at alle de indeholdte objekter<br />

destrueres, hvis Billedet går ud af skop †<br />

. Den eneste metode, der ikke er defineret ovenfor er<br />

kopi-konstruktøren. Her er nemlig et problem, overvej følgende implementation:<br />

Billede::Billede (Billede& b) : Figur (b) {<br />

antal = b.antal;<br />

liste = new Figur* [kapacitet = b.kapacitet];<br />

for (int i = 0; i < antal; i++) liste [i] = b.liste [i];<br />

}<br />

Denne metode vil kopiere alle Figur-pointerne fra det ene Billede til det andet. Da der blot<br />

er tale om kopier af pointere, vil de to Billeders destruktører referere til de samme Figurer,<br />

når de deallokeres. Dermed deletes samme objekter to gange, hvilket er en alvorlig fejl. En<br />

anden fremgangsmåde, som er mere naturlig, når vi antager, at Billeder "ejer" de indeholdte<br />

objekter, er at kopiere dem. Men hvordan kopieres et polymorft objekt? Vi kan ikke skrive<br />

Billede::Billede (Billede& b) : Figur (b) {<br />

antal = b.antal;<br />

liste = new Figur* [kapacitet = b.kapacitet];<br />

for (int i = 0; i < antal; i++)<br />

liste [i] = new Figur (*b.liste [i]); // fejl:<br />

abstrakt<br />

}<br />

fordi allokeringen af en Figur er ulovlig, da den er abstrakt. Hvis vi på den anden side skriver<br />

alt andet end new Figur, vil tildelingen ikke være korrekt. En rimelig løsning er at skrive en<br />

polymorf metode i alle Figur-klasser, som skaber en kopi af det underforståede objekt:<br />

// eksempel 4-16: eksplicitte kopi-rutiner<br />

† Det er i dette eksempel Billede-klassens ansvar at deallokere sine "egne" objekter. Problemet med ejerforhold<br />

i container-klasser beskrives nærmere i afsnit 5.6.1.<br />

4.4.11 En løsning med virtuelle metoder 323


class Figur {<br />

// ...<br />

public:<br />

//<br />

virtual Figur* Kopier () const = 0;<br />

};<br />

Figur* Punkt::Kopier () const { return new Punkt (*this); }<br />

Figur* Linie::Kopier () const { return new Linie (*this); }<br />

Figur* Billede::Kopier() const { return new Billede(*this); }<br />

og derefter implementere Billedes kopi-konstruktør som følger:<br />

Billede::Billede (Billede& b) : Figur (b) {<br />

antal = b.antal;<br />

liste = new Figur* [kapacitet = b.kapacitet];<br />

for (int i = 0; i < antal; i++)<br />

liste [i] = b.liste [i]->Kopier ();<br />

}<br />

De afledte klasser kan blandes i en langt højere grad end ved lineær arv. Først og fremmest kan<br />

vi skabe polymorfe datastrukturer uden bekymringer, som i<br />

Output:<br />

void main () {<br />

const antal = 2;<br />

Figur* tegning [antal];<br />

tegning [0] = new Punkt (Koordinat (100,100), "Prik");<br />

tegning [1] = new Linie (Koordinat (0,0),<br />

Koordinat (50,50), "Streg");<br />

for (int i = 0; i < antal; i++)<br />

cout


afledt fra:<br />

// skab en kopi af figuren og flyt den lidt væk fra<br />

originalen<br />

Figur* skyggeEffekt (const Figur& fig) {<br />

Figur* f = fig->Kopier ();<br />

f->Flyt (Koordinat (-2,-2));<br />

return f;<br />

}<br />

// undersøg, om to figurer overlapper<br />

bool Overlap (const Figur& fig1, const Figur& fig2) {<br />

return fig1->startPunkt() == fig2->startPunkt ();<br />

}<br />

Sådanne datastrukturer og metoder er uafhængige af den egentlige type, på hvilken den arbejder,<br />

og kan følgelig behandle typer, som endnu ikke er skrevet. Det tillader, at et generisk bibliotek<br />

kan skrives sideløbende med et klassehierarki, hvilket især i større projekter giver hver<br />

programmør en meget stor frihed og sikkerhed.<br />

Polymorfi er givetvis det sværeste koncept at forstå for programmører, der arbejder med<br />

strukturerede metoder eller med proceduralt orienteret design. Og det er ikke nok blot at forstå.<br />

Selve programudviklingen skal anskues på en helt anden måde, fordi design af klasser og<br />

klassehierarkier ligger meget langt fra design af funktioner og datastrukturer under de gængse<br />

paradigmer. I <strong>kapitel</strong> 5 gennemgår jeg nogle retninglinier for, hvordan klasser bør designes og<br />

hvordan polymorfi skal - og ikke skal - anvendes. Der ligger en utrolig styrke i polymorfi, men<br />

metoden kan også bruges på forkerte måder. Et dårligt skrevet objekt-orienteret program kan<br />

være meget værre end et dårligt skrevet proceduralt orienteret program, fordi der er flere<br />

underforståede afhængigheder. Der er nogle vigtige regler for anvendelsen af især polymorfi, som<br />

gennemgås i afsnit 5.9.<br />

4.5 MULTIPEL ARV<br />

Indtil nu har vi kun arbejdet med klasser, som nedarver fra en enkelt baseklasse. C++ tillader som<br />

et af de få objekt-orienterede sprog at en klasse nedarver fra et vilkårligt antal baseklasser, en<br />

teknik, som kaldes flersidet eller multipel arv (engelsk multiple inheritance). Multipel arv er en<br />

fremgangsmåde, som lader en klasse overtage data og metoder fra mere end én baseklasse,<br />

hvorved karakteristika fra alle disse klasser bliver en del af den multipelt afledte klasse.<br />

Teknikken er i mange henseender en determinant for, om et system er ægte objekt-orienteret.<br />

Med multipel arv kan et klassehierarki opbygges mere modulært, fordi modulariteten i de enkelte<br />

klasser kan deles på flere niveauer. Egenskaberne i baseklasserne smelter sammen i den afledte<br />

klasse, som på den måde bliver langt simplere at skrive.<br />

Lad os fortsætte med fauna-eksemplet fra afsnit 4.3.1. I dette hierarki arver alle klasser fra en<br />

baseklasse, og den generelle træstruktur repræsenterer klasser af faldende generalitet nedefter. Vil<br />

4.4.11 En løsning med virtuelle metoder 325


vi udvide med en ny klasse finder vi blot en klasse i hierarkiet, som kommer vores nye begreb<br />

nærmest og arver fra denne. Den nye klasse specialiserer sig indenfor sit eget område i<br />

programmet og indgår i hierarkiet på linie med alle de andre.<br />

Hvor kommer multipel arv så ind i billedet? Man skulle tro, at arv fra en enkelt baseklasse er en<br />

tilstrækkelig teknik til opbygning af klassehierarkier. Brugbarheden af multipel arv skal<br />

imidlertid ses i sammenhæng med, at begreber normalt ikke er isolerede. Mange egenskaber ved<br />

klasser kan abstraheres i uafhængige sidespor, som rent funktionelt ikke er relaterede. Hvis vi<br />

således ønsker at udvide hierarkiet med egenskaber som truede dyrearter, kød- og planteædende<br />

dyr eller intelligensgrad finder vi, at de på ingen måde strukturelt kan sættes ind i det eksisterende<br />

hierarki. De går med andre ord på tværs af træet, som er opdelt efter familier og racer. De nye<br />

egenskaber har intet med familier og racer at gøre, og kan dermed ikke integreres uden at<br />

introducere redundans i programmet, hvilket nærmest er en synd i objekt-orienteret<br />

programmering.<br />

Figur 4-9: Multipel arv.<br />

Med multipel arv kan vi introducere en helt ny klasse, som beskriver for eksempel en truet<br />

dyreart, og nedarve både fra denne og fra en eksisterende klasse i hierarkiet. Datastrukturen, som<br />

beskriver træet ændres dermed, og kan beskrives som en acyklisk orienteret graf (dag), hvis<br />

opadgående afhængigheder kan gå på kryds og tværs af træet. For eksempel kan vi skrive to<br />

klasser, Koedaedende og Planteaedende, og lade de klasser, som beskriver kødædende<br />

dyr nedarve fra den første, de, som beskriver planteædende dyr fra den anden og sidst de, som<br />

både æder dyr og planter fra begge klasser - se figur 4-10.<br />

Denne opdeling tillader os ikke blot at abstrahere funktionalitet ud af sidebenene på<br />

eksisterende klassehierarkier, men giver os mulighed for at indkapsle denne funktionalitet et<br />

enkelt sted, med de dertil hørende fordele hvad angår fejlbehandling, vedligeholdelse osv.<br />

Derudover kan vi, som vi skal se, med polymorfe klasser under multipel arv arbejde med den<br />

baseklasse, som vi har brug for i en given situation og ignorere den anden sålænge det er<br />

nødvendigt. Vi kan således forestille os lister af planteædende dyr og lister af fugle, hvilket<br />

ingenlunde er det samme. Syntaksen for multipel arv er en simpel udvidelse af den eksisterende<br />

teknik. De baseklasser, som ønskes for en ny klasse skrives som en kommasepareret liste i<br />

erklæringen af klassen:<br />

class Afledt : Base1, Base2 { ...<br />

326 Multipel arv 4.5


Alle regler for public og private arv gælder også under multipel arv, så syntaksen kan<br />

også være<br />

class Afledt : public Base1, public Base2 { ...<br />

Klassen Afledt arver således fra både Base1 og fra Base2.<br />

Figur 4-10: Multipel arv i fauna-hierarkiet.<br />

4.5.1 Multipel arv med uafhængige baseklasser<br />

Det simpleste eksempel på multipel arv er en arvefølge, hvor den afledte klasse har to uafhængige<br />

klasser som baseklasser. Med uafhængige klasser menes, at der ikke eksisterer et typeforhold<br />

mellem de to klasser. Lad os udvide Figur-hierarkiet fra afsnit 4.4.11 med nogle nye<br />

egenskaber, som beskriver mere end blot figurer.<br />

Grafiske objekter forestiller et eller andet. Denne dimension af deres indhold kan vi beskrive i<br />

et par nye klasser, som beskriver de attributter i figurerne, som ikke åbenlyst fremgår af Figurklasserne.<br />

Denne løsning på en parallel udvidelse bevarer klassernes generalitet, så de grafiske<br />

objekter ikke får et specifikt, applikations-afhængigt indhold. Vi antager at ville skrive et<br />

program, som simulerer en stjernehimmel, og skal derfor bruge begreber som Stjerne og<br />

Planet, som skal være konceptuelt uafhængige af grafiske primitiver:<br />

// eksempel 4-17: et generelt himmellegeme<br />

class Himmellegeme {<br />

protected:<br />

4.5 Multipel arv 327


char* navn; // himmellegemets navn<br />

int dist; // distance fra solen<br />

double magnitude; // lysstyrke<br />

public:<br />

Himmellegeme (char*, int, double&);<br />

~Himmellegeme () { delete navn; }<br />

const char* operator char* () { return navn; }<br />

};<br />

// konstruktør for himmellegemer<br />

Himmellegeme::Himmellegeme (char* n, int d, double& m) :<br />

dist (d), magnitude (m) {<br />

navn = new char [strlen (n) + 1];<br />

strcpy (navn, n);<br />

}<br />

// en stjerne-klasse<br />

class Stjerne : public Himmellegeme {<br />

double parallax; // relativt fra solplanet<br />

public:<br />

Stjerne (char* n, int d, double& m, double& p) :<br />

Himmellegeme (n, d, m), parallax (p) { }<br />

~Stjerne ();<br />

long Temparatur (); // udregnet i Kelvin<br />

// ... andre stjerne-relaterede metoder ...<br />

};<br />

// en planet-klasse<br />

class Planet : public Himmellegeme {<br />

double sideral; // omløbstid i forhold til Jorden<br />

double density; // i gram/kubikcentimeter<br />

public:<br />

Planet (char*n, int d, double& m, double& r, double& y) :<br />

Himmellegeme (n, d, m), sideral (r), density (y) { }<br />

double EscapeVelocity (); // udregnet i km/s<br />

// ... andre planet-relaterede metoder ...<br />

};<br />

Himmellegeme-hierarkiet består af en ikke-abstrakt baseklasse, som indeholder data om<br />

legemets navn, distance fra Solen (km × 10 4<br />

) samt dets lysstyrke. Afledt fra Himmellegeme<br />

udbygger de to klasser Stjerne og Planet med egne data, nemlig henholdsvis stjernens<br />

trigonometriske parallakse i forhold til solplanet og planetens omløbstid om solen i dage samt<br />

dens tæthed i gram pr. kubikcentimeter.<br />

328 Multipel arv 4.5


Figur 4-11: Himmellegeme-delhierarki<br />

Klasserne kan instantieres i objekter efter de normale regler:<br />

Planet IV ("Mars", 227.94, -2.0, 686.98, 3.94);<br />

Planet II ("Venus", 108.21, -4.4, 224.701, 5.25);<br />

Stjerne CenA ("Alpha Centauri", 4.3, 4.4, 0.751);<br />

Stjerne Ori ("Betelgeuse", 650, -6.0, 0.005);<br />

Med to isolerede klassehierarkier som Figur og Himmellegeme, der hver beskriver helt<br />

urelaterede koncepter, kan vi ved hjælp af multipel arv skabe nye afledte klasser, som blander<br />

kvaliteterne fra de to eksisterende. Figur, som er den grafiske repræsentation af et objekt på<br />

computerskærmen, og Himmellegeme, som er den matematiske repræsentation af et objekt på<br />

nattehimmelen, skal ikke modificeres for at foretage denne udvidelse:<br />

class GrafiskStjerne : public Punkt, public Stjerne {<br />

// ... ingen ny data ...<br />

public:<br />

GrafiskStjerne (char* n, int d, double& m, double& p)<br />

: Stjerne (n, d, m, p),<br />

Punkt (Koordinat (sin (p) * d, cos (p) * d)) { }<br />

};<br />

class GrafiskPlanet : public Punkt, public Planet {<br />

// .. ingen ny data ...<br />

public:<br />

GrafiskPlanet (char* n, int d, double& m, double& r,<br />

double& y) : Planet (n, d, m, r, y),<br />

Punkt (Koordinat (sin (p) * d, cos (p) * d)) { }<br />

};<br />

4.5.1 Multipel arv med uafhængige baseklasser 329


De to ny klasser GrafiskStjerne og GrafiskPlanet beskriver begge et astronomisk<br />

objekt, som - omend i teorien - har en grafisk repræsentation på computerskærmen. Det er<br />

bemærkelsesværdigt, hvor lidt klasserne indeholder i forhold til, hvor stor funktionaliteten er. Når<br />

en GrafiskStjerne instantieres, udregnes - stadig i teorien - stjernens position på skærmen,<br />

som skal svare til positionen på nattehimmelen. Således kan eksisterende klasser som Punkt<br />

udbygges med sideløbende og uafhængige klasser, uden at de behøver den mindste smule<br />

modifikation.<br />

Der er en vigtig læresætning her: vær ikke bange for at introducere nye klasser. Begrebet<br />

dataabstraktion kan desværre have den utilsigtede virkning på nytilkommere til det objektorienterede<br />

paradigme, at en klasse skal være ren og hel - næsten holistisk. Abstrakte datatyper<br />

giver et lidt ensidigt indtryk, fordi de minder så meget om de fundamentale typer vi kender og<br />

under alle omstændigheder ikke vil - eller kan - lave om på. Arv, og specielt multipel arv, viser,<br />

at en ny klasse blot er en udvidelse af programmet på en eller anden måde, og at denne udvidelse<br />

skal ses som en helt naturlig del af udviklingen. I store objekt-orienterede projekter kan findes op<br />

mod 500 klasser, hvoraf mange blot varetager en minimal funktion i forhold til hele programmet<br />

og på den måde klistrer dele af det sammen, så det er overskueligt og genbrugeligt. Se derfor ikke<br />

religiøst på en klasse som en isoleret og ubrydelig del af programmet, men som en byggesten, der<br />

kan raffineres. Når du bruger klasser i arv, skal du tænke mere på programmets opbygning og<br />

design end på brugeren.<br />

Figur 4-12: Multipel arv i Figur-hierarkiet.<br />

Et andet forhold ved arv, som specielt gør sig gældende under multipel arv, er nødvendigheden af<br />

330 Multipel arv 4.5


dummy-konstruktører, dvs. konstruktører, som blot videregiver parametre til baseklasserne uden<br />

selv gøre brug af disse. Det er nødvendigt at skrive disse kopier af baseklassernes konstruktører<br />

af den simple grund, at konstruktører ikke arves i C++. Under multipel arv kan der derudover<br />

være to konstruktører, som begge entydigt skal initieres. Dette kan kun foregå via en konstruktør<br />

i den multipelt afledte klasse.<br />

Den store fordel ved multipel arv af den art, vi har behandlet i dette afsnit er, at de afledte<br />

klasser får en flertydig funktionalitet. Både metoderne fra Figur-arvingerne og fra<br />

Himmellegeme-arvingerne bliver et umiddelbart indhold i GrafiskStjerne og<br />

GrafiskPlanet. Objekter af disse klasser kan således både slettes sig selv fra skærmen og<br />

udregne sine forskellige matematiske variable:<br />

GrafiskStjerne CenA ("Alpha Centauri", 4.3, 4.4, 0.751);<br />

CenA.slet (); // kalder Punkt::slet ()<br />

long t = CenA.Temperatur(); // kalder Stjerne::Temperatur()<br />

cout


...<br />

};<br />

void g () {<br />

C c;<br />

c.j = 1; // ok, c.A::j tildeles<br />

c.k = 1.0; // ok, c.B::j tildeles<br />

c.i = 0; // tvetydig, c.A::i eller c.B::i ?<br />

c.f(); // tvetydig, c.A::f() eller c.B::F() ?<br />

}<br />

Det er med andre ord ikke ulovligt at arve en tvetydighed, det er blot forbudt at gøre brug af den.<br />

Funktionen g() ovenfor kunne reddes med henvisning til baseklasserne ved reference til de<br />

tvetydige data:<br />

void g() {<br />

c.A::i = 0; // A::i i c-objektet tildeles<br />

c.B::i = 0; // B::i i c-objektet tildeles<br />

c.A::f (); // A::f() kaldes<br />

c.B::f (); // B::f() kaldes<br />

}<br />

Denne løsning er dog ikke særlig pæn, idet den forudsætter klientens kendskab til baseklassen.<br />

Den rigtige løsning er at undgå tvetydigheder i designet. Læg iøvrigt mærke til, at en erklæring<br />

som<br />

class Afledt : public Base, public Base { // ...<br />

er ulovlig. Det er umuligt at redde den åbenlyse tvetydighed i den dobbelte arv fra samme<br />

baseklasse med eksplicit reference til denne. En klasse må derfor kun arve fra en given baseklasse<br />

én gang.<br />

Jeg har nævnt, at den rigtige løsning på tvetydighederne er at undgå dem. Der er imidlertid<br />

situationer, hvor kildeteksten til implementationen af de klasser, der arves fra, ikke er tilgængelig.<br />

I de tilfælde er tvetydigheden ikke mulig at rette i den multipelt afledte klasse, men kan klares<br />

med introduktionen af to nye klasser, som blot redefinerer navnet på den eller de funktioner, der<br />

er tvetydige. Et rimeligt eksempel er to baseklasser, en grafisk med metoden Tegn() og en<br />

forsikringsklasse, der også indeholder en Tegn()-metode til tegning af forsikringer:<br />

class GrafiskKlasse {<br />

// ...<br />

public:<br />

// ...<br />

void Tegn ();<br />

};<br />

332 Multipel arv 4.5


class ForsikringsKlasse {<br />

// ...<br />

public:<br />

// ...<br />

void Tegn ();<br />

};<br />

Det er selvsagt ikke muligt for en ny klasse at aflede fra både GrafiskKlasse og fra<br />

ForsikringsKlasse uden at introducere en tvetydighed for metoden Tegn() i den nye<br />

klasse. Derfor skriver vi to andre nye klasser, som ligger mellem disse to og den nye multipelt<br />

afledte klasse:<br />

class GrafiskSubKlasse : public GrafiskKlasse {<br />

public:<br />

virtual void GrafiskTegn () = 0;<br />

void Tegn () { GrafiskTegn (); }<br />

};<br />

class ForsikringsSubKlasse : public ForsikringsSubKlasse {<br />

public:<br />

virtual void ForsikringsTegn () = 0;<br />

void Tegn () { ForsikringsTegn (); }<br />

};<br />

For en klasse, som arver fra både GrafiskSubKlasse og ForsikringsSubKlasse,<br />

skal metoderne GrafiskTegn() og ForsikringsTegn() implementeres, da de er rene<br />

virtuelle metoder. Da metoderne Tegn() redefineres i begge klasser, så de kalder de respektive<br />

virtuelle tegnemetoder, er tvetydigheden opløst. Den multipelt afledte klasse får nye navne for de<br />

to metoder. Teknikken kaldes omdøbning, og ophæver tvetydigheder og navnekonflikter ved at<br />

introducere nye klasser for hver tvetydig baseklasse, som indeholder en virtuel metode med det<br />

nye navn samt en omskrivning af den tvetydige metode. Den multipelt afledte klasse får således<br />

ingen problemer:<br />

class GrafiskForsikring : public GrafiskSubKlasse,<br />

public ForsikringsSubKlasse {<br />

public:<br />

virtual void GrafiskTegn (); // implementation af<br />

disse<br />

virtual void ForsikringsTegn (); // metoder<br />

};<br />

De mellemliggende klassers funktion er blot at undgå tvetydigheder, og klistrer således en<br />

baseklasse sammen med en afledt klasse, mens tvetydigheden ophæves med en omdøbning.<br />

4.5.2 Tvetydigheder ved multipel arv med uafhængige baseklasser 333


4.5.3 Multipel arv med relaterede baseklasser<br />

Hidtil har vi kun beskæftiget os med multipel arv i situationer, hvor baseklasserne ikke har haft<br />

noget med hinanden at gøre. Der er imidlertid mange anvendelser, specielt i dele af<br />

klassehierarkier, som implementerer meget ens klasser, hvor baseklasserne selv igen kan være<br />

afledt fra den eller de samme klasser. I en sådan situation kan den grafiske repræsentation af<br />

klassens arvefølge for eksempel se ud som i figur 4-13. Til spørgsmålet om, i hvilke situationer<br />

en sådan opbygning kan være nødvendig må svares, at en multipelt afledt klasse kan have<br />

baseklasser, som selv deler funktionalitet. Et eksempel på dette er en udvidelse af Figurhierarkiet<br />

med en ny klasse, som repræsenterer to uafhængige figurer, en linie og et punkt:<br />

// eksempel 4-18: multipel arv med urelaterede baseklasser<br />

class LiniePunkt : public Linie, public Punkt {<br />

public:<br />

LiniePunkt (Koordinat& s, Koordinat& e, Koordinat& x) :<br />

Linie (s, e), Punkt (x) { }<br />

void tegn () { Linie::tegn (); Punkt::tegn (); }<br />

void slet () { Linie::slet (); Punkt::slet (); }<br />

void flyt () { Linie::flyt (); Punkt::flyt (); }<br />

};<br />

Figur 4-13: Multipel arv med relateret baseklasse.<br />

Når der arves fra flere baseklasser, som<br />

igen har en fælles baseklasse, er der ofte en sikker kandidat for tvetydigheder, især for metoder<br />

eller data, som er erklæret i den fjerneste baseklasse. Derfor er multipelt afledte klasser fra<br />

sådanne baseklasser, som LiniePunkt, ofte behængt med dummy-metoder, som blot kalder<br />

videre bagud. I dette tilfælde er det klart, at LiniePunkt bliver nødt til at fortælle både<br />

Streg og Punkt hvad de skal gøre.<br />

334 Multipel arv 4.5


Arvetræet for LiniePunkt ser således ud:<br />

Figur 4-14: Multipel arv med urelaterede baseklasser.<br />

Klassen LiniePunkt arver<br />

altså to gange fra Figur, hvilket dermed godt kan lade sig gøre i andet eller højere led af arv. I<br />

dette tilfælde er det også ønskværdigt, da stregen og punktet i LiniePunkt skal være<br />

positions-uafhængige - de skal have hver deres Figur-objekt. Eksempler på brug af<br />

LiniePunkt:<br />

Output:<br />

void main () {<br />

Koordinat a (100,100), b (200,200);<br />

LiniePunkt skew (a, b, b - Koordinat (0, 100));<br />

skew.Flyt (-a);<br />

}<br />

Linie tegnet fra (100,100) til (200,200)<br />

Punkt tegnet på (200,100);<br />

Linie slettet fra (100,100) til (200,200)<br />

Punkt slettet på (200,100);<br />

Linie tegnet fra (0,0) til (100,100)<br />

Punkt tegnet på (100,0)<br />

4.5.4 Virtuelle baseklasser<br />

Ved multipel arv fra baseklasser, som har indbyrdes relationer via egne baseklasser, kan den<br />

gentagne instantiering af den øverste baseklasse gå hen og blive et problem. Et eksempel på dette<br />

er en udvidelse af LinkedList-klassen fra afsnit 3.10.1. Først foretager vi en mindre<br />

4.5.3 Multipel arv med relaterede baseklasser 335


modifikation i Node-klassen, så den tillader to hægter at blive sammenlignet med hinanden<br />

relationelt:<br />

struct Node {<br />

Node* next; // næste hægte<br />

virtual bool operator> (Node&) = 0; // sammenlign hægter<br />

virtual ~Node () { };<br />

virtual void printOn (ostream& = cout) = 0;<br />

};<br />

Klassen Node er nu en abstrakt klasse (to rene metoder) og har ydermere den egenskab, at den<br />

ikke har et dataelement! Ideen er, at vi arver fra Node, og udbygger med de data, vi har brug for.<br />

Node er ikke længere en privat klasse, da vi i dette eksempel ønsker, at klassen skal være visibel<br />

for klienten. LinkedList skal derfor også modificeres en smule, så den modtager og afsender<br />

Node-pointere:<br />

// eksempel 4-19: hægtet liste med polymorfe hægter<br />

class LinkedList {<br />

protected:<br />

Node* head;<br />

public:<br />

LinkedList () { head = 0; }<br />

~LinkedList ();<br />

void Insert (Node*);<br />

Node* Retrieve ();<br />

bool isEmpty () { return !head; }<br />

void printOn (ostream&) const;<br />

};<br />

// dealloker alle hægter hvis listen udgår af skop<br />

LinkedList::~LinkedList () {<br />

Node* cursor = head, *temp;<br />

while (cursor) {<br />

temp = cursor, cursor = cursor->next;<br />

delete temp;<br />

}<br />

}<br />

// indsæt en hægte i listen<br />

void LinkedList::Insert (Node* n) {<br />

n->next = head, head = n;<br />

}<br />

336 Multipel arv 4.5


hent en hægte fra listen<br />

Node* LinkedList::Retrieve () {<br />

Node* n = head;<br />

head = head->next;<br />

return n;<br />

}<br />

// udskriv en liste<br />

void LinkedList::printOn (ostream& os) const {<br />

for (Node* n = head; n; n = n->next) n->printOn (os);<br />

}<br />

Der er i ovenstående eksempel tale om en meget enkel implementation af en hægtet liste, som<br />

vi nu vil udbygge med arv for at øge funktionaliteten. To egenskaber, som hægtede lister ofte har,<br />

er indeksering af de individuelle hægter og sortering af alle hægterne. Vi nedarver således to nye<br />

klasser fra LinkedList: en sorteret hægtet liste, SortedList og en indekseret hægtet liste,<br />

IndexedList. De er begge ganske normale hægtede lister med en smule ændret karakter,<br />

nemlig for den førstes vedkommende en ordning i hægter i stigende orden (hvilket er grunden til<br />

ændringen i Node ovenfor) og for den andens vedkommende en mulighed for indeksering i<br />

listen med indeks-operatoren ([]).<br />

// eksempel 4-20a: en sorteret liste med polymorfe hægter<br />

class SortedList : public LinkedList {<br />

public:<br />

void Insert (Node*);<br />

};<br />

// Indsæt den ny hægte på sin rette plads<br />

void SortedList::Insert (Node* newnode) {<br />

Node* cursor = head;<br />

Node* track = 0;<br />

while (cursor && *cursor > *newnode) // find pladsen<br />

track = cursor, cursor = cursor->next;<br />

if (track) // indsæt hægten<br />

newnode->next = track->next, track->next = newnode;<br />

else newnode->next = head, head = newnode;<br />

}<br />

// eksempel 4-20b: en indekseret liste med polymorfe hægter<br />

class IndexedList : public LinkedList {<br />

4.5.4 Virtuelle baseklasser 337


public:<br />

Node& operator[] (int);<br />

};<br />

// find den rette hægte<br />

Node& IndexedList::operator[] (int index) {<br />

Node* cursor = head;<br />

while (index && cursor) cursor = cursor->next, index--;<br />

return cursor;<br />

}<br />

Med disse to ny klasser har vi et normalt træ, hvor en klasse er baseklasse til to afledte klasser.<br />

Sorterede hægtede lister er brugbare i sammenhænge, hvor rækkefølgen for eksempel skal være<br />

afhængig af hægtens værdi, mens indekserede hægtede lister er lettere at have med at gøre fra<br />

klientens side. En blanding af disse to klasser vil give det bedste fra begge i en enkelt klasse. Vi<br />

arver en sorteret, indekseret hægtet liste:<br />

// eksempel 4-21: en sorteret, indekseret polymorf liste<br />

class SortedIndexedList : public SortedList, // sorteret og<br />

public IndexedList { // indekseret<br />

public:<br />

void Insert (Node* n) { SortedList::Insert (n); }<br />

};<br />

Tvetydigheden ved arven af Insert()-metoden fra både LinkedList (via<br />

IndexedList) og SortedList reddes ved et videreført kald til SortedList i en<br />

omskreven funktion. Klassehierarkiet ser ud som i figur 4-15.<br />

Denne struktur demonstrerer imidlertid et stort problem ved multipel arv. Når en forekomst af<br />

SortedIndexedList instantieres, får SortedIndexedList to LinkedList-baser,<br />

og dermed to pointere til den første hægte i listen. Det giver en utilsigtet tvetydighed, fordi de to<br />

head-pointere ikke refererer den samme Node. Når objekter indsættes i en<br />

SortedIndexedList, foretages et videreført kald til SortedList::Insert(), som<br />

sætter hægten ind det rette sted. Hvis en hægte senere hentes med []-operatoren, vil kaldet<br />

foregå til den entydige metode operator[], som er nedarvet fra IndexedList.<br />

338 Multipel arv 4.5


Figur 4-15: Et tvetydigt baseobjekt i en hægtet liste.<br />

Da SortedList og IndexedList har hver deres LinkedList-baseklasser, og<br />

dermed hver deres head-pointere, vil indekseringen returnere en NULL-pointer. Problemet<br />

bliver endda tydeligere, hvis klienten forsøger at skrive<br />

SortedIndexedList sil;<br />

// ... Indsæt data i listen ...<br />

Node* np = sil.Retrieve (); // tvetydig<br />

fordi kaldet til SortedIndexedList::Retrieve() ikke kan identificeres som en entydig<br />

metode. Metoden er nedarvet fra både SortedList og IndexedList, som begge igen har<br />

arvet metoden fra LinkedList. Klienten bliver således nødt til at specificere hvilken<br />

Retrieve(), der menes ved for eksempel at skrive<br />

Node* np = sil.SortedList::Retrieve (); // entydig<br />

Det samme gælder for kald til isEmpty(). Problemet i en nøddeskal er, at der ikke må<br />

forekomme to LinkedList-subobjekter i en SortedIndexedList. Den ønskede<br />

orienterede acykliske graf for arvefølgen skal se ud som i figur 4-16. Hvis vi studerer figuren ser<br />

vi, at problemet ikke ligger i LinkedLists erklæring, men er et problem, der opstår i de<br />

afledte klasser. Løsningen skal altså findes ved at ændre i specifikationen af baseklasserne i de<br />

afledte klasser. I C++ er det muligt fra den afledte klasses synspunkt at erklære en baseklasse<br />

virtuel, hvilket betyder, at en eventuel dobbelt forekomst af den virtuelt erklærede klasse vil blive<br />

smeltet sammen i én forekomst. Med syntaksen<br />

class A { /* ... */ };<br />

class B : public virtual A { /* ... /* };<br />

fortæller vi oversætteren, at såfremt en klasse nedarver multipelt fra B og fra en anden klasse,<br />

4.5.4 Virtuelle baseklasser 339


som også er afledt direkte eller indirekte fra A, skal de to A-subobjekter være ét og samme<br />

objekt, hvis placering er fysisk identisk. Virtuelle baseklasser er sandsynligvis et af de sværeste<br />

koncepter umiddelbart at forstå, fordi en virtual-erklæring hverken har betydning for den aktuelle<br />

klasse eller for baseklassen, men kun for afledte klasser. I eksemplet ovenfor er det for både A og<br />

B ligegyldigt, om A erklæres som virtuel baseklasse i B eller ej. Kun eventuelle fra B afledte<br />

klasser bliver påvirket af erklæringen. Den primære grund til dette er, at det er A, problemet<br />

ligger i, og en klasse, der er afledt fra B skal ikke nødvendigvis vide noget om A.<br />

Figur 4-16: Et entydigt baseobjekt i en hægtet liste.<br />

For SortedIndexedList betyder dette således ikke, at vi skal erklære de to baseklasser<br />

SortedList og IndexedList som virtuelle baseklasser, men derimod erklære<br />

LinkedList som virtuel baseklasse i både SortedList og IndexedList. Dette har<br />

ingen betydning for forekomster af hverken LinkedList, SortedList eller<br />

IndexedList, som alle opfører sig som før, men for SortedIndexedList betyder det, at<br />

forekomster af denne klasse kun indeholder én LinkedList:<br />

class LinkedList { // ...<br />

class SortedList : public virtual LinkedList { // ...<br />

class IndexedList : public virtual LinkedList { // ...<br />

class SortedIndexedList : public SortedList,<br />

public IndexedList { // ...<br />

Af disse erklæringer kan det tydeligt ses, at den virtuelle egenskab af LinkedList som<br />

baseklasse er en funktion af arven og ikke af LinkedList selv. Det er nødvendigt at erklære<br />

baseklassen virtuel i alle afledte klasser, da C++ kun smelter objekter sammen, som er erklæret<br />

virtuelle alle steder. Hvis vi således kun erklærer LinkedList virtuel i SortedList, vil<br />

det ikke have nogen effekt.<br />

Det er muligt, og til tider ønskværdigt, at en afledt klasse både indeholder virtuelle og ikkevirtuelle<br />

forekomster af en baseklasse. Givet følgende erklæringer<br />

340 Multipel arv 4.5


class A { /* ... */ };<br />

class B : public virtual A { /* ... */ };<br />

class C : public virtual A { /* ... */ };<br />

class D : public A { /* ... */ };<br />

class E : public B, public C, public D { /* ... */ };<br />

vil et E-objekt indeholde to A-subobjekter, et virtuelt nedarvet gennem B og C og et ikkevirtuelt<br />

nedarvet gennem D. En forekomst af E vil resultere i en struktur, som kan visualiseres i<br />

følgende skema †<br />

:<br />

B-objekt<br />

---------------<br />

B's og C's<br />

A-objekt<br />

C-objekt<br />

D-objekt<br />

---------------<br />

D's A-objekt<br />

E-objekt<br />

Virtuelle baseklasser er en avanceret metode til differentiering af klasser, som har forhold, der<br />

ikke kan beskrives med multipel arv alene. For SortedIndexedLists vedkommende<br />

betyder virtualiseringen af LinkedList automatisk, at metoderne Retrieve() og<br />

isEmpty() bliver entydige, fordi de arbejder på det samme objekt. Når klienten kalder<br />

metoden Retrieve(), vil der kun være én head-pointer i konteksten, og den rigtige Node<br />

vil blive returneret.<br />

Før vi kan bruge SortedIndexedList skal vi imidlertid erklære en ikke-abstrakt afledt<br />

klasse fra Node, som definerer data og implementerer de rene virtuelle metoder printOn()<br />

og operator>(). En god kandidat til hægteindhold er et struktur, der består af to relaterede<br />

værdier, for eksempel et navn og dets tilhørende værdi. Vi nedarver Symbol fra Node:<br />

// eksempel 4-22: konkretisering af hægte-klassen<br />

class Symbol : public Node {<br />

int value;<br />

String name; // klassen fra eksempel 3-7<br />

public:<br />

† C++ giver ingen garanti for, i hvilken rækkefølge de forskellige subobjekter af et multipelt afledt objekt ligger.<br />

Ligesom under simpel arv kan de forskellige nedarvede objekter ligge i vilkårlig rækkefølge, så programmer, som<br />

gør sig afhængige af en bestemt rækkefølge vil ikke være flytbare.<br />

4.5.4 Virtuelle baseklasser 341


Symbol (const char* s, int v) : name (s), value (v) { }<br />

virtual bool operator> (Node& n) { // sortér Symboler<br />

return ((Symbol&)n).value > value; // efter deres værdi<br />

}<br />

virtual void printOn (ostream& = cout) const;<br />

};<br />

void Symbol::printOn (ostream& strm) const {<br />

strm


hensyn til både multiple og virtuelle baseklasser. Initieringsrækkefølgen for et multipelt afledt<br />

objekt er således:<br />

1. Eventuelle virtuelle baseklasser, i den rækkefølge de findes ved en dybdeprioriteret,<br />

venstre-mod-højre-traversering af arvetræet. Initieringen af de virtuelle baseklasser<br />

starter med de klasser, der ligger nærmest den aktuelle klasse i træet.<br />

2. Ikke-virtuelle baseklasser i den rækkefølge, de er erklæret i den multipelt afledte klasses<br />

definition. Initieringsrækkefølgen har dermed intet at gøre med den rækkefølge,<br />

baseklasserne har i den afledte klasses konstruktørs initieringsliste.<br />

3. Eventuelle medlemsvariable i den multipelt afledte klasse, i den rækkefølge de er<br />

erklæret, som igen ikke har relation til konstruktørens initieringsliste.<br />

4. Det aktuelle objekts konstruktør eksekveres.<br />

Hvis konstruktøren ikke eksplicit indeholder baseklassernes og medlemsvariablenes navne i<br />

initieringslisten, vil disse blive initieret med et kald til den underforståede konstruktør. Som for<br />

lineær arv gælder reglerne også her rekursivt, hvilket gør, at de fire ovenstående punkter bliver<br />

prøvet for ethvert objekt, der initieres som del af et multipelt afledt objekt. Overvej følgende<br />

komplekse objekt E:<br />

class A {<br />

public:<br />

A () { cout


};<br />

class E : public C, public D {<br />

D ed;<br />

public:<br />

E () { cout


A::A()<br />

B::B()<br />

C::C()<br />

D::D()<br />

E::E()<br />

Når klienten skriver<br />

E e;<br />

bliver resultatet dermed ikke færre end 20 underforståede konstruktør-kald. Det er i denne<br />

sammenhæng vigtigt at påpege, at objekter af klasser som E ikke er egnede til vektorerklæringer.<br />

Skriver klienten for eksempel<br />

E e_vektor [5000];<br />

kaldes 100000 (hundrede tusinde) kontstruktør-metoder. Det er ikke så meget tidsforbruget, der<br />

gør sig gældende, da initieringen kun sker én gang, men langt mere, at designet er for komplekst<br />

til denne type instantiering. Det er en god idé at gennemgå ovenstående initieringsrækkefølge<br />

grundigt, da den har betydning for alle multipelt afledte objekter. I den forbindelse kan listen<br />

eventuelt læses bagfra, hvilket også omvender prioriteterne for initieringsrækkefølgen. Med<br />

kommentarer og indrykninger bliver det en del mere overskueligt at se, hvad der initieres:<br />

(20) E::E() // E-objektet<br />

(19) D::D() // E's D-medlemsvariabel<br />

(18) C::C() // D's C-medlemsvariabel<br />

(17) B::B() // C's B-medlemsvariabel<br />

(16) A::A() // B's A-medlemsvariabel<br />

(15) A::A() // C's A-baseobjekt<br />

(14) B::B() // D's B-baseobjekt<br />

(13) A::A() // B's A-medlemsvariabel<br />

(12) A::A() // D's A-baseobjekt<br />

(11) D::D() // E's D-baseobjekt<br />

(10) C::C() // D's C-medlemsvariabel<br />

(9) B::B() // C's B-medlemsvariabel<br />

(8) A::A() // B's A-medlemsvariabel<br />

(7) A::A() // C's A-baseobjekt<br />

(6) B::B() // D's B-baseobjekt<br />

(5) A::A() // B's A-medlemsvariabel<br />

(4) C::C() // E's C-baseobjekt<br />

(3) B::B() // C's B-medlemsvariabel<br />

(2) A::A() // B's A-medlemsvariabel<br />

(1) A::A() // C's og D's A-baseobjekt (virtuel)<br />

4.5.5 Initiering og nedbrydning af multipelt afledte objekter 345


Subobjekterne 19-12 udgør E's medlemsvariabel D, 11-5 er E's ene baseklasse D og 4-1 er E's<br />

anden baseklasse C. Læg mærke til forskellen mellem de to initieringer af D-subobjekterne,<br />

hvor den første (medlemsvariablen) instantierer et A-objekt (nr. 12), mens den anden ikke har<br />

den tilsvarende initiering, da der er tale om en virtuel klasse, som først initieres som nr. 1 under<br />

C-baseobjektet. Husk, at ovenstående initieringsrækkefølge er den omvendte virkelighed, da<br />

baseobjekter altid skal initieres før afledte objekter.<br />

Nedbrydning af multipelt afledte objekter foregår i den omvendte rækkefølge af<br />

konstruktionen, analogt med lineær arv. Det er en nødvendighed for korrekt oprydning i<br />

komplekse objekter, at destruktionen er en direkte spejling af konstruktionen.<br />

4.5.6 Typekonvertering under multipel arv<br />

Type-systemet har næsten samme funktion under multipel arv som under lineær arv. En pointer<br />

eller reference til en multipelt afledt klasse kan konverteres underforstået til en pointer eller<br />

reference til en af baseklasserne, mens det omvendte ikke er tilladt. Dermed kan kode, der<br />

arbejder på én bestemt klasse, stadig kaldes for multipelt afledte objekter af denne klasse:<br />

// ombyt to elementer i en indekseret liste<br />

void swap (IndexedList& list, int first, int second) {<br />

Node& temp = list [first];<br />

list [first] = list [second];<br />

list [second] = temp;<br />

}<br />

Ikke-medlemsfunktionen swap() arbejder på en reference til en IndexedList, men kan<br />

efter de normale regler kaldes med reference til alle klasser, som ligger i arvefølge under<br />

IndexedList, både enkelt og multipelt. Hvis funktionen kaldes med en forekomst af<br />

SortedIndexedList, bliver objektet underforstået konverteret til den IndexedList, som<br />

udgør et subobjekt i dette objekt:<br />

void f () {<br />

SortedIndexedList SymbolTable;<br />

SymbolTable.Insert (new Symbol ("første", 1));<br />

SymbolTable.Insert (new Symbol ("andet", 2));<br />

SymbolTable.Insert (new Symbol ("tredie", 3));<br />

swap (SymbolTable, 0, 2);<br />

}<br />

I dette fragment bliver objektet SymbolTable automatisk konverteret fra en<br />

SortedIndexedList til en IndexedList, fordi den første klasse er afledt fra den anden.<br />

Følgende skema viser, hvordan konverteringen fra SymbolTable-objektet til list-objektet<br />

sker i selve kaldet til swap():<br />

SymbolTable-objekt<br />

346 Multipel arv 4.5


┌─────────────────────────┐<br />

&SymbolTable ─────│ SortedIndexedList-delen │<br />

├─────────────────────────┤<br />

│ SortedList-delen │<br />

class Base {<br />

public:<br />

virtual operator== (Base& bp);<br />

};<br />

├─────────────────────────┤<br />

&list ────────────│ IndexedList-delen │<br />

├─────────────────────────┤<br />

│ LinkedList-delen │<br />

└─────────────────────────┘<br />

Efter konverteringenses kun den del af objektet, der ligger i IndexedLists skop.Desværre er<br />

det omvendte, dvs. underforstået konvertering fra baseklasse til afledt klasse, ikke tilladt, hvilket<br />

ville være yderst brugbart i en situation som denne:<br />

class Afledt : public Base {<br />

int x;<br />

public:<br />

virtual operator== (Afledt& ap); // fejl, afvigende<br />

};<br />

Med en sådan specifikation på en virtuel funktion, som modtager en parameter som øjensynligt<br />

bliver konverteret efter objektets type, vil det kunne lade sig gøre at skrive kode som<br />

Base& bp1 = *new Afledt;<br />

Base& bp2 = *new Afledt;<br />

if (bp1 == bp2) // ...<br />

hvilket ville løse en del af de problemer, man kommer ud for, når objekter af forskellige typer<br />

skal blandes sammen. I metoden Base::operator==() ville parameteren have den korrekte<br />

type, og skulle ikke behøve en tvungen konvertering. Hvis det var tilladt, ville det gøre livet<br />

lettere i situationer, hvor virtuelle funktioner er klassebestemte og kræver parametre af den type,<br />

som den aktuelle metode befinder sig i. Desværre er det ikke tilladt, da større og værre problemer<br />

bliver det uafvigelige resultat:<br />

Base& bp1 = *new Afledt;<br />

Base& bp2 = *new Base1;<br />

if (bp1 == bp2) // ...<br />

Her kaldes metoden Afledt::operator==() med et baseobjekt, som, hvis koden var<br />

4.5.6 Typekonvertering under multipel arv 347


legal, vil blive konverteret til et afledt objekt. Problemet er selvfølgelig, at der ikke er et afledt<br />

objekt at arbejde på i denne situation - parameteren er en forekomst af Base, og baseobjekter<br />

kan som bekendt ikke typekonverteres til afledte objekter - så den konverterede pointer vil ikke<br />

pege på definerede data:<br />

┌─────────┐<br />

bp2 ───────────────│ Base1 │<br />

└─────────┘<br />

ap ──────────────── ?<br />

I tilfælde, hvor man med sikkerhed ved, at et bestemt objekt er af en bestemt type, kan problemet<br />

løses med tvungen konvertering. Er vi for eksempel sikre på, at Afledt::operator==()<br />

kun kan kaldes med forekomster af Afledt, kan vi implementere denne metode som<br />

int Afledt::operator== (Base& b) { // en virtuel funktion<br />

return ((Afledt&)b).x == x; // tvungen konvertering<br />

}<br />

Det er vigtigt at behandle multipelt afledte objekter med forsigtighed, når der opereres med<br />

tvungne konverteringer, fordi C++ ikke vil advare om konverteringer, der resulterer i en reference<br />

til et ikke-eksisterende objekt eller subobjekt. Den ovenstående funktion vil derfor, hvis den<br />

kaldes med en Base-forekomst komme i alvorlige vanskeligheder. I afsnit 5.8 beskrives,<br />

hvordan større klassehierarkier kan indeholde en vis form for eksplicit, programmørkontrolleret<br />

type-check, for at give klienten større frihed i blandingen af typerne.<br />

4.5.7 Multipel arv og polymorfi<br />

Virtelle funktioner spiller en mere kompleks rolle i forbindelse med multipelt afledte objekter,<br />

fordi referencen til det polymorfe objekt ikke altid er den samme. Under multipel arv kan et<br />

virtuelt kald resultere i et faktisk metodekald, som ligger udenfor den naturlige arvefølge i<br />

klassehierarkiet, og som derfor skal designes mere forsigtigt. Eksemplet med Stjerne og<br />

Punkt fra afsnit 4.5.1 kan for eksempel udbygges med metoder til statisk udlæsning af de<br />

enkelte objekters interne data for fejlfinding, lagring eller andet. En sådan metode bør være<br />

virtuel, da klienten selv bør bestemme hvilken baseklasse, der skal arbejdes med under egne<br />

forudsætninger. Vi erklærer således metoden<br />

virtual void Udskriv () const;<br />

i public-delen af klasserne Punkt, Stjerne, Planet, GrafiskStjerne og<br />

GrafiskPlanet, og implementerer dem som følger:<br />

// eksempel 4-23: multipel arv med virtuelle funktioner<br />

void Punkt::Udskriv () {<br />

348 Multipel arv 4.5


cout


Tæthed: 5.25 gcm3]<br />

Punkt [(-6,6)]<br />

Stjerne ["Betelgeuse"<br />

Distance: 650 lysår<br />

Magnitude: -6<br />

Parallax: 0.08333]<br />

De to kald til Udskriv() foretages med reference til to forskellige polymorfe objekter. Det<br />

interessante er, at der i kaldet til Planet::Udskriv() forekommer et underforstået kald til<br />

Punkt::Udskriv(), og at der i kaldet til Punkt::Udskriv() forekommer et<br />

underforstået kald til Stjerne::Udskriv(). Som det fremgår af koden er de to klasser<br />

Planet og Punkt uden direkte relation, ligesom Punkt og Stjerne heller ikke har noget<br />

med hinanden at gøre.<br />

Multipel arv blandet med virtuelle metoder tillader altså en afledt klasse at binde et metodekald<br />

på tværs af arvetræet. En vigtig konsekvens at dette er, at multipel arv kan udnyttes på en måde,<br />

som binder eksisterende klasser sammen ikke blot i indhold, men også i funktion. Et glimrende<br />

eksempel på dette er definitionen på en adjacency matrix (incidensmatrice), som bruges til<br />

implementering af orienterede grafer. En incidensmatrice er en "liste af lister", og er visualiseret i<br />

figur 4-19.<br />

Figur 4-18: Virtuelle funktioner som "klister" mellem baseklasser.<br />

En graf består af et antal knuder, som er forbundet med kanter. En implementation af en graf<br />

med generelle hægtede lister (som vi har behandlet i de forrige afsnit) indeholder knuderne som<br />

en liste, der igen hver især indeholder en liste af referencer til de andre knuder. De primære<br />

knuder (gråtonede felter i figur 4-19) repræsenterer knuder (en knude kaldes en edge på engelsk)<br />

mens sublisterne (de hvide felter) repræsenterer kanterne (en kant kaldes en vertex på engelsk)<br />

mellem grafernes knuder. Normalt indeholder en kant i en computerrepræsentation af en graf en<br />

numerisk identifikation af en headnode, men af to væsentlige årsager bruger den følgende<br />

implementation pointere til headnodes i stedet: For det første er det mere effektivt og for det<br />

andet strider indeksreferencer mod enkle hægtede listers natur, da de jo netop bygger på<br />

pointerbegrebet.<br />

Vi begynder med at arve en ny abstrakt klasse fra Node, som blot beskriver en headnode eller<br />

350 Multipel arv 4.5


en knude i grafen. Berettigelsen for denne klasse, som ikke indeholder væsentlige udbyggelser<br />

over Node, bliver klar senere. Foreløbig er klassens funktion at relatere knuder med hægter.<br />

// eksempel 4-24a: en abstrakt "incidenshægte"<br />

class AdjacencyNode : public Node {<br />

public:<br />

AdjacencyNode (Node* n = 0) : Node (n) { }<br />

virtual void printOn (ostream& = cout) = 0;<br />

};<br />

Figur 4-19: En incidensmatrix implementeret med hægtede lister.<br />

4.5.7 Multipel arv og polymorfi 351


Den næste klasse definerer en headnode. I figur 4-19 kan det ses, at en headnode (de gråtonede<br />

felter i figur 4-19) både indeholder en pointer til en første hægte i en ny liste samt til en næste<br />

hægte i en eksisterende liste. De er dermed både hægter og hægtede lister, og skal derfor afledes<br />

multipelt fra AdjacencyNode og LinkedList:<br />

// eksempel 4-24b: en headnode, en egentlig knude i grafen<br />

class HeadNode : public AdjacencyNode, public LinkedList {<br />

friend Edge;<br />

// her indsættes grafens egentlige data, nu blot et navn<br />

const char* name;<br />

public:<br />

HeadNode (const char* n) : name (n) { }<br />

virtual void printOn (ostream& = cout);<br />

};<br />

// udskriv en headnode, både hægte og liste<br />

void HeadNode::printOn (ostream& strm) {<br />

strm


void Edge::printOn (ostream& strm) {<br />

strm


kan for eksempel repræsentere arvefølgen for fire klasser, hvor B og C er afledt fra A, og hvor<br />

D er multipelt afledt fra A, B og C eller fire punkter i det centrale København forbundet med<br />

ensrettede gader. Grafens incidensmatrix har en fysisk repræsentation som i figur 4-22 - den er<br />

mindre kompliceret end den ser ud til.<br />

Figur 4-22: En graf repræsenteret ved hægtede lister.<br />

Med vores nye klasser kan vi oprette en tilstødende liste med en LinkedList af<br />

HeadNoder, der igen indeholder hægter af typen Edge og udskrive dem:<br />

ostream& operator


Output:<br />

HeadNode* A = new HeadNode ("A");<br />

HeadNode* B = new HeadNode ("B");<br />

HeadNode* C = new HeadNode ("C");<br />

HeadNode* D = new HeadNode ("D");<br />

B->Insert (new Edge (A)); // B "peger på" A<br />

C->Insert (new Edge (A)); // C "peger på" A<br />

D->Insert (new Edge (A)); // D "peger på" A<br />

D->Insert (new Edge (B)); // - og B<br />

D->Insert (new Edge (C)); // - og C<br />

AdjacencyList.Insert (D); // opret selve grafen<br />

AdjacencyList.Insert (C);<br />

AdjacencyList.Insert (B);<br />

AdjacencyList.Insert (A);<br />

cout


virtual char* id () { return "B"; }<br />

};<br />

class C : public virtual A { };<br />

class D : public B, public C { };<br />

Hierarkiet ligner grafisk det fra figur 4-16. Nu forestiller vi os en implementation af forskellige<br />

behandlinger på forekomster af én af klasserne, C:<br />

void printID (C& c) {<br />

cout


ubelejligt.<br />

Hvis en klasse for eksempel implementerer en virtuel funktion, som udlæser objektets indhold<br />

på en strøm, er det en naturlig ting for funktionen som det første at kalde baseklassens<br />

udlæsningsmetoder. Er der mere end en baseklasse, skal de naturligvis alle kaldes, så hele<br />

objektet kan blive udlæst. Men hvis der forekommer en virtuel klasse et sted i de gentagne kald til<br />

baseklasserne, kan det ikke undgås, at den kaldes flere gange. For eksempel,<br />

class A {<br />

public:<br />

virtual void readOut () {<br />

cout


B<br />

A<br />

C<br />

D<br />

altså to gange A. Selv om klassen er virtuel, og således kun forekommer én gang i objektet, er<br />

det altså ingen garanti for, at det ikke kan optræde flere gange i en logisk sekvens af<br />

funktionskald eller lignende.<br />

Der er flere måder at løse dette problem på, men de er som sådan ikke støttet af C++. Enten<br />

kan en virtuel funktion, erklæret i den virtuelle baseklasse, bruges til kun at udlæse indholdet af<br />

den aktuelle klasse i det komplekse objekt, mens en anden virtuel funktion også udlæser<br />

baseklassens data. På denne statiske måde kan en multipelt afledt klasse kontrollere kaldene til de<br />

virtuelle klasser. En anden mulighed er at lade den virtuelle klasse huske, om den er blevet kaldt<br />

før, eventuelt med et katalog over this-pointere, der allerede er blevet behandlet. Den virtuelle<br />

funktion deles op i to, en "hovedindgang", som først sletter kataloget og så kalder den anden<br />

(eventuelt protected), der indsætter data i kataloget.<br />

4.6 OPGAVER TIL KAPITEL 4<br />

1. Udbyg eksempelet med himmellegemer fra afsnit 4.5.1, så planeternes måner også står i<br />

Planet-objekterne, og tillad, at nyopdagede måner kan tillægges objekterne med tiden<br />

(godt råd: brug LinkedList-klassen fra afsnit 3.10.1).<br />

2. Skriv tegne og slettemetoderne i Figur-hierarkiet, så de rent faktisk tegner de<br />

forskellige figurer grafisk på dit system.<br />

3. Skriv en polymorf kø-klasse og stak-klasse, som tillader indsættelse af objekter af<br />

forskellige typer (kig på LinkedList)<br />

4. Hvad skal der til for at lade en polymorf kø indeholde stakke af objekter? Eller<br />

omvendt?<br />

5. Beskriv alle tvetydigheder ved arv.<br />

7. Beskriv dynamisk binding. Prøv så at oversætte et kald til en virtuel funktion til C-kode<br />

(eller hvad du nu har lyst til), som eksplicit foretager den dynamiske binding.<br />

8. Udbyg Figur-hierarkiet med nye abstrakte klasser og multipel arv, således at der kan<br />

skrives funktioner, der arbejder på figurer af forskellige arter.<br />

9. Udbyg Figur-hierarkiet, så figurer kan ind- og udlæses på en strøm i<br />

standardbiblioteket.<br />

358 Multipel arv 4.5


4.7 REFERENCER OG UDVALGT LITTERATUR<br />

Rationalet for udvidelse af C++ med multipel arv er beskrevet i [Stroustrup 87]. Dynamisk<br />

binding i C++ beskrives af [Koenig 88]. [Lippmann 89] forklarer i detaljer om this, og<br />

hvordan den har forskellige betydninger i nogle versioner af sproget. Generelt om arv henvises<br />

også til litteraturen om Smalltalk og Ada.<br />

4.5.9 Problemer med virtuelle klasser 359


Programkonstruktion<br />

De mange facetter i et nyt paradigme og programmeringssprog er tit<br />

uoverskuelige og forvirrende. De nye teknikker til implementation af abstrakte<br />

datatyper, afledte klasser, polymorfi osv. giver anledning til en stor række<br />

spørgsmål om programkonstruktion, som ikke kan besvares med syntaktiske<br />

eksempler alene. Dette <strong>kapitel</strong> går i dybden med objekt-orienteret<br />

programmering og fokuserer på konstruktion af rigtige programmer under det<br />

ny paradigme.<br />

5.1 INTRODUKTION<br />

Programkonstruktion er en anden og mere bramfri betegnelse for programdesign. Denne bog<br />

behandler ikke design i den traditionelle og formelle forstand, men beskriver de<br />

problemstillinger, som programmering indebærer. En kort indføring i objekt-orienteret<br />

programdesign vil dog ikke være af vejen.<br />

Først kunne man stille det mere fundamentale spørgsmål, om hvad design i det hele taget er.<br />

Softwaredesign går videre end blot det programmeringstekniske (uden dog at udelukke dette) og<br />

fokuserer på problemer som programudvikling og -test, support og konfiguration, vedligeholdelse<br />

og dokumentation, administration samt mange menneskelige aspekter. Et program er ikke blot<br />

binære tal i computerens lager, det er også:<br />

• en platform for videre udvikling, en slags videnbase for den person eller virkomhed,<br />

som ejer det.<br />

• et produkt, som skal kunne forbedres, konfigureres og understøttes under hele dets<br />

levetid.<br />

• en intellektuel udfordring, som skal løses på den bedst mulige måde, med de færrest<br />

mulige ressourcer og på den kortest mulige tid.<br />

• et arbejdsredskab, som skal kunne forstås og anvendes af brugerne.<br />

360 Opgaver til <strong>kapitel</strong> 4 4.6<br />

5


Kort og godt overlever et design ofte både hardware- og softwarespecifikationerne på et problem.<br />

Seriøse applikationer bør designes før de programmers for at sikre, at budgetter overholdes og at<br />

de bliver færdige til tiden. Selv om et færdigt program løser den nødvendige opgave, er det ikke<br />

sikkert at programdesignet tillader mindre ændringer i koden uden at det bryder sammen. Det<br />

forhold, at definitionen på programmet og dets anvendelse kan ændre sig under<br />

udviklingsforløbet siger noget om, at man bør tænke fremad og arbejde dynamisk, så<br />

programmeringen og teknikken ikke bliver bindende for resultatet.<br />

Traditionel struktureret design lægger sig tæt op af det proceduralt orienterede paradigme, idet<br />

programmet anskues i del-problemer, som igen kan brydes op i mindre komponenter. Man<br />

arbejder top-down og fokuserer på programmets funktion fra start til slut. De mindste dele af<br />

nedbrydelsen bliver direkte til globale eller modulært indkapslede funktioner. Der anvendes<br />

desuden en teknik, der hedder data-flow analyse, som beskriver, hvordan data udveksles mellem<br />

de enkelte dele af programmet for at sikre konsistensen og berretigelsen af disse. Top-down<br />

filosofien er en direkte dekomposition af det enkelte program, og arbejder på den måde efter en<br />

model, som anskuer ethvert nyt program som en ny opgave, uden at indeholde muligheder for<br />

hverken teknisk eller konceptuelt at drage fordel af erfaringer fra tidligere projekter. I det hele<br />

taget er struktureret design en abstraktion fra det rent tekniske, idet der tages mest hensyn til<br />

udviklingsproblemer, konsistens og problemanalyse.<br />

Som sådan er strukturerede metoder ganske gode og bruges i vid udstrækning. De opgaver, der<br />

stilles under en struktureret udviklingsproces er, i grove træk, følgende:<br />

1. Analyse: Analysér programmets funktion og beskriv dets virkemåde fra start til slut i et<br />

tekstuelt design.<br />

2. Funktionel nedbrydning: Identificér programmets funktionelle bestanddele, og gentag<br />

denne proces indtil de atomiske (ikke-delelige) programstumper er fundet.<br />

3. Data-flow analyse: Identificér dataudvekslingen mellem de enkelte dele og skriv<br />

datastrukturer til at indeholde disse data.<br />

4. Implementering: Skriv funktioner til de enkelte dele, som behandler datastrukturerne<br />

samt modtager og afsender dem til hinanden som parametre og returværdier.<br />

Der er imidlertid nogle - omend ikke fatale - mangler ved en denne model, specielt når den<br />

sammenlignes med objekt-orienterede principper. For det første tages der meget lidt hensyn til<br />

genbrug af eksisterende kode og data, hvilket er den største udfordring i fremtidens udvikling.<br />

Problemerne, som skal løses, bliver næsten eksponentielt større, og med en ensartet, top-down<br />

fremgangsmåde bliver forbruget af ressourcer uacceptabelt. Som konsekvens af dette bliver<br />

metoden meget statisk i sin grundvold. Hvis programmets design behøver modifikation midt i et<br />

projekt, er det svært at konfigurere designet uden store kvaler.<br />

Grunden hertil er, at struktureret design resulterer i meget specialiserede kodestumper, som er<br />

svære at genbruge i nye programmer. De generelle dele af strukturelt designede programmer er<br />

sjældent meget mere end "klister-funktioner", som indeholder et antal kald til mere specielle<br />

funktioner, men som ikke selv indeholder egentlig funktionalitet. Ved at ændre på disse<br />

5.1 <strong>Introduktion</strong> 361


funktioner opnår man blot en ombytning af sekvensen i programmet. Nu ville det være<br />

vidunderligt, hvis jeg kunne udnævne objekt-orienteret design til at være redningen. Desværre er<br />

formel objekt-orienteret design på et meget tidligt stadie, og er genstand for megen diskussion og<br />

forskning. Min fortolkning af dette er, at der i dag ikke findes reelle metoder til objekt-orienteret<br />

design. Selv om litteraturlisten indeholder glimrende forslag, er ingen af dem bredt accepterede<br />

endnu. Der findes for eksempel ingen standarder for klassebiblioteker eller design af<br />

klassebiblioteker. Derfor vil jeg fokusere på programkonstruktion på en programmeringsteknisk<br />

baggrund, og prøve at løse nogle af design-problemerne med eksempler på, hvordan det objektorienterede<br />

paradigme giver os nogle guldkorn at arbejde med.<br />

Under objekt-orienteret programkonstruktion bør man gå frem efter følgende retningslinier:<br />

1. Dekomposition: Analysér programmets nøglekoncepter, uden hensyntagen til<br />

eksekveringssekvens eller data-flow. Koncepterne er programmets byggestene og er<br />

analoge til dets klasser.<br />

2. Specialisering: Identificér klassernes attributter, dvs. indhold af data, samt deres<br />

opførsel, dvs. deres grænseflader til omverdenen.<br />

3. Differentiering: Identificér klassernes indbyrdes relationer og beskriv disse ved arv eller<br />

instantiering.<br />

4. Rekomposition: Identificér eksisterende klasser, som kan genbruges som baseklasser<br />

eller instanser.<br />

Denne model (som bliver uddybet i afsnit 5.9) er fundamentalt anderledes fra den strukturerede<br />

på to punkter: den følger ikke en vertikal, funktionelt nedbrydende sekvens og den indeholder<br />

store muligheder for genbrug af eksisterende kode. Uanset hvor langt objekt-orienteret design er<br />

kommet i dag, er det et faktum, at der ikke er tale om top-down dekomposition. Det kan heller<br />

ikke siges at være en bottom-up model, da der arbejdes med både abstraktion, konception og<br />

baseklasser. En god definition findes faktisk ikke, men jeg er tilbøjelig til at kalde udviklingen for<br />

here-and-there, fordi modellen afspejler en ikke-sekventiel udvikling. Objekt-orienterede<br />

programmer udvikles ved en inkremental, iterativ proces, hvor de enkelte bestanddele vokser,<br />

som de har brug for. De fire ovennævnte punkter skal altså ikke ses som en opskrift, men som fire<br />

uafhængige retningslinier, som optræder igen og igen under designet. De to væsentligste<br />

betydninger af en sådan udviklingsproces er, at<br />

1. jo mere eksisterende kode, der kan genbruges, des mere tid kan der bruges på<br />

arbejdsområder som design, udvikling og programmering. Objekt-orienterede<br />

programmer er tillige lettere at vedligeholde på grund af deres opbygning med<br />

dataskjulning, implementationsuafhængighed og klare grænseflader. Tiden kan bruges<br />

på mere spændende og produktive opgaver.<br />

2. inkrementaliteten muliggør en flydende transition fra prototype til færdig applikation.<br />

Det er til alle tider muligt at ekstrahere programmets byggestene og skabe et kørende<br />

362 <strong>Introduktion</strong> 5.1


program, der kan testes. De ofte benyttede "milepæls-versioner" af programmer mister<br />

deres betydning som del af designet.<br />

I projekter med mange programmører giver objekt-orienteret programmering meget store<br />

fordele i form af privatisering af data. Anskues hver programmør som en klient for alle de andre,<br />

og skjuler hver programmør sine egne implementationsdetaljer fra klienterne, er de alle så at sige<br />

teknisk uafhængige af hinanden, så længe der findes en veldokumenteret protokol til<br />

kommunikation mellem programmets klassebestanddele. I sprog som C++ er denne teknik en<br />

integreret del af sproget som sikrer helt ned i koden, at ingen kan få våde fødder i andres<br />

domæner.<br />

Objekt-orienteret programmering er en teknik, som kan gøre gode programmører brilliante,<br />

men dårlige programmører elendige. Det bunder i, om hele paradigmet er forstået, og ikke blot<br />

den syntaks, der udgør et bestemt sprog. Problemet med softwaredesign er, at et design alt for<br />

ofte bliver forældet, fordi det ikke opdateres gennem og efter projektets udførelse. Programmører<br />

er mere interesserede i at programmere, og administratorer er mere interesserede i at få et færdigt<br />

produkt. Som værktøj giver objekt-orienterede systemer en usynlig gevinst i denne sammenhæng,<br />

fordi dataskjulning og indkapsling af kompleksitet kan tillade, at der skrives program før design<br />

uden at det har en fatal effekt på projektets succes.<br />

5.2 HVORNÅR SKAL MAN BRUGE OBJEKTER?<br />

Det ses ofte, at et paradigmeskift leder til et uhensigtsmæssigt programdesign, fordi de nye<br />

begreber ikke kan ses i sammenhæng med gængse metoder. Der er således tidspunkter, hvor<br />

objekter er meget brugbare og tidspunkter, hvor de ikke har så megen relevans. Når man tager et<br />

nyt begreb som OOP op, er det nødvendigt at kunne skelne mellem de fordele, som de nye<br />

muligheder giver og de gamle metoders effektivitet. Objekter har deres store fordel i<br />

modularisering af data på en konsistent, sprog-understøttet facon, og er ikke en universalløsning<br />

for alle problemer.<br />

5.2.1 Overvejelser om klasser<br />

Hvordan skal man stille sig overfor et problem, når man har klasser som redskab? Den enkleste<br />

måde er at følge de fire retningslinier fra introduktionen i dette <strong>kapitel</strong>, men der findes også nogle<br />

"læresætninger", som letter forståelsen af, hvornår programmering med klasser gavner designet.<br />

Brug klasser til repræsentation af virkeligheden.<br />

Klasser er perfekte til en direkte implementation af de begreber, som programmets byggestene<br />

repræsenterer. Ved at modellere programmet med klasser, som kan anskues analogt til den<br />

abstrakte, eksterne forståelse af dem, bliver programmets opbygning let at forstå, og designet<br />

5.1 <strong>Introduktion</strong> 363


eflekteres direkte i koden. Som beskrevet i afsnit 3.3 gør klassernes egenskaber i form af<br />

indkapsling af både data og metoder, at de kan have samme mening både logisk og fysisk som<br />

den virkelighed, de repræsenterer. Klassens data er virkelighedens fysiske attributter, mens dens<br />

metoder er virkelighedens opførsel.<br />

Brug klasser til at skjule kompleksitet.<br />

Klasser indkapsler rent teknisk data og metoder, men i en større sammenhæng skjuler en klasse<br />

med mange data og mange metoder en del af programmets kompleksitet, og skaber en navngiven<br />

fællesnævner for denne. En opdeling af programmet efter dets enkelte bestanddele isolerer de<br />

dele af koden, som har - og i yderste konsekvens kun har - noget at gøre med én bestemt<br />

bestanddel. Resten af programmet kan overlade de nærmere detaljer til klassen, som altid er i<br />

stand til at udføre en opgave, der har relation til klassen selv. Den egenskab at kunne skjule data<br />

fra klienten, og dermed gøre ekstern kode uafhængig af klassens implementation giver klienten<br />

langt større frihed og langt mindre ansvar. Det er til enhver tid muligt at ændre på den enkelte<br />

klasses data, uden at dette har den mindste betydning for, om klientkoden kan oversættes. Dette<br />

betyder, at programmet behøver færre globale modifikationer, og at en fejl lettere kan isoleres til<br />

en bestemt klasse.<br />

Brug klasser til at skabe bedre grænseflader.<br />

En klasse er en udvidelse af reglerne for skop. Hver klasse kan have samme syntaks for<br />

relaterede metoder, som gør forståelsen for klassen i en større sammenhæng meget bedre. Uanset<br />

hvilken klasse der arbejdes med, kan metoder af samme navn altid være en fællesnævner for den<br />

samme operation på alle klasser. Således kan metoder for ind- og udlæsning have samme navne i<br />

alle klasser, hvilket gør det lettere at arbejde med dem. Den implementation af klasser, som C++<br />

indeholder, giver mulighed for en meget bred beskrivelse af klassernes grænseflader udadtil. De<br />

fleste datatyper kan med gavn gøre brug af brugerdefinerede overstyringer af standardoperatorerne,<br />

så klienten kan arbejde med naturlige udtryk i forbindelse med forekomster af<br />

klassen.<br />

Brug klasser til sikkerhed for initiering og oprydning.<br />

Et fundamentalt, programmeringsteknisk problem som datakonsistens er det nødvendigt at løse<br />

på en sikker måde, hvis programmet skal fungere korrekt. I objekt-orienterede systemer arbejdes<br />

ofte med såkaldte forudsætninger for og følgevirkninger af objekters instantiering, hvilket vil sige<br />

de usynlige operationer, der skal udføres før objektet kan tages i brug samt ved endt brug. I<br />

traditionelle, proceduralt orienterede programmer er sikkerheden for korrekt initierede og<br />

opryddede data dårlig, fordi det er klientens ansvar at kalde specielle funktioner til at udføre<br />

opgaverne. Ved brug af konstruktører og destruktører lægges dette ansvar over på klassens<br />

udbyder, som dermed kan være sikker på klassens datakonsistens.<br />

364 Hvornår skal man bruge objekter? 5.2


Brug klasser til et bedre udviklingsmiljø.<br />

En konsekvens af indkapsling, dataskjulning og udvidet kontrol med modulære data er, at<br />

udviklingsmiljøet bliver lettere at arbejde med. Projekter med mange involverede programmører<br />

drager mange fordele af et bedre udviklingsmiljø, fordi hver programmør kun har ansvar for sine<br />

egne klasser, dens indre data og metoder samt dens grænseflade til resten af programmet.<br />

Udviklingsmiljøet giver alle større produktivitet i og med, at de ikke er teknisk afhængige af<br />

hinanden. Det viser sig også, at programmering med klasser resulterer i langt mindre stumper af<br />

kode, som er lettere at overskue og arbejde med. Forståelsen for det enkelte programmodul,<br />

klassen, er meget højt. Kildeteksten bliver på den måde mere organiseret, både på grund af en<br />

mindre kompleksitet og også fordi polymorfi tillader isolering af kode, som arbejder på en<br />

bestemt klasse i samme modul eller fil. Alt dette har betydning for udviklingsmiljøet, som bliver<br />

mere overskueligt og gavner administrationen af et projekt.<br />

Brug klasser til genbrug af kode.<br />

Som nævnt i introduktionen er software-genbrug †<br />

den største udfordring i morgendagens<br />

programmering. Det nytter ikke længere, at hvert projekt opfattes som en ny opgave, som skal<br />

løses fra bunden. Hvis vi skal kunne gøre os forhåbninger om nogensinde at blive færdige med de<br />

kæmpesystemer, vi står overfor at udvikle, må vi i stedet anskue et projekt som en udvidelse eller<br />

modifikation af et tidligere projekt. Programmering med klasser og arv mellem klasser er en<br />

hjørnesten i OOP til at løse genbrugsproblemet på en generel måde. Findes en klasse, der udviser<br />

en funktionalitet, som minder om den ønskede, kan en ny klasse konstrueres og afledes fra denne,<br />

hvorefter forskellen kan udtrykkes ved omskrivning af de metoder, der afviger fra baseklassen til<br />

den ny afledte klasse. Denne teknik kaldes ofte for differentialprogrammering, fordi opgaven<br />

består i at differentiere én klasse fra en anden. Denne teknik åbner desuden nødvendigheden af<br />

generelle klasser, som har en funktionalitet, der er fællesnævner for et stort antal klasser i et<br />

program, men som ikke nødvendigvis selv skal bruges til noget konkret. Generelle klasser<br />

fungerer som byggesten i programmeringen, og gør udviklingen af en ny klasse lettere, fordi<br />

mange metoder allerede findes til den ønskede opgave. Skabeloner (afsnit 3.8) er en endnu mere<br />

tilgængelig mekanisme for direkte genbrug, fordi den parametiserer typer og ikke objekter.<br />

Brug klasser til udvikling af generisk kode.<br />

Som udvidelse af genbrugs-princippet ligger generisk kode som den funktionelle efterfølger.<br />

Generisk kode er funktioner, som ved udviklingen er skrevet med fremtiden i tankerne. En<br />

† Genbrug af kode skal forstås på den måde, at tidligere skrevne programmer og udarbejdede designs kan anvendes<br />

igen uden (væsentlige) problemer. Der sigtes ikke mod genbrug af kode, der er smidt væk - den engelske term er reuse<br />

og ikke re-cycling - kode, der er kastet bort, vil sjældent kunne genbruges, men rettere mod kode, der kan<br />

anvendes igen og igen i forskellige sammenhænge.<br />

5.2.1 Overvejelser om klasser 365


generisk funktion arbejder således med objekter, hvis egentlige typer ikke er kendt på det<br />

tidspunkt, funktionen oversættes. Det tillader os at udvide programmet med nye klasser, som kan<br />

indgå og bruges af de generiske funktioner uden at disse behøver modifikation. På den måde kan<br />

en stor del af et projekt skrives, så det ikke er afhængigt af bestemte typer, men overlader dette til<br />

oversætteren og miljøet. Generiske metoder er mulige via polymorfe metodekald, som i C++<br />

implementeres som virtuelle funktioner i klassers arvefølger. Her er det vigtigt at forstå, at<br />

polymorfi ikke er et redskab i selve klasserne, men en egenskab, de har overfor klientkoden.<br />

Polymorfi skal altid ses udefra, og når der skrives virtuelle funktioner er det vigtigt at tænke på<br />

klassens indhold, funktionalitet og grænseflade i forhold til både klienten og til de klasser, som<br />

skal nedarve fra den. Generisk kode repræsenterer således også genbrug, omend på et andet plan<br />

end arv.<br />

Brug klasser til modulærprogrammering.<br />

Modulærprogrammering er programmering med indkapslede data og funktioner i moduler, en<br />

teknik som sædvanligvis er baseret på filen som et modul. Det er dog de færreste sprog, som i sig<br />

selv understøtter moduler med en udvidet syntaks for skop-kontrol og adgang, der er nødvendig<br />

for, at alle holder sig til reglerne. Klasser er oplagte til en modulær opbygning af programmer,<br />

fordi klassens typenavn kan bruges direkte i koden som reference til det ønskede modul. Den<br />

umiddelbare fordel ved modulærprogrammering er, at globale data elimineres fra programmet.<br />

Globale data har den ulempe, at alle dele af programmet har adgang til dem, og at en fejl i disse<br />

data dermed er svær at lokalisere arnestedet for. Modulærprogrammering i C++ foretages oftest<br />

som udvikling af datastrukturer, der indeholder statiske data, og som derved sætter navn på de<br />

data, der i en given situation er tale om.<br />

Brug klasser til indkapsling af funktionsbiblioteker.<br />

Teknikken for modulærprogrammering kan bruges i forbindelse med paradigmeskift, så<br />

eksisterende funktionsbiblioteker kan integreres i det nye miljø. Et bibliotek som C's<br />

kan med fordel indkapsles i et antal moduler, som relaterer de forskellige metoder<br />

med et overordnet navn og et par eventuelle ekstra metoder og overstyringer af operatorer. På den<br />

måde får det eksisterende bibliotek mulighed for at overleve skiftet til OOP, og kan stadig bruges<br />

fornuftigt.<br />

Brug klasser til sikring af flytbarhed.<br />

Ved at indkapsle hardware-afhængigheder i modulære klasser, så klasserne selv har ansvar for<br />

maskinspecifikke operationer, sikrer de klientkodens flytbarhed fra en platform til en anden.<br />

Sålænge klienten støtter sig til klassen, når de sensitive funktioner skal kaldes, vil en ændring af<br />

366 Hvornår skal man bruge objekter? 5.2


operativsystem, skærmtype, printer, disk, processor osv. kunne beskrives i klassen uden fare for,<br />

at klientens kode bryder sammen. Dette er et meget illustrativt og konkret eksempel på<br />

nødvendigheden af implementationsuafhængighed.<br />

Brug klasser, når der er brug for mere end én forekomst.<br />

Den enkleste definition af, hvornår en ny klasse skal introduceres er, når et program viser<br />

symptomer på redundante datastrukturer eller lignende funktioner. Har et program for eksempel<br />

mere end én definition på en hægtet liste, mange behandlinger af streng-variable, sortering af data<br />

på forskellige måder eller blot et forhold mellem to dele af programmet, kan de næsten altid<br />

indkapsles i klasser. Før i tiden var klasser noget, der blev opfattet som meget store byggestene i<br />

programmerne, og der skulle meget til, før en del af programmet blev isoleret i en klasse. Faktisk<br />

blev langt de fleste C++-programmer essentielt skrevet som C-programmer med kun de største og<br />

vigtigste datastrukturer indkapslet med bredt definerede metoder. Mindre grupper af klasser giver<br />

imidlertid et objekt-orienteret system den egentlige funktionalitet, så vig absolut ikke tilbage for<br />

at introducere nye klasser selv for en minimal del af programmet.<br />

Vær kreativ, men ikke over-kreativ, med klasser.<br />

Programmering med klasser appellerer til programmørens kreativitet i langt højere grad end<br />

programmering med isolerede funktioner og datastrukturer. Idet grænsefladen mellem klassen og<br />

klienten kan beskrives i meget udførlige detaljer, er det klassedesignerens opgave at skrive denne<br />

så fyldestgørende som muligt. En sideeffekt ved dette er, at klasser ofte har alt for mange<br />

behandlende funktioner, specielt overstyrede operatorer, i forhold til, hvor meget de bruges<br />

udefra. Til gengæld giver de klienten frihed, fordi de under alle omstændigheder findes. En god<br />

retningslinie er, at kun de operationer, der er brug for, når klassen skrives, bør medtages i den.<br />

Resten kan tilføjes senere, hvis der er brug for dem.<br />

5.2.2 Fremgangsmåder<br />

Nu er spørgsmålet hvordan man udfra en idé kommer frem til et klassedesign. Klassens design er<br />

ikke blot afhængigt af, hvordan den opfører sig og hvilket indhold den har, men også hvad den<br />

egentlig repræsenterer. Der er visse kendetegn at se efter, men først skal der ridses to<br />

hovedområder op, som har betydning for opbygningen af klassen:<br />

• hvad er klassens attributter, eller med andre ord, hvad repræsenterer klassen? I mange<br />

situationer er dette spørgsmål ligegyldigt, fordi klassens data altid er startpunktet, men<br />

ofte kan en ekstern brug af klassen tilskynde en anden datarepræsentation. Klassen<br />

Koordinat fra afsnit 4.2.2 er et eksempel på dette, idet denne klasse ville kunne<br />

fungere fint med en intern repræsentation med heltal, men hvor en senere brug i<br />

5.2.1 Overvejelser om klasser 367


transformationer nødvendiggør kommatal.<br />

• hvad er klassens opførsel, eller med andre ord, hvordan skal klassen bruges set fra<br />

klientens side. Igen har det relation til klassens egentlige idé, da de metoder, som<br />

klassen udbyder, skal være nært forbundne med klassens repræsentation. Når man<br />

skriver medlemsfunktioner er det en god idé altid at have klientens program i<br />

baghovedet, og eventuelt skrive et lille test-program, som benytter klassen. Opførsler<br />

som returværdityper, parametertyper, underforståede parametre, konstante og statiske<br />

metoder samt overstyringer af operatorer er alle bestemmende for, hvor god klassen er i<br />

brug.<br />

Man kan sige, at klasser altid sætter funktion over form, fordi implementationen skjules fra<br />

klienten, som må gå gennem grænsefladen i stedet. Derfor er klassens attributter relateret til<br />

implementationen af klassens metoder, mens klassens grænseflade er relateret til klientens kode.<br />

Nu følger nogle retningslinier for, hvilke spørgsmål der gør sig gældende under konstruktionen af<br />

en klasse:<br />

• Repræsenterer klassen et fysisk eller et konceptuelt begreb? Konceptuelle begreber egner<br />

sig bedst som abstrakte klasser med rene virtuelle metoder og til polymorf ekstern brug.<br />

• Er klassen kendetegnet som aktiv eller passiv, dvs. er klassen en del af kernen i<br />

programmet, eller udfører den en simpel, generel funktion? Aktive klasser er som regel<br />

større og har tendens til at være fritstående - for eksempel en klasse, der behandler en<br />

seriel port - mens passive klasser er små og ofte indgår i et klassehierarki.<br />

• Hvad repræsenterer klassens indhold i forhold til programmet, der bruger klassen?<br />

Relevansen er, at en forekomst af en klasse kan have forskellige levetider, afhængig af,<br />

hvad den repræsenterer. Der findes tre fundamentale levetider for objekter: midlertidige,<br />

permanente og bestandige. Et midlertidigt objekt allokeres og fjernes dynamisk under<br />

kørslen, og tilskynder en lille, operationel klasse med hurtige konstruktører og<br />

destruktører. Permanente objekter er statisk allokeret, dvs. har sit skop fra programmets<br />

starter til det slutter, og der findes sædvanligvis kun ét objekt - eller en vektor af<br />

objekter - af en bestemt klasse. Permanante objekter giver dermed anledning til klasser,<br />

som kan have store konstruktører og destruktører, flere statiske medlemmer, og som har<br />

en eller få forekomster i applikationen. Bestandige objekter har en ubegrænset levetid,<br />

kun afbrudt af en bestemt operation i programmet. De eksisterer altså på et ydre lager,<br />

når applikationen ikke kører. Sådanne objekter har normalt en meget stor<br />

repræsentation, og indeholder metoder for ind- og udlæsning af data, som eventuelt er<br />

implementeret i konstruktøren og destruktøren. Bestandighed (eller persistens) har stor<br />

anvendelse i databasesystemer, og behandles i afsnit 5.6.<br />

• Repræsenterer klassen et helt eller et delt begreb? En hel (også kaldet afsluttet) klasse<br />

har for det meste en gennemført grænseflade, har ingen rene virtuelle funktioner og en<br />

public-erklæret konstruktør. En delt klasse har det meste af sin funktionalitet fra en<br />

368 Hvornår skal man bruge objekter? 5.2


aseklasse, og har indtil flere "søskende-klasser", som også er afledt fra denne base. Den<br />

delte klasse deler funktionalitet og/eller data med hinanden gennem baseklassen. I afsnit<br />

5.3.1 gennemgås nogle retningslinier for, hvordan man afgør om en klasse er delt eller<br />

ej.<br />

• Repræsenterer klassen et generelt eller et specifikt emne? En generel klasse bør<br />

indeholde (eventuelt rene) virtuelle funktioner, og ofte ikke mange datamedlemmer. En<br />

specifik klasse kan ofte arves fra en generel klasse, og udbygge/omskrive/implementere<br />

funktionerne i denne.<br />

• Hvem er klassens klient? Hvis klassen kun har én klient, for eksempel en anden klasse,<br />

bør den skrives som privat klasse. Klienten erklæres som friend i klassen, og der<br />

skrives ydermere en konstruktør, som ved at være privat eller beskyttet sikrer, at ingen<br />

andre end den ønskede klient kan instantiere objekter af klassen. Klassen Node fra<br />

afsnit 3.10.1 er et eksempel på en sådan klasse. Både klasser og specifikke funktioner i<br />

klasser kan erklæres som friend. På den måde kan en klasse skrives til en helt<br />

specifik opgave, som kun vedrører en bestemt funktion eller klasse, uden at skabe<br />

forvirring andetsteds. Klassen Node kan for eksempel ikke ses af LinkedLists<br />

klient, som slet ikke mærker Nodes eksistens.<br />

Med disse overvejelser kan vi opfylde de første generelle retningslinier for objekt-orienteret<br />

konstruktion, nemlig identifikation af klassens koncepter, dens repræsentation og grænseflade.<br />

5.2.3 Organisering af kildetekst<br />

Som beskrevet i afsnit 2.2 er udviklingsmiljøets faciliteter bestemmende for, hvor let udviklingen<br />

kan forløbe. Dog er den fysiske organisering af kildeteksten det første, der skal være i orden.<br />

Kildeteksten bør opdeles i flere moduler eller oversættelses-enheder, som normalt er det samme<br />

som filer på systemet. Et enkelt modul er logisk opdelt i to filer:<br />

• en specifikation, som indeholder klasseerklæringen, konstanter, typedefinitioner og<br />

eventuelle makroer og inline-metoder. Specifikations-filen, eller header-filen, som den<br />

også kaldes, medoversættes i alle andre moduler af programmet, som bruger netop dette<br />

modul.<br />

• en implementation, som indeholder koden for klassens metoder, initiering af statiske<br />

medlemsvariable og eventuelle lokale variable i modulet.<br />

Denne opdeling sikrer, at alle moduler, som benytter sig af et bestemt eksternt modul, alle vil<br />

følge samme specifikation. Dette gælder også for det eksterne moduls implementation selv. Et<br />

enkelt modul bør kun indeholde én klasse eller få meget tæt relaterede klasser. Tæt relaterede<br />

klasser er friend-klasser (som for eksempel String og Substring fra afsnit 3.7.1) eller<br />

private klasser, men ikke arvede klasser. Et modul, som benytter klasser, som findes i andre<br />

moduler, må ved hjælp af præprocessorens #include-direktiv indlæse specifikationen på disse<br />

5.2.2 Fremgangsmåder 369


klasser. Figur 5-1 viser, hvordan de fire klasser fra afsnit 4.5.3 opdeles i specifikationer (.h-filer)<br />

og implementationer (.cpp-filer). Stregerne mellem kasserne beskriver et #include-forhold<br />

opefter.<br />

De enkelte moduler i LinkedList-hierarkiet indeholder følgende direktiver og erklæringer<br />

(klassernes kroppe samt metodernes implementationer vises ikke):<br />

// linkedlist.h<br />

class Node { /* ... */ };<br />

class LinkedList { /* ... */ };<br />

// linkedlist.cpp<br />

#include "linkedlist.h"<br />

// indexedlist.h<br />

#include "linkedlist.h"<br />

class IndexedList : virtual public LinkedList { /* ... */ };<br />

// indexedlist.cpp<br />

#include "indexedlist.h"<br />

// sortedlist.h<br />

#include "linkedlist.h"<br />

class SortedList : virtual public LinkedList { /* ... */ };<br />

// sortedlist.cpp<br />

#include "sortedlist.h"<br />

// sortedindexedlist.h<br />

#include "indexedlist.h"<br />

#include "sortedlist.h"<br />

class SortedIndexedList :<br />

public IndexedList, public SortedList { /* ... */ };<br />

// sortedindexedlist.cpp<br />

#include "sortedindexedlist.h"<br />

370 Hvornår skal man bruge objekter? 5.2


Figur 5-1: Afhængigheder mellem moduler i LinkedList-hierarkiet.<br />

Erklæringerne i specifikationsfilerne indeholder alle klassernes metoder som prototyper samt<br />

eventuelle offentlige datamedlemmer. Klassens erklæring i specifikationsfilen bliver dermed<br />

udgangspunktet for den eksterne definition af dens brug for klienten. Ved at undersøge klassens<br />

specifikationsfil kan klitenten se, hvordan grænsefladen er bygget op, og kan dermed danne sig et<br />

indtryk af, hvordan klassen bruges. Dokumentationen af en klasse bør derfor være grundlag i<br />

klassens specifikation, og beskrive de enkelte dele i denne udførligt, hvilket behandles i nærmere<br />

detaljer i afsnit 5.10.<br />

En klasses specifikation bør derfor følge et ensartet mønster. De forskellige dele af klassens<br />

5.2.3 Organisering af kildetekst 371


erklæring bør opdeles i klart afgrænsede dele i kildeteksten, så det er muligt for en klient at se<br />

hvad, der er hvad. Det forhold, at klassens implementation skinner igennem i specifikationen ved<br />

private datamedlemmer og metoder gør, at der er en smule unødvendig information, som skal<br />

være let at filtrere fra. En opdeling kan tage udgangspunkt i de forskellige grænseflader, en klasse<br />

har:<br />

// klasse.h<br />

// --------- afhængigheder -----------------<br />

#include-direktiver<br />

// --------- ekstern specifikation --------class<br />

klasse {<br />

friend-funktioner<br />

friend-klasser<br />

public:<br />

konstruktør(er)<br />

destruktør<br />

konverteringsmetoder<br />

offentlige metoder<br />

// --------- reserveret specifikation -----protected:<br />

konstruktør(er)<br />

beskyttede metoder<br />

datamedlemmer<br />

// --------- intern specifikation ---------private:<br />

konstruktør(er)<br />

private metoder<br />

datamedlemmer<br />

};<br />

inline metodeimplementation(er)<br />

// klasse.cpp<br />

metodeimplementation(er)<br />

Ved at opdele klassen på ovenstående måde i selve kildeteksten kan en bruger af klassen finde de<br />

relevante oplysninger fra specifikationsfilen i en fart. De fire dele af specifikationen udgør<br />

henholdsvis:<br />

• Modulets eksterne afhængigheder, dvs. de specifikationsfiler, som det aktuelle modul<br />

enten arver fra eller benytter som forekomster, indirekte referencer, friend-<br />

372 Hvornår skal man bruge objekter? 5.2


funktioner eller direkte funktionskald. Den eksterne afhængighed fortæller noget om,<br />

hvad specifikationen bruger udover erklæringerne i den aktuelle klasse.<br />

• Klassens eksterne specifikation, dvs. den grænseflade, som den har mod klientens kode.<br />

Når klienten skal bruge en bestemt klasse, skal denne sektion altså refereres, og intet<br />

andet.<br />

• Klassens reserverede specifikation, dvs. den grænseflade, som den har mod afledte<br />

klasser. En klasseudbyder, som vil bruge den aktuelle klasse som baseklasse skal altså<br />

referere til denne sektion i specifikationen.<br />

• Klassens interne specifikation, dvs. klassens implementation af datamedlemmer og<br />

private metoder. Den interne specifikation og implementation har kun relevans for<br />

klassens udbyder, og er både en del af specifikationsfilen og implementationsfilen.<br />

I mange henseender kan der være flere klasser i samme modul, hvilket vil være tilfældet for<br />

klasserne String og Substring samt for Node og LinkedList. I andre sprog, som<br />

støtter modularisering i højere grad end C++, er det muligt fuldstændigt at skjule<br />

implementationen og den interne specifikation fra klienten, så denne slet ikke kan se de elementer<br />

af modulerne, som ikke er vedkommende. I C++ er en mulig løsning via operativsystemet at<br />

tillade skrive-adgang til et modul alene for den bruger, som er udbyder af klassen eller klasserne i<br />

modulet. Det er givet, at en stor udfordring indenfor CASE-teknologien er udvikling af redskaber<br />

til behandling af klassespecifikationer, så de kan lagres i en central database og indgå i<br />

udviklingsmiljøet.<br />

5.3 ORGANISERING AF KLASSER<br />

Isolerede klasser, og metoderne for udvikling af isolerede klasser, har størst anvendelse som<br />

abstrakte datatyper i enten numerisk form eller som parallel til de fundamentale typer, hvor de<br />

udnytter den første objekt-orienterede hovedingrediens, indkapsling, og drager fordel af<br />

implementationsuafhængigheder. I den større sammenhæng må klasserne imidlertid organiseres,<br />

så de udnytter de to andre basiselementer, arv og polymorfi, og realiserer genbrugelighed og<br />

udvikling af generisk kode. Men organisering af klasser handler ikke blot om arv og polymorfi.<br />

Klasser kan relateres til hinanden på mange forskellige måder, afhængigt af det forhold, som gør<br />

sig gældende mellem dem. En forståelse for klassernes afhængigheder af hinanden er en vigtig<br />

faktor i organiseringen af klasserne.<br />

5.3.1 Overvejelser om arv<br />

Ét er altså hvornår, der skal bruges klasser, et andet, hvornår der skal bruges arv. I de fleste<br />

tilfælde vil arv være et redskab i en større sammenhæng, hvor flere klasser indgår i samme modul<br />

eller sekvens af moduler, eventuelt fordelt på flere programmører. Andre gange vil arv ikke være<br />

5.2.3 Organisering af kildetekst 373


en direkte og åbenlys del af designet, hvorfor der er nogle regler at følge:<br />

• Når en ny klasse introduceres, bør det eksisterende klassebibliotek gennemgås og<br />

efterses for eventuelle fælles træk. Hvis der allerede findes en klasse, som kan siges at<br />

være et subset, dvs. en del af, den klasse der skal skrives, bør der nedarves fra denne<br />

klasse. Den eksisterende klasse behøver ikke være et helt rent subset af den ny klasse, da<br />

forskellene kan udtrykkes ved omdefinitioner af metoder.<br />

• Når to klasser i programmet udviser fælles træk, bør en tredje klasse introduceres og<br />

benyttes som baseklasse for disse to klasser. Der er to fundamentale årsager til denne<br />

regel: for det første kan de to klasser deles om både data og metoder i baseklassen, som<br />

derfor ekstraherer kompleksitet fra de to afledte klasser, og for det andet betyder det<br />

faktum, at der er to kongruente klasser i programmet, at der med stor sandsynlighed på<br />

et tidspunkt kommer flere klasser, som også viser sammenfald med disse. Derfor gælder<br />

denne regel også for grupper af klasser. En baseklasse vil derfor være en udmærket<br />

sikkerhed for en reduktion af redundans.<br />

Når der arves, er der flere hensyn at tage for at sikre, at det er en korrekt klasse, der bruges som<br />

base. Opdelingen af kildetekstens specifikationsfil er faktisk ikke fyldestgørende, fordi der er<br />

andre elementer end blot klassens interne specifikation, som har betydning for den afledte klasse.<br />

Således kan offentligt arvede metoder være uønskede i den ny klasse ligesom privat arvede data<br />

måske ikke har relevans. En definition af, hvad "fælles træk" er, vil derfor være på sin plads:<br />

• Hvis to klasser har samme data, bør disse data og de metoder, der udelukkende arbejder<br />

på dem skrives om i en baseklasse. Det betyder ikke, at alle data skal være ens, men blot<br />

at en del af klassernes implementationer er de samme. Hvis den ene klasse bliver "tom"<br />

af en sådan omfordeling, bør denne klasse blot være baseklasse for den anden.<br />

• Hvis to klasser bruger den samme globale funktion, eller har ens implementerede<br />

medlemsfunktioner, bør disse funktioner skrives om i en baseklasse. Baseklassen får<br />

karakter af en proces-klasse, som blot indeholder funktionalitet og ikke data.<br />

• Hvis to klasser afspejler den samme brug, dvs. hvis de fra klientens side har meget ens<br />

grænseflader, bør denne grænseflade beskrives i en baseklasse. Hvis de to klasser er for<br />

forskellige til, at baseklassen kan implementere metoderne, bør de erklæres virtuelle i<br />

baseklassen og forblive uændrede i de afledte klasser. Dermed tillades klienten at<br />

benytte de to klasser polymorft, hvilket der er gode chancer for, når de demonstrerer det<br />

samme mønster i grænsefladen.<br />

• Hvis programmets logiske sekvens ser ud til at ville bestå af mange betingelser med<br />

konstante test-værdier (if-sætninger og specielt switch/case-strukturer), bør disse<br />

sekvenser erstattes af virtuelle kald og værdierne med afledte klasser. Et fælles træk er<br />

altså ikke altid åbenlyst - der kan forekomme fælles træk i dele af et program, som slet<br />

ikke indeholder klasser. En vigtig detalje at se grundigt efter er, om der foretages<br />

374 Organisering af klasser 5.3


eksplicit type-kontrol i programmet, dvs. om en betingelse undersøger ét af mange<br />

specielle forhold ved en datastruktur eller andet. Sådanne betingelser har det med at<br />

være centrale, og vil være genstand for regelmæssige modifikationer. Ved erstatning<br />

med polymorfe metoder (se afsnit 4.4.6) bliver ansvaret for typekontrollen lagt over på<br />

oversætteren, og alle de behandlende led decentraliseres.<br />

• Hvis to klasser udviser type-ækvivalens. Dette er nok det vigtigste, fordi det tillader<br />

klienter af klasserne at blande dem sammen i deres kode. Type-ækvivalente klasser kan<br />

kun identificeres ved betydningen af deres indhold. I afsnit 4.3.1 beskrives de forhold<br />

ved type-ækvivalens, der gør arv til en præcis abstraktionsmekanisme for at udtrykke<br />

forhold mellem klasser. Det handler om at identificere generelle og specifikke klasser og<br />

opdele disse i hierarkier udfra klasernes semantisk. Vi kan faktisk sige, at hvis der findes<br />

en kongruens mellem to klasser er det en helt klar indikation af nødvendigheden for arv.<br />

Det sikrer nemlig, at klienten både kan opfatte og også i selve sproget behandle dem<br />

som kongruente.<br />

Programkonstruktion med arv har den bekvemmelighed, at programstumperne organiseres ved<br />

analogi. Analogier er lette at forstå for mennesker, og slægtsskaber mellem klasser er både<br />

overskuelige og elegante. Et lille minus er, at en baseklasse kan have mere eller mindre skjulte<br />

egenskaber, som det ikke er hensigtsmæssigt at arve. Hvordan ser en dårlig baseklasse-kandidat<br />

så ud? Igen er der flere kendetegn:<br />

• Hvis baseklassen indeholder medlemsvariable, som ikke er nødvendige i den afledte<br />

klasse, bør der ikke arves. Alle forekomster af den afledte klasse vil indeholde de<br />

ligegyldige data, hvilket kan blive til en hel del. En mulig anden løsning er at skabe en<br />

ny baseklasse, som beskriver forskellen mellem baseklassen og den afledte klasse, og<br />

lade dem begge arve fra denne. Arv af unødvendige data er den største synd i objektorienteret<br />

programmering.<br />

• Hvis baseklassen indeholder metoder, som ikke er relevante i den afledte klasse, bør der<br />

i reglen ikke arves. Udviser baseklassen denne forskel, er der som regel tale om en<br />

klasse af en helt anden natur, hvis type-forhold til den aktuelle klasse er meget<br />

afvigende. Hvis baseklassen på den anden side indeholder udelukkende relevante data i<br />

forhold til den aktuelle klasse kan de unødvendige metoder fjernes fra klientens domæne<br />

ved enten at arve privat (hvis det ikke skjuler andre nødvendige metoder fra klassen<br />

selv), ved at omskrive dem til at udføre en anden funktion eller ved at reerklære deres<br />

tilgang i den private del af klassen. Det sidste er normalt det bedste, og gøres på<br />

følgende måde:<br />

class Base {<br />

// ...<br />

public:<br />

// ...<br />

void Irrelevant ();<br />

5.3.1 Overvejelser om arv 375


};<br />

class Afledt : public Base {<br />

// ...<br />

void Base::Irrelevant (); // skjul metoden<br />

public:<br />

// ...<br />

};<br />

Hvis forholdet mellem de to klasser udviser disse symptomer bør en anden teknik tages i brug,<br />

hvilket normalt betyder, at baseklassen må indgå i den afledte klasses implementation som<br />

forekomst eller pointer-variabel. I næste afsnit beskrives de forskellige egenskaber ved og<br />

forskelle mellem repræsentationer af objekter i en klasse og arv. Under arv bør man også stille<br />

spørgsmålet, om hvornår baseklassen skal arves offentligt eller privat i den afledte klasse.<br />

Generelt er svaret, at klasser designes med den tanke, at afledte klasser arver offentligt fra den, så<br />

alle offentlige medlemmer i baseklassen også er offentlige i den afledte klasse. Privat arv bruges<br />

således i to situationer: hvis den afledte klasse ikke har behov for tilgang til private eller<br />

beskyttede medlemmer i baseklassen eller hvis den afledte klasses eventuelle efterfølgere ikke<br />

skal tillades adgang til disse.<br />

Det bringer os frem til spørgsmålet om, hvordan den aktuelle klasses medlemmer opdeles i<br />

forskellige tilgangs-områder. Nøgleordet her er, at man bør være visionær, dvs. at tænke på<br />

klassens mulige anvendelser i fremtiden:<br />

• Skriv ikke metoder i klassen, som går ud over klassens egentlige anvendelsesområde.<br />

Metoder, som gør klassen mere specifik, end den giver sig ud for at være, kan med<br />

større fordel skrives i en afledt klasse. Begynd altid med generaliteten og lad<br />

funktionaliteten vente til senere. Det er lettere at se, om programmet bevæger sig i den<br />

rigtige retning, når de generelle komponenter er på plads. De detaljerede metoder kan<br />

sagtens vente til senere, hvilket også mindsker sandsynligheden for, at de skal ændres<br />

eller måske smides væk.<br />

• Erklær altid klassens data i den private (eller beskyttede, se nedenfor) del af<br />

specifikationen. Private data er ikke tilgængelige for klienten af klassen, og sikrer<br />

dermed til en rimelig grænse konsistensen af objekter af klassen. Det betyder også, at<br />

klassen til alle tider kan ændre implementation, fordi klienten går gennem metoder i<br />

klassen for at få tilgang til de private data. Det er altså ikke blot et spørgsmål om formel<br />

sikkerhed, men mere om frihed til at ændre på en del af et program i midten af et<br />

projekt. Det kan betale sig at skrive bittesmå inline tilgangs-metoder til private data<br />

i stedet for at lade klienten referere dem direkte.<br />

• Tænk både på klienten og på eventuelle afledte klasser. Der er to formelle grænseflader<br />

at tage hensyn til, nemlig den offentlige del til klienten og den beskyttede del til den<br />

afledte klasse. Sørg for at erklære metoder og specielt data, som eventuelt vil have<br />

fordel af at være tilgængelig fra en afledt klasse, beskyttet. Men pas på, at der ikke er<br />

376 Organisering af klasser 5.3


tale om meget sensitive data, da en erklæring i den beskyttede del af klassen i princippet<br />

ikke er mere "sikker" end i den offentlige: en klient kan blot arve fra klassen og<br />

modificere de private data.<br />

• Brug så få friend-funktioner som muligt. Den primære anvendelse af sådanne<br />

funktioner er i sammenhæng med operator-overstyring, hvor en brug af den aktuelle<br />

klasse som anden-operant og en fundamental type som første-operant tvinger<br />

overstyringen til at være en global funktion (se afsnit 3.5.11). Hvis friend-funktioner<br />

bruges i andre sammenhænge er det muligvis en svaghed i designet: for det første har<br />

friend-funktioner ikke et underforstået objekt, hvorved de ikke kan bruges polymorft,<br />

og for det andet distribuerer de klassens implementation til vilkårlige steder i<br />

programmet. friend-klasser er derimod ofte brugbare i forbindelse med private<br />

klasser, som ikke skal kunne ses af andre end netop de af klassen udvalgte. Men husk<br />

på, at en friend-klasse ikke nedarves. En afledt klasse har ikke nogen form for<br />

adgang til baseklassens friend-klasser.<br />

En overvejende del af de forslag til objekt-orienteret design som er publiceret til dato, ser<br />

objektet som det centrale element. Det kan imidlertid være gavnligt at se et enkelt element som<br />

en organiseret klynge af klasser, som spreder kompleksiteten fra selve elementet ud i de enkelte<br />

klasser. Den normale læresætning, når objekterne i et system skal identificeres, er at klasserne<br />

allerede har en definition i den konceptuelle beskrivelse af systemet. Klasserne er med andre ord<br />

til at plukke fra det tekstuelle designs indhold. Denne metode tager, selvom den fungerer fint i<br />

praksis, ikke hensyn til klassernes brug set fra klientens side. Det er en alt for ofte begået fejl, at<br />

klassebiblioteket opbygges udfra klassernes interne funktion, uden egentlig hensyntagen til deres<br />

reelle brug udefra. Når man som klassedesigner arbejder med et design er klassens anvendelighed<br />

det vigtigste. Ellers bliver resultatet en stor mængde klasser med alsidig, dog isoleret,<br />

funktionalitet. Det er nødvendigt, at der tages hensyn til klassernes samspil under<br />

programkonstruktionen, hvis det endelige resultat ikke skal løbe ud i sandet.<br />

Således er arv en teknik, som kan bruges i sammenhænge, hvor fordelen egentlig ikke er særlig<br />

stor. Hvis klassehierarkiet består af en mængde container-klasser (klasser, som indeholder andre<br />

objekter), hvis indhold nedefter i hierarkiet blot udbygger den overordnede klasse med et par<br />

objekter og et par tilgangs-metoder til disse objekter, er det objekt-orienterede aspekt gået fløjten.<br />

Indkapsling bliver ligegyldig, fordi klientens billede af den enkelte klasse er defineret af klassens<br />

indhold og ikke af dens funktionalitet. Polymorfi bliver heller ikke brugt, fordi klienten altid<br />

arbejder på enkelte forekomster af klasserne. I situationer, hvor container-klasser blot indeholder<br />

objekter og tilgangsmetoder til disse objekter gør objekt-orienteret programmering intet positivt<br />

og er bedst udeladt.<br />

Lad os tage et eksempel. Vi tænker os et par klasser til definition af rapport-generering til et<br />

administrativt system, som skal kunne behandle videnskabelige, administrative og interne<br />

rapporter. En typisk arvefølge på en sådan definition er:<br />

class Report {<br />

String text;<br />

public:<br />

5.3.1 Overvejelser om arv 377


void Create (); // indtast en rapport<br />

void Analyze (); // analysér en rapport<br />

void Print (); // udskriv en rapport<br />

};<br />

class FinancialReport : public Report {<br />

public:<br />

void Create (); // indtast en administrativ rapport<br />

void Analyze (); // analysér en administrativ rapport<br />

void Print (); // udskriv en administrativ rapport<br />

};<br />

class ScientificReport : public Report {<br />

public:<br />

void Create (); // indtast en videnskabelig rapport<br />

void Analyze (); // analysér en videnskabelig rapport<br />

void Print (); // udskriv en videnskabelig rapport<br />

};<br />

Vi ender med et antal klasser, som alle har Report som baseklasse, men som alle dog har<br />

helt specifik funktionalitet. At klasserne arver fra samme baseklasse giver kun den fordel, at de er<br />

type-ækvivalente, og kan bruges polymorft, hvis medlemsfunktionerne erklæres virtuelle. Men<br />

polymorfien er egentlig ikke interessant, fordi alle klasserne implementerer deres egne metoder<br />

for indlæsning, analyse og udskrivning af deres egen type rapport, og således ofte vil blive brugt<br />

af klienten som en forekomst af netop denne klasse. Med andre ord er klasserne ret forskellige,<br />

både i attributter og i opførsel, selvom deres betydninger for klienten forsøges ligestillet.<br />

Sådanne hierarkier er det fristende at oprette. Problemet er, at det eneste klasserne arver er en<br />

smule data og ingen eller kun få metoder. Metoderne drejer sig i reglen blot om udskrivning og<br />

indlæsning af klassens data. Problemet med denne organisering er, at den er alt for statisk.<br />

Nedarvningsprincippet bruges kun til at definere et forhold, men tager ikke hensyn til de<br />

relationer, der egentlig skaber de afledte klasser. Hvis vi ser nærmere på klasserne viser det sig, at<br />

det egentlig ikke er de forskellige rapporters indhold, som er væsentlig. De data, som definerer en<br />

videnskabelig rapport er meget forskellige fra de, som definerer en administrativ rapport. Det er<br />

faktisk metoderne til indlæsning, udskrivning og anslyse af rapporter, som gør en rapport-klasse<br />

til en rapport. Det er disse metoder, som klienten bør forstå en rapport ud fra. Problemet kan også<br />

forstås ud fra mulighederne for at arve fra en af de eksisterende klasser. Arver vi for eksempel en<br />

medarbejderrapport-klasse fra Report skal klassen stort set skrives helt fra bunden. Hvis den<br />

videnskabelige klasses Analyze() og den administrative klasses Print() eventuelt var<br />

brugbare i denne nye klasse er den eneste udvej at arve multipelt fra disse to klasser. Men en<br />

medarbejderrapport er hverken videnskabelig eller administrativ, hvorved der opstår en forkert<br />

forståelse for klassens position i forhold til de andre klasser.<br />

Der skal en anden fremgangsmåde til, som kan kaldes for kompositorisk klassedesign, og som<br />

går ud på, at der skal nedarves mellem klasser, som har direkte og entydig type-relevans. I<br />

ovenstående eksempel er type-relevansen ikke åbenbar mellem de forskellige rapporter, men<br />

378 Organisering af klasser 5.3


mellem de metoder, rapporten indeholder. Det, vi har forsøgt, er at anvende abstraktionen arv<br />

som genbrugsmekanisme. Som vi skal se gennem de næste afsnit er det absolut ikke<br />

hensigtsmæssigt.<br />

5.3.2 Alternativer til arv<br />

For at komme problemet med Report-eksemplet til livs, må vi ekstrahere størstedelen af<br />

funktionaliteten i baseklassen, nemlig de tre medlemsfunktioner:<br />

class RCreator {<br />

// ...<br />

public:<br />

// ...<br />

virtual void doIt() ();<br />

};<br />

class RAnalyzer {<br />

// ...<br />

public:<br />

// ...<br />

virtual void doIt ();<br />

};<br />

class RPrinter {<br />

// ...<br />

public:<br />

// ...<br />

virtual void doIt ();<br />

};<br />

Fra disse klasser kan vi nedarve specifikke versioner af rapportgenerering, analysering og<br />

udskrivning, som kan udvælges efter behov, når en egentlig rapport skal bruges. Således<br />

indeholder Report blot referencer til elementerne af den egentlige funktionalitet i en rapport,<br />

som kan bestemmes af klienten. Klienten får dermed yderligere den frihed, at en bestemt del af<br />

rapporten kan udskiftes uden at skulle nedarve og omskrive komplekse metoder fra selve rapportklassen.<br />

De tre klasser RCreator, RAnalyzer og RPrinter er helt isolerede og er derfor<br />

lettere at forstå (forståelsen kan yderligere øges ved at implementere disse klasser som<br />

funktionoider, se afsnit 5.5). Da Report indeholder referencer til de tre klasser, vil kaldet til<br />

deres doIt()-metoder binde dynamisk for nedarvede klasser, hvilket gør Report til en<br />

meget dynamisk byggesten i forhold til før:<br />

class Report {<br />

RCreator& rc;<br />

5.3.1 Overvejelser om arv 379


RAnalyzer& ra;<br />

RPrinter& rp;<br />

public:<br />

Report (RCreator& c, RAnalyzer& a, RPrinter& p) :<br />

rc (c), ra (a), rp (p) { }<br />

void Create () { rc.doIt (); }<br />

void Analyze () { ra.doIt (); }<br />

void Print () { rp.doIt (); }<br />

};<br />

Klassen Report er et eksempel på en indkapslet polymorf datastruktur, hvis grænseflade blot<br />

kalder metoder i strukturens elementer og intet andet. En sådan klasse kaldes for en aggregat,<br />

fordi den sammenføjer kompleksitet fra andre klasser. Forskellen fra arvede klasser er, at klienten<br />

er i en kontrolposition i forhold til klassen, fordi det dynamisk kan lade sig gøre at opbygge<br />

funktionaliteten i en Rapport. Aggregate klasser giver således meget større frihed for klassens<br />

klient, hvilket må siges at være et vigtigt kriterium for en klasse, som oftest netop er skrevet til en<br />

klient. Klassen AdjacencyList fra afsnit 4.5.3 er et eksempel på en aggregat klasse i en<br />

mere virkelig situation.<br />

Når klasser skal organiseres er det således ikke blot et spørgsmål om, hvad man skal arve fra.<br />

Ved kun at udtrykke klasseforhold ved arv bliver den enkelte klasse meget statisk set fra<br />

klientens synspunkt, og bliver svær at forme til det egentlige formål. Vi opstiller derfor nogle<br />

relationer mellem klasser og mellem objekter. Klasserelationer eller typerelationer foregår på et<br />

entitetsplan, hvor klasserne har visse ligheder i struktur og form mens objektrelationer sker på et<br />

instansplan, hvor objekterne relaterer direkte til klasser. Følgende relationer er væsentlige:<br />

• Hvis klasse A er-en klasse B, bør A arves fra B. Undertonen i dette spørgsmål er, at<br />

så mange metoder som muligt skal kunne genbruges, og at alle datamedlemmer i B har<br />

mening i A. Et eksempel på dette er forholdet mellem Figur og Linie i afsnit<br />

4.2.5. På normal dansk betyder dette typeforhold, at en Linie er en Figur, og det<br />

faktum, at vi kan beskrive det direkte i kildeteksten gør, at programmets design<br />

umiddelbart er lettere at forstå. En anden måde at sige det på, er at en Linie er en<br />

slags Figur, mens ikke alle Figurer er Linier. Normalt taler vi om er-en<br />

relationer i entitetsplanet og ikke i instansplanet. De hjælper til at identificere generelle<br />

karakteristika og ikke specifikke forhold. Afsnit 4.3.1 og 4.4.1 indeholder en diskussion<br />

af en sådan opbygning.<br />

• Hvis klasse A har-en klasse B, bør A være klient af B. Spørgsmålet om typen af<br />

reference (forekomst, pointer eller reference) er ikke ligefremt, og behandles nedenfor.<br />

Forholdet mellem Figur og Koordinat i afsnit 4.2.5 er et eksempel på denne<br />

typerelation, som normalt også kan kaldes bruger-i-implementationen relation (klasse A<br />

bruger klasse B i implementationen). I modsætning til er-en forholdet, som<br />

kun giver mening i entitetsplanet, definerer et har-en forhold mellem enten to klasser, en<br />

klasse og et objekt eller to objekter. Hvis vi begynder i entitetsplanet og ser på eksempel<br />

4-4 i afsnit 4.3.10 opdager vi, at klassen Figur har et typeforhold til en Koordinat<br />

380 Organisering af klasser 5.3


som kan beskrives som, at Figur har-en Koordinat. Da dette gælder for alle<br />

figurer, også de, som arver fra Figur, er dette et konceptuelt ejerforhold som kun<br />

eksisterer på entitetsplanet, altså på klasseniveau. Anderledes forholder det sig med<br />

klassen Billede fra afsnit 4.4.7. Denne klasse har et forhold til Figurer som er<br />

temmeligt kompliceret. Ikke nok med, at klassen er-en Figur, og dermed opretter et<br />

typeforhold på entitetsplanet, den har også Figurer. Men en given forekomst af<br />

Billede kan indeholde både Firkanter og Linier. Her har Billede et har-en<br />

forhold til alle de klasser, der er arvet fra Figur. På samme måde forholder det sig<br />

med klasser, hvis arvinger kan være klienter af andre klasser. Sådanne baseklasser har et<br />

løst objekt-per-klasse forhold til forekomster af andre typer.<br />

• Hvis klasse A er-en-del-af klasse B, bør der enten slet ikke arves (da typeforholdet<br />

ikke er entydigt) eller også bør der skabes en fælles baseklasse for A og B, som<br />

indeholder fællesnævnerne for de to klasser. Alternativet er, hvis den del, som B har i<br />

overskud er meget lille, at omskrive B's afvigende metoder i A, men vel at mærke kun,<br />

hvis metoderne har mening i A. Klassen Transformation (afsnit 4.2.3) er en del af<br />

et Koordinat, men arver ikke, fordi en Transformation ikke kan erstatte et<br />

Koordinat i praksis. Derimod arver SortedList fra LinkedList (afsnit<br />

4.5.3), fordi typeforholdet kan udtrykkes ved at omskrive netop den metode, som<br />

differentierer de to klasser.<br />

• Vi definerer også et bruger-en relation, som opstår, hvis en klasses medlemsfunktion<br />

modtager en forekomst af en anden klasse som parameter. Klassen HeadNode fra<br />

afsnit 4.5.7 bruger-en forekomst af ostream til at udskrive sine data. Det er ikke et<br />

har-en forhold, fordi kaldene til medlemsfunktionerne i ostream-objektet ikke<br />

medfører, at en ostream er indeholdt i en HeadNode-forekomst som del af dette<br />

objekt.<br />

• Det sidste forhold er forholdet skaber-en, som betyder, at et objekt skaber et andet<br />

objekt gennem et kald til dens konstruktør (enten gennem et kald til operator new<br />

eller som automatisk allokeret returværdi fra en medlemsfunktion). Det minder meget<br />

om bruger-en forholdet, men er her mellem to objekter og ikke mellem en klasse og et<br />

objekt.<br />

Hvis klasse A har relationer i entitetsplanet af ovenstående karakter med mere end én anden<br />

klasse er der i reglen to muligheder, enten multipel arv eller aggregat klasse. Forskellen er ikke så<br />

stor, som man skulle tro, og valget ligger i en erkendelse af, hvordan klassen skal bruges fra<br />

klientens side. Det drejer sig altså konkret om, hvorvidt den ene klasse skal være arvtager eller<br />

klient af den anden. Det fundamentale ligger i, om klassen A er en speciel slags af klassen B,<br />

eller om A har en B. Det lyder simpelt, men kan i praksis være lidt af et problem. I eksemplet<br />

med Figur-hierarkiet fra afsnit 4.2.5 er det klart at se, hvordan de afledte klasser er specielle<br />

typer af baseklassen. Abstraktionen er med andre ord letforståelig. Programmer er desværre oftest<br />

ikke nær så simple i deres natur, fordi de repræsenterer dårligt forståede abstraktioner og ikke<br />

objekter fra virkeligheden. Er UnderBillede et slags Billede eller har et Billede et<br />

5.3.2 Alternativer til arv 381


UnderBillede? Er et BinaryTree en slags LinkedList eller omvendt? Det er<br />

spørgsmål som disse, der optager hovedparten af udviklingen af en fundamental objekt-orienteret<br />

designmetode, og som jeg vil forsøge at belyse i resten af dette <strong>kapitel</strong>.<br />

De ovenstående regler for type-relationer virker altså kun, hvis begreberne, som klasserne<br />

repræsenterer, er forstået fuldt ud af programmøren. Modsat den gældende mening er denne<br />

forståelse ikke umiddelbar, og en fremgangsmåde til at hjælpe forståelsen for en klasses<br />

repræsentation er nødvendig. Der er to udgangspunkter at tage, klassens data og klassens<br />

metoder. Beskrives klassen udfra dens data, er reglen om ikke at arve unødvendige data<br />

tilstrækkelig. Producerer arvefølget objekter med overskudsdata, som klassen ikke har brug for,<br />

er det et sikkert symptom på, at arven ikke er korrekt. Det betyder ikke, at de to klasser intet har<br />

til fælles, men blot at de skal relateres til hinanden på en anden måde, for eksempel ved<br />

introduktion af en mellemliggende klasse eller en ny baseklasse, som klistrer de to klasser<br />

sammen. De arvede metoder er sværere at fastsætte betydningen af. Hvis arven medfører<br />

metoder, som ikke direkte kan bruges af den afledte klasse, hverken som den ser ud eller i<br />

forbindelse med en omskrivning, betyder det, at de to klasser ligger længere fra hinanden end de<br />

måske ser ud til. Det ses ofte, at en arvet metode kun næsten er korrekt for den afledte klasse,<br />

men må skrives helt om for at være fuldstændig korrekt. Et eksempel på dette er en klasse, som<br />

definerer en kø. Klassen har meget tilfælles med en hægtet liste, og da en kø er bredere defineret<br />

end en hægtet liste, ser en arvefølge fra denne rigtig ud:<br />

class Queue : public LinkedList {<br />

Node* tail;<br />

public:<br />

// ...<br />

};<br />

Klassen Queue har udover en arvet pointer til den første hægte i listen også en ny pointer til den<br />

sidste hægte i listen. Data fyldes i listen i starten og hentes fra listen i slutningen. De to metoder<br />

LinkedList::Insert() og LinkedList::Retrieve() arbejder begge på starten af<br />

listen, og en omskrivning af den sidste skulle derfor være tilstrækkelig. Desværre skal det ny<br />

datamedlem opdateres i både indsættelse og udtagning af data, og den arvede metode<br />

Insert() kan ved nærmere eftersyn ikke bruges, fordi den ikke opdaterer pointeren til<br />

sluthægten. Men det er ikke det værste: Den arvede metode kan heller ikke bruges som del af en<br />

ny rutine:<br />

void Queue::Insert (Node* p) {<br />

LinkedList::Insert (p);<br />

// hvordan opdateres tail?<br />

}<br />

En undersøgelse af LinkedList::Insert() viser, at informationen til sluthægten vil gå<br />

tabt under visse omstændigheder, hvorved denne skal opdateres. Det kan reddes ved at traversere<br />

listen fra start til slut og tildele sluthægte-pointeren den sidst fundne. Men effektiviteten vil lide<br />

under, at listen skal traverseres ved hver indsættelse, en væsentlig bivirkning ved en sådan<br />

382 Organisering af klasser 5.3


implementation. Queue må derfor helt omskrive metoden, og det arvede materiale indskrænker<br />

sig faktisk til et enkelt datamedlem, pointeren til den første hægte. Eksemplet viser, hvordan to<br />

klasser, hvis relation ser åbenbar ud, viser sig at udvise forskellighed på et par nøglepunkter, som<br />

gør de fleste af de arvede metoder ubrugelige. Til gengæld er fordelen ved arven, at klienten kan<br />

bruge en Queue som en LinkedList og således kalde alle eksisterende funktioner, som<br />

arbejder på hægtede lister.<br />

Somme tider er arv dog ikke den rette løsning. I eksemplet fra afsnit 4.2 kan vi se på forholdet<br />

mellem klasserne Koordinat og Figur. En Figur har samme egenskaber som et<br />

Koordinat, nemlig en bestemmelse i et plan og metoder til at ændre på dette. Men de to<br />

klasser er ikke type-ækvivalente: koordinater og figurer er to helt forskellige begreber. En måde<br />

at afsløre et sådant forhold er at se på klasserne fra klientens side, og opstille følgende<br />

overvejelse: Hvis en funktion, som arbejder på baseklassen, uden modifikation og med mening<br />

vil kunne arbejde på den afledte klasse, bør der arves. For klasserne Koordinat og Figur<br />

gælder, at Koordinat har en grænseflade, som går langt ud over Figur's omfang, for<br />

eksempel de mange operator-metoder samt udskrivning af indholdet. Polymorfe kald arbejder på<br />

metoder, og jo flere overskuds-metoder, baseklassen indeholder, des flere eksterne generiske<br />

funktioner vil ikke kunne bruges polymorft for den afledte klasse. Figur benytter en<br />

Koordinat som medlemsforekomst i stedet, og er dermed klient i stedet for arvtager.<br />

Hvis typeforholdet mellem to klasser indebærer, at den ene klasse er klient af den anden, rejser<br />

spørgsmålet sig om hvilken type af reference, klientklassen skal have til udbyderklassen:<br />

medlemsforekomst (objekt), pointer eller reference. Hvis vi erklærer en klasse A med en<br />

medlemsforekomst af en anden klasse B, og dermed gør A til klient af B med<br />

class A {<br />

B b_object;<br />

// ...<br />

};<br />

vil enhver forekomst af A indeholde et B-objekt, ganske som under arv. Det betyder<br />

selvfølgelig, at en klasse ikke kan være sin egen klient, ligesom en klasse ikke kan arve fra sig<br />

selv. En fundamental forskel mellem klientering og arv er, at de to klassers relation ikke<br />

udtrykker et semantisk forhold mellem de to klasser. Det er umuligt at bruge A som polymorf<br />

reference i kode, som arbejder på B-referencer. En anden forskel er, at det er muligt for klientklassen<br />

at styre hvilke funktioner, der skal skinne igennem til klassens grænseflade: da der ikke<br />

arves, vil ingen af B's metoder kunne ses af A's klient. Hvis B har en public metode f(),<br />

kan A's grænseflade indeholde en lignende metode f(), som blot viderefører kaldet til B - vi<br />

siger, at A er en delegerende klasse:<br />

class B {<br />

public:<br />

// ...<br />

void f ();<br />

};<br />

5.3.2 Alternativer til arv 383


class A {<br />

B b_object;<br />

public:<br />

// ...<br />

void f () { b.f(); }<br />

};<br />

På den måde kan kun de metoder, som udbyderen af A ønsker at "arve" fra B skrives som del af<br />

klassens grænseflade. Den lille, men væsentlige forskel fra eksplicit skjulning af data og metoder<br />

i en afledt klasse, som vi så i afsnit 5.3.1 er, at metodernes prototyper tillades at afvige i<br />

specifikation. Hvis B's metode modtager en double, kan A's metode specificeres på en anden<br />

måde, som passer til A. Her er A og B erstattet af en Name-klasse og en Person-klasse:<br />

class Name {<br />

char* s;<br />

public:<br />

// ...<br />

void Set (char*);<br />

};<br />

class Person {<br />

Name first, last;<br />

public:<br />

// ...<br />

void Set (char* f, char* l) {<br />

first.Set (f);<br />

last.Set (l);<br />

}<br />

};<br />

De to klassers Set()-metoder afviger i specifikation, og hvis Person arvede fra Name, ville<br />

der optræde to overstyrede metoder af dette navn. I dette eksempel antages, at klassen Person<br />

kun kan sætte sit indhold til både et fornavn og et efternavn, så metoden Set(char*) er<br />

meningsløs. Under arv kan vi kun ændre på adgangskriterierne til metoden, men kan ikke fjerne<br />

den. Her er den nye Set()-metode entydig for Person-klassen. Der er i øvrigt et andet<br />

forhold i denne relation mellem Person og Navn som umuliggør arv, nemlig Persons<br />

dobbelte instantiering af Navn som forekomst. Skulle klassen i stedet arve, ville det ikke være<br />

muligt at specificere Navn to gange i erklæringen. Et udmærket tegn på, at medlemsforekomster<br />

er at foretrække frem for arv er således, at hvis den ny klasse skal bruge flere forekomster af den<br />

eksisterende klasse, er klientering den uafvigelige løsning.<br />

En tredie metode til relatering af klasser er at bruge den eksisterende klasse med en indirekte<br />

reference i den ny klasse, enten med pointer eller med reference. Hvis klassen A indeholder en<br />

indirekte reference til B kaldes A for en løs klient af B, fordi instantieringer af A ikke<br />

umiddelbart resulterer i underforståede B-objekter. B's konstruktør kaldes først, når et B-objekt<br />

384 Organisering af klasser 5.3


instantieres, og med følgende klasseerklæring vil det ikke ske under konstruktionen af A:<br />

class A {<br />

B* B_ptr;<br />

public:<br />

// ...<br />

};<br />

Forskellen fra instantiering som i forrige eksempel er, at ingen af B's data automatisk bliver<br />

instantieret for objekter af A. En lighed med forrige eksempel, men en forskel fra arv, er, at ingen<br />

af B's metoder bliver defineret for objekter af A. De samme regler gælder således for selektion<br />

af metoder, som skal bruges i A med eksplicit reference til den indeholdte B-pointer. Med en<br />

pointer kan referencen i A ydermere henvise til ethvert objekt, som er afledt fra B. Det betyder<br />

flere ting:<br />

• Det indeholdte B-objekt kan være polymorft. Klienten kan, med en eksisterende Bklasse<br />

og en passende grænseflade i A, arve en ny klasse C fra B og lade objekter af<br />

A arbejde på forekomster af C. Polymorfe kald vil være mulige gennem virtuelle<br />

metoder erklæret i B.<br />

• Da referencen til B kan henvise til enhver subtype under denne klasse, kan der i praksis<br />

skabes en reference til et A-objekt, såfremt A er afledt fra B. Ved normal instantiering<br />

i A, eller ved arv, er det ikke muligt for en klasse at indeholde en forekomst af sig selv.<br />

• Abstrakte klasser kan refereres, hvilket ikke er muligt under normal klientering.<br />

• Det er muligt for to A-objekter at dele den samme forekomst af et B-objekt ved at lade<br />

begge objekters pointere pege på samme data.<br />

Et andet forhold ved pointere til forekomster indeholdt i objekter er, at det refererede objekts<br />

levetid ikke nødvendigvis falder sammen med det omsluttende objekt. Det skaber først og<br />

fremmest et udvidet ansvar for, at forekomster af det refererede objekt administreres korrekt og<br />

allokeres og deallokeres som det skal, mens det for medlemsforekomster varetages af<br />

oversætteren. Videre betyder det, at den omsluttende klasse blot er en midlertidig holdeplads for<br />

det indeholdte objekt, og på den måde kan anskues som en container. Container-klasser skaber<br />

således avancerede relationer mellem andre objekter, uden at være statisk bundne til forekomster<br />

af bestemte klasser. Som beskrevet i afsnit 3.5.12 er indirekte referencer i objekter en kilde til<br />

fejl, når det omsluttende objekt kopieres. Hele klassen skal designes således, at en kopiering<br />

enten skaber en kopi af pointeren eller rent faktisk kopierer hele det refererede objekt. I praksis<br />

betyder det, at specielle metoder som kopi-konstruktører og tildelings-operatorer skal defineres<br />

for klasser, som indeholder pointere til objekter. At bruge indirekte referencer til<br />

medlemsvariable er således farligere og kræver mere arbejde, men er tillige en langt mere<br />

fleksibel teknik. Brug af referencer i stedet for pointere gør ikke den store forskel, og begrænser<br />

endda fleksibiliteten en smule, fordi det ikke er muligt at tildele en værdi til en referencevariabel<br />

5.3.2 Alternativer til arv 385


efter erklæringen og initieringen. I princippet bør prioriteringen ved undersøgelse af et<br />

typeforhold være:<br />

• Undersøg, om arv er fornuftigt ved brug af de beskrevne regler for type-ækvivalens.<br />

Hvis den ene klasse, set fra klientens side, kan drage fordel af at være en subtype af den<br />

anden klasse ved for eksempel polymorft brug er arv den tvungne løsning.<br />

• Hvis arv ikke er realistisk, undersøg da muligheden for klientering. Hvis den ene klasse<br />

har alle forudsætninger for arv med undtagelse af subtype-forholdet, er klientering den<br />

rigtige løsning.<br />

• Hvis type-forholdet er for statisk med klientering, brug da en indirekte reference. Er den<br />

eksisterende klasse abstrakt, er en pointer til en afledt forekomst ligefrem nødvendig, da<br />

klassen ikke kan instantieres.<br />

Om de specifikke C++ friend-funktioner og -klasser skal det nævnes, at de ikke udtrykker et<br />

typeforhold. Brug dem derfor aldrig til at underbygge det egentlige design.<br />

5.3.3 Medlemsvariable<br />

En klasses medlemsvariable er klassens egentlige repræsentation. Et objekt af en klasse vil have<br />

en opbygning, som er defineret af klassens variable †<br />

. Det er derfor vitalt, at klassens data er<br />

defineret korrekt. Hvilke data, en klasse skal have, afhænger helt af, hvad klassens funktion er.<br />

Valg af type for medlemsvariable kan der derfor ikke opstilles regler for. Der er imidlertid<br />

forskellige overordnede attributter for medlemsdata, som har betydning for objekters brug.<br />

Først og fremmest skelnes mellem tre slags klasser:<br />

• Tomme klasser: klasser uden variable,<br />

• Simple klasser: klasser med variable af udelukkende fundamentale typer, og<br />

• Komplekse klasser: klasser, som indeholder andre brugerdefinerede objekter.<br />

Klasser uden data er normalt abstrakte klasser, som enten blot er prototyper på korrekte<br />

implementationer i afledte klasser, eller som har en "klister-funktion" idet de sørger for et korrekt<br />

type-forhold mellem baseklasser og afledte klasser. En klasse uden medlemsvariable består<br />

således kun af metoder og/eller prototyper på metoder og er simpelthen et indkapslet modul af<br />

funktioner. Har en klasse udelukkende variable, som er af fundamentale typer, vil klassens<br />

funktion ofte være som grundpille i programmet. Idet klassen har en ekstern definition, som går<br />

ud over de fundamentale typer, mens den selv blot indeholder fundamentale typer, skaber denne<br />

klasse en egentlig abstraktion for det koncept, den repræsenterer på et maskinnært niveau. Hvis<br />

† Forekomsten indeholder også en pointer til eventuelle virtuelle metodetabeller, se afsnit 4.4.5.<br />

386 Organisering af klasser 5.3


klassen indeholder forekomster af andre objekter, er der oftest tale om et aggregat, dvs. en klasse,<br />

som samler funktionaliteten fra andre klasser. De kaldes også for komplekse klasser, fordi et<br />

større antal konstruktører kan blive resultatet af en enkelt instantiering, se afsnit 4.5.5.<br />

Medlemsvariable kan erklæres statiske, hvorved alle forekomster af klassen deler den samme<br />

forekomst af medlemsvariablen. Statiske data bruges i sammenhænge, hvor en global variabel<br />

brugtes før i tiden, blot relateret til et begrænset antal funktioner. Ved statiske medlemsdata opnås<br />

to ting: pladsbesparelse, fordi der kun findes én forekomst, ligegyldigt hvor mange objekter, der<br />

instantieres og konsistenssikring, hvis alle objekter af den pågældende klasse afhænger af samme<br />

variabel. En medlemsvariabel bør erklæres static, hvis den ikke er relateret til et bestemt<br />

objekt, men til klassen som helhed. For eksempel er en god kandidat til en statisk<br />

medlemsvariabel en pointer til en fejlbehandlings-funktion, som er den samme for alle<br />

forekomster af klassen. Husk, at en statisk medlemsvariabel skal initieres udenfor klassens krop.<br />

Konstante medlemsdata har meget begrænset anvendelighed, men kan hjælpe med at sikre<br />

konsistensen af et objekt. Hvis en medlemsvariabel ikke skal ændre værdi efter konstruktøren har<br />

initieret den, kan den med fordel erklæres konstant. Hermed sikres, at ingen metoder fejlagtigt<br />

ændrer på variablens værdi. Et eksempel på en konstant medlemsvariabel er pointere til<br />

specifikke adresser i lageret, for eksempel I/O-adresserum.<br />

5.3.4 Metoder<br />

Hvilke metoder bør en klasse indeholde? Generelt er en klasses metoder definitionen på dens<br />

grænseflade, men de er mere end det. Der er nogle væsentlige grundfunktioner, som alle klasser<br />

har, og som gør valget af metoder lettere at overskue. En klasse har typisk brug for:<br />

• Administrative metoder, herunder konstruktører og destruktører, kopieringsmetoder,<br />

sammenlignignsmetoder, konverteringsmetoder, operator-overstyringer og<br />

filtreringsmetoder. Disse metoder ændrer sjældent på objektets data, men har en<br />

underforstået betydning for objektets brug i en større sammenhæng, dvs. i sammenhæng<br />

med andre objekter. Administrative metoder kaldes også grænseflade-metoder.<br />

• Bearbejdende metoder, hvilke er de metoder, som laver det grove arbejde i forbindelse<br />

med objektets data. De bearbejdende metoder er oftest de største og mest komplicerede<br />

at skrive, og er ydermere ofte de elementer, som er årsagen til klassens oprettelse.<br />

• Statusmetoder, som returnerer et objekts status på en veldefineret måde, normalt uden at<br />

ændre på objektets data. En statusmetode er for eksempel en angivelse af objektets<br />

størrelse, lokation eller hash-værdi.<br />

De administrative metoders omfang er afhængig af systemet som helhed, og ikke af den enkelte<br />

klasse. Derfor er antallet af og størrelsen på disse metoder ofte ret konstante for de fleste klasser.<br />

At skrive dem er trivielt og skaber ikke de største problemer. De bearbejdende metoder er<br />

definitionen på klassens egentlige funktionalitet, og indeholder de algoritmer, som klassens<br />

objekter manipuleres med. Disse er normalt de største, mest komplicerede metoder, hvis omfang<br />

5.3.3 Medlemsvariable 387


og antal afhænger af den enkelte klasse. Statusmetoderne er de mindste og mindst<br />

betydningsfulde metoder i klassen. Denne definition af metodetyper er rent indholdsmæssig. Der<br />

er eksterne krav, som har indflydelse på metodernes specifikation og implementation, og som<br />

enten kan gøre en metode enkel eller kompliceret at skrive.<br />

Det vigtigste at huske er følgende: tænk på klienten. Klassens metoder skal bruges af en klient,<br />

og de skal derfor skrives så klienten får færrest mulige problemer med at bruge og forstå dem.<br />

Det første og vigtigste valg, der skal tages er, om en metode skal være virtuel eller ej. Virtuelle<br />

metoder har den egenskab, at de kan bruges polymorft af en klient, og skaber dermed en blød<br />

relation til eventuelle base- og afledte klasser. Er den aktuelle klasse afledt fra en anden klasse<br />

med virtuelle metoder, vil klassen arve alle disse. En nedarvet virtuel metode betyder normalt, at<br />

den bør redefineres i den afledte klasse. Hvis en ny metode introduceres i den aktuelle klasse bør<br />

den erklæres virtuel, hvis der er den mindste smule mistanke om, at en afledt klasse kan erstatte<br />

den aktuelle klasse i eksterne generiske funktioner.<br />

I C++ kan man stille det andet, ikke særlig objekt-orienterede spørgsmål, om en metode<br />

overhovedet skal være medlem af klassen. Der er visse forhold, specielt i forbindelse med<br />

numeriske datatyper og operator-metoder, som nødvendiggør eksternt erklærede, globale<br />

funktioner med relation til klassen. Normalt erklæres metoder udenfor klasser for at opnå en<br />

bedre grænseflade og dermed bedre syntaks i klientens program. Skrives for eksempel en metode<br />

sin() for Complex-klassen fra <strong>kapitel</strong> 3 til udledning af sinus for det komplekse tal, vil den<br />

sandsynligvis se således ud:<br />

Complex Complex::sin () {<br />

return Complex (sin(re) * sinh (im), sin (re) * sinh<br />

(im));<br />

};<br />

og bruges på følgende måde:<br />

void f () {<br />

Complex a (2, -1);<br />

Complex b = a.sin ();<br />

// ...<br />

}<br />

Denne syntaks følger den objekt-orienterede forståelse for en afsendt besked til et objekt, her<br />

sin() anvendt på Complex. Set ud fra en normal behandling af numeriske typer, for<br />

eksempel en double, er syntaksen imidlertid dårlig. Der burde stå:<br />

void f () {<br />

Complex a (2, -1);<br />

Complex b = sin (a);<br />

}<br />

For at opnå dette må metoden sin() udtages af Complex og skrives som normal, global<br />

388 Organisering af klasser 5.3


metode med speciel tilgang til et Complex-objekt gennem en friend-erklæring. En<br />

friend-funktion modtager som regel et eller flere objekter som parametre af den type, metoden<br />

er friend af. Andre tvungent nødvendige anvendelser for friend-funktioner omfatter<br />

generelle operator-metoder mellem en bestemt klasse og en fundamental type. Som illustreret i<br />

afsnit 3.5.11 kan en medlemsmetode misfortolkes, hvis den bruges forkert af klienten:<br />

a = 1 + b; // 1.operator+ (b) ?<br />

Metoden kan ikke være medlem af konstanten 1, men må erklæres udenfor klasseskop og<br />

erklæres friend for den eller de klasser, operatoren arbejder på. Der er intet teknisk problem i<br />

at skrive en medlemsfunktion om som en global funktion. Overstyring af funktionsnavne sikrer,<br />

at der ikke opstår navnekonflikter med andre globale funktioner.<br />

Der er et specielt forhold ved friend-funktioner, som gør dem uegnede til visse<br />

implementationer. Disse metoders manglende underforståede objekt gør det nemlig umuligt for<br />

en friend-funktion at være virtuel. Det kan skabe problemer, for eksempel i forbindelse med<br />

polymorfe kald til operator-funktioner:<br />

class A {<br />

public:<br />

// ...<br />

friend ostream& operator


};<br />

class B : public A {<br />

// ...<br />

public:<br />

// ...<br />

virtual void printOn (ostream&);<br />

};<br />

inline ostream& operator


• Konstante metoder bruges til at tillade klienten at oprette og behandle konstante<br />

forekomster af den aktuelle klasse. Hvis klienten kalder en ikke-konstant metode for et<br />

konstant objekt, vil oversætteren advare om det. Husk, at en konstant og en ikkekonstant<br />

metode afviger semantisk fra hinanden og fortolkes af C++ som en overstyring,<br />

se afsnit 3.3.17.<br />

Der har også været fokuseret på en ensartet opbygning af klasser efter et bestemt mønster, som<br />

skulle give større mulighed for integration af disse i hierarkier. [Coplien 92] nævner som<br />

eksempel den ortodokse, kanoniske form som en mulighed. Det er en konvention, der siger, at en<br />

klasse skal indeholde et minimum af medlemsfunktioner, som gør det muligt at benytte dem i<br />

tildelinger, initieringer, som parametre og som returværdier ganske som de fundamentale typer.<br />

Følgende klasseerklæring er et eksempel på denne kanoniske struktur:<br />

class X {<br />

// ... repræsentationen af abstraktionen "X" ...<br />

public:<br />

// underforstået konstruktør<br />

X ();<br />

// kopi-konstruktør, vigtig i initieringer<br />

X (const X&);<br />

// destruktør<br />

~X ();<br />

// tildelingsoperator<br />

const X& operator= (const X&);<br />

// sammenligningsoparator<br />

int operator== (const X&) const;<br />

// ... andre offentlige medlemsfunktioner i "X" ...<br />

};<br />

5.3.5 Adgangskontrol<br />

En klasses data bør i næsten alle tilfælde erklæres i den private sektor af klassen, så klienten ikke<br />

kan se dem direkte. Den første grund til denne meget fundamentale regel er, at kun klassens<br />

metoder kan arbejde direkte på disse data, og at eventuelle fejl isoleres til klassen selv. I bredere<br />

forstand sikrer en indskrænkelse af tilgangen til en klasses data, at klienten gør sig uafhængig af<br />

implementationen (de indkapslede datas natur) af klassen. Det skal forstås således, at en<br />

klasseudbyder kan ændre på klassens interne repræsentation uden at klientens kode bryder<br />

sammen. Det bunder i en syntaksmæssig konvention om altid at gå gennem en funktion i stedet<br />

for at gå direkte til klassens data, hvilket i grunden er en indirekte måde at skrive den samme<br />

kode på. Faktisk specificerer klassens udbyder en udvidet semantik formelt i form af klassens<br />

grænseflade, hvilket før i tiden kun var en del af det designet. Mens det øgede forbrug ved<br />

5.3.4 Metoder 391


indirekte referencer til data gennem funktioner kan normaliseres ved brug af inline-kode, kan<br />

den syntaksmæssige ulempe ikke siges at være et problem. Tværtimod kan en tekstuel<br />

beskrivelse af en grænseflade være konsistent fra design til kode.<br />

Et eksempel vil være på sin plads. Overvej følgende stak-klasse og en simpel anvendelse for<br />

ombytning af elementer:<br />

// eksempel 5-1a: vektor-repræsenteret stak af heltal<br />

class IntStack {<br />

public:<br />

int *s;<br />

IntStack (int capacity = 100) { s = new int [capacity]; }<br />

~IntStack () { delete s; }<br />

void Push (int i) { *s++ = i; }<br />

int Pop () { return *--s; }<br />

};<br />

// ombyt de to øverste elementer på stakken<br />

void swap (IntStack& stk) { // uha: gør sig afhængig<br />

int temp = *stk.s; // af implementationen<br />

*stk.s = stk.s [-1];<br />

stk.s [-1] = temp;<br />

}<br />

Funktionen swap() arbejder direkte på et datamedlem for at ombytte de to øverste elementer i<br />

stakken. Sæt nu, at vi omskriver klassen og giver den en anden intern repræsenation:<br />

// eksempel 5-1b: hægtet liste-repræsenteret stak af heltal<br />

class IntStack {<br />

public:<br />

class IntNode {<br />

public:<br />

IntNode* next;<br />

int d;<br />

IntNode (IntNode* n, int i) : next (n), d (i) { }<br />

};<br />

IntNode* s;<br />

IntStack (int capacity = 100) { s = 0; }<br />

~IntStack () { delete s; }<br />

void Push (int i) { s = new IntNode (s, i); }<br />

int Pop () {<br />

if (!s) { cerr next;<br />

392 Organisering af klasser 5.3


};<br />

delete remove;<br />

return i;<br />

}<br />

Hvis klienten genoversætter ovenstående kode vil funktionen swap() bryde sammen med en<br />

oversætterfejl, der handler om en typekonflikt i den første tildeling. Typekonflikten er opstået,<br />

fordi klassen har ændret sin implementation fra vektor til hægtet repræsentation af stakken.<br />

Klienten må nu ændre sin kode, fordi den er afhængig af klassens implementation: Ændres<br />

klassen, må klientens kode også ændres. I de ovenstående klasser har klienten adgang til alle data.<br />

Ved at erklære pointer-variablen s i den private del af klassen vil klientens swap()-funktion<br />

give en fejl ved første oversættelse, som beklager, at der ingen adgang er til de nævnte data.<br />

Klienten må anmode om en special adgangsmetode i stedet, som passer til problemet. En sådan<br />

metode kunne for eksempel være en overstyring af []-operatoren, som indekserer stakken, og<br />

som kan give klassen og swap() følgende udseende:<br />

class IntStack {<br />

// klassens implementation, skjult fra klienten...<br />

public:<br />

IntStack ();<br />

~IntStack ();<br />

void Push (int);<br />

int Pop ();<br />

int& operator[] (int);<br />

};<br />

// ombyt de to øverste elementer på stakken<br />

void swap (IntStack& stk) {<br />

int tmp = stk [0];<br />

stk [0] = stk [-1];<br />

stk [-1] = tmp;<br />

}<br />

Nu kan klassens udbyder repræsentere stakken på vilkårlige måder, foretage ændringer i klassens<br />

data og omdøbe interne navne, fordi ingen udefra gør brug af dem. Den grænseflade, som en<br />

tilgangsfunktion giver klassen, gør klientens kode langt mere robust, fordi den ikke fejler på<br />

grund af omstændigheder, som ligger udenfor dennes domæne. Både klassen og klientens kode<br />

kan oversættes separat uden risiko for konflikter. Klassens specificerede grænseflade er således<br />

en protokol for dataudveksling mellem klassen og dens klienter.<br />

Tilgangsmetoder i klassen skal dog skrives, så de ikke ved en fejltagelse afslører den interne<br />

repræsentation. Enhver klasse indeholder en eller flere metoder til filtrering af den information,<br />

som klassen indeholder, så den præsenteres til klienten på en meningsfuld måde. En sådan<br />

filtreringsmetode er den skrøbeligste del af klassens grænseflade, fordi klienten kan gøre sig<br />

afhængig af implementationen, selv om den er skjult. For eksempel,<br />

5.3.5 Adgangskontrol 393


class String {<br />

char* s;<br />

public:<br />

// ...<br />

operator char* () { return s; }<br />

};<br />

I denne klasse findes, ganske fornuftigt, en konverteringsmetode, som laver en String om til<br />

en char*. Dermed kan forekomster af String bruges i alle sammenhænge, hvor en char*<br />

er påkrævet. Men uden at oversætteren beklager sig, er konverteringsmetoden en fuldstændig<br />

blottelse af klassens interne repræsentation. Den returnerer en pointer-variabel til klassens private<br />

data, som derefter kan manipuleres af klienten. Beskyttelsen er dermed intet værd. Metoder, som<br />

returnerer pointere eller referencer til de private data bør altid returnere konstanter, så klienten<br />

ikke kan manipulere med de sensitive data:<br />

class String {<br />

char* s;<br />

public:<br />

// ...<br />

operator const char* () { return s; }<br />

};<br />

Indkapsling af klassens implementation er derfor en nødvendighed, hvis klientens kode skal være<br />

robust og ikke bryde sammen ved mindre ændringer. Er en grænseflade først defineret, er det blot<br />

et spørgsmål om fra både klasseudbyderen og klienten om at holde sig til denne specifikation.<br />

Klassens anden grænseflade, forholdet til den afledte klasse, skal derimod sjældent trækkes så<br />

skarpt op. Et sæt af klassens data bør erklæres protected, så en afledt klasse kan arbejde på<br />

dem uden at skulle gå gennem filtermetoder. Ofte vil en filtermetode ikke være nær så fleksibel<br />

som en direkte tilgang, og vil i alle tilfæde være mindre effektiv. Da den indkapslede del af både<br />

baseklassen og den afledte klasse er usynlig for klienterne, skal effektiviteten være højest mulig i<br />

selve klassens kode. Derfor vil en overspecificeret grænseflade fra baseklasse til afledt klasse gå<br />

ud over klienten, som skal vente unødigt på disse to klassers interkommunikation. Argumentet<br />

den anden vej lyder, at en klient blot kan arve fra klassen og få adgang til de private dele. Det<br />

gøres imidlertid næppe uden at være højest tilsigtet, så en tilfældig afhængighed af klassens<br />

implementation vil ikke opstå på grund af en løs specificeret grænseflade i den beskyttede del af<br />

klassen.<br />

Klassens metoder bør følge stort set de samme regler for indkapsling, blot af den grund, at en<br />

metode, som ikke kommer klienten ved let kan skabe kaos i klassen, hvis den kaldes. Et utilsigtet<br />

metodekald kan skabe større rod end en utilsigtet tildeling til interne data. Metoder, som klienten<br />

ikke har brug for, bør erklæres private, hvis de er helt specifikke til den aktuelle klasse eller<br />

protected, hvis de har betydning udover den og eventuelt kan bruges i en afledt klasse. Ofte<br />

har en klasse brug for et par mindre private metoder, som gør de offentlige, større metoder lettere<br />

at skrive. Et eksempel på dette er de to private metoder i klassen Set fra afsnit 3.8.4, som<br />

394 Organisering af klasser 5.3


udfører logaritmiske beregninger for næsten alle generelle metoder i klassen. Disse metoder er,<br />

selvom de ikke har betydning for objekter af Sets status, ikke brugbare for klienten. Under arv<br />

opstår ydermere det problem, at en nedarvet offentlig metode mister sin betydning for den afledte<br />

klasse, hvorved den kan skjules ved privat arv eller eksplicit reerklæring i den private del af<br />

klassen. Det er på en måde den samme problematik som problemerne med transparensen til de<br />

private data i forhold til klienten, så det er et spørgsmål om at balancere medlemmerne mellem de<br />

private og de beskyttede dele af klassen.<br />

Der kan opstå et mindre problem i forbindelse med arv af virtuelle metoder, som ikke har<br />

betydning for den afledte klasse. Ofte er disse tilfælde en konsekvens af et ugennemtænkt design,<br />

men kan også være resultatet af en meget specialiseret afledt klasse. En virtuel metode vil altid<br />

have den adgangsspecifikation, som den klasse der bruges i klientens reference erklærer metoden<br />

som. Med andre ord, hvis en baseklasse erklærer en public virtuel metode, der skrives i en<br />

afledt klasse i den private del, vil et kald til metoden under reference til baseklassen til et<br />

objekt af den afledte klasse rent faktisk kalde den private metode i denne. For eksempel:<br />

// eksempel 5-2a: en endimensionel mængde-klasse<br />

class Set {<br />

public:<br />

Set (int);<br />

// ...<br />

virtual int operator[] (int);<br />

};<br />

// eksempel 5-2b: en todimensionel mængde-klasse<br />

class Grid : public Set {<br />

virtual int operator[] (int);<br />

public:<br />

Grid (int x, int y) : Set (x * y) { }<br />

// ...<br />

virtual int operator[] (int, int);<br />

};<br />

Klassen Grid er en udvidelse af Set fra afsnit 3.8.4, og udvider mængde-begrebet med en<br />

dimension. Indekserings-operatoren [] med én parameter mister således sin betydning, og<br />

erklæres derfor i den private del af Grid, mens en ny metode med to parametre erklæres i den<br />

offentlige del. Det forhindrer imidlertid ikke klienten i at bruge Grid som erstatning for Set i<br />

polymorfe referencer, og kalde operator[](int) for Grid:<br />

void f () {<br />

Set& s = *new Grid (320,256);<br />

cout


Problemet er, at den nødvendige information for kaldet til en virtuel metode ligger i baseklassen<br />

alene, her Set. Den sene binding er mulig, fordi oversætteren ikke behøver kende til afledte<br />

klasser på oversættelsestidspunktet. Derfor kan den ikke se, at Grid skjuler den virtuelle<br />

metode fra klienten, og metoden kaldes alligevel. Et sådant kald vil normalt resultere i en runtime<br />

fejl, fordi en metode kaldes, for hvilken der ikke findes en meningsfuld repræsentation. I<br />

disse tilfælde bør den private virtuelle metode udskrive en fejlmeddelelse, returnere et fejlobjekt<br />

eller foretage en anden form for undtagelsesaktion. I afsnit 5.9 vises, hvorfor netop sådanne<br />

arvefølger er eksempler på dårligt klassedesign.<br />

Læg mærke til, at referencen altid er relativ til den klasse, som kaldet foretages fra. Hvis<br />

klienten i ovenstående kodefragment i stedet skrev<br />

void f () {<br />

Grid& s = *new Grid (320,256);<br />

cout


Arv er en lineær form for genbrug, fordi baseklassens elementer er entydige i måden de<br />

genbruges på. Det er indlysende, hvilke data og metoder, som den ny klasse direkte kan overtage<br />

og arbejde med. Arv er, som beskrevet i afsnit 5.3.1, i første omgang en genbrugs-teknik, som<br />

vedrører klassedesigneren. Men arv gør også ekstern kode mere genbrugelig, fordi de opsatte<br />

type-forhold mellem baseklasser og afledte klasser tillader udvikling af generisk kode. Generisk<br />

kode er defineret ved funktioner og programfragmenter, som kan arbejde på mange forskellige<br />

typer af objekter uden at skulle modificeres. Generisk kode er ikke det samme som generel kode;<br />

forskellen ligger i en definition af generalitet som noget statisk og anvendt og genericitet som<br />

noget dynamisk, klient-kontrolleret og ofte typeuafhængigt. Disse forhold diskuteres i detaljer i<br />

afsnit 5.9. Til en klasse med virtuelle metoder hører ofte en eller flere funktioner (enten<br />

medlemmer i klassen eller ej), som arbejder på de virtuelle metoder. Disse funktioner har karakter<br />

af genericitet, fordi de typer, de arbejder på, kan erstattes af klienten uden at skulle modificere<br />

eller genoversætte dem. Generel kode arbejder derimod på homogene datatyper.<br />

De generiske elementer udgør således en mere omfattende og subtil form for genbrug, og er<br />

mere fremadrettede, fordi de under klientens kontrol tillader en anden funktionalitet. Et typisk<br />

eksempel på en generisk funktion er<br />

bool operator== (const LinkedList& a, const LinkedList& b) {<br />

return a.isEqual (b);<br />

}<br />

som sammenligner to hægtede lister ved et kald til en virtuel metode. Denne funktion kan kaldes<br />

med alle subtyper under LinkedList, og kan således bruges for både SortedList,<br />

IndexedList og SortedIndexedList fra afsnit 4.5.3, hvis metoden isEqual()<br />

implementeres i de nødvendige klasser. Generisk kode hænger således sammen med virtuelle<br />

metoder, dynamisk binding og polymorfi. En samling af funktioner, som ikke er medlemmer af<br />

klasser, og som benytter virtuelle metodekald er derfor højest genbrugelig, som visualiseret i<br />

figur 5-2, hvor adskillige klientmoduler og klasser bindes sammen af genericiteten i den<br />

mellemliggende kode. Generisk kode er genbrugelig i den forstand, at den klistrer forskellige<br />

klient-elementer dynamisk sammen med objekter af forskellige klasser uden at skulle undergå<br />

ændringer.<br />

Figur 5-2: Generisk kode som klister-element mellem klienter og klasser.<br />

Det er klassens virtuelle metoder, som suverænt bestemmer, hvordan klassen kan anvendes i<br />

5.3.6 Polymorfi og genbrug 397


generiske kodefragmenter. Da generisk kode er ønskværdig, er virtuelle metoder også<br />

ønskværdige. Spørgsmålet bliver altså, hvilken regel der bestemmer om en given metode skal<br />

være virtuel eller ej. Det simple svar er, at en generel klasse skal indeholde mange virtuelle<br />

metoder og at en specifik klasse skal indeholde få nye virtuelle metoder, dvs. ud over de, der er<br />

defineret i baseklassen. Men det kan også betale sig at se på, hvor meget polymorfi der egentlig<br />

er brug for i forbindelse med den aktuelle klasse.<br />

Generelt er de virtuelle metoder meget små og har karakter af filtreringsmetoder og andre af de<br />

administrative slags. En virtuel metode udfører næsten altid en simpel beregning, et opslag i en<br />

tabel eller en sammenligning af data. Metoder, som kun har relevans for den aktuelle klasse skal<br />

ikke være virtuelle. Metoderne Insert() og Retrieve() i LinkedList-klassen i afsnit<br />

4.5.3 er virtuelle, fordi de af natur kan have en anden betydning i en afledt klasse. Metoden<br />

len() i String-klassen i afsnit 3.7.1 er ikke virtuel, fordi en String sandsynligvis ikke<br />

skal erstattes af andre klasser i generiske funktioner. Problemet er, at man ikke kan vide sig<br />

sikker på, om en klasse alligevel skal bruges polymorft på et senere tidspunkt, og en forhastet<br />

regel vil være, at er man i tvivl, bør metoden erklæres virtuel. Grunden til, at det er forhastet er, at<br />

der også er minusser ved at erklære virtuelle metoder.<br />

Har en klasse virtuelle metoder, vil en forekomst af klassen indeholde en skjult pointer til<br />

klassens virtuelle tabel. Denne tabel, som eksisterer én gang for hver klasse med virtuelle<br />

metoder, indeholder adresserne på implementationerne af de virtuelle metoder for den aktuelle<br />

klasse. Har en klasse således virtuelle metoder, vil en forekomst have et skjult ekstra<br />

pladsforbrug. Under multipel arv vil en instantiering resultere i et antal skjulte<br />

pointermedlemmer, der svarer til antallet af baseklasser med virtuelle metoder. På mit system vil<br />

en forekomst af en klasse med virtuelle metoder, afledt fra 3 baseklasser, indeholde 12 bytes i<br />

ekstra skjulte medlemmer. Og 1000 forekomster kræver 12.000 bytes. Objekter med virtuelle<br />

metoder egner sig således slet ikke til at blive instantieret alt for mange gange i et program, og<br />

vektorer (lineære lister) er da heller ikke typisk komponeret af objekter, hvis typer ikke er kendt.<br />

5.3.7 Generalitet<br />

Generalitet er defineret som muligheden for at skabe programelementer, der kan bruges i mange<br />

forskellige sammenhænge. Polymorfi er til dels en generalitets-mekanisme, fordi den tillader<br />

samme kode forskellige anvendelser. Men der er alligevel begrænsninger for polymorfe klasser,<br />

som beskrevet i forrige afsnit. Det handler på en måde om forskellien mellem arv og generalitet.<br />

I mange objekt-orienterede systemer findes muligheden for, at klienten bestemmer visse<br />

statiske parametre af selve klassen (ikke objektet). I C++ er parameteriserede typer (se afsnit 3.8)<br />

en meget velfungerende mulighed. Det tillader klienten at specificere parametre til selve typen<br />

under instantiering af et objekt. På den måde bliver det muligt at instantiere et objekt af typen A<br />

med relation til typen B, hvor B er bestemt af klienten. For eksempel kan et stak-objekt<br />

instantieres med klient-kontrol over hvilke typer af data, stakken skal arbejde på.<br />

I tidligere versioner af C++ er generalitet ofte realiseret gennem avanceret brug af<br />

præprocessor-makroer eller en ekstern makro-processor (UNIX M4 eller lignende), som tillader<br />

ekspansion af kode under klientens parameteriserede kontrol. En simplificering af dette, blot for<br />

at tjene til eksempel, er en parameteriseret type med brug af typedef:<br />

398 Organisering af klasser 5.3


typedef int T;<br />

// enkel stakklasse, kopierer medlemmer ved Push og Pop<br />

class Stack { // parameteriseret ved T<br />

T* _vec;<br />

unsigned size;<br />

public:<br />

Stack (int sz) : size (sz) { _vec = new T [sz]; }<br />

~Stack () { delete _vec; }<br />

void Push (T& elem) { *_vec++ = elem; }<br />

T Pop () { return *--_vec; }<br />

};<br />

I C++ 3.0, og efter al sandsynlighed også i ANSI C++, vil parameteriserede typer kunne forbedre<br />

sådanne klasser. Det er især fordi sproget selv overtager parameteriseringen og således kan lade<br />

de nye klasser indgå i typesystemet. Ovenstående klasse vil med parameterisering se således ud:<br />

// enkel stakklasse, kopierer medlemmer ved Push og Pop<br />

template <br />

class Stack { // parameteriseret ved T<br />

T* _vec;<br />

unsigned size;<br />

public:<br />

Stack (int sz) : size (sz) { _vec = new T [sz]; }<br />

~Stack () { delete _vec; }<br />

void Push (T& elem) { *_vec++ = elem; }<br />

T Pop () { return *--_vec; }<br />

};<br />

5.3.8 Overvejelser om multipel arv<br />

Multipel arv findes kun i få objekt-orienterede sprog, og er som sådan ikke determinant for, om et<br />

system er rigtig objekt-orienteret. Multipel arv kan gøre programudviklingen mere fleksibel, fordi<br />

modulariseringen kan flettes sammen på flere niveauer, men kan også gøre et klassehierarki<br />

meget kompliceret. Med multipel arv er det vigtigt, at klasserne ikke bliver for specialiserede, at<br />

de er små og funktionelle og at de strengt holder sig til deres egne domæner. Multipel arv er på<br />

sin vis en revers teknik i forhold til lineær arv: hvor lineær arv er en dekomposition af<br />

kompleksitet i programmet, som søger at specialisere de enkelte og fjerneste klasser, er mulitpel<br />

arv derimod en rekomposition af de enkelte klasser i hybride, komplekse moduler. Figur 5-3(a)<br />

viser, hvordan arv delegerer opgaver fra et kompleks ud i de enkelte klasser og senere i figur 5-<br />

3(b), hvordan disse klasser kan kombineres igen med multipel arv. Således reflekterer multipel<br />

arv en forståelse af de enkelte klasser, som skal kunne opfattes som isolerede entiteter i<br />

klassebiblioteket.<br />

5.3.7 Generalitet 399


Figur 5-3: Modulær dekomposition (a) og rekomposition (b).<br />

Det første spørgsmål som falder for, er om det altid er muligt og fornuftigt at arve fra to eller<br />

flere tilfældige klasser og få fordelen ved alle disse klassers funktionalitet i en ny klasse. Svaret er<br />

sådan set nej. Multipel arv er en teknik, der ikke kan bruges vilkårligt. Baseklassernes funktion,<br />

arbejdsområde og specielt dataindhold er bestemmende for, om to eller flere klasser kan<br />

kombineres i en multipelt afledt klasse. Det er derfor nødvendigt med et indgående kendskab til<br />

alle klasser i hierarkiet fra baseklasserne til hierarkiets rod, og det er i praksis tit nødvendigt at<br />

modificere disse klasser mere eller mindre før en multipel afledning er fornuftig. Det skal derfor<br />

understreges, at multipel arv ikke er en grundpille i objekt-orienterede systemer og ikke udgør et<br />

fundamentalt aspekt. Ydermere er multipel arv en relativ ny teknik, set i forhold til eksistensen af<br />

det objekt-orienterede paradigme. I litteraturen er multipel arv defineret med syntaksmæssige<br />

regler og måder at komme udenom problemer på, men en egentlig anvendelse i et større<br />

perspektiv findes ikke endnu. Der er med andre ord ingen bredt accepteret designmetode, som<br />

anvender en objekt-orienteret datamodel med multipel arv som en grundlæggende mekanisme.<br />

Der skal med under alle omstændigheder tænkes målrettet og fremadrettet, når der skal bruges<br />

multipel arv. I eksemplet med de fire LinkedList-klasser i afsnit 4.5.3 er det tydeligt, at de er<br />

konstrueret på samme tid og på den måde er tiltænkt hinanden. De fleste anvendelser for multipel<br />

arv indebærer, at de klasserne opefter i hierarkiet indgår i en kombineret klynge, som alle er med<br />

til at realisere en eller flere multipelt afledte klasser. Det hænger i høj grad sammen med, at en<br />

multipel afledning af to fuldstændig forskellige klasser ikke giver den store fordel for hverken<br />

klassen eller klienten:<br />

class LinkedList;<br />

class Complex;<br />

class ComplexLinkedList :<br />

public LinkedList, public Complex { // ... ?!<br />

En multipel afledt klasse skal derudover i næsten alle tilfælde indeholde ekstra metoder for at<br />

skabe den rigtige forbindelse mellem baseklasserne. Det er sjældent nok at kombinere to klasser<br />

uden at omskrive enkelte metoder eller flytte rundt på dem. Multipel arv er således ikke en teknik<br />

400 Organisering af klasser 5.3


til impulsive udvidelser af hierarkiet. Det er nødvendigt at tænke fremad.<br />

En af grundene hertil er, at multipel arv med relaterede baseklasser vil skabe flere subobjekter<br />

af en fælles baseklasse, som demonstreret i afsnit 4.5.3. En sammenlægning af disse subobjekter<br />

indebærer i C++ en modifikation i de klasser, som ligger umiddelbart under klassen med de<br />

multiple forekomster, hvor den fælles baseklasse erklæres virtuel.<br />

Eksemplet ovenfor kan også overføres på brugen af kommercielle klassebiblioteker, som<br />

forsøges integreret i samme program. Det er sjældent fordelagtigt, og ofte umuligt, at skabe<br />

multipelt afledte klasser af baseklasser, som tilhører hvert sit isolerede hierarki. To<br />

klassebiblioteker, for eksempel med ophav i henholdsvis vinduesstyring og matematik, har tit en<br />

så forskellig opbygning med virtuelle metoder, konstruktøropbygninger og statiske medlemmer,<br />

at en multipel afledning simpelthen ikke kan lade sig gøre. Konsekvensen heraf bliver før eller<br />

siden, at generelle retningslinier for samarbejde mellem klassebiblioteker bliver nødvendig,<br />

eventuelt med perifere grænseklasser til brug for integration med andre, isolerede klasser. Indtil<br />

da må koncentrationen centreres om at anvende multipel arv mellem klasser, der har en vis<br />

kohærens.<br />

Multipel arv er ofte udråbt som en avanceret mekanisme til genbrug af kode. I virkeligheden er<br />

teknikken mere en metode til at relatere klasser på, så de kan bruges polymorft fra flere<br />

forskellige baseklasser. En multipel afledt klasse indeholder typisk ikke mange medlemmer<br />

udover de, der eventuelt skal løse tvetydigheder, fordi baseklasserne normalt er i bunden af<br />

hierarkiet og derfor sjældent er abstrakte klasser. Ved generelt at anskue multipel arv som en<br />

relationsmekanisme i stedet for en genbrugsmekanisme bliver vi automatisk mere fremsynede.<br />

Det gælder nu ikke om at få en "hurtig profit" ved at arve, men om senere at kunne benytte den<br />

multipelt afledte klasse polymorft fra flere forskellige baseklasser. Et eksempel herpå er et<br />

generelt hierarki til organisering af vinduer i et grafisk miljø. Vinduer har ofte forskellige<br />

attributter, bla. menuer og gensidigt udelukkende "knapper", som danner indholdet af vinduerne.<br />

Disse attributter bliver determinanter for, hvordan et vindue kan manipuleres udefra. I følgende<br />

eksempel findes fire forskellige attributter, som normalt er til stede i en grafisk brugerflade:<br />

redigeringsboks (EditBox), tænd/sluk-knap (RadioButton), menuvalg (Menu) og ikon<br />

(Icon). Disse fire elementer er erklæret i hver sin isolerede klasse, ligesom et generelt vindue<br />

(Window), som beskriver dimensioner og ramme.<br />

Med disse grænsefladeprimitiver skabes nu fire nye klasser, alle lineært afledt fra Window,<br />

som beskriver henholdsvis vinduer med redigeringsboks(e), vinduer med tænd/sluk-knap(per),<br />

vinduer med menu(er) og vinduer med ikon(er). De fire nye klasser er således subklasser af<br />

Window, men ikke af de fire attribut-klasser. Der afledes altså ikke multipelt. Der er flere<br />

principielle grunde: et vindue med attributter er for forskellig fra en enkelt attribut til, at de kan<br />

have et tilhørsforhold i type. Desuden skal "et vindue med ikoner" klart kunne have mere end én<br />

ikon, hvorfor der bruges klientering (med en pointer) i stedet. Men allerede under oprettelsen af<br />

de fire specifikke vindues-klasser kan vi begynde at tænke fremad. Der er sandsynligvis brug for<br />

vinduer med både menuer og ikoner, redigeringsbokse og knapper osv., og disse klasser kan<br />

drage fordel af multipel arv. Derfor erklærer vi Window virtuel i alle klasserne, da der for hvert<br />

vindue kun skal være et enkelt objekt af denne type.<br />

class EditBox; // redigeringsboks<br />

class RadioButton; // tænd/sluk-knap<br />

5.3.8 Overvejelser om multipel arv 401


class Menu; // menu<br />

class Icon; // ikon<br />

class Window; // vindue<br />

// vindue med tilhørende redigeringsbokse<br />

class WinEditBox : virtual public Window {<br />

EditBox* EBlist; // liste af redigeringsbokse<br />

// ...<br />

};<br />

// vindue med tilhørende tænd/sluk-knapper<br />

class WinRadioButton : virtual public Window {<br />

RadioButton* RBlist; // liste af knapper<br />

// ...<br />

};<br />

// vindue med tilhørende menuer<br />

class WinMenu : virtual public Window {<br />

Menu* Mlist; // liste af menuer<br />

// ...<br />

};<br />

// vindue med tilhørende ikoner<br />

class WinIcon : virtual public Window {<br />

Icon* Ilist; // liste af ikoner<br />

// ...<br />

};<br />

At skabe kombinationer af disse klasser er nu trivielt. Vinduer med ikoner og menuer og vinduer<br />

med redigeringsbokse og knapper samt vinduer med alle fire attributter kan erklæres på følende<br />

vis:<br />

class WinMenuIcon : public WinMenu, public WinIcon {<br />

// ...<br />

};<br />

class WinEditBoxRadioButton : public WinEditBox,<br />

public WinRadioButton {<br />

// ...<br />

}<br />

class WinTotal : public WinMenu,<br />

public WinIcon,<br />

public WinEditBox,<br />

402 Organisering af klasser 5.3


...<br />

};<br />

public WinRadioButton {<br />

Det er tydeligt, at disse klasser hænger solidt sammen, at der er tænkt på klasserne i bunden af<br />

hierarkiet, før deres baseklasser er færdigtskrevede. Denne kendsgerning afspejler kompleksiteten<br />

af hierarkidesign med multipel arv og viser samtidig, at samspillet mellem klasser i en større<br />

sammenhæng skaber nye perspektiver i programdesign. Det typesystem, som nye klasser<br />

repræsenterer, skal konstrueres med klassernes egentlige attributter som grundlag. Som<br />

eksemplificeret i afsnit 4.3 er dette ikke en triviel opgave, fordi de færreste reelle klasser<br />

repræsenterer letforståelige abstraktioner.<br />

Figur 5-4: Hierarki af klasser i et vinduessystem.<br />

En vis hjælp er dog at se efter de enkelte klassers egenskaber, og hæfte substantiver og adjektiver<br />

på disse, for eksempel åbent vindue. Skal en ny klasse integreres, bruges disse termer til en tidlig<br />

identifikation af klassens placering. Deler to klasser, som begge skal bruges i forbindelse med<br />

den ny klasse, et substantiv, benyttes multipel arv - uanset, om der forekommer uens adjektiver<br />

mellem de to. Deler to klasser adjektiv, arves fra den vigtigste (med flest datamedlemmer, med<br />

flest afledte klasser eller med flest virtuelle metoder) og den anden klienteres. Metoden kan<br />

karakteriseres som opbyggelsen af et semantisk katalog.<br />

5.3.8 Overvejelser om multipel arv 403


Med et semantisk katalog er det muligt at identificere de væsentligste attributter i alle klasser,<br />

en vigtig ledetråd i valget mellem klientering og arv. En mangel i metoden er hensyntagen til<br />

polymorfi, som kræver nedarvning for at kunne fungere. At det kun er en ledetråd skal derfor<br />

understreges med, at den væsentligste baseklasse ofte også er den, som har egentlig type-relevans<br />

med den ny klasse og derfor vil indeholde de virtuelle metoder, der skal implementeres.<br />

Klassenavn Substantiv Adjektiv(er) Afledt fra Klient af<br />

Window Vindue (ingen) (ingen) (ingen)<br />

Icon Ikon (ingen) (ingen) (ingen)<br />

Menu Menu (ingen) (ingen) (ingen)<br />

WIconMenu Window Icon<br />

Vindue Ikon, Menu<br />

Menu<br />

IconWin Icon Window<br />

Ikoniseret Vindue<br />

Tabel 5-1: Et eksempel på et semantisk katalog over klasser.<br />

Selv om multipel arv virker kompliceret og lidet brugbar kan en model således hjælpe med<br />

udvælgelsen. Tilbage er spørgsmålet om, i hvilke situationer multipel arv gør mest nytte og<br />

hvordan den - set fra den ny klasses synspunkt - skal iværksættes for at opnå de optimale<br />

betingelser i et objekt-orienteret system med genbrugelighed, generalitet og type-ækvivalens. Her<br />

følger et antal sammenhænge, som multipel arv oftest bruges i:<br />

• En multipelt afledt klasse kan kombinere rene virtuelle metoder i en abstrakt klasse med<br />

implementationer af samme metoder i en anden klasse og dermed ved en simpel<br />

nedarvningskonstruktion skabe en fungerende, aktiv klasse for den givne abstrakte<br />

klasse. På den måde kan man tænke sig en generel liste-klasse, som indeholder en<br />

virtuel metode Sort(). Ved multipelt at aflede fra både denne klasse og en anden<br />

klasse, som implementerer metoden Sort() opnås en aktiv klasse, der er i stand til at<br />

sortere elementerne i listen. Ved videre at indkapsle forskellige sorteringsalgoritmer i<br />

klasser er det herved muligt at vælge sorteringsmetode (QuickSort, HeapSort, Merge<br />

Sort etc.) efter behov ved blot at specificere den ønskede sorteringsklasse. Et andet<br />

eksempel kunne være en abstrakt stak, som ved multipel arv kan kombineres med en<br />

liste-implementation, en array-implementation eller lignende.<br />

• Når der arbejdes med polymorfi er det ofte ønskværdigt at have flere forskellige<br />

indgange til samme klasse gennem virtuelle metoder, for derigennem at kunne gøre brug<br />

af så mange generiske funktioner som muligt. En multipelt afledt klasse er en subtype af<br />

alle de klasser, den arver fra, og kan således erstatte enhver af disse i funktionskald, som<br />

forventer den som parameter. Her er tale om en semantisk kontekst, som gør det lettere<br />

at bruge samme objekt i ellers urelaterede dele af et program. En<br />

SortedIndexedList som beskrevet i afsnit 4.5 har den fordel, at den både kan<br />

404 Organisering af klasser 5.3


optræde som indekseret liste, som sorteret liste og som almindelig hægtet liste, og<br />

følgelig derfor kan bruges som parameter i kald til funktioner, der arbejder på objekter<br />

af disse typer. Det giver klassen et umiddelbar stort klientel, som allerede er skrevet og<br />

kan genbruges for instanser af den nye klasse.<br />

• En speciel familie af klasser indkaplser blot konstanter, statiske metoder og/eller statiske<br />

data og kan på sin vis karakteriseres som et "modul". Eksempeler på en sådan klasse er<br />

et matematisk eller grafisk bibliotek, som ikke egner sig til egentlig instantiering, men<br />

som blot samordner et antal relaterede metoder og datastrukturer. En multipel afledning<br />

fra en vilkårlig klasse og et indkapslet "modul" giver den nye klasse adgang til modulet,<br />

som kan benytte sig af de indkapslede faciliteter. Dette er egentlig rudimentær<br />

modulærprogrammering, men viser, hvordan klasser i C++ har alsidighed.<br />

• En af baseklasserne kan have egenskaber, som har betydning for behandlingen af de<br />

andre baseklasser, og som derved mere eller mindre transparent bidrager til en<br />

specialisering af disse klasser. Et eksempel er en klasse, der implementerer bestandige<br />

objekter, dvs. objekter, hvis levetid er på et højere niveau end programmets eksekvering.<br />

Bestandige objekter gemmes på et sekundærlager, og det er administrationen af denne<br />

lagring, der varetages i den bestandige klasse. De andre baseklasser behøver for så vidt<br />

ikke at være klare over denne specialisering, hvorved egenskaben helt kan isoleres i én<br />

klasse. Den multipelt afledte klasse indeholder i dette tilfælde ofte ingen medlemmer,<br />

men erklærer blot et typeforhold mellem baseklasserne (en videre diskussion af<br />

bestandige objekter findes i afsnit 5.6).<br />

• Til test-formål kan det være givtigt at vedligeholde en standardiseret test-klasse, som<br />

varetager nogle basale metoder så som brugerkommunikation, inspektion af virtuelle<br />

metodetabeller og opgørelse af lagerforbrug. Da mange klasser skal gennemgå den<br />

samme type tests, kan en multipel afledning mellem en nyskreven klasse K og en<br />

generel klasse Test samles i K_Test. Der spares både tid og sikres konsistens ved at<br />

bruge samme test-klasse i forbindelse med test af alle nye klasser.<br />

I mange tilfælde er multipel arv altså en teknik, som tillader et kald i et objekt at resultere i et<br />

transparent kald til et andet, ellers urelateret, objekt. I version 2.0 af AT&T C++ Language<br />

System er det nødvendigt at reerklære rene virtuelle metoder i en afledt klasse, der ikke<br />

implementerer dem. Dette krav er fjernet fra og med version 2.1, hvorved multiple afledninger<br />

ofte kan være helt tomme klasseerklæringer. I det følgende eksempel ses, hvordan et kald til en<br />

klasse resulterer i et "parallelt" kald til en anden, fra klassens synspunkt helt urelateret klasse.<br />

class A { public: f(); };<br />

class B { public: virtual f() = 0; };<br />

class C : public A, public B { };<br />

B& bref = new C;<br />

int i = b.f(); // kalder a.f()<br />

5.3.8 Overvejelser om multipel arv 405


Tilbage er at diskutere virtuelle baseklasser. Generelt bør en klasse, der er base for flere andre<br />

klasser erklæres virtuel i disse klasser, for simpelthen at undgå en uønsket dublering af objekter.<br />

Det er sandsynligt, at der i et lavere lag i hierarkiet vil forekomme en multipel afledning, som<br />

resulterer i en dobbelt instantiering af base-objektet. Typesystemet kan omgås ved brug af skopoperatoren,<br />

og et dobbelt-objekt vil i nogle tilfælde ikke kunne opdages og vil være en potentiel<br />

fejlkilde. Man kan sige, at virtuelle baseklasser i næsten alle tilfælde er det ønskelige, og det er<br />

bare ærgeligt, at der ikke arves underforstået på denne måde. Det er på den anden side svært at<br />

opstille endegyldige regler for, hvornår der ikke skal bruges virtuelle baseklasser. Et eksempel<br />

tjener derfor bedre:<br />

I et banksystem er hovedklassen en Kunde. Denne klasse arver fra forskellige administrative<br />

klasser, bla. PersonData og LoenKonto. Klassen LoenKonto er igen nedarvet fra den<br />

generelle klasse Konto, som arbejder med indbetalinger og udtræk samt saldo- og<br />

renteberegninger. Et andet sted i systemet findes andre klasser, som også er nedarvet fra Konto,<br />

nemlig EtableringsKonto og PensionsKonto. Da en kunde udmærket kan have<br />

forskellige konti i samme bank, skal arvingerne af Konto ikke erklære denne klasse virtuel, da<br />

der vil opstå seriøse problemer med pengenes beliggenhed! Der kan i dette tilfælde være mange<br />

Konto-subobjekter i et Kunde-objekt.<br />

Beslutningen om, at en baseklasse skal være virtuel er en smule svær at tage, ikke mindst fordi<br />

den syntaktisk skal nedfældes på et sted, hvor hverken den virtuelle baseklasse eller den afledte<br />

klasse, der bliver direkte berørt af virtualiseringen befinder sig. Der er to omstændigheder, der<br />

skal være til stede for, at en virtuel baseklasse er tilrådelig: først skal baseklassen repræsentere et<br />

subobjekt, der egner sig til multipel arv og dernæst skal dette subbjekt være af så tilpas generel<br />

karakter, at enhver instantiering af objektet kun har en og samme betydning. Med andre ord, hvis<br />

et objekt af baseklassens type repræsenterer et begreb, som i sig selv er udeleligt (for eksempel en<br />

printer), er en virtualisering nødvendig. Modsat, hvis baseklassen kan opfattes på forskellige<br />

måder, givet sammenblandingen med andre objekter under multipel arv (som i eksemplet<br />

ovenfor) er det en dårlig idé at bruge virtuelle baseklasser. En forhastet beslutning på denne front<br />

vil enten skabe unødig tvetydighed i de fjernere afledte klasser eller umuliggøre den lineære<br />

genbrugelighed, der ligger i normal arv. Det er imidlertid sådan, at der meget sjældent er brug for<br />

ikke at erklære en baseklasse virtuel. At den ikke er virtuel som standard har syntaksmæssige og<br />

historiske årsager (multipel arv blev først introduceret i version 2).<br />

5.3.9 Overblik<br />

Objekt-orienteret programmering bliver først en rigtig profitabel teknik, når stadiet over<br />

indkapsling og generel dataabstraktion, nemlig opbygningen af avancerede relationer mellem<br />

klasserne, forstås og sættes i perspektiv til et givet problem. I dette afsnit har vi set, hvordan<br />

valget af relationer skal gribes an, og hvordan klientering, arv og polymorfi på hver sin måde har<br />

grundlæggende egenskaber og muligheder i forhold til klassens grænseflade. I C++ er der visse<br />

andre forhold, en programmør er nødt til at være opmærksom på, hvis en applikation ikke skal<br />

ende i urede. Udover rationaler for abstraktion og relatering er det vigtigt at være opmærksom på<br />

sprogets anvendelse af disse teknikker, så en forståelse for pladsforbrug og effektivitet kommer<br />

på plads. Vi tager tre hovedpunkter op:<br />

406 Organisering af klasser 5.3


• Enhver virtuel metode er i virkeligeheden en pointer til en funktion, som ligger i ethvert<br />

objekt, der er en forekomst af klassen. En klasse med en eller flere virtuelle metoder<br />

indeholder en virtuel metodetabel med adresser, der indekseres med et fortløbende<br />

nummer på en given metode i klassen, hvorefter der foretages et indirekte kald til denne<br />

adresse. Hvis et array på 1000 elementer af en type, der indeholder virtuelle metoder<br />

erklæres, vil et skjult dataforbrug i form af tabeller udgøre 1000 pointere. På mit system<br />

er det 4000 bytes, hvilket forøges med 4000 bytes for hver yderligere baseklasse (under<br />

multipel arv). Vær derfor opmærksom på ikke at overdynge klasser, som skal<br />

instantieres mange gange (dvs. mere end et par snese), med virtuelle metoder. Der er<br />

også et mindre tidsmæssigt overhead i kaldet til en virtuel metode, fordi metodens<br />

effektive adresse først skal beregnes. I praksis er der dog tale om en eller to primitive<br />

instruktioner, hvilket ikke udgør en alarmerende faktor.<br />

• Metoder, der defineres indenfor klassens erklæring eller som erklæres inline<br />

erstatter fysisk metodekaldet på kalderens side. For selv små inline-metoder kan<br />

frekvente kald til dem medføre en betydelig stigning i størrelsen af det eksekverbare<br />

program. Specielt overstyrede operatorer, typekoverteringer, konstruktører og<br />

destruktører, som alle kaldes underforstået og på en måde skjult, kan udgøre en<br />

væsentlig del af programkoden. Her er egentlig tale om en klassisk hastighed-versuspladsforbrug<br />

problemstilling, som ikke lader sig løse ved simple regler. Det er blot<br />

vigtigt kun at bruge inline-metoder i situationer, hvor høj hastighed virkelig er påkrævet.<br />

• Medlemsfunktioner, der kaldes underforstået, skal generelt være meget korte. Idet<br />

klienten ikke direkte kan styre kaldene, vil ineffektive eller overadministrative<br />

konstruktører, typekonverteringer eller lignende tage en stor bid af programmets<br />

kørselstid. Det gælder også overstyrede operatorer samt new og delete, ligesom<br />

tildelinger og initieringer medfører disse skjulte kald. Hvis programmet på mystisk vis<br />

kører langsomt er det sandsynligvis en af de specielle metoder, der er synderen.<br />

Udover konkretiserede områder som disse er kommunikation mellem objekter et<br />

tidskonsumerende element. Gør så vidt muligt brug af referencer og/eller pointere under<br />

interobjekt-kommunikation eller flyt metoder, der modtager parametre af store objekter by-value<br />

ind i klasserne. Ofte skrives metoder i de forkerte klasser (på grund af procedural tænkning), og<br />

modtager unødigt mange og identiske parametre.<br />

5.4 GRÆNSEFLADER<br />

Indkapsling gør ikke blot objekter til centrum for programkonstruktion med både data og<br />

funktionalitet under samme tag, det åbner også nye perspektiver i kommunikationen mellem<br />

programmets bestanddele - objekterne - konkretiseret i udvekslingen af data, visibiliteten og<br />

opfattelsen af datastrukturer samt den semantiske forståelse for objektet: kort og godt,<br />

grænsefladerne mellem objekterne. Grænsefladen er bestemmende for, hvor anvendelig objektet<br />

5.3.9 Overblik 407


er i praksis, og da grænsefladen beskrives ved adgangskontrol og tilgangsmetoder, er en god<br />

grænseflade en fornuftig indkapsling og et sæt brugbare metoder til at jonglere med objektet. En<br />

klasses metoder kan kategoriseres i to grupper, behandlende metoder og grænseflademetoder.<br />

Mens de behandlende metoder implementerer algoritmer og arbejder på objektets data er<br />

grænseflademetoderne ansvarlige for ind- og udlæsning, konvertering og præsentation af data og<br />

er på en måde en hjørnestenen i bestræbelsen på at opnå en ensartethed mellem den eksterne<br />

opfattelse af relaterede objekter.<br />

Grænseflademetoder findes sjældent under andre paradigmer. Struktureret programmering<br />

definerer en grænseflade som en parameterliste og/eller en returværdi i en funktionserklæring. I<br />

den forstand er en grænseflade mere eller mindre begrænset til datastrukturernes udseende. Når vi<br />

arbejder objekt-orienteret indfører vi praktiske, håndgribelige metoder til varetagelse af<br />

objekternes grænseflader og lader disse være den mest betydende faktor for, hvordan objektet<br />

behandles syntaktisk og opfattes semantisk af resten af programmet. Fordi denne type metoder<br />

ikke bruges under andre former for programkonstruktion og derfor udgør et nyt fundament, er en<br />

forståelse for deres sammenhæng med objekt-orienteret programmering vigtig.<br />

Skal man følge de basale objekt-orienterede designmetoder, der eksisterer i dag, er<br />

grænsefladen identisk med skillelinien mellem den åbne og den lukkede del af objektet. Den<br />

lukkede del af objektet, som indeholder de implementations-sensitive data, kan ikke manipuleres<br />

udefra. To ting sikres, nemlig intern konsistens og sikkerhed samt robustheden af den eksterne<br />

kode. Denne definition er korrekt, men ufuldstændig i forbindelse med et konkret<br />

programmeringssprog. Som de fleste sprog indeholder C++ specielle konstruktioner og<br />

muligheder, som gør en udvidet definition nødvendig. Overstyring af operatorer, brugerdefineret<br />

typekonvertering og skop-kontrol mellem baseklasser og afledte klasser i flere led komplicerer<br />

ikke blot selve designet af grænsefladen, men gør den reelt todelt: grænsefladen til klienten og<br />

grænsefladen til den afledte klasse. Klasser skrives med klienter som modtager eller med en<br />

potentiel afledt klasse som modtager, og i de fleste tilfælde, begge. Disse to modtagergrupper er<br />

helt forskellige og et godt grænsefladedesign er derfor i yderste konsekvens bestemmende for en<br />

klasses succes.<br />

Figur 5-5: Dualisme i klassens grænseflade.<br />

408 Grænseflader 5.4


5.4.1 Grænsefladen til klienten<br />

Fra klientens side er det primære spørgsmål "gør klassen det arbejde, den skal", altså fungerer<br />

dens metoder korrekt og indkapsler den ikke fejl, som klienten ikke kan gøre noget ved. Det<br />

næste kriterion er, hvordan klassen gør dette arbejde, stadig set fra klientens side. Når en klasse,<br />

der repræsenterer et specifikt koncept skal tages i brug, er graden af "klient-venlighed" nemlig<br />

bestemt af grænsefladens opbygning. Faktisk er selve abstraktionen nedfældet i<br />

grænseflademetoderne.<br />

Hvilke krav bør en klient så stille til en grænseflade? Spørgsmålet kan ikke besvares entydigt,<br />

da det helt og holdent kommer an på klassens indhold. Men der er overordnede mål, som enhver<br />

klassedesigner bør sætte sig, før klassen udbydes til klienten. Klassens grænseflade skal være<br />

konsistent på flere måder. I metoder, der arbejder på et subset af klassens data, skal dette subset så<br />

vidt muligt altid være det samme. Med andre ord, hvis en Hent()-metode returnerer en<br />

bestemt datastruktur, bør en tilsvarende Indsaet()-metode modtage samme datastruktur.<br />

Ligeledes skal klassens natur æres gennem grænsefladen. Hvis klassen repræsenterer et<br />

algebraisk eller numerisk begreb, bør der være metoder til at behandle objekter af denne klasse<br />

algebraisk eller numerisk. Denne konsistens er til dels tvungen gennem brug af arv og polymorfi,<br />

men i mange andre tilfælde, hvor der ikke er arvefølge mellem to objekter, bør ensartetheden<br />

alligevel være til stede. Hvadenten der er tale om en hægtet liste eller et binært træ, bør den<br />

overordnede grænseflade for de to klasser minde om hinanden. Så der er tale om både<br />

grænsefladens udseende, en kvalitativ faktor, og om dets omfang, en kvantitativ faktor. Klassen<br />

bør være konsistent både i form af metodernes betydning (og det kræver en forståelse for andre<br />

klasser i systemet) og deres antal. hverken for få eller for mange grænseflademetoder er en fordel.<br />

Selvom det ofte siges, at designet skal starte langt væk fra et bestemt programmeringssprog, har<br />

de fleste sprog imidlertid faciliteter, som kan gavne konstruktionsfasen væsentligt, specielt hvad<br />

angår interobjektkommunikation. Derfor følger nu et antal generelle overvejelser om brugen af<br />

konkrete abstraktionsmekanismer i C++, når de bruges i forbindelse med en grænseflade til en<br />

klasse.<br />

• Overstyring af operatorer giver klasser, især de mindre, en mulighed for at udvide<br />

sprogets semantik. Klienten får mulighed for at benytte sig af en standardiseret syntaks i<br />

sammenhæng med forekomster af den nye type. Desværre kan valget af operatorer give<br />

problemer, fordi klassens indhold ofte ikke lader sig blande med andre objekter, hverken<br />

brugerdefinerede eller fundamentale. I så tilfælde må operatorerne vælges meget<br />

forsigtigt og dokumenteres meget præcist. Der skal først og fremmest fokuseres på<br />

klassens indhold på følgende måde: hvordan kan (dele af) objekter af denne type<br />

relateres med andre (dele af) objekter af denne type? Kan de adderes, sammenlignes,<br />

åbnes, lukkes, komprimeres, tømmes eller måske skrives ud? Findes en operator, der<br />

med sund fornuft dækker den operation, der er tale om? Hvis der ikke findes en sådan,<br />

bør en ganske normal metode benyttes i stedet. I anden omgang kan samme spørgsmål<br />

gentages for objekter af denne type sammenblandet med objekter af andre typer -<br />

samtidig med, at vi husker, at typekonverteringer kan erstatte mange operator-<br />

5.3.9 Overblik 409


overstyringer.<br />

• Overstyring af funktioner giver klienten stor frihed, hvadenten funktionerne er metoder i<br />

en klasse eller ej. Denne form for flertydighed er meget håndgribelig, fordi den tillader<br />

samme syntaks (funktionsnavn) med parametre af forskellige typer. Det er egentlig en<br />

syntaksmæssig fordel, fordi klienten ganske simpelt undgår at skulle benytte specielle<br />

funktionsnavne afhængig af typen †<br />

. Klienten skal blot huske, at funktionen f() har en<br />

bestemt mening, og kan så kalde den for alle objekter, for hvilke den er skrevet.<br />

• Brugerdefineret typekonvertering er en teknik, der findes i meget få objekt-orienterede<br />

systemer. Det er en meget avanceret måde, som tillader os at sidestille "klasser" og<br />

"typer" med hinanden uden forbehold, og fortælle klassen, hvordan andre klasser i<br />

systemet kan konverteres til og fra den aktuelle. Det interessante ved netop C++ er, at<br />

konverteringen kan foregå uden klientens indblanding, hvorved klassen selv er ansvarlig<br />

for at kaste og gribe boldene fra andre klasser. Et eksempel på denne form for<br />

abstraktion ses i afsnit 3.7.1, hvor klienten aldrig opdager, at han undertiden arbejder på<br />

substrenge. Brugerdefineret typekonvertering giver også klasseudbyderen en enorm<br />

frihed, fordi han kan overlade det til oversætteren at give objekter af de rette typer til<br />

metoder i klassen eller tilbage til klienten. Når først der findes en konvertering til det<br />

objekt, der skal bruges i en given situation, kan en metode være fællesnævner for<br />

objekter af mange typer. Denne håndgribelige form for polymorfi er illustreret i figur 5-<br />

6, hvor det tydeligt ses, at ved implementation af en enkelt konverteringsmetode (for<br />

eksempel fra A til X) får en klasse umiddelbar adgang til mange andre metoder.<br />

Konverteringen kan både tage formen "fra type" og "til type", implementeret som enten<br />

reel konverteringsmetode i fra-klassen eller som konstruktør i til-klassen.<br />

† Oversætteren vil normalt generere et unikt internt navn på alle overstyrede funktioner og metoder, ofte med ekstra<br />

tegn, der har sammenhæng med parameterlisterne. Dette forhold kan skabe problemer i situationer, hvor der bruges<br />

meget lange overstyrede funktionsnavne: oversætteren kan være nødt til at addere så mange tegn til<br />

funktionsnavnene, at de løber ud over den tillade bredde. Denne fejl fanges ikke af alle oversættere, og vil normalt<br />

resultere i en "dobbeltreferencefejl" fra lænkeren.<br />

410 Grænseflader 5.4


Figur 5-6: Brugerdefineret typekonvertering har også skjulte polymorfe egenskaber.<br />

Der er, for alle typer af grænseflademetoder, en gylden regel: dyng ikke klassen til med et væld<br />

af konverteringsmetoder, operatoroverstyringer og andre metoder, der blot udgør én af mange<br />

måder at komme til klassens data på. Jo simplere des bedre, des mere konsistent og des lettere<br />

forståeligt.<br />

5.4.2 Grænsefladen til den afledte klasse<br />

Enhver klasse bør skrives, så den på et senere tidspunkt kan bruges som baseklasse i en afledning.<br />

Der rejser sig derfor, omend mindre end i sidste afsnit, adskillige spørgsmål vedrørende<br />

grænsefladen mellem de to klasser, basen og den afledte. Det er klart, at grænsefladen skal<br />

implementeres i baseklassen, da den er primær, og at den vigtigste funktion af denne grænseflade<br />

er at beskytte data. Der er imidlertid en åbenlys modsætning i dette mål.<br />

Den afledte klasse skal kunne stå alene og opføre sig overfor klienten som en singularitet, der er<br />

fungerende og effektiv - den skal altså ikke belastes for meget af intern administration og<br />

beskyttelse af data i andet og tredie led. Samtidig skal den afledte klasse ikke kunne manipulere<br />

direkte med data i baseklassen, medmindre denne tillader det. Hvis det altid var lovligt for en<br />

afledt klasse at røre ved baseklassens implementation, kunne enhver klient, der ønskede dette<br />

privilegium blot gøre klassen til base. Så problemet bliver: hvordan skrives baseklassens<br />

grænseflade til den afledte klasse, således at den afledte klasse både bliver tilpas<br />

implementationsuafhængig og samtidig ikke belastes af for mange indirekte kald? Som så ofte er<br />

svaret ikke entydigt, men må bygge på en helhedsvurdering med de obligatoriske forhandlinger<br />

mellem kodestørrelse og eksekveringstid. Der er for så vidt tre muligheder:<br />

1. Baseklassens sensitive data indkapsles private, og skinner slet ikke igennem til den<br />

afledte klasse, der ganske som klienter af baseklassen må benytte de offentlige metoder<br />

som indgang. Denne løsning er kendt fra mange andre objekt-orienterede systemer, har<br />

en stor grad af beskyttelse, men er i praksis meget bindende for effektiviteten i den<br />

afledte klasse. Hvis der forekommer indirekte kald op gennem mange klasser i et<br />

hierarki efter denne metode, kan en klient af en afledt klasse i femte generation komme<br />

til at vente længe på, at en metode gør sig færdig mens den bobler op og ned gennem<br />

5.4.1 Grænsefladen til klienten 411


klassernes tunge, klientorienterede metoder.<br />

2. Baseklassens sensitive data indkapsles private, og der udbydes et antal<br />

adgangsmetoder i den beskyttede del af klassen, som implementeres inline. Herved<br />

får den afledte klasse en stor uafhængighed af baseklassens data for en minimal pris i<br />

eksekveringstid. Kodens størrelse vokser ikke desto mindre for hvert kald til<br />

adgangsmetoderne.<br />

3. Baseklassens sensitive data indkapsles protected. Den afledte klasse tillades<br />

dermed at gøre sig afhængig af disse datas beskaffenhed med en effektivitetsforøgelse i<br />

eksekveringstid til følge. Denne løsning er god i forbindelse med manipulation af data<br />

gennem pointere i den afledte klasse. Det kræver blot, at der lægges vægt på, at den<br />

afledte klasse skal følge visse regler vedrørende implementationen i baseklassen.<br />

Et andet interessant forhold ved den afledte klasse er, at den kan udbygge og/eller ændre på<br />

baseklassens grænseflade mod klienten. Det er på den måde muligt at skabe forskellige<br />

grænseflade-specifikke subklasser af en generel klasse, som hver opfører sig på en speciel måde<br />

og er designet til specielle klienter. Specialiseringen omfatter både konverteringer, operatoroverstyringer,<br />

adgangskontrol og konstruktion. De fire klasser, der alle er former for hægtede<br />

lister gennem afsnit 4.5 er et eksempel på dette, idet de afledte klasser næsten udelukkende<br />

udbygger med grænseflademetoder.<br />

5.5 KLASSER SOM PROCESSER<br />

Det meste af programmeringsaktiviteten i C++ handler om at skabe abstrakte datatyper, som<br />

bruges på samme måde som de fundamentale. Vi har også set, hvordan datatyper ved brug af arv<br />

og polymorfi kan blandes sammen og præsenteres for klienten på en ensartet og robust måde. En<br />

anden anskuelse af klassebegrebet går ud fra klassens processerende egenskaber frem for dens<br />

dataindhold; klassen beskriver en aktiv del af programmet, en proces. Faktisk er forskellen blot,<br />

at det væsentlige, der skinner igennem til klienten ikke er klassens data, men klassens algoritme.<br />

De data, der arbejdes på, er enten mindre væsentlige eller bliver leveret af klienten selv, og<br />

klassen bliver til en slags indkapslet og velpræsenteret algoritme. Et eksempel på en indkapslet<br />

algoritme er en liste-klasse, som arbejder på polymorfe hægter. Klienten kan indsætte vilkårlige<br />

data i listen, hvis egentlige abstraktion er mekanismerne for indsættelse, søgning osv. Listeklasser<br />

har da også normalt blot et enkelt pointer-medlem.<br />

De forskellige metoder og sproglige mekanismer i C++ gør proces- eller algoritmeindkapslende<br />

klasser lette at skrive. Det eneste, der skal være klart, er algoritmens beskaffenhed,<br />

altså hvad den egentlig gør og hvordan. Det er til gengæld ikke så let endda, fordi en algoritme<br />

netop er så svær at abstrahere fra en given programkompleksitet. De funktions- og<br />

procedurebiblioteker, der benyttes i de fleste systemer er ikke helstøbte elementer, der blot kan<br />

sammensættes til et fungerende program.<br />

412 Grænseflader 5.4


5.5.1 Algoritmers transparens<br />

En vigtig, og i konventionelle systemer ofte overset faktor, er en given algoritmes transparens.<br />

Spørgsmålet er rettet mod hvor meget af algoritmens indre funktionalitet, der er brug for på den<br />

anden side af grænsefladen, altså hos klienten. I nogle sammenhænge kan klienten funktionelt<br />

abstrahere fra hele algoritmen, for eksempel i grafiske subsystemer eller matemetiske rutiner. I<br />

andre må elementer af klientens verden blive en del af algoritmen, for eksempel en søgning eller<br />

sortering.<br />

At tale om en algoritmes transparens er altså en kvalitativ vurdering af, hvor meget vi kan<br />

tillade os at skjule fra klienten uden at miste noget væsentligt. Metoderne bør ideelt indkapsles, så<br />

klienten slet ikke bør bekymre sig om implementationerne. Det virker også i praksis, men det<br />

glemmes ofte, at klienten i høj grad skal bekymre sig om hvad og i hvilken rækkefølge metoden<br />

laver. Enten brydes en enkelt proces på et objekt op i flere efterfølgende metodekald, hvor<br />

klienten har mulighed for indblanding mellem kaldene, eller også må metodekaldet involvere<br />

ekstra information om klientens ønske. Overvej for eksempel en traversering af en binær<br />

træstruktur, som forekommer triviel, men alligevel problematisk:<br />

class BinTree {<br />

struct BinNode { BinNode *Left, *Right; } *root;<br />

public:<br />

// ...<br />

void inOrderTraverse (BinNode* = root);<br />

};<br />

void BinTree::inOrderTraverse (BinNode* cursor) {<br />

if (cursor->Reft) inOrderTraverse (cursor->Reft);<br />

// gør noget ved hægten ...<br />

if (cursor->Right) inOrderTraverse (cursor->Right);<br />

};<br />

I midten af traverseringsmetoden skal BinTree pludseltig tage stilling til, hvad der skal gøres<br />

ved indholdet af hægten. Enten må metoden skrives, så en virtuel funktion kaldes, som klienten<br />

kan skrive i en afledt klasse, eller også må klienten som parameter overføre information til<br />

inOrderTraverse(), så metoden ved, hvad den skal gøre. I dette tilfælde er transparensen af<br />

algoritmen inorder traversering af binært træ meget lille - klienten skal være så meget med i<br />

arbejdsgangen, at den skal forstås til det inderste. Det kan godt være, at implementationen af<br />

metoden kan skrives på en ikke-rekursiv eller anden måde, hvor klienten kan være ligeglad, men<br />

for at benytte sig af metoden, skal en vis viden om algoritmens opbygning være kendt. Det er<br />

naturligvis klart, at en algoritme og dens implementation ingenlunde er det samme, hvorfor det er<br />

algoritmens, og ikke implementationens, transparens, der er interessant.<br />

Klasseudbyderen skal altså i høj grad tage stilling til, hvor transparent en given operation på<br />

objekter af klassen er, og finde en passende metode, hvorpå klienten kan være med.<br />

5.5.1 Algoritmers transparens 413


5.5.2 Datastrukturers transparens<br />

Helt analogt med algoritmers transparens er en transparensen af en indkapslet datastruktur præcis<br />

ligeså vigtig at være klar over. Det er ydermere en erkendelse, som har betydning for både<br />

grænseflademetoder og adgangskontrol. En datastruktur bruges i næsten alle klasser som<br />

fundament for klassens indre virkemåde. Klasseudbyderen må tage stilling til, om den<br />

datastruktur, som klassen bygger på, er væsentlig for klienten eller ej. I mange tilfælde er<br />

klassens fundamentale datastruktur en implementationsdetalje, som klienten ikke behøver være<br />

vidende om, men ofte er den identisk med klassens abstraktion, og må skinne igennem til klienten<br />

i en højere grad.<br />

Eksemplet fra forrige afsnit med et binært træ ses nedenfor i sammenligning med en generel<br />

stak-klasse. Begge klasser er en abstraktion af en datastruktur, men stakken er mere afgrænset. En<br />

klient af Stack skal blot vide, hvordan en stak fungerer. Datastrukturen, der underbygger<br />

klassen, kan følgelig være en hægtet liste, et array eller andet; klassen sørger for den eksterne<br />

forståelse af strukturen som en stak. Med BinTree-eksemplet er situationen næsten den<br />

samme. Klassen repræsenterer en kendt datastruktur, men i dette tilfælde er anvendelsen af<br />

strukturen så meget mere kompliceret, at implementationen bliver meget lig den eksterne<br />

forståelse, ligemeget hvad klasseudbyderen gør for at indkapslse den. Når der skal arbejdes med<br />

en binær træstruktur er det nødvendigt at kunne komme tæt på de enkelte hægter, bytte dem<br />

rundt, rearrangere træet osv. Derfor er datastrukturen ikke transparent, og må skrives, så den<br />

skinner igennem til klienten.<br />

class Element;<br />

class Stack {<br />

// ...<br />

public:<br />

void Push (Element&);<br />

Element& Pop ();<br />

};<br />

class BinTree {<br />

// ...<br />

public:<br />

Element& getParent ();<br />

Element& getLeftChild ();<br />

Element& getRightChild ();<br />

void insertParent (Element&);<br />

void insertRightChild (Element&);<br />

void insertLeftChild (Element&);<br />

};<br />

414 Klasser som processer 5.5


Den mest åbenlyse måde at gør dette på for BinTrees vedkommende er at placere<br />

strukturens implementation i den beskyttede del af klassen, så klienten i en afledt klasse kan<br />

udbygge med de metoder, der er brug for.<br />

5.5.3 Modulærprogrammering<br />

Man skal ikke overse klassemekanismens mulighed for indkapsling af konstanter og globale<br />

funktioner i "modul-klasser", som letter organiseringen af biblioteker, header-filer og meget<br />

andet. Et klasse, der optræder som modul, karakteriseres ved, at den kun indeholder statiske<br />

konstanter og eventuelt et antal friend-metoder. Fordelen ved denne modularisering er dels en<br />

samling af alle konstanter, der har med et bestemt overordnet område at gøre, og dels, at navnene<br />

på disse konstanter ikke forurener det globale navnerum. Modularisering er, som eksemplificeret<br />

i afsnit 3.3.10, blot en administrativ teknik.<br />

5.5.4 Differentialprogrammering<br />

At anskue klasser som processer er i virkeligheden en måde at se klasserne som miniprogrammer<br />

på. En proces-klasse er blot en indkapslet del af en større sammenhæng, men er ikke<br />

et egentligt objekt, fordi det aldrig instantieres. Forskellen fra dette til modulærprogrammering er<br />

ikke stor, men kan underbygges med proces-klassernes mulighed for at benytte sig af en form for<br />

differentialprogrammering. Tilgangen til metoderne sker gennem kvalificerede navne, altså med<br />

reference til klassen, hvorved polymorfi er udelukket fordi der ikke er et underforstået objekt og<br />

dermed ingen virtuel metodetabel. Men teknikken er god, fordi den dels sikrer en bestemt<br />

grænseflade til relaterede klasser og dels organiserer relaterede metoder. Det giver mulighed for<br />

mange andre ting, blandt andet overvågning og fejlfindings-dæmoner, som lettere kan integreres i<br />

differentierede klasser end i globale funktioner.<br />

I det følgende eksempel vises nogle skeletter på sorteringsklasser, som bruges på næsten<br />

samme måde som standard-sorteringsfunktioner i C-biblioteker. En klient af klassen Sort kan<br />

kalde sorteringsklassens Sort-metode som om, det var en normal funktion, blot her med skopet<br />

sat til klassens navn.<br />

class Sort {<br />

public:<br />

virtual void Sort (void*, int) = 0;<br />

virtual void Swap (void*, void*) = 0;<br />

};<br />

void IntSort : public Sort {<br />

public:<br />

virtual void Sort (void*, int);<br />

virtual void Swap (void*, void*);<br />

};<br />

5.5.2 Datastrukturers transparens 415


void IntArray {<br />

int* _vec;<br />

unsigned size:<br />

public:<br />

// ...<br />

void Sort () { IntSort::Sort (_vec, size); }<br />

};<br />

Parametiserede typer (se afsnit 3.8) er en anden mulighed. Her differentieres klasserne direkte i<br />

skabelonen, hvilket frigør klienten fra at vælge den rette funktion:<br />

template <br />

void Sort (T* vector, int size);<br />

5.5.4 Funktionoider<br />

Syntaks er vigtig for abstrakte datatyper og for proces-klasser. Klienten er bedre tjent med selv at<br />

implementere en simpel funktion end at bruge en eksisterende klasse med en dårlig grænseflade<br />

og dermed syntaks i anvendelsen af dens objekter. I forbindelse med proces-klasser er det ofte en<br />

enkelt indgang, der er interessant for klienten. Faktisk er en proces-klasse kendetegnet ved, at den<br />

instantieres og derefter kaldes igen og igen med samme metode.<br />

Da instantieringen foregår ved konstruktionen er det metoden, der gentagne gange kaldes, der<br />

er interessant. En god kandidat til omslutning af denne metode er funktionskalds-operatoren, som<br />

kan overstyres med henblik på brugen af objekter af klassen som ganske normale funktioner. Der<br />

er blot den ikke så lidt væsentlige forskel, at klassen har mulighed for at operere med en indre<br />

"status". Skulle en global funktion gøre det samme med statiske data, ville kun den funktion have<br />

mulighed for at operere på dem. Idet en proces-klasse af denne karakter på nogle punkter er<br />

hævet over normale funktionskald og har mulighed for indre statusbehandling, men samtidig<br />

deler syntaks med funktioner, kaldes de funktionoider.<br />

Funktionoider kan bruges i næsten alle sammenhænge, hvor en lille funktion skal bruges igen<br />

og igen for forskellige simple data. Ofte bruges en makro eller en stor, global datastruktur med<br />

samme resultat, men funktionoider giver klienten en større bevægelsesfrihed i form af en nydelig<br />

syntaks. Specielt i multiprogrammerede systemer er funktionider brugbare, fordi de kan deles<br />

mellem flere processer. De kan således repræsentere en sanhedsværdi for, om en enhed er fri, om<br />

en besked er modtaget eller om der er lavvande i lageret. De er også som syet til implementering<br />

af dæmoner, små overvågningsprocesser, der af og til skal bruges af applikationerne.<br />

class Prime {<br />

unsigned currentPrime;<br />

unsigned nextPrime ();<br />

// ...<br />

public:<br />

416 Klasser som processer 5.5


Prime () : currentPrime (0) { }<br />

virtual operator unsigned () { return nextPrime (); }<br />

// ...<br />

};<br />

Funktionoider kan også benytte andre operatorer end (), men hovedreglen er, at en eller få<br />

syntaksmæssigt enkle indgange giver objekter af klassen sin styrke. I eksemplet nedenfor vises,<br />

hvordan semaforstyring kan indkapsles i en funktionoide-klasse med overstyringer af<br />

fortegnsoperatorerne + og - samt dernæst benyttes til simpel ressourcedeling. Semafor-klassen<br />

kan faktisk med en smule tilpasning bruges i multiprogrammerede systemer og sørge for en<br />

meget enkel, men effektiv kontrol med delte enheder og gensidig udelukkelse.<br />

// eksempel 5-3<br />

// en generel lineær semafor<br />

class Semaphore {<br />

int nr; // antal ressourcer<br />

public:<br />

Semaphore (int i = 1) : nr (i) { }<br />

void operator+ () { // fortegnsplus (monadisk)<br />

while (nr


sammenhænge, enten implementeret som letvægtsprocesser eller som bestandige objekter.<br />

Når to eller flere processer skal deles om et objekt, kræver det en global instantiering af klassen,<br />

som alle de separat oversatte programmer lænker sig til. Operativsystemet spiller en stor rolle, og<br />

på systemer, som ikke understøtter objekt-orienteret dynamisk lænkning er det mest af akademisk<br />

interesse. En objekt, der skal deles mellem mange forskellige processer må naturligvis have et<br />

globalt skop og en levetid, der ikke er bestemt af de enkelte processer, der kører på et vilkårligt<br />

tidspunkt. Objekterne skal have en vis bestandighed, et forhold, en egenskab, der diskuteres i<br />

næste afsnit. Eksempler på emner til objekter, der kræver deling mellem processer er semaforer,<br />

I/O-klasser, message-passing mekanismer og pipe-klasser. Såfremt det tillades af systemet kan<br />

statiske objekter befinde sig i dynamisk-link biblioteker (DLL'er eller lignende), som kan<br />

benyttes af flere processer på samme tid.<br />

5.5.6 Arv og brugerdefineret lageradministration<br />

Det følgende eksempel på brugerdefineret lageradministration er en klasse CustMemMgr<br />

(Custom Memory Manager), der implementerer en simpel first-fit lageradministration, som på<br />

klasse-basis kan erstatte den indbyggede. Det er tanken, at andre klasser arver fra CustMemMgr<br />

og dermed overtager overstyringerne af new og delete. CustMemMgr er en statisk klasse,<br />

som vil benytte samme lager for alle afledninger og instantieringer. Den anvender et lagerområde<br />

som heap, dvs. som helt frit lager, og har altid en pointer til starten på det. Den indeholder også<br />

en liste over frie blokke, som tidligere har været allokeret men som er blevet frigivet til<br />

"systemet" igen. Disse blokke, som ved hyppig allokering af objekter af forskellig størrelse leder<br />

til fragmentering af lageret, bliver gennemgsøgt inden lageret tages fra heap'en.<br />

// eksempel 5-4<br />

// lageradministrationsklassen CustMemMgr<br />

#include <br />

#include <br />

const unsigned MEMSIZE = 0x4000; // størrelsen på lageret<br />

class CustMemMgr {<br />

typedef struct free_block { // beskriver en fri blok<br />

uns igned size; // størrelsen på blokken<br />

free_block* next; // adressen på næste blok<br />

} *FBP; // en pointer til en blok<br />

static char memory [MEMSIZE]; // det præallokerede lager<br />

static FBP heap; // starten af det fri lager<br />

static FBP freelist; // liste af fragmentering<br />

static unsigned avail, hwm; // frit lager i bytes<br />

protected:<br />

de i sig selv er opdelt i mindre objekter.<br />

418 Klasser som processer 5.5


void Dump (const char*); // dump alt til en fil<br />

public:<br />

static void* operator new (size_t); // vores allokator<br />

static void operator delete (void*); // vores deallokator<br />

};<br />

// initiér de statiske medlemmer i CustMemMgr<br />

char CustMemMgr::memory [MEMSIZE];<br />

CustMemMgr::FBP CustMemMgr::heap = FBP (CustMemMgr::memory);<br />

CustMemMgr::FBP CustMemMgr::freelist = 0;<br />

unsigned CustMemMgr::avail = MEMSIZE;<br />

unsigned CustMemMgr::hwm = 0;<br />

// allokeringsfunktion/operator, kaldes før konstruktøren<br />

void* CustMemMgr::operator new (size_t size) {<br />

if (freelist) { // fragmenteret lager?<br />

FBP last = 0; // husk forrige hægte<br />

for (FBP fbp = freelist; fbp; fbp = fbp->next) {<br />

if (fbp->size >= size) { // er blokken stor nok?<br />

if (last) last->next = fbp->next;<br />

else freelist = fbp->next;<br />

void* ptr = (void*)<br />

((char*)(fbp) + sizeof (free_block));<br />

avail -= fbp->size + sizeof (free_block);<br />

return ptr; // returnér da denne<br />

}<br />

last = fbp;// ellers følg efter hægten<br />

}<br />

}<br />

// ingen klumper data i det fragmenterede lager var<br />

// store nok, brug i stedet lager fra heap'en<br />

if (hwm + size + sizeof (free_block) next = 0, heap->size = size;<br />

void* ptr = ((char*) heap + sizeof (free_block));<br />

heap = FBP ((char*) ptr + size);<br />

avail -= size + sizeof (free_block);<br />

hwm += size + sizeof (free_block);<br />

return ptr;<br />

}<br />

else return NULL; // ikke mere lager!<br />

}<br />

// deallokeringsfunktion, kaldes efter destruktøren<br />

5.5.6 Arv og brugerdefineret lageradministration 419


void CustMemMgr::operator delete (void* ptr) {<br />

FBP p = FBP ((char*) ptr - sizeof (free_block));<br />

p->next = freelist, freelist = p;<br />

avail += p->size + sizeof (free_block);<br />

}<br />

// en enkel dump-funktion af lageret<br />

void CustMemMgr::Dump (const char* file) {<br />

ofstream of (file, ios::binary | ios::out);<br />

of.write (memory, MEMSIZE);<br />

}<br />

Klassen CustMemMgr er et godt eksempel på modularisering. Men arvemekanismen i C++<br />

tillader os at nedarve egenskaberne (stort set kun de to operatoroverstyringer) i andre klasser, som<br />

blot behøver angive CustMemMgr som baseklasse for at erstatte lageradministrationen. For<br />

eksempel,<br />

class String : public CustMemMgr {<br />

char* rep;<br />

public:<br />

String ();<br />

String (const char*);<br />

String (const String&);<br />

~String ();<br />

String& operator= (const String&);<br />

operator const char* () const;<br />

int operator== (const String&) const;<br />

// ... andre interessante funktioner ...<br />

};<br />

Forekomster af String vil kunne allokeres dynamisk med new således at det er den<br />

brugerdefinerede lagerstyring fra CustMemMgr, der benyttes. Læg mærke til, at String ikke<br />

foretager sig andet end at angive baseklassen, mere skal der ikke til. En lille detalje i denne<br />

implementation er for øvrigt interessant. Alle medlemmer i CustMemMgr er erklæret<br />

static, hvorfor de kun eksisterer én gang. Der kan med andre ord kun være en "kopi" af<br />

lageradministratoren. Hvis disse medlemmer ikke var statiske, kunne de afledte klasser ved hjælp<br />

af virtuelle og ikke-virtuelle baseklasser styre individuelle, separerede lageradministratorer.<br />

5.6 CONTAINERKLASSER<br />

Vi har stiftet bekandskab med en bestemt type objekter flere gange gennem de sidste kapitler,<br />

som har den særlige egenskab, at de dynamisk indeholder objekter af andre typer. Klasser, der<br />

beskriver disse typer objekter går under fællesbetegnelsen containere. En container er et objekt,<br />

420 Klasser som processer 5.5


som indeholder en pointer eller reference til et andet objekt, eller hvis medlemsforekomster har<br />

denne egenskab. LinkedList og de relaterede klasser, der repræsenterer køer, stakke og<br />

buffere er typiske eksempler på containere.<br />

Man kan sige, at en container er en avanceret datastruktur med udbygget anvendelse i form af<br />

medlemsfunktioner. Containere er de fundamentale byggesten i programmerne, selv i normal<br />

procedural programmering forekommer containere også i form af komplekse datastrukturer og<br />

funktioner til manipulation af dem. Mange objekt-orienterede systemer leveres med et standard<br />

sæt af containerklasser (ligesom C++ normalt leveres med et standard I/O-bibliotek), for<br />

eksempel indeholder både Smalltalk og Eiffel et stort antal, der kan udbygges og modificeres af<br />

programmøren. Eftersom C++ ikke har et standard sæt af containere, er det fornuftigt at opbygge<br />

et antal generelle klasser til eget brug, der kan videreudvikles og genbruges. En container har<br />

typisk en grænseflade, som indeholder en indsættelses-metode og en udtagnings-metode.<br />

Klienten er dermed i stand til at indsætte objekter og udtage dem igen efter behov. Afhængig af<br />

den datastruktur, som containeren beskriver, kan der også findes metoder til traversering,<br />

sortering, søgning eller beregning.<br />

5.6.1 Objekters ejerforhold<br />

Inden vi begynder at skrive containerklasser, skal et fundamentalt koncept på plads i forståelsen<br />

af, hvor et objekt hører til. Grunden til dette er, at objekternes destruktion skal sikres - de skal<br />

deallokeres og de skal kun deallokeres en gang. Når et objekt befinder sig i en container, dvs. når<br />

containeren indeholder en pointer til objektet, siger vi, at containeren "ejer" det pågældende<br />

objekt. Selv om objektet teknisk set ikke er instantieret inde i containerklassen er det alligevel en<br />

vigtig erkendelse. For siden en container refererer sine objekter indirekte, skal disse objekter<br />

allokeres dynamisk. Og eftersom det skal være klienten, der bestemmer typen af objekter i en<br />

container, skal klienten allokere dem.<br />

Hermed opstår så spørgsmålet om, hvem der skal deallokere dem igen. Klienten kan have dette<br />

ansvar, men det er besværligt og trivielt. Derudover kan man forestille sig, at en container<br />

kopieres med en simpel tildeling, hvorved klienten ikke har de nødvendige adresser på de<br />

dynamiske objekter, der er allokeret. Så skal kilde-containeren (som klient) selv adminstrere<br />

deallokeringen, og med mange forskellige containere, der kan kopieres til hinanden, begynder det<br />

at blive rigtigt kompliceret.<br />

Derfor er reglen, at containeren ejer de objekter, den indeholder, og dermed er ansvarlig for<br />

udtrykkelig destruktion af dem. Af denne regel følger, at det er ulovligt at indsætte andet end<br />

dynamisk allokerede objekter i en container. Hvis vi nemlig gør det, vil både containeren og<br />

oversætteren forsøge at deallokere objektet henholdsvis underforstået og udtrykkeligt. Når en<br />

container forlader sit skop, vil dens destruktør automatisk fjerne alle objekter, der befinder sig i<br />

containeren på det tidspunkt ved udtrykkeligt at delete dem. Af reglen følger envidere, at når<br />

et objekt tages ud af en container, så har den pågældende klient ansvaret for destruktionen.<br />

Hvis flere containere har brug for at deles om objekter, er det muligt at undgå en dobbelt<br />

destruktion ved introduktion af en mellemliggende container, som ejer det pågældende objekt, og<br />

som er transparent for containeren i den forstand, at alle kald til medlemsfunktioner i den<br />

mellemliggende klasse vil fortsætte direkte til det egentlige objekt - med undtagelse af<br />

5.5.6 Arv og brugerdefineret lageradministration 421


destruktøren, som er tom. På den måde undgås multiple destruktioner af de samme objekter. Det<br />

kan også lade sig gøre gennem at overtage kontrollen med lageradministrationen i de klasser, der<br />

skal ind i containeren. Klassespecifikke overstyringer af operator delete kan undersøge,<br />

om lageret allerede er frigivet for det pågældende objekt og blot returnere, hvis det er tilfældet.<br />

5.6.2 Polymorfe containere<br />

Containere er mest interessante, når de har en bred anvendelse og kan bruges i flere forskellige<br />

sammenhænge. For en containerklasse er polymorfi todelt. Dels er det en fordel, hvis koden for to<br />

klasser (for eksempel en kø og en stak) er den samme, og dels er det ønskværdigt, at den samme<br />

container kan indeholde objekter af forskellige typer.<br />

For at opnå det første, er det nødvendigt at skrive en abstrakt containerklasse, som definerer,<br />

hvordan containere generelt skal opføre sig samt en generel klasse, der definerer et objekt i<br />

containeren. Prototyper på sådanne klasser kan se ud som følger:<br />

class ContainedObject { // et objekt i containeren<br />

};<br />

class AbstractContainer { // containeren selv<br />

protected:<br />

unsigned count; // antal objekter i containeren<br />

AbstractContainer () : count (0) { }<br />

public:<br />

virtual ~AbstractContainer () { };<br />

virtual Insert (ContainedObject*) = 0; // indsæt<br />

virtual ContainedObject* Retrieve () = 0; // udtag<br />

// ...<br />

};<br />

Klassen ContainedObject er en overordnet type, der beskriver de objekter, der kan befinde<br />

sig i enhver container, mens AbstractContainer er den abstrakte type, der beskriver de<br />

minimale forudsætninger for en container. Der skal arves fra disse to, før der fås instantiérbare<br />

klasser, der kan bruges til noget. Eksempelvis kan vi skabe en Stack-klasse, der repræsenterer<br />

en ikke-indeksérbar først-ind-sidst-ud buffer, implementeret internt som en hægtet liste:<br />

class Stack : public AbstractContainer {<br />

class SLink {<br />

friend Stack;<br />

StackLink* next;<br />

ContainedObject* data;<br />

};<br />

SLink* s;<br />

public:<br />

422 Containerklasser 5.6


Stack () : AbstractContainer (), s (0);<br />

virtual ~Queue ( delete s; }<br />

virtual void Insert (ContainedObject*);<br />

ContainedObject* Retrieve ();<br />

};<br />

void Stack::Insert (ContainedObject* o) {<br />

SLink* sl = new SLink, s->next = s, s = sl, s->data = o;<br />

}<br />

ContainedObject* Stack::Retrieve () {<br />

ContainedObject* o = s->data; SLink* sl = s, s = s->next;<br />

delete sl;<br />

return o;<br />

}<br />

Graden af polymorfi på containerklassen kan ses, hvis vi yderligere erklærer en lignende klasse,<br />

for eksempel en Queue, som blot er en først-ind-først-ud buffer. Al kode, der arbejder på en<br />

Stack, vil også kunne arbejde på en Queue, så længe de to klasser arver fra den fælles base<br />

AbstractContainer og anvender dennes grænseflade. På indholdssiden kan en Stack<br />

arbejde med alle typer af objekter, der er nedarvet fra ContainedObject, så vi på den måde<br />

kan forestille os både heltal, kommatal, komplekse tal osv. i samme Stack.<br />

5.6.3 Programmering med containere<br />

Anvendelsen af containerklasser frigør os fra al den trivielle administration af placering og<br />

organisering af data. Containerne overtager simpelthen alle operationer, som vedrører gentagne<br />

operationer på multiple forekomster af homogene data, normalt benyttet i forbindelse med lister<br />

af en bestemt datastruktur. Hvor ofte har man ikke skrevet en datastruktur med en indbygget<br />

hægte, for derefter at konstruere funktioner til administration af disse hægter for netop den<br />

struktur. Med containere skrives koden en gang for alle i en klasse, som kan udbygges i en<br />

afledning eller anvendes på polymorfe objekter. Klienten af en container får en sikkerhed for en<br />

kompleks datastrukturs konsistens og kan koncentrere sig om behandlingen af objekterne ud fra<br />

problemstillingen.<br />

Et sprogmæssigt problem med containerklasser er, at en abstrakt baseklasse ikke altid erklærer<br />

prototyper, som direkte kan bruges i en afledt klasse. For eksempel kan en container, som finder<br />

et bestemt objekt på en nøgle, ikke implementere den rene virtuelle medlemsfunktion<br />

Retrieve(), da der også skal være en parameter. På samme måde med indsættelsesmetoderne,<br />

som måske skal modtage mere end én parameter. Endvidere vil en afledt container ofte arbejde<br />

på bestemte typer af objekter, men bliver grundet de virtuelle metoders prototyper nødt til at<br />

returnere en polymorf reference. Resultatet bliver, at klienten må benytte tvungen<br />

typekonvertering på sin side af kaldet, hvilket er farligt og repetitivt. Den bedste løsning, hvis<br />

containeren er specifik, at omskrive grænsefladen i en afledt klasse og returnere objekter af de<br />

5.6.2 Polymorfe containere 423


ette typer. Den tvungne konvertering bliver således foretaget i klassen, og klienten kan forvente<br />

objekter af de faktiske typer i kald til containeren.<br />

En næsten uundværlig container er en associativ vektor, dvs. en samling af relaterede objekter,<br />

som kan hentes fra containeren ved brug af en nøgle. I eksemplet nedenfor vises en sådan klasse,<br />

kaldet Dictionary, som gør brug af polymorfe indeholdte objekter. Containeren indeholder<br />

en liste af associationer mellem to objekter af typen Associateable, eller subklasser af<br />

denne, og tillader klienten at forespørge om en association med en nøgle. Associative lister har<br />

blandt andet stor anvendelighed indenfor symboltabelbehandling og katalogadministration i det<br />

hele taget. For enkelthedens skyld er klassen simplificeret på en del vigtige områder, for<br />

eksempel under dynamisk allokering af strenge, som blot kopieres som pointere.<br />

// eksempel 5-5: et generelt, associérbart objekt<br />

class Associateable {<br />

public:<br />

virtual int operator== (Associateable& a) { return 0; }<br />

} NoAssociationFound; // bruges som fejlkode<br />

// et katalog over associationer<br />

class Dictionary {<br />

struct AssociatedPair {<br />

Associateable* First, *Second;<br />

AssociatedPair* Next;<br />

AssociatedPair (Associateable* a, Associateable* b) :<br />

First (a), Second (b) { }<br />

~AssociatedPair () { delete First; delete Second; }<br />

} *assocList;<br />

public:<br />

Dictionary () : assocList (0) { }<br />

virtual ~Dictionary ();<br />

void Insert (Associateable&, Associateable&);<br />

Associateable* Retrieve (Associateable&);<br />

};<br />

Dictionary::~Dictionary () {<br />

for (AssociatedPair* p = assocList; p;) {<br />

AssociatedPair* temp = p;<br />

p = p->Next; delete temp;<br />

}<br />

}<br />

// indsæt en association i kataloget<br />

void Dictionary::Insert(Associateable& a,Associateable& b) {<br />

AssociatedPair* temp = assocList;<br />

424 Containerklasser 5.6


assocList = new AssociatedPair (&a, &b);<br />

assocList->Next = temp;<br />

}<br />

// hent en association i kataloget<br />

Associateable* Dictionary::Retrieve (Associateable& o) {<br />

for (AssociatedPair* p = assocList; p; p = p->Next) {<br />

if (*(p->First) == o) return p->Second;<br />

if (*(p->Second) == o) return p->First;<br />

}<br />

return &NoAssociationFound;<br />

}<br />

Der arves nu to klasser fra Associateable for at teste containeren. Country indeholder<br />

navnet på et land, mens Code repræsenterer en landekode:<br />

struct Country : Associateable {<br />

char* s;<br />

Country (char* i) : s (i) { }<br />

int operator== (Associateable& a) {<br />

return !strcmp (s, ((Country&) a).s);<br />

}<br />

// ...<br />

};<br />

struct Code : Associateable {<br />

unsigned c;<br />

Code (unsigned i) : c (i) { }<br />

int operator== (Associateable& a) {<br />

return c == ((Code&) a).c;<br />

}<br />

// ...<br />

};<br />

Disse to klasser er ingenlunde komplette, men vil dog kunne oversættes. Den tvungne<br />

konvertering i operator==()-funktionerne er temmelig farlig, da der ingen garanti er for om<br />

parameteren er af samme type som klassen. Ikke desto mindre kan disse klasser benyttes i<br />

sammenhæng med Dictionary med følgende funktionalitet til følge:<br />

// eksempel 5-6: anvendelse af associérbare lister<br />

void main () {<br />

Dictionary D;<br />

D.Insert (*new Country ("Denmark"), *new Code (45));<br />

D.Insert (*new Country ("England"), *new Code (44));<br />

5.6.3 Programmering med containere 425


Output:<br />

D.Insert (*new Country ("USA"), *new Code (1));<br />

// hvilket land har kode nummer 44?<br />

Country* c = (Country*) D.Retrieve (Code (44));<br />

cout


underforståede konvertering til en heltalsværdi, så gennemløbet kan sluttes på den rigtige måde.<br />

class IndexableContainer : public AbstractContainer {<br />

// ...<br />

public:<br />

// ...<br />

ContainedObject* operator[] (unsigned);<br />

};<br />

class ContainerIterator {<br />

IndexableContainer& ic;<br />

unsigned currentIndex;<br />

public:<br />

ContainerIterator (IndexableContainer& i) :<br />

ic (i), currentIndex (0) { }<br />

unsigned operator++ () { return currentIndex++; }<br />

operator int () { return ic [currentIndex] != 0; }<br />

ContainedObject* operator*() { return ic [currentIndex]; }<br />

};<br />

Forekomster af klassen ContainerIterator har næsten sammme semantik som en generel<br />

pointer til en lineær liste. Det letter containergennemløb for klienten, som får en ensartet og<br />

overskuelig måde at besøge alle objekter på. For eksempel,<br />

void printContents (IndexableContainer& c) {<br />

for (ContainerIterator p = c; p; p++) cout


administrationen af dette påhviler enten et dedikeret databasesystem, der kaldes fra programmet,<br />

eller programmet selv gennem filmanipulation. Der er fordele og ulemper ved begge dele. En<br />

database-underbygning betyder mindre kode og større fleksibilitet, for eksempel i forbindelse<br />

med standarder for datalagring og flerbrugeradgang, men gør til gengæld næsten altid<br />

tilgangstiden meget formaliseret og langsom. Et specialskrevet fil-lag i programmet kræver<br />

programmørressourcer, tager tid og kan være en stor potentiel kilde til fejl. Men i begge tilfælde<br />

skal programmøren være klar over, at programmets data kan findes på et sekundærlager.<br />

Principielt er det underordnet, om data findes på disk eller i hovedlageret. Det, vi som<br />

programmører er interesserede i, er at behandle disse data og ikke at læse dem ud og ind og holde<br />

styr på, hvor de befinder sig. Faktisk er selve bevidstheden om et sekundærlager en unødig faktor,<br />

der blot sinker programmets udvikling og gør programmeringen triviel. For at kunne opnå en<br />

transparens i forhold til objekternes placering i systemet må vi skrive klasser, der selv varetager<br />

ind- og udlæsning til sekundærlageret, og som på den måde introducerer et fil-lag, der ikke kan<br />

ses af klienten.<br />

5.7.1 Objekters levetid<br />

Et objekt kan have tre levetider,<br />

• temporært, hvilket vil sige, at objektet oprettes og nedbrydes dynamisk i programmets<br />

eksekverignsforløb, kontrolleret af programmet selv. Det er klart, at brugeren kan har<br />

stor indflydelse på, hvornår temporære objekter allokeres.<br />

• permanent, som er et objekt, der oprettes ved programmets start og nedbrydes, når det<br />

afsluttes. Brugeren har ingen kontrol med allokeringen.<br />

• bestandigt, som gør objektet uafhængigt af programmets kørsel, og som i princippet kan<br />

benyttes af forskellige uafhængige programmer eller processer, endda på samme tid<br />

givet adækvat samtidighedskontrol. Brugeren har samme grad af kontrol over<br />

bestandige objekter som over temporære objekter, og programmet arbejder i princippet<br />

på samme måde med de to.<br />

Temporære og statiske objekter understøttes i C++ ved henholdsvis automatisk/dynamisk<br />

allokering og statisk allokering. Der er faciliteter i sproget til at foretage en (minimal) intern<br />

lageradministration og sikre korrekt oprettelse og nedbrydning. Bestandige objekter understøttes<br />

ikke af C++, og må således implementeres. Det er ofte blevet pointeret, at en udvidet<br />

lageradministration er et kriterion for et objekt-orienteret sprog, men i henhold til minimalistfilosifien<br />

i C++ er faciliteten udeladt.<br />

5.7.2 Implementation af bestandige objekter<br />

428 Bestandighed 5.7


En simpel implementation af bestandighed er to medlemsfunktioner i alle objekter, som skal have<br />

egenskaben, til indlæsning og udlæsning. Det gør ikke objekterne bestandige i sig selv, men giver<br />

dem muligheden for at ind- og udlæse sig selv, når de bliver bedt om det:<br />

class Complex {<br />

double re, im;<br />

public:<br />

// ...<br />

void dumpOn (ostream&); // udlæsning<br />

static Complex* readFrom (istream&); // indlæsning<br />

};<br />

void Complex::dumpOn (ostream& strm) {<br />

strm >> re >> "\n" >> im >> "\n";<br />

}<br />

Complex* Complex::readFrom (istream& strm) {<br />

Complex* c = new Complex;<br />

strm re im;<br />

}<br />

De to medlemsfunktioner dumpOn() og readFrom(), som minder meget om de simple<br />

objekt-I/O-metoder fra afsnit 3.7.4, kan kaldes af klienten for at ind- og udlæse objekter af<br />

Complex med<br />

ostream file ("test.dat");<br />

Complex* c = Complex::readFrom (file);<br />

c.dumpOn (file);<br />

Indlæsningsmetoden er statisk og skal kaldes med direkte reference til typenavnet, der ønskes<br />

indlæst, og vil returnere en ny - dynamisk allokeret - forekomst. Grunden er, at der på<br />

kaldetidspunktet ikke findes et objekt af typen i hvilket nye data kan læses ind, hvorfor metoden<br />

ikke kan kaldes for et eksisterende objekt. Udlæsningsmetoden er en normal medlemsfunktion,<br />

som kaldes med en reference til en forekomst af et aktuelt objekt. Denne metode kan med fordel<br />

være virtuel, fordi en generisk mekanisme for udlæsning af mange forskellige typer objekter så<br />

kan skrives et andet sted i programmet.<br />

Der er to fundamentale svagheder ved denne simple I/O-model. For det første påhviler<br />

organiseringen af data på det eksterne lager klienten, og for det andet er det ikke muligt at udlæse<br />

komplekse objekter, for eksempel polymorfe containerklasser (5.6.2). Det første problem venter<br />

vi lidt med, da det griber ind i principper for dataorganisering på lageret med nøgler, hash- og<br />

søgetabeller. Det andet problem afslører imidlertid et meget dunkelt hjørne i objekt-orienteret<br />

programmering, som er svært at løse med et sprog som C++ uden at foretage adskillige<br />

manøvrer.<br />

5.7.2 Implementation af bestandige objekter 429


Overvej følgende situation: En polymorf container er udlæst i en fil og ønskes nu genindlæst.<br />

Containeren, for eksemplets skyld en LinkedList, kan indeholde objekter af forskellige typer<br />

og skal under indlæsningen rekonstruere alle disse objekter til deres respektive originale status.<br />

Men hvordan ved containeren, hvilken type, den indlæser? Da containeren med stor<br />

sandsynlighed ikke er i arvefølge med de objekter, den indlæser, skal den have eksplicit viden om<br />

typen af det objekt, den læser, af objektet selv. Og da objektet ikke eksisterer endnu, må denne<br />

information stå skrevet på sekundærlageret. Containeren skal altså have objektets type at vide, så<br />

readFrom()-metoden i den rette klasse kan kaldes, ellers render LinkedLists<br />

indlæsningsmetode ind i følgende problem:<br />

LinkedList* LinkedList::readFrom (istream& strm) {<br />

while (data_at_laese)<br />

Insert (???::readFrom (strm));<br />

};<br />

De tre spørgsmåltegn repræsenterer den uvidenhed, som LinkedLists funktion har. Den ved<br />

ganske simpelt ikke, hvilken type den er i færd med at læse ind. For at løse problemet må<br />

dumpOn() skrive en typeidentifikation på strømmen før selve objektet udlæses. Det giver en<br />

indlæsningsmetode mulighed for at identificere objektets type og via en tabel kalde den rette<br />

indlæsningsmetode. Alle klasser får dermed et typenavn eller -nummer, hvilket til dels går stik<br />

imod den objekt-orienterede filosofi om, at et polymorft objekts type i princippet er underordnet<br />

set fra klientens side. Men det er nødvendigt, fordi en baseklasse ikke har den nødvendige viden<br />

om de afledte klasser til at identificere en objekttype. Oversætteren har denne viden, men er ude<br />

af billedet, når programmet eksekveres.<br />

Vi implementerer altså en virtuel Type()-metode (undertiden kaldet isA() i gængse<br />

klassebiblioteker), som returnerer en identifikation af objektets type. En sådan metode findes<br />

indbygget i enkelte andre objekt-orienterede sprog, og giver her en heltalskonstant, som unikt<br />

beskriver den givne klasse. I C++ er vi nødt til at gøre dette eksplicit, for eksempel med en<br />

opremsning af alle ikke-abstrakte (dvs. læsbare) klasser i systemet:<br />

enum Class { aString, aComplex, aLinkedList, NoOfClasses };<br />

Type()-metoden har en meget simpel implementation, og returnerer blot typeidentifikationen<br />

på objektet. Men vi er nødt til at relatere klasser, der kan indlæses fra sekundærlageret i samme<br />

klynge, fordi Type() skal bruges polymorft. Vi afleder således alle klasser fra en fælles<br />

baseklasse, for eksempel Persistent:<br />

class Persistent {<br />

// ...<br />

public:<br />

// ...<br />

virtual Class Type () const = 0;<br />

};<br />

430 Bestandighed 5.7


Herefter implementeres Type() på følgende måde i de afledte klasser:<br />

class String : public Persistent {<br />

// ...<br />

public:<br />

// ...<br />

virtual Class Type () const { return aString; }<br />

};<br />

class Complex : public Persistent {<br />

// ...<br />

public:<br />

// ...<br />

virtual Class Type () const { return aComplex; }<br />

};<br />

class LinkedList : public Persistent {<br />

// ...<br />

public:<br />

// ...<br />

virtual Class Type () const { return aLinkedList; }<br />

};<br />

Ethvert objekt er altså bekendt med sin type. Læg mærke til, at typeinformationen ikke optager<br />

plads i objektet som en konstant, men i stedet som en virtuel funktion. Det tillader os at<br />

undersøge typen på et bestandigt objekt i en situation, hvor vi er nødt til at have denne<br />

information, for eksempel når et objekt skal udlæses. Dermed kan vi også skrive funktioner af<br />

den type, der ikke bør findes i et velskrevet objekt-orienteret program:<br />

const char* typeNavn (Persistent& p) {<br />

switch (p->Type ()) { // eksplicit typecheck!<br />

case aString : return "String";<br />

case aComplex : return "Complex";<br />

case aLinkedList : return "LinkedList";<br />

default: return "Unknown";<br />

}<br />

}<br />

På samme måde er det nu muligt, når et objekt udlæses, at skrive objektets klassenummer, så en<br />

indlæsningsmetode i Persistent kan kalde en anden metode i den rette afledte klasse. Når<br />

dumpOn() kaldes, udskrives således returværdien af klassens Type()-metode, efterfulgt af<br />

objektets egentlige data. Da denne metode er virtuel, er en udlæsning af en polymorf container<br />

ikke noget problem. Indlæsningen involverer et eksplicit type-check på det indlæste objekt og et<br />

kald til den rette readFrom()-metode.<br />

5.7.2 Implementation af bestandige objekter 431


5.7.3 Administration af bestandige objekter<br />

Klassebiblioteket, der beskrives i <strong>kapitel</strong> 8, indeholder en simpel mulighed for bestandige<br />

objekter. I denne implementation bruges eksplicit type-information i alle klasser,<br />

udlæsningsmetoderne er virtuelle og indlæsningsmetoderne er statiske. Hierarkiets øverste klasse,<br />

Object (som blandt andet har samme funktionalitet som Persistent fra forrige afsnit) har<br />

en tabel over adresser på readFrom()-metoder i alle ikke-abstrakte klasser. Denne tabel<br />

initieres af en statisk erklæring i det respektive klassemodul, som kalder en<br />

"annonceringsmetode" i Object. På den måde garanteres, at tabellen er initieret før main():<br />

// Tabel over adresser på indlæsningsmetoder<br />

Object* (*readFromTable) (istream&) [NoOfClasses];<br />

// en simpel annonceringsmetode<br />

bool Object::Announce (Class n, Object* (*p) (istream&)) {<br />

readFromTable [n] = p;<br />

}<br />

// prototype på en readFrom-metode<br />

Object* String::readFrom (istream&);<br />

// initiering af adressetabel-indgang for denne klasse<br />

static init = Object::Announce (aString, String::readFrom)<br />

Et krav til alle dumpOn()-metoder er, at de først skal kalde baseklassens dumpOn() og<br />

dernæst udlæse sig selv. Object::dumpOn() ser således ud:<br />

void Object::dumpOn (ostream& s) { s classNumber;<br />

return (readFromTable [classNumber]) (s);<br />

}<br />

432 Bestandighed 5.7


Denne måde at administrere kald til statiske metoder i polymorfe objekter kan sammenlignes<br />

med programmørkontrollerede virtuelle kald. Der er ikke megen forskel fra dette til oversætterens<br />

egen implementation af rigtige virtuelle funktioner. Den eneste reelle forskel er, at systemet er<br />

mere statisk i sit design. Hver gang, en ny klasse skal introduceres, skal klassenavnet også<br />

oprettes i opremsningen, som står i en global benyttet headerfil. Det betyder direkte, at hele<br />

systemet skal genoversættes, når en ny klasse introduceres, en ikke lille pris at betale for eksplicit<br />

typekontrol. Det er muligt at forestille sig en oversætter, som er i stand til automatisk at fortælle<br />

en klasse, hvilket nummer den har. En sådan oversætter gør det muligt at påføre systemet en ny<br />

klasse uden at skulle foretage en global genoversættelse.<br />

Figur 5-7: En adressetabel udgør programmørkontrolleret polymorfi.<br />

Formatet på sekundærlageret indeholder således al information tilstrækkelig til at oprette<br />

objekter, der er blevet udlæst på et tidligere tidspunkt. Formatet på en sådan fil kan se ud som<br />

følger for en LinkedList, der indeholder to String-objekter og et Complex-objekt<br />

(formatteret pænt for læsbarhedens skyld og med kommentarer i parantes):<br />

2 4 (en LinkedList med fire objekter)<br />

0 13 Første streng (en String på 13 tegn)<br />

0 12 Anden streng (en String på 12 tegn)<br />

1 4 -1 (en Complex (4,-1))<br />

1 3.3333333 0 (en Complex (3.3333333,0))<br />

Hele strukturen kan indlæses med et enkelt kald fra klienten, som kalder fællesnævnermetoden i<br />

Object-klassen:<br />

5.7.3 Administration af bestandige objekter 433


Object& o = *Object::readFrom (file); // indlæs et objekt<br />

if (o->Type () == aLinkedList)<br />

LinkedList& l = (LinkedList&) o;<br />

else<br />

cerr


1 2 2 (objekt 1: en LinkedList på 2 elementer)<br />

2 0 6 Første (objekt 2: en String på 6 tegn)<br />

3 0 5 Anden (objekt 3: en String på 5 tegn)<br />

4 2 3 (objekt 4: en LinkedList på 3 elementer)<br />

5 0 6 Tredie (objekt 4: en String på 6 tegn)<br />

3 (objekt 3 igen)<br />

2 (objekt 2 igen)<br />

De to sidste objekter på sekundærlageret er direkte referencer til to objekter, der allerede findes et<br />

andet sted. Indlæses disse to strukturer lineært, må systemet således holde rede på, at<br />

objektnumrene 2 og 3 allerede er indlæst og blot oprette en reference til det eksisterende objekt. I<br />

nogle objekt-orienterede databasesystemer til C++ skelnes mellem klasserelationer og<br />

objektrelationer på følgende måde. En klasserelation er en pointervariabel (ofte af en bestemt<br />

type, en transparent reference, som er i stand til at udlæse sig selv som en pointer til et<br />

objektnummer) skrevet i klassens implementation, mens en objektrelation er en forekomst af en<br />

bestemt associeringsklasse, som relaterer to objekter. Forskellen er graden af polymorfi i enten<br />

det ene eller i begge objekter.<br />

Systemet kan konfigurere filerne på sekundærlageret, så det bliver let at finde et bestemt<br />

objektnummer, hvilket egentlig er det essentielle. Indekseringstabeller eller hash-tabeller er ofte<br />

brugte metoder til dette, ligesom objekter og objektreferencer typisk vil skrives med ud i to<br />

forskellige filer, da de har forskellig mening.<br />

En anden vigtig bestanddel i OODBMS er, at objekterne skal kunne benyttes af en C++programmør<br />

helt uden bekymring om fil-laget. For at opnå en total abstraktion fra sekundærlageret<br />

må operatorerne new og delete overstyres for alle bestandige klasser. Det har<br />

yderligere den fordel, at systemet dynamisk kan udlæse sjældent brugte objekter, hvis<br />

hovedlageret viser sig at løbe tomt.<br />

5.8 KLASSEBIBLIOTEKER<br />

Når de mange forskellige typer af klasser, som er beskrevet gennem dette <strong>kapitel</strong> skal integreres i<br />

en større sammenhæng, en reel applikation, bliver resultatet et helt bibliotek af klasser. Et<br />

klassebibliotek er fundamentet i alle objekt-orienterede udviklingsprojekter, hvor alle involverede<br />

må være enige om opbygningen for at sikre alle klassernes integritet. Klassebiblioteker sælges<br />

også kommercielt til specifikke formål, for eksempel lettere tilgang til grafiske brugerflader eller<br />

databaser. Ofte støder man på udtrykket et klassehierarki i stedet for et klassebibliotek, en term<br />

der dækker næsten det samme. Et klassehierarki indebærer en opbygning, hvor en enkelt klasse er<br />

rod for alle andre klasser i systemet, mens et klassebibliotek kan indeholde mange ikke-relaterede<br />

klasser.<br />

Udviklingen af et klassebibliotek rejser flere designspørgsmål, som går ud over de enkelte<br />

klassers opbygning. Hvorfor er dette så vigtigt? Fordi klassebiblioteket repræsenterer selve<br />

strukturen af programmet, relationerne mellem klasserne. Specifikationen på de abstrakte<br />

baseklasser i et system, opbygningen af container-klasserne og en direkte associering mellem<br />

objekter, der skal dele funktionalitet, så instantiérbare klasser får den rette grænseflade og kan<br />

5.7.4 OODBMS 435


læne sig bedst muligt op af baseklasserne er de væsentlige punkter. I et udviklingsprojekt er det<br />

essentielt, at alle involverede er enige om en bestemt klasses placering i biblioteket, om dens<br />

grænseflade og virkemåde og dens relation til andre klasser.<br />

Et klassebibliotek skal fra klientens side anskues som et enkelt objektmodul, som lænkes med<br />

applikationskoden til et færdigt program. Klienten modtager typeinformation om klassernes<br />

grænseflade fra header-filerne, som blot indeholder erklæringer på klasserne og eventuelle inlinemetoder<br />

og konstanter. Der er i princippet ingen forskel i anvendelsen af et klassebibliotek i<br />

forhold til et normalt funktionsbibliotek, set fra et oversættermæssigt synspunkt.<br />

5.8.1 Klassebibliotekets anvendelse<br />

Et klassebibliotek har typisk en bestemt hensigt, nemlig at være basis for applikationer af en<br />

særlig type. For at nævne blot et par,<br />

• Grafiske brugerflader, som i sig selv er objekt-orienterede. Vinduer og subvinduer,<br />

præsentationsflader, ikoner, menuer og submenuer, knapper og input-felter kan alle<br />

anskues som objekter med et indhold og en bestemt måde at opføre sig på. Et<br />

klassebibliotek, der arbejder som et mellemliggende lag mellem en applikationsprogrammør<br />

til GUI'er (grafiske brugerflader) og et vinduessystem, kan præsentere<br />

miljøet på en måde, der hæver abstraktionen til et naturligt og passende niveau samt<br />

skjuler ikke blot kompleksitet men også irrelevante, trivielle og forstyrrende elementer.<br />

• Operativsystemer, som har mange softwarekomponenter, der har en relevant betydning<br />

for et program. Arbejdsopgaver som afsending og modtagelse af beskeder, systemkald,<br />

filmanipulation, lagerstyring, semaforstyring og kontrol med ydre enheder kan med stor<br />

fordel for en applikationsprogrammør indkapsles i klasser, som samler relaterede<br />

datatyper og systemkald under ét.<br />

• Under opgaver med datatransmission og datakommunikation, som traditionelt er<br />

opbygget i flere "lag". Hvert lag i en kommunikationsmodel kan implementeres som en<br />

klasse, hvor en baseklasse udgør det laget lige under og en afledt klasse udgør det<br />

umiddelbart øvre. En klient kan på den måde bruge netop det lag, som der er brug for,<br />

uden tab af abstraktion på nogen måde i forhold til den konceptuelle model. Denne<br />

fremgangsmåde er af stor fordel i alle applikationer, som implementerer en trinvist<br />

opbygget model.<br />

• Databasesystemer og dataopsamlingssytemer, som arbejder med bestemte typer og<br />

relationer. Et klassebibliotek, som er let at udvide med nye typer gør systemets<br />

robusthed og ensartethed stor, og kan en gang for alle indeholde implementationer af de<br />

metoder, der er ens for alle typer af data i et system.<br />

436 Klassebiblioteker 5.8


Der er naturligvis uanede muligheder, for objekt-orienteret teknologi er løsningen til mange af de<br />

aktuelle problemer i softwaredesign. De eneste applikationer, der ikke drager nytte af<br />

klassebiblioteker, er enten små og i udviklingstid og kompleksitet meget enkle programmer eller<br />

dele af systemer, der af specifikationsmæssige årsager er nødt til at skrives på en anden måde.<br />

En anden type klassebibliotek, som både er illustrativ som eksempel og som er anvendelig i<br />

mange forskellige applikationer er det generelle klassebibliotek. Et generelt bibliotek indeholder<br />

datatyper, som kan benyttes i næsten alle sammenhænge, fordi de repræsenterer datastrukturer og<br />

algoritmer, der forekommer i ethvert program. Klassebiblioteket XCL i <strong>kapitel</strong> 7 er af en sådan<br />

type, og er brugbart både som fundament for mere komplekse klasser eller direkte som<br />

underbygning for specifikke programmer.<br />

5.8.2 Klassebibliotekets indhold og opbygning<br />

Indholdet af et klassebibliotek kan opdeles i fem klynger af klasser, som har hver deres bestemte<br />

plads og funktion:<br />

• Abstrakte klasser, som er prototyper på aktive, instantiérbare klasser, og som er et typisk<br />

indgangspunkt for polymorfe kald,<br />

• Aktive klasser, som er de klasser, klienten direkte instantierer og arbejder med eller på,<br />

• Containerklasser, som indeholder indirekte referencer til forekomster af andre klasser af<br />

bestemte typer, både abstrakte og aktive,<br />

• Private klasser, som kun har betydning for bestemte andre klasser i biblioteket, og som<br />

klienten kun arbejder indirekte med eller på,<br />

• Aggregate klasser, eller komplekse klasser, som indeholder forekomster af andre klasser<br />

i biblioteket.<br />

Containerklasser og private klasser kan også selv være abstrakte, mens aktive klasser altid er<br />

instantiérbare. De aktive klasser er enten generelle, applikationsorienterede eller<br />

systemorienterede. Containerklasser er altid associeret med en algoritme eller en datastruktur, der<br />

skal fjernes fra klientens domæne. Aggregate klasser indeholder kun aktive klasser.<br />

Et ofte stillet spørsmål er, hvordan klasserne identificeres. Problemet med at finde de relevante<br />

klasser i et projekt har ofte udgangspunkt i en forkert indgangsvinkel til et objekt-orienteret miljø.<br />

Hvis der kun fokuseres på den sproglige struktur af for eksempel C++, og ikke på anvendelsen af<br />

klassebegrebets forskellige faciliteter, vil man overse og måske forbigå det essentielle. Uden at gå<br />

ind på forskellige teorier for hvordan et objekt-orienteret design bør se ud, er der imidlertid visse<br />

retningslinier for identifikationen af klasserne. Først skal systemets grundlag revideres med<br />

henblik på, hvordan data er organiseret. Der er normalt en eller anden form for entitetrelationsmodel<br />

i en systemspecifikation, altså en opstillig af datatyper og deres indbyrdes forhold.<br />

5.8.1 Klassebibliotekets anvendelse 437


Denne model kan bruges til at opstille nogle konkrete termer. Entiteter med et simpelt indhold<br />

bliver til en klasse, mens komplekse entiteter bliver til aggregate klasser. Relationerne mellem<br />

klasserne specificeres enten på klasse-basis eller på objekt-basis (se afsnit 5.7.4) afhængigt af, om<br />

der er tale om en aggregering eller en association. For entiteter som er relateret på andet end<br />

indhold (det kan ses på måden, entiteterne omtales på - en bruger kan indtaste både en ansøgning<br />

og et memorandum) skabes abstrakte klasser, der bruges som baseklasser for de behandlingsrelaterede<br />

(aktive) klasser. Enhver behandling på en entitet bliver til en metode. Hvis<br />

behandlingen kun vedrører én entitet, indkapsles den som medlemsfunktion i den klasse. Hvis<br />

behandlingen er centreret omkring en anden entitet, skrives den som medlemsfunktion i den<br />

klasse og modtager som parameter et objekt af den første klasse.<br />

5.8.3 Topologi<br />

Der er mange forskellige måder at opbygge arvefølgen i et klassebibliotek på, men der er til<br />

gengæld ingen af dem, som definitivt er den bedste. Det kommer ganske simpelt an på de enkelte<br />

klassers beskaffenhed.<br />

Der skelnes mellem to slags opbygninger: enten findes en særlig rod-klasse i toppen af et<br />

hierarki, som er baseklasse for alle andre klasser i hierarkiet, eller også er klasserne distribueret<br />

på flere mindre hierarkier og helt separate, ikke-afledte klasser. Spørgsmålet om hvorvidt et<br />

enkelt træ eller en skov af træer er bedst er det ikke muligt at give et generelt svar på. Mindre<br />

biblioteker har dog en tendens til at tage form som et træ, mens større biblioteker med mange<br />

forskellige klassearter er implementeret som en skov. Skoven af separerede træer er også lettere<br />

at have med at gøre i større udviklingsprojekter, fordi den gør det lettere for de enkelte deltagere<br />

at arbejde koncentreret på deres eget træ i skoven.<br />

Brugen af multipel arv i en større sammenhæng er også lettere at adminstrere, hvis<br />

klassebiblioteket indeholder flere uafhængige hierarkier. Hvert hierarki i biblioteket får en<br />

bestemt ansvarsflade og henvender sig til specifikke klienter. To eller flere hierarkier kan lettere<br />

samles med multipel arv på denne måde, fordi der ikke skal tages hensyn til virtuelle baseklasser<br />

og de relaterede problemer (afsnit 4.5.8). Multipel arv skal bruges til at dele en egenskab fra én<br />

klasse med en andre klasser, og ved at arve multipelt fra aktive klasser skal kun et minimum af<br />

kode skrives om.<br />

438 Klassebiblioteker 5.8


Figur 5-8: Klassetopologisk opbygning: enkelt træ (a) og skov af træer (b).<br />

En god regel er imidlertid følgende. Selv om arv inspirerer til mange led af klasser i et stort<br />

hierarki, er graden af genbrug ikke altid ligefrem proportional med hierarkiets størrelse. Ofte vil<br />

en introduktion af en ny klasse i et kompekst klassehierarki være meget besværligt med mange<br />

trivielle virtuelle metoder, der skal skrives (og også optager plads i lageret), mens de mindre<br />

hierarkier i en skov af træer er lettere at udbygge.<br />

Eksempler på kommercielle klassebiblioteker, som benytter forskellige filosofier for designet af<br />

hierarkiet er<br />

• CommonView, som er en løs grænseflade til et (egentligt vilkårligt) vinduessystem. Det<br />

er implementeret træ-hierarkisk, og indeholder meget få klasser. Systemet har kun en<br />

enkelt abstrakt klasse, som alle de aktive klasser arver direkte fra. Der benyttes ikke<br />

multipel arv. CommonView findes til næsten alle miljøer, blandt andet Macintosh,<br />

DOS, Windows, HP NewWave, OS/2 Presentation Manager og OSF/Motif, og<br />

programmer, der benytter CommonView er (i teorien) flytbart mellem alle disse<br />

systemer.<br />

• NIH Class Library, udviklet af det amerikanske sundhedsvæsen. Det er et generelt<br />

system til brug i databaseapplikationer. Dette system er implementeret som ét hierarki,<br />

men gør stor brug af abstrakte klasser og multipel arv. NIH Class Library indeholder<br />

mange generelle klasser, for eksempel lister, kataloger, strenge og numeriske typer og<br />

indeholder en enkel form for bestandighed via en speciel klasse, der implementerer I/O<br />

på nøglebasis. BIDS-biblioteket, som følger med Borland C++ oversætterne er løst<br />

inspireret af NIH Class Library.<br />

• Andre sprog, der kommer med et standard sæt af generelle klasser. Smalltalk [Goldberg<br />

83a] sælges for eksempel med et stort antal generelle datatyper og containerklasser, der<br />

er basis for al Smalltalk-udvikling, mens Eiffel [Meyer 88] også har indbyggede klasser<br />

for listebehandling, filmanipulation og lageradministration.<br />

Desværre er en uheldig konsekvens af mange klassebibliotekers opbygning, at de ikke arbejder<br />

sammen med andre klassebiblioteker. De består ikke blot af indkapslede klasser, men har også<br />

globale sideeffekter, som gør det umuligt at integrere dem i den samme applikation. For eksempel<br />

er det med sikkerhed ikke muligt at foretage en multipel afledning mellem en grafisk klasse i et<br />

vinduessystem-bibliotek med en bestandigheds-klasse i et database-orienteret bibliotek. Hvad der<br />

skal til, er en standard for grænseflader til hele biblioteker eller hierarkier, og ikke blot klasser.<br />

5.8.3 Topologi 439


En sådan standard findes ikke de første mange år, så integration af store klassebiblioteker er ikke<br />

en fornuftig målsætning foreløbig.<br />

5.9 GENBRUGSMEKANISMER I C++<br />

Termen software-genbrug har været anvendt mange gange gennem denne bog. Genbrug har groet<br />

fra en disciplin af akademisk interesse til en nødvendighed, fordi det har vist sig, at<br />

kompleksiteten af software er meget stejlt stigende. De programmer, vi skriver i dag, er mange<br />

gange så komplicerede som bare for ti år siden. Dengang var genbrug ikke en kardinal faktor,<br />

fordi selve opgaverne var relativt simple. Programmerne var simpelthen begrænsede. Det faktum,<br />

at kompleksiteten har vokset eksponentielt lader os til at tro, at denne udvikling sandsynligvis vil<br />

fortsætte på samme måde i fremtiden. Det betyder, at vi står overfor programmeringsopgaver af<br />

en kompleksitet, som ikke har været forudset endsige overvejet for bare få år siden.<br />

Genbrug er et nøglepunkt i forsøget på at opløse denne kompleksitet. Termen betyder ikke, at vi<br />

genbruger softwareelementer, som er blevet smidt væk - på engelsk kaldes softwaregenbrug reuse<br />

og ikke re-cycling - målet er tværtimod at forsøge at udvikle programmer, som tillader os at<br />

frakoble visse dele og skabe nye programmer med dem uden at tabe abstraktion, dvs. uden at<br />

skulle tænke så meget over det detaljerede indhold af disse dele. Genbrug forudsætter på den<br />

måde abstraktion. I andre fagområder såsom industri og elektronik er genbrug et vigtigt element.<br />

Når en bilfabrik udvikler en ny bil, starter de ikke forfra med et hvidt stykke papir, de anvender<br />

både tidligere design, teknologi og maskinel til opgaven. Ligeledes benytter elektronikbranchen<br />

sig i høj grad af genbrugsprincippet. Computere, CD-afspillere, telefonsvarere osv. bliver ikke<br />

bygget op helt fra bunden. Der findes allerede etableret og tilgængelig teknologi, der kan<br />

anvendes (genbruges) til de forskellige applikationer. Bortset fra standardkomponenter bruges i<br />

meget stor stil integrerede kredse til alle elektroniske produkter. Disse kredse har en intern<br />

funktionalitet og en ydre grænseflade til omverdenen. De kan anvendes ved blot at kende til hvad<br />

de gør og hvordan grænsefladen ser ud (kredsens pin-out, de små bens betydninger) mens selve<br />

måden de virker på er et abstraktionsniveau længere nede og er i konteksten ligegyldig. Vi kan<br />

således belejligt sammenligne sådanne kredse med vores klassebegreb. Det, vi søger efter, er at<br />

udvikle software-VLSI'er som kan anvendes i mange forskellige applikationer, fordi de har<br />

generiske træk. Et softwareprojekt bør tage udgangspunkt i et antal standardkomponenter, som<br />

modulariseres, parametiseres og integreres med et minimum af ekstra kode til en færdig<br />

applikation. Men dette mål kræver, at vi klart definerer, hvad vi mener med genbrug.<br />

I eksisterende udviklingssystemer og -sprog anvender vi allerede eksisterende kode og<br />

definitioner af typer om og om igen. For eksempel findes de fleste steder standardbiblioteker,<br />

som indeholder funktioner til lavniveau-I/O, tegntransformation, matematik og grafisk<br />

databehandling. At de kan genbruges skyldes netop, at de er defineret som standarder. Men det<br />

repræsenterer ikke egentlig genbrug, fordi der oftest er tale om meget statiske biblioteker af kode,<br />

som ikke følger et egentligt designkoncept. En klient af disse biblioteker er således nødt til at<br />

gøre sig afhængig af deres opbygning og især indhold af prædefinerede typer på en måde, der<br />

ikke fremmer dataabstraktionen mellem klienten og biblioteket. Jeg vil betegne denne form for<br />

integration mellem standardkomponenter og klientkode som en avanceret form for inklusion af<br />

kode, ikke som en reel genbrugsmekanisme.<br />

440 Klassebiblioteker 5.8


I udviklingsprojekter har man i mange år på samme måde anvendt tidligere skrevne<br />

programmer og designkomponenter i nye projekter. Fremgangsmåden har ofte været, at en del af<br />

et færdigt program er blevet skåret fra og modificeret, så det passer til det nye programs krav.<br />

Denne praksis er sandsynligvis meget kendt blandt de fleste programmører, men jeg vil dog<br />

hævde, at den ikke repræsenterer egentlig genbrug. De småstykker, der klippes ud af de gamle<br />

programmer, bliver ændret med det samme, hvorfor der straks bliver tale om to forskellige<br />

versioner af samme delprogram. Det er således et eksempel på reverse engineering, hvor man<br />

tager udgangspunkt i et kompleks og går tilbage og finder virkemåden i de konkrete dele, og<br />

bærer ofte benævnelsen software-bjærgning. Man bjærger simpelthen den kode i eksisterende<br />

programmer, der lige netop opfylder ens behov på et givet tidspunkt. Det er imidlertid klart, at<br />

denne praksis, hvis den anvendes i for eksempel sideløbende projekter, vil være meget<br />

uhænsigtsmæssig, fordi delprogrammer, der repræsenterer abstraktioner på skæve plan<br />

distribueres og således findes adskillige steder i samme virksomhed. Når den originale forfatter af<br />

koden finder et par fejl, er det en større administrativ opgave at lokalisere alle de bjærgede kopier<br />

af koden, og den person, der i første omgang foretog bjærgningen (og overtog ansvaret for selve<br />

koden) skal bruge lang tid på at forstå, hvordan den egentlig virker. Det er ikke en velfungerende<br />

abstraktionsmekanisme og er derfor (jvf. ovenfor) ikke genbrug.<br />

Et langt mere repræsentativt eksempel for genbrug er hvad der sker, når en hovedbestanddel af<br />

et projekt overlever og kan bruges senere. Sådan en bestanddel kan være en underliggende<br />

database-administrator, der behandler data på en måde, der er uniform for de fleste af de<br />

projekter, der udvikles. Det kan benævnes software-overlevering og er et eksempel på, hvordan<br />

modulærprogrammering, fordi det bygger på abstraktioner, støtter genbrug. Det kræver imidlertid<br />

disciplin at udvikle programmer på denne måde. Der skal allokeres ressourcer til programmører,<br />

således at de bruger en væsentlig del af deres arbejdstid i et specifikt projekt til at udvikle<br />

generelle programmer.<br />

Genbrug af software er med andre ord ikke noget, der bare sker. Genbrugstanken starter<br />

allerede ved designet af de programmer, der selv senere skal genbruges. Det er en udbyderopgave<br />

og ikke en klient-opgave. Det omfatter både et godt design, en robust implementation,<br />

faciliteter for distribution af udbudte klasser og moduler, dokumentationsprincipper og<br />

projektledelse. Selve udbyder-klient modellen er en underliggende faktor for genbrug, fordi den<br />

bygger på en klasse, der tilbyder visse faciliteter til en klient, og ikke omvendt, hvor en klient<br />

anvender en klasse efter behov. Der skal nødvendigvis være overensstemmelse mellem<br />

udbyderen og klienten (og deres kode) for at få det hele til at virke. Klienten anvender en<br />

genbrugelig klasse ved at konsultere udbyderen (eller dennes dokumentation) og forholder sig<br />

iøvrigt strengt til de regler, der er opsat af denne. Af disse grunde har vi brug for sprog og<br />

udviklingsmiljøer, der understøtter indkapsling og informationsskjul. I elektronikverdenen er det<br />

af gode grunde ikke muligt at åbne en integreret kreds og patch'e hist og her for at kunne<br />

integrere den i en aktuel applikation. Grænsefladereglerne skal simpelthen følges til punkt og<br />

prikke. Softwaregenbrug kan også kun lykkes, hvis grænsefladerne reguleres, så ansvaret for<br />

vilkårlige dele af programmet kan placeres. C++ indeholder i rimelig grad de mekanismer, vi har<br />

brug for i form af privatisering af data, men udviklingsmiljøet bør også konfigureres på en måde,<br />

hvor oversættelsesenheder beskyttes mod skrivning af uautoriserede programmører. De regler,<br />

der er fremsat for grænseflader i afsnit 5.4 bør tillige følges af udbyderen. Det er i grunden en<br />

analyseopgave at skrive grænseflader, fordi det foregår på entitetsplanet (se afsnit 5.3.2) og<br />

5.9 Genbrugsmekanismer i C++ 441


forholder sig udelukkende til anvendelsen af det, der ligger bag grænsefladen. En dårlig<br />

grænseflade medfører derfor uvilkårligt dårlige klientprogrammer.<br />

En høj grad af genbrugelighed i koden forudsætter derfor genbrugelighed i andre domæner end<br />

blot i kildeteksten. Hvis vi skal kunne anvende abstraktionsmekanismerne i det sprog og den<br />

programmeringsmetode, vi har valgt, må vi være vores domæner bevidste til alle tider, ellers kan<br />

vi ikke finde de rette abstraktionsniveauer. Hvis vi taber abstraktion taber vi også<br />

genbrugelighed.<br />

5.9.1 Domæneanalyse<br />

Det, vi forstår ved en klasse, eksisterer ikke blot som en konkretisering af et begreb i vores<br />

kildetekst. Som nævnt i afsnit 4.3.1 er klasser fundamentet for den måde, vi inddeler alle former<br />

for begreber på. I traditionel forstand et det en intuitiv proces at klassificere, fordi begreberne<br />

ligger os lige for, men indenfor programmering kræver det, at vi forstår abstraktionerne bag<br />

klasserne for at kunne konstruere dem rigtigt.<br />

For at kunne skabe software, der er så generisk som muligt, må vi først forstå på hvilket<br />

abstraktionsniveau, den aktuelle klassificering foregår. Som nævnt er et genbrugeligt design<br />

fundamentalt for genbrugelig software. Et design af en klasse forudsætter en solid forståelse for<br />

semantikken bag det koncept, den repræsenterer, for at grænsefladen til klassen kan skrives så<br />

robust som muligt. I designdomænet beskæftiger vi os således med begreberne type-ækvivalens<br />

og type-evolution. Det er et designspørgsmål, om en type A skal kunne konverteres til en type<br />

B, fordi det indebærer en semantisk forståelse for begge klasser samt en fremsynethed i omfanget<br />

af dem. Det giver mening at konvertere en Complex til en double, som det er gjort i afsnit<br />

3.6, men en given containerklasse skal for eksempel ikke nødvendigvis kunne konverteres til en<br />

anden. Selvom en LinkedList og et Array begge er containere, ligger type-ækvivalensen<br />

på et plan, som gør dem til ligestillede subtyper af en mere generel type - altså en ensartet<br />

grænseflade - og ikke på et plan, som gør dem til ligestillede typer i semantisk forstand - et<br />

ensartet indhold. Det giver på samme måde heller ikke mening at konvertere fra en Linie til et<br />

Punkt eller fra en Cykel til en Bil. I designdomænet må vi tage stilling til, hvordan<br />

klasserne forholder sig til hinanden for at kunne producere den bedst mulige organisering af dem.<br />

Designdomænet kan underinddeles i et applikationsdomæne og et variansdomæne.<br />

Applikationsdomænet repræsenterer det aktuelle projekt og de begreber, der skal komme til<br />

udtryk deri. Typisk vil et design primært fokusere på de klasser, som applikationen anvender i<br />

processeringen og beskrive dem som entiteter med solid binding til implementationen. Hvis der<br />

udelukkende fokuseres på applikationsdomænet i designet, vil meget af både designet og senere<br />

af koden gå tabt, fordi klasserne i systemet hurtigt bliver specifikke og applikationsafhængige.<br />

<strong>Introduktion</strong>en af et variansdomæne som afgørende faktor i udformningen af selve klasserne<br />

skaber et fundament for genbrugeligheden i systemets bestanddele. Det gælder om at tænke i<br />

varianter hele tiden for at opnå den største generalitet, mens applikationen, selvom den er i fokus,<br />

bliver et produkt af variansen. Det lyder som en bagvendt måde at udvikle programmer på, men<br />

vil faktisk sænke produktionstiden, fordi genbrugeligheden allerede manifesterer sig indenfor<br />

samme projekt. En typisk applikation indeholder dusinvis af implementationer af hægtede lister,<br />

sorteringer, skærmbehandlinger, tilstandsmaskiner og brugerinteraktioner fordi der hele tiden<br />

442 Genbrugsmekanismer i C++ 5.9


fokuseres på applikationen. Hvis enhver udvidelse i stedet foretages med udgangspunkt i de<br />

varianter, den pågældende klasse kan tænkes at optræde i, vil applikationen hurtigt drage fordel af<br />

variansen. Variansdomænet er imidlertid det sværerste at sætte i system, fordi det umiddelbart<br />

stiller spørgsmål om graden af den ønskede generalitet. Hvis vi udvikler en simulator for et IC3tog<br />

skal vi overveje, om der skal fokuseres på diesel-baserede tog eller også lade systemet<br />

omfatte eldrevne tog, om det kun skal være Scandia-udviklede tog eller tog af andre fabrikater,<br />

om det skal være køretøjer af enhver art, der kører på skinner, om det skal være alle køretøj eller<br />

som det skal være alle simulatorer. Det er faktisk et spørgsmål om at finde balancepunktet i<br />

variansdomænet og indebærer en definition af hensigten med softwareudviklingen i det hele<br />

taget. Balancepunktet bør findes i konsultation med alle involverede parter og bygge på<br />

udviklingsafdelingens fremtidige planer for produktion og markedsføring.<br />

Alt dette skal stå i kontrast til implementationsdomænet, eller løsningsdomænet, fordi de<br />

abstraktionsmekanismer, det aktuelle system tilbyder i form af indkapsling, arv, delegering,<br />

parametisering, polymorfi osv. ikke nødvendigvis kan afspejles i designet. I det næste afsnit<br />

beskrives de abstraktionsmekanismer i C++ som er fundamentale for et genbrugeligt design. I<br />

figur 5-9 vises, hvordan de fire nævnte domæner kan relateres til hinanden på en måde, som<br />

skaber en balancelinie. Afhængigt af, om det aktuelle projekt er meget enestående eller meget<br />

generelt vil denne linie være mere eller mindre vandret.<br />

5.9.1 Domæneanalyse 443


5.9.2 Abstraktionsmekanismer<br />

I dette afsnit rekapituleres og samles en hel del af de overvejelser om anvendelsen af<br />

abstraktionerne i C++ og de idiomatiske særegenheder, de lægger op til. De fremgangsmåder for<br />

valg af grænseflader, medlemsfunktioner og operatorer samt organisering af klasser og<br />

konverteringer kan grupperes i fire familier af reele abstraktionsmekanismer, som har konkrete<br />

forbindelser til både design og implementation.<br />

Figur 5-9: Balancen i et objekt-orienteret design.<br />

Den første abstraktionsmekanisme benævnes delegering og indebærer, at en klasse selektivt<br />

udbyder medlemmer til klienten på forskellige måder. Delegering er en anvendt teknik i mange<br />

andre sprog og simuleres i C++ ved forwarding, hvilket vil sige, at vi ved hjælp af indkapsling og<br />

offentlige medlemsfunktioner kan differentiere eksisterende klasser. Delegering er den rigtige<br />

løsning, når der ikke findes entydig type-ækvivalens mellem to eller flere klasser eller når en<br />

eksisterende klasse uden polymorfe egenskaber skal udbygges. Den nye klasse indkapsler den<br />

eksisterende klasse som forekomst eller via en pointer og klassens grænseflade designes, så den<br />

reflekterer de ønskede transformationer og den ønskede transparens i relation til den eksisterende<br />

klasse. Et eksempel på, hvornår delegering er den rette mekanisme at anvende er forholdet<br />

mellem en LinkedList og en Node (fra afsnit 4.5.4). Typeforholdet mellem disse to klasser<br />

er en mellemting af er-en og bruger-en. LinkedList indkapsler en Node* og arbejder<br />

således indirekte (som en slags "priviligeret klient") på forekomster af Node. En egentligt typeækvivalens<br />

er ikke til stede, fordi abstraktionen i en Node (en next-pointer, for eksempel)<br />

ikke giver mening i en LinkedList. I stedet uddelegerer LinkedList attributter fra<br />

Node i form af medlemsfunktioner, der hæver forståelsen for hægter ét niveau - for eksempel<br />

kan LinkedLister sammenlignes med hinanden. I designdomænet betyder dette, at der<br />

444 Genbrugsmekanismer i C++ 5.9


lægges vægt på varians, fordi vilkårlige klienter kan arve fra Node og igen tilbyde nye klasser til<br />

andre klienter, som ikke er afhængig af den originale implementation af de originale klasser. Den<br />

delegerende klasse er generisk, fordi den ikke er afhængig af subtypeforhold i de indkapslede<br />

klasser og er derfor direkte genbrugelig i både design- og i implementationsdomænet: Variansen i<br />

en LinkedList er meget høj.<br />

Den næste og måske mest betydningsfulde abstraktionsmekanisme er arv. Dette koncept er<br />

blevet behandlet på mange forskellige plan gennem denne bog, både i helt abstrakt og ikkedatalogisk<br />

forstand, som en generaliserende teknik og som konkret idiom i C++. I forhold til<br />

førnævnte domæneanalyse er arv i stand til at udvide anvendelsesmulighederne i design og kode,<br />

der selv benytter den klasse, der arves fra. Med andre ord, entiteter, der allerede arbejder med en<br />

klasse bliver mere generisk, når der arves fra denne klasse, fordi arv erklærer et lineært<br />

typeforhold. I designdomænet lægges således mere vægt på applikationsdelen end på<br />

variansdelen, fordi den afledte klasse altid vil være mere specifik og applikationsafhængig end<br />

baseklassen. I den forstand, at arv fremmer generalitet gennem større muligheder i fremmede<br />

klientklasser, er genbrugeligheden i sig selv ikke manifesteret i variansen af designet. Arv er ikke<br />

en genbrugelighedsmekanisme i sig selv; brug aldrig arv med henblik på at genbruge kode i<br />

baseklassen. Dén form for genbrug er netop, hvad ovenfor er beskrevet som softwareoverlevering<br />

og er ikke spor generisk. Som eksempel kan nævnes en klasse, der arver fra Node<br />

og udbygger begrebet en hægte med nye egenskaber. Det forhold, som Node har med for<br />

eksempel LinkedList gør LinkedLists anvendelsesområde større. Genericiteten er<br />

allerede beskrevet i en anden klasse og det er den, programmøren skal have i tankerne, når der<br />

anvendes arv som abstraktionsmekanisme. Husk, at det ikke er baseklassen selv, der nedarves<br />

men derimod baseklassens egenskaber. Nævnes skal også, at man arver både gode og dårlige<br />

sygdomme, så vær altid opmærksom på at analysere de abstraktioner, som baseklassen<br />

repræsenterer inden der arves. Ellers går de dårlige sygdomme i arv generation efter generation.<br />

Som eksempel på en arvefølge, der foretages hændeligt men uhensigtsmæssigt, enten med<br />

udgangspunkt i egalitet mellem klassernes grænseflader eller på grundlag af deres anvendelse<br />

som containerklasser eller andet, kan vi erklære to anvendelige klasser, en liste (afsnit 3.8.1) og<br />

en liste (afsnit 3.8.4). Klassedefinitionen på en generel LinkedList kan se således ud:<br />

template <br />

class LinkedList {<br />

// ... implementation skjult ...<br />

public:<br />

void Insert (Element*);<br />

Element* Head ();<br />

Element* Tail ();<br />

Element* Next ();<br />

int hasElement (Element*);<br />

int memberCount ();<br />

};<br />

Denne klasse indeholder referencer til objekter af typen Element (gennem skabelonparametisering)<br />

og kan for eksempel tænkes at sortere disse. LinkedList gør ikke forskel på<br />

5.9.2 Abstraktionsmekanismer 445


objekterne i listen, samme objekt kan forekomme flere gange og være af enhver type, for<br />

eksempel void-pointere. Mængdeklassen Set ligner List en hel del:<br />

template <br />

class Set {<br />

// ... implementation skjult ...<br />

public:<br />

void Insert (Element*);<br />

int hasElement (Element*);<br />

int memberCount ();<br />

};<br />

Faktisk er de offentlige medlemmer i Set syntaktisk et subset af de, vi finder i LinkedList,<br />

hvorfor vi i en iver efter at arve kan skabe et subtypeforhold mellem de to klasser som følger:<br />

template <br />

class Set : public LinkedList {<br />

// ... implementation skjult ...<br />

public:<br />

void Insert (Element* e) {<br />

if (!hasElement (e)) LinkedList::Insert (e);<br />

}<br />

};<br />

Nu har vi gjort Set type-ækvivalent med LinkedList idet vi har taget udgangspunkt i<br />

klassernes grænseflader. Problemet (og det er stort) i det er, at semantikken bag de enkelte<br />

medlemsfunktioner er dybt forskellige. En LinkedList kan indeholde det samme objekt flere<br />

gange, mens et Set er defineret som en container, der kun kan omfatte et enkelt objekt én gang.<br />

Ved at arve siger vi, at et Set er-en LinkedList og at alle klientmoduler, der anvender<br />

LinkedList-objekter også kan anvende Set-objekter. Det er blot ikke korrekt. Den<br />

semantik, der ligger bag operationerne i medlemsfunktionerne Head(), Tail() og Next()<br />

i LinkedList giver ingen mening i Set, så hvordan skal vi forholde os til dem? Vi kan ikke<br />

selektivt gøre dem private ved eksplicit at reerklære dem i den private del af klassen, fordi vi ikke<br />

kan ændre på adgangskriterierne, der er beskrevet i baseklassen. Så vi ser, at selvom de to klasser<br />

udviser homonyme træk - de ser næsten ens ud - så er abstraktionerne bag klasserne og<br />

semantikken bag operationerne i grænsefladen meget forskellige. Ved at beskrive forholdet<br />

mellem dem ved arv, skaber vi et seriøst hul i vores brugerdefinerede typesystem, fordi<br />

klientkoden vil slå fejl ved brug af Set-objekter gennem LinkedList-pointere eller<br />

lignende. [Liskov 88] beskriver kriterierne for at arrangere klasser og udtrykke forhold ved arv på<br />

følgende måde:<br />

Hvis der for hvert objekt o1 af typen S er et objekt o2 af typen T, således at<br />

ingen programmer P defineret i forhold til T ændrer opførsel, hvis o1 skiftes<br />

ud med o2, så er S en subtype af T.<br />

446 Genbrugsmekanismer i C++ 5.9


Dette princip (ofte kaldet the Liskov Substitution Principle) er en grundpille i objekt-orienteret<br />

design og repræsenterer et væsentligt kriterium for designets succes. Hvis dette princip ikke<br />

følges er det ofte fordi, arv benyttes i implementationsdomænet og ikke i designdomænet. Det<br />

leder til en slags bagvendt opfattelse af det objekt-orienterede paradigme, fordi man forsøger at<br />

generalisere på de afledte klasser og ikke på baseklasserne ved kun at benytte sig af en del af<br />

baseklassens indhold.<br />

Vi kan også kalde polymorfi for en abstraktionsmekanisme. Polymorfi er en teknik, der kun får<br />

ægte styrke gennem gennemført analyse og design, fordi det indebærer et større kompleks af<br />

klasser, der anvender både arv og delegering. Polymorfi er i sig selv ikke en konkret<br />

genbrugsfaktor i implementationsdomænet, fordi det handler om anvendelser af klasser et sted<br />

midt mellem variansdomænet og applikationsdomænet. Det genbrugelige i polymorfi ligger<br />

således i anvendelsen af de polymorfe elementer og er en udfordring, der starter i<br />

designdomænet. Jævnfør ovenstående substitutionsprincip vil alle programmer P være generiske<br />

og dermed genbrugelige og dette gælder også for det design, der ligger til grund for P. Designet<br />

af en abstrakt datatype, som indgår i et klassehierarki på en eller anden måde og anvender<br />

polymorfe funktioner eller datastrukturer, vil kunne udnyttes i andre sammenhænge i fremtiden,<br />

fordi de typer, den er defineret i forhold til er erstattelige. Faktisk er det sådanne programmer<br />

(funktioner, klasser, oversættelsesenheder eller datastrukturer) af mængden P, som i realiteten er<br />

det mest generiske, vi kan opnå med objekt-orienteret programmering.<br />

Den sidste form for abstraktionsmekanisme i C++ er muligheden for parametrisk polymorfi,<br />

hvilket er implementeret som skabeloner eller parametiserede typer (afsnit 4.9). Det er<br />

sandsynligvis den mekanisme, som i højest grad opfordrer til genbrug, fordi den arbejder på<br />

abstrakte typer. Genericiteten i en skabelonklasse eller -funktion er meget udtalt, idet der<br />

abstraheres fuldstændigt fra de typer, der arbejdes med - de skal ikke nødvendigvis være<br />

ækvivalente med andre typer og skal ikke indgå i et klassehierarki med genericiteten<br />

implementeret som virtuelle funktioner. Det forhold, at klienten har kontrol over de typer, som<br />

skabelonklasser og -funktioner arbejder med internt gør koden lineært genbrugelig på et plan,<br />

som afviger fra alle andre genbrugsmekanismer. Klienten er i stand til at parametisere<br />

eksisterende kode på typer og ikke på objekter, hvilket maksimerer friheden i genbrugsfasen fra<br />

komplet integration i en programmeringsmodel til en slags lykkehjul, hvor der er frit valg på alle<br />

hylder. At både fundamentale typer, brugerdefinerede typer men også pointer-typer og flerledet<br />

afledte typer kan anvendes i parametiseringen siger noget om, hvor stor anvendelse skabeloner<br />

har. Som vist ovenfor i eksemplet med List og Set kan parametisering anvendes i<br />

sammenhæng med arv, hvorved klienten kan blande parametrisk polymorfi med anvendt<br />

polymorfi og opnå det bedste fra begge verdener. Som genbrugsmekanisme har parametiserede<br />

typer størst relevans i implementationsdomænet.<br />

Når vi taler om abstraktionsmekanismer er generalisering nøgleordet for at få magien til at virke<br />

og for at maksimere genbrugeligheden i design og i kode. Sørg altid for at generalisere nedefter i<br />

klassehierarkier, for at specialisere de afledte klasser og for at anvende de rigtige<br />

abstraktionsmekanismer i C++ i forhold til de typerelationer, der gør sig gældende i det aktuelle<br />

design. Det fremmer robustheden i klientens kode, dvs. at den ikke bryder sammen på grund af<br />

eksterne omstændigheder hvilket igen fremmer typeevolutionen i et softwareprojekt, fordi<br />

afhængighederne mellem klasserne og programdelene kan blive meget kompliceret. Husk altid at<br />

5.9.2 Abstraktionsmekanismer 447


anvende udbyder/klient-modellen.<br />

5.9.3 Granularitet og genbrug<br />

I lyset af de retningslinier for forståelse af abstraktionsmekanismerne i C++, som er blevet<br />

opremset gennem sidste afsnit, skal vi her se på, hvad klassernes omfang har af betydning for<br />

genbrugeligheden. Dette er i modsætning til de konkrete konstruktioner i C++ et spørgsmål, der<br />

rejser sig allerede i designdomænet og som har stor betydning for, hvordan klasserne organiseres<br />

så genbrugeligt som muligt.<br />

Som nævnt tidligere i dette <strong>kapitel</strong> kan programmørens opfattelse af "en klasse" og "en abstrakt<br />

datatype" i forhold til traditionelle udviklingsmetoder være uhensigtsmæssig, fordi man ofte får<br />

en opfattelse af disse entiteter som helskabte og rene. Det er en fejl at antage, at klasser skal<br />

repræsentere letforståelige abstraktioner. De skal derimod være de byggeklodser, softwaremodstykkerne<br />

til integrerede kredse, som skal være det bærende element i et program. Af den<br />

grund er det ofte en fordel at fokusere på, hvilke funktioner og datamedlemmer en klasse ikke bør<br />

indeholde i stedet for det omvendte. Generaliteten i en klasse er netop en funktion af, hvad<br />

klassen ikke indeholder og hvad det er muligt at udbygge den med i form af delegering, arv eller<br />

parametisering.<br />

5.9.4 Identifikation af abstraktioner og entiteter<br />

I introduktionen til dette <strong>kapitel</strong> beskrev jeg fire hovedpunkter, som repræsenterer de aktiviteter,<br />

som ethvert objekt-orienteret softwareprojekt bør rette sig efter. Efter dette <strong>kapitel</strong>s gennemgang<br />

af organisation og modellering af klasser samt anvendelser af paradigmet og C++'s<br />

abstraktionsmekanismer vil jeg nu rekapitulere og raffinere disse punkter i en generel men<br />

uformel "recept" for objekt-orienteret design.<br />

Først skal de egentlig koncepter og abstraktioner i selve målsætningen lokaliseres og<br />

identificeres. Vi befinder os her i applikationsdomænet og arbejder på et meget generelt niveau,<br />

hvor entiteter og andre veldefinerede områder er til at forstå for både bruger og udvikler. De kan<br />

repræsenteres af de substantiver, der blev beskrevet i det semantiske katalog i tabel 5-1 i afsnit<br />

5.3.8 og bliver ofte direkte til klasser eller små klassehierarkier i koden. Hvordan identificeres da<br />

entiteterne i et program? Spørgsmålet er faktisk det samme, som blev stillet på flere forskellige<br />

måder gennem afsnit 5.2 og er blandt de mest spurgte blandt nyankomne til den objektorienterede<br />

verden: "Hvordan finder jeg mine klasser?". Som nævnt kan det være et problem,<br />

fordi entiteterne i applikationsdomænet ofte repræsenterer dårlige abstraktioner som ikke<br />

umiddelbart kan beskrives entydigt i et design. Dog kan vi opstille nogle heuristikker for<br />

identifikation af entiteter i designet:<br />

• Hvis et begreb anvendes som substantiv, når man taler om programmet, og hvis det har<br />

en veldefineret opførsel, er det en god kandidat til en implementering som abstrakt<br />

datatype - ellers skal der måske anvendes en ikke-medlemsfunktion eller en anden form<br />

for abstraktion i C++. En "bankkunde" egner sig som klasse mens et "overtræk" bør<br />

448 Genbrugsmekanismer i C++ 5.9


være en form for tilstandsvariabel og en "rentetilskrivning" mere ligner en<br />

medlemsfunktion i en konto-klasse.<br />

• Gode, traditionelle datalogiske begreber kan være svære at identificere som entiteter og<br />

placere i et design. Her kan det være en fordel at se på, hvordan de abstraktionsmekanismer<br />

som sproget tilbyder, kan anvendes. Et begreb som en containerklasse, som er et<br />

idiom og ikke en abstraktion, er den objekt-orienterede løsning på mange af de kendte<br />

strukturer som stakke, køer og lister. Værre er det med prædefinerede algoritmer, de bør<br />

enten integreres fuldstændigt i en klasse eller baseklasse eller måske implementeres som<br />

parametiseret funktion. Det giver ikke mening at skabe klasser for algoritmer.<br />

• Granulariten i entiteten kan have en afgørende betydning for genbrugeligheden i<br />

designet. Overvej eventuelt at opdele en entitetskandidat i flere klasser og anvend C++faciliteter<br />

som arv, delegering og klientering til at relatere klasserne. Det, der kommer<br />

ud af en sådan differentiering af en entitet, er et lille del-hierarki, som udbydes på en<br />

måde, der er mere fri end hvad ville være tilfældet med en stor, kompliceret og meget<br />

"lukket" klasse.<br />

• Hvis en formodet entitet har en opførsel, som resulterer i medlemsfunktioner, der virker<br />

uden relation til hinanden, er der sikkert ikke tale om et veldefineret begreb i designet.<br />

Det vil ofte resultere i "beskidte" klasser med karakter af forhastede implementationer.<br />

Sådanne er meget ugenbrugelige.<br />

I dette afsnit har vi set på, hvordan en programmeringsopgave ved en analyse af abstraktioner<br />

kan angribes ved at fokusere på henholdsvis applikationsdomænet og variansdomænet. Det er<br />

afhængigt af det enkelte projekt, hvorvidt designet skal balancere i retning af<br />

applikationsafhængige entiteter, klasser og funktioner. Denne balance har også betydning for, om<br />

genbrugeligheden er vertikal eller horisontal i forhold til andre udviklingsprojekter. Vertikal<br />

genbrugelighed betyder, at komponenterne i projektet kan genbruges andre steder i det aktuelle<br />

projekt, mens horisontal genbrugelighed betyder, at komponenternes anvendelighed kan<br />

defineres mere bredt og kan genbruges i andre projekter end lige netop det aktuelle. Begge<br />

retninger er lige ønskværdige at fremme, hvorfor det er en fordel at balancere designet jævnfør<br />

figur 5-9.<br />

5.10 UDVIKLINGSMÆSSIGE OVERVEJELSER<br />

Der er flere følgevirkninger af objekt-orientering på udviklingsprocessen, end de åbenlyse fordele<br />

og ulemper, der er diskuteret i dette <strong>kapitel</strong>. Systemudvikling er næsten altid et spørgsmål om at<br />

forsøge på at tilfredsstille et antal krav med indbyggede modsætninger, for eksempel<br />

funktionalitet, tid og plads. Det strengeste af kravene vil altid tynge udviklingen ned. Forskellen<br />

mellem softwareudvikling og andre former for udvikling er, at der i for eksempel industri og<br />

håndværk altid er mulighed for at ændre på faktorer og nå en anden specifikation på et problem.<br />

Softwareudvikling er mere kompliceret, fordi det først skal designes meget omhyggeligt, da en<br />

5.9.4 Identifikation af abstraktioner og entiteter 449


ettelse i sidste led næsten altid er umulig.<br />

Den udviklingsmæssige forskel på traditionel programmering og objekt-orienteret<br />

programmering er, at der ikke forekommer de kendte "milepælsdage", hvor alle samler den sidst<br />

fungerende version af deres del af programmet. Et objekt-orienteret program startes med en<br />

simpel prototype, som ganske langsomt udvikler sig til en færdig applikation. Denne<br />

inkrementale proces er mulig, fordi alle involverede altid må sørge for at have specifikationerne<br />

på deres klasser i orden, for at deres egen kode kan fungere, og dermed er specifikationen også<br />

sikret for andre medlemmer af projektet.<br />

Der er adskillige fordele at se i dette:<br />

• Det vil altid være muligt at få en eksekverbar version op at køre og se, hvordan projektet<br />

ser ud, så det kan sikres, at det lever op til forventningerne og specifikationen,<br />

• Programmørerne kan se programmet udvikle sig støt, hvilket hæver moralen og gør<br />

arbejdet sjovere,<br />

• Test og kodning kan begyndes allerede før designet er helt færdigt,<br />

• De vigtigste dele af hele programmet bruges oftest og testes oftest,<br />

• Det er altid muligt at se, om programmet er på linie med tidsplanen.<br />

Tillige gør inkrementale prototyper det muligt at revidere designet på et tidligt tidspunkt, og med<br />

få omkostninger, hvis det viser sig, at der er en fejl i specifikationerne.<br />

5.10.1 Evolutionære ændringer<br />

Der er fem typer af ændringer, som typisk foretages hyppigt i en objekt-orienteret applikation,<br />

1. <strong>Introduktion</strong> af en ny klasse,<br />

2. Modifikation af en klasses medlemsfunktion,<br />

3. Modifikation af en klasses medlemsdata,<br />

4. Modifikation af en klasses grænseflade,<br />

5. Omstrukturering af klassehierarkiet.<br />

Der er forskellige årsager for og konsekvenser af disse ændringer. En ny klasse introduceres, når<br />

nye abstraktioner er forstået og specificeret eller når der er plads til klassen under de påkrævede<br />

forudsætninger. Prisen for dette er temmelig proportional med klassens kompleksitet, men har<br />

ingen direkte negativ faktor. At ændre en medlemsfunktion, for eksempel på grund af nye<br />

datamedlemmer eller en bedre algoritme, indebærer normalt heller ikke nogen negative<br />

konsekvenser, da specifikationen på funktionen skal overholdes, så andre dele af programmet<br />

ikke bryder sammen. Blot skal den nye implementation af funktionen være fejlfri. Medlemsdata<br />

ændres, når der er brug for en forbedring i enten plads- eller tidsforbrug i klassens<br />

450 Udviklingsmæssige overvejelser 5.10


medlemsfunktioner. Prisen for dette er, at mange medlemsfunktioner også skal ændres, og det<br />

kan tage unødigt meget tid i forhold til fordelen ved at foretage modifikationen.<br />

En modifikation af klassens grænseflade har til gengæld flere konsekvenser. Hvis prototypen på<br />

en medlemsfunktion ændres, skal al klientkode, der benytter funktionen, også skrives om, hvilket<br />

igen med stor sandsynlighed indebærer substantielle ændringer i klientens kode. Er der tale om en<br />

ny medlemsfunktion, der bare introduceres i klassen, fordi en ny abstraktion for klassens<br />

repræsentation er fundet, er det ikke så stort et problem, blot ikke funktionsnavnet kolliderer med<br />

en funktion i en eventuel baseklasse. En medlemsfunktion fjernes meget sjældent, kun hvis den<br />

ikke behøver ses af klienten flyttes den undertiden ind i den beskyttede eller private del af<br />

klassen. Som et resultat heraf er en klasses grænseflade næsten altid opadkompatibel, hvilket gør<br />

integration af mange moduler med forskellige programmører smertefri.<br />

Hvis klassehierarkiets struktur skal ændres, skal det helst foregå meget tidligt i programmets<br />

udviklingsforløb, helst før klientering er udbredt. At ændre en klasses placering i hierarkiet har<br />

betydning for alle klienter, som i bedste fald skal genoversætte deres kode og i værste fald skal<br />

omskrive den. Alle afledte klasser af den pågældende omplacerede klasse skal sandsynligvis også<br />

ændres, og klienter af disse skal ligeledes genoversættes eller revideres. Derfor er det en fordel,<br />

hvis en forkert placeret klasse hurtigt identificeres, før der er for mange afhængigheder i<br />

systemet.<br />

Det er i øvrigt god praksis at foretage periodiske evalueringer af hierarkiet/hierarkierne i<br />

biblioteket, for at sikre klassernes konsistens og berettigelse. Ofte bærer en klasse på en arv, som<br />

til dels er unødvendig i form af overflødige data eller redundante metoder. En evaluering af<br />

biblioteket foretages med en test af alle klasser én for én og en analyse af deres anvendelse i<br />

relation til applikationen.<br />

Mange CASE-redskaber findes i dag, som kan lette mange af de trivielle dele af<br />

programkonstruktionen. Blandt andet indenfor opbygning af containere, generisk kode og<br />

parameterisering er mulighederne store og kan automatisere processen. Analyseværktøj findes til<br />

pladsforbrug, metodeplacering og klasseidentifikation, som letter arbejdet med at oprette klasser,<br />

finde datamedlemmer og skrive medlemsfunktioner.<br />

5.10.2 Dokumentation<br />

Det er, som altid, vigtigt at dokumentere software på en ensartet og koncis måde. Dokumentation<br />

af objekt-orienteret software forenkles en del på grund af klassebregrebet: Da en klasse i sig selv<br />

er en specifikation på en datatype med et indhold og en grænseflade, kan klasseerklæringen<br />

bruges som udgangspunkt for en dokumentation. Headerfilen med klassens specifikation, påført<br />

en tekstuel anvendelsesbeskrivelse og hensigtserklæring samt de tekniske kommentarer, der<br />

måtte være væsentlige, er ofte nok for, at en klasse umiddelbart kan forstås af en klient.<br />

I afsnit 5.2.3 så vi et forslag til organisering af kildeteksten i specifikationsfilerne. Fra denne<br />

specifikation vil det med et enkelt værktøj være muligt at uddrage essentielle data om klassen,<br />

blandt andet<br />

• Navnet på klassen,<br />

• Eventuelle baseklasser,<br />

5.10.1 Evolutionære ændringer 451


• Eventuelle indeholdte klasser,<br />

• Polymorfe muligheder i klassen (og antallet af virtuelle funktioner),<br />

• Grænsefladen (syntaks og semantik) til klassen,<br />

• Grænsefladen til en eventuel afledt klasse,<br />

• Konstruktionssyntaksen.<br />

Det er ligeledes muligt automatisk at determinere, om en klasse er aggregat, container, abstrakt<br />

eller aktiv. Disse data kan opstilles i standardskemaer (på et elektronisk medie), så det altid er<br />

muligt at se, hvordan en given klasse er repræsenteret.<br />

5.10.3 Udviklingstekniske følger<br />

Omstillingen til objekt-orienterede metoder har også klare arbejdsmæssige konsekvenser. Før var<br />

det en smal sag at skrive en headerfil med en specifikation på et modul, og den store<br />

koncentration blev lagt i selve koden. Headerfilen indeholdt blot prototyper på datastrukturer,<br />

som blev modificeret et par gange i udviklingsforløbet. Nu er det faktisk størstedelen af tiden, der<br />

bliver brugt på headerfilerne. Det er på grund af, at klasseudbyderen selv skal rette sig efter den<br />

samme specifikation som klienten, at headeren bliver central.<br />

Det vil også vise sig, at linieantallet mindskes. Programmerne fylder simpelthen mindre i<br />

kildetekst. Objektkoden er større for de mindre applikationer, men har en mindre stigning i<br />

størrelse for mere komplekse applikationer end for traditionelt skrevne programmer. Linieantallet<br />

er således ikke en indikator for, hvor godt et udviklingsprojekt former sig, men i højere grad en<br />

rettesnor for sandsynligheden for fejl.<br />

Alt taget i betragtning indebærer objekt-orienteret programmering, at den tidligere meget<br />

underforståede konsens mellem deltagere i et udviklingsprojekt nu bliver mere eksplicit. Et<br />

udviklingsprojekt bliver mindre frustrerende, fordi ansvar for fejl hurtigt kan placeres og fordi der<br />

aldrig hersker tvivl om, hvordan en klasse skal anvendes.<br />

5.11 OPGAVER TIL KAPITEL 5<br />

1. Overvej forskellen mellem bestandighed, der administreres af et klassebibliotek og<br />

kontrolleres af klienten, og bestandighed, der både administreres og kontrolleres af<br />

klassebiblioteket. Hvilket arbejde skal biblioteket foretage sig for at give klienten denne<br />

transparens?<br />

2. Hvornår bør klassebiblioteker designes med en enkelt rod (ét træ), og hvornår er det<br />

mere hensigtsmæssigt at skrive det med flere uafhængige rod-klasser (en skov af træer)?<br />

Hvad er følgerne i polymorf henseende? Hvad er forskellen, hvis der skal arves<br />

multipelt?<br />

3. Implementér en containerklasse, der indeholder referencer til polymorfe objekter. Skriv<br />

nu medlemsfunktioner i containerklassen, der er i stand til at udlæse og indlæse<br />

indholdet på en strøm, så et containerobjekt kan lagres på sekundærlageret. Hvilke<br />

452 Udviklingsmæssige overvejelser 5.10


problemer render du ind i, og hvorfor?<br />

4. Forklar nødvendigheden af abstrakte klasser i objekt-orienteret programmering.<br />

5. Forklar om forskellen på udvikling af software i C og C++. Bliver arbejdsdagen<br />

anderledes? Hvordan?<br />

6. Forklar begrebene (a) Abstrakte klasser, (b) Containerklasser og (c) Aktive klasser og<br />

uddyb deres funktion i et objekt-orienteret program, helst med eksempler. Er der<br />

paralleller i disse begreber til elementer i traditionel programmering, for eksempel C<br />

eller Pascal?<br />

7. Beskriv med dine egne ord Liskov's substitutionsprincip. Hvorfor er det så vigtigt, at<br />

dette princip efterkommes i objekt-orienteret design?<br />

8. Implementér klasser til repræsentation af (a) stakke, (b) associative lister og (c)<br />

sorterede lister. Argumentér for dine valg af abstraktions- og genbrugsmekanismer.<br />

5.12 REFERENCER OG UDVALGT LITTERATUR<br />

Der findes mange gode referenceværker om objekt-orienteret design og opbygning af<br />

klassebiblioteker. Forfatteren kan dog ikke anbefale nogen af dem som fundamental. I [Goldberg<br />

83a] findes interessante bemærkninger om udviklingen af OOP-software, og om hvordan det har<br />

indflydelse på vores syn på software. [Meyer 88] har sin egen definition af et objekt-orienteret<br />

system, Eiffel, i sin bog Object-oriented Software Construction. Information om det tidlige<br />

objekt-orienterede sprog, Smalltalk, kan findes i [Goldberg 83b]. [Kirslis 86] har nogle bud på et<br />

standardformat for klassespecifikationer til brug i integration mellem biblioteker. I [Johnson 88]<br />

findes forslag til design af genbrugelige klasser, med udgangspunkt i entydige og konsistente<br />

specifikationer. [Coplien 92] indeholder en meget robust gennemgang af idiomer i C++, dvs.<br />

sproglige egenarter, som i konsistent brug gør C++-programmering sikrere. Der henvises også til<br />

[Liskov 88] for definitioner af generelle objekt-orienterede synspunkter og argumentationer for<br />

konstruktionsfremgangsmåder og til [Gorlen 88] for gode historier om problemerne med<br />

klassehierarkier i C++.<br />

5.11 Opgaver til <strong>kapitel</strong> 5 453


Standardbiblioteket<br />

Den primære årsag til C++-programmers høje grad af flytbarhed, dvs. den lethed,<br />

hvormed de kan genoversættes på et andet system, er sprogets distance fra den<br />

underliggende arkitektur. C++ er designet uden nogen væsentlig specifikation af<br />

system-afhængighed og er helt uden indbyggede faciliteter for I/O. Som i C<br />

udføres I/O-operationer normalt gennem kald til et standardbibliotek, som tager<br />

sig af tegnbaserede konverteringer til og fra sprogets typer. Dette <strong>kapitel</strong> beskriver<br />

standardbibliotekets funktion og omfang. Standardbiblioteket er udover en<br />

uundværlig del af C++-miljøet også en god inføring i objekt-orienteret<br />

programkonstruktion.<br />

6.1 INTRODUKTION<br />

I/O-operationer ligger ikke fast som en indbygget del af C++, men er en defineret standard, som<br />

er implementeret på det enkelte system, og som normalt følger med et oversættersystem. Ved at<br />

bruge standardbiblioteket til simple operationer som skærm-output, læsning fra tastaturet og<br />

filmanipulation kan et C++-program flyttes til andre arkitekturer og operativsystemer uden<br />

besvær, sålænge disse også understøtter denne standard. I/O-biblioteket, som beskrives her,<br />

følger med stort set alle C++-implementationer, og refereres normalt som iostream-biblioteket.<br />

iostream erstatter det tidligere stream-bibliotek, som er standard under C++-systemer med<br />

versionsnumre under 2.0. Med ganske få undtagelser er programmer, som bruger<br />

stream-biblioteket opadkompatible med iostream, så gammel kode vil også kunne oversættes<br />

under det nye bibliotek. Filosofien med eksternt definerede I/O-operationer er den samme i C,<br />

hvor der benyttes specielle funktioner og datastrukturer til ind- og udlæsning af diverse typer.<br />

Således findes funktioner til at læse og skrive tegn, strenge, lagerblokke og forskellige<br />

fundamentale typer, formateret efter regler, som kan opstilles af programmøren. Både i C og i<br />

C++ er I/O-bibliotekets funktion at konvertere sprogets typer til og fra sekvenser af tegn og være<br />

i stand til at ind- og udlæse disse kontinuerligt fra og til en "strøm" på den givne arkitektur.<br />

Forskellen mellem C-biblioteket stdio og C++biblioteket iostream er, at iostream er opbygget af<br />

klasser og tillader således via specialisering og arv, at biblioteket kan udvides dynamisk med<br />

I/O-operationer for de abstrakte datatyper, der findes i en vilkårlig applikation. stdio fungerer<br />

naturligvis også sammen med C++.<br />

En strøm er abstraktionen ved en kontinuert sekvens af tegn, som i bibliotekets laveste niveau<br />

454 Opgaver til <strong>kapitel</strong> 5 5.11<br />

6


kan læses og skrives fra og til forskellige enheder (indre og ydre) i systemet. På dette niveau<br />

behandler iostream blot de hardware-afhægige aspekter af I/O, og arbejder ikke med datatyper,<br />

hverken fundamentale eller bruger-definerede. På det bruger-orienterede niveau repræsenterer en<br />

sekvens mange og eventuelt blandede forekomster af typer, som i bibliotekets specialiserede lag<br />

bliver identificeret og konverteret. iostream indeholder et omfattende sæt af operationer for<br />

manipulation af de forskellige fundamentale typer, og kan let udvides til at arbejde med<br />

brugerdefinerede typer ved simple overstyringer. Biblioteket er således en modulær og konsistent<br />

grænseflade til maskinens hardware-arkitektur. En strøm kan være til læsning, til skrivning eller<br />

til begge dele. Klassen istream (input-stream) læser tegn og konverterer til type-forekomster<br />

mens klassen ostream (output-stream) konverterer type-forekomster til tegnsekvenser. En<br />

hybrid mellem dem, iostream, er bidirektionel og kan konvertere begge veje. iostream er et<br />

fuldt klassehierarki, som består af relaterede klasser med forskellig funktionalitet. På toppen<br />

findes klassen ios, som indeholder de basale hardware-konstanter og metoder til<br />

maskinspecifik læsning og skrivning. Fra ios arves klasserne istream og ostream, som<br />

udbygger med en brugergrænseflade til henholdsvis input og output, og dermed introducerer<br />

abstraktionen "en strøm". Klassen iostream er multipelt arvet fra istream og ostream.<br />

Udover disse klasser, som erklæres i header-filen , findes fil-relaterede<br />

strømme i og streng-relaterede strømme i . I figur 6-1 ses<br />

klassehierarkiet for standardbiblioteket i C++. Fra klientens synspunkt er en strøm definitionen<br />

på en kilde hvor data af vilkårlige typer kan ifyldes eller hentes fra. Standardbiblioteket erklærer<br />

fire forekomster, som udgør standard input, standard output, standard fejl-output og en bufferet<br />

version af sidstnævnte:<br />

istream cin;<br />

ostream cout, cerr, clog;<br />

I konventionelle UNIX-lignende miljøer er cin normalt bundet til tastaturet mens de tre<br />

andre er bundet til skærmen. Derudover er det oftest muligt at omdirrigere destinationerne til<br />

andre enheder, for eksempel filer eller serielle porte. Specialiseringer af strøm-klasserne kan også<br />

skrives, så de kommunikerer med specifikke dele af et bestemt system, for eksempel beskeder<br />

mellem processer i et operativsystem.<br />

6.1 <strong>Introduktion</strong> 455


Figur 6-1: Standardbibliotekets klassehierarki.<br />

Klassenavn Headerfil Funktion<br />

ios<br />

streambuf<br />

istream<br />

ostream<br />

iostream<br />

fstreambase<br />

filebuf<br />

ifstream<br />

ofstream<br />

fstream<br />

<br />

<br />

<br />

<br />

<br />

<br />

<br />

<br />

<br />

<br />

I/O-operationer<br />

Buffer<br />

Input-strøm<br />

Output-strøm<br />

I/O-strøm<br />

Fil-operationer<br />

Diskbuffer<br />

Fil-input<br />

Fil-output<br />

Fil-I/O<br />

456 <strong>Introduktion</strong> 6.1


strstreambase<br />

strstreambuf<br />

istrstream<br />

ostrstream<br />

strstream<br />

6.1.1 Klasser og objekter i biblioteket<br />

<br />

<br />

<br />

<br />

<br />

Tabel 6-1: Klasser i standardbiblioteket.<br />

Streng-strøm<br />

Tekstbuffer<br />

Streng-input<br />

Streng-output<br />

Streng-I/O<br />

Standardbibliotekets klasser ses i tabel 6-1. Klasserne i bruges til standard-<br />

I/O, dvs. input fra tastaturet og output til skærmen. De tilsvarende klasser i har<br />

med læsning og skrivning af filer at gøre, mens læasserne i er helliget<br />

formatering af tekst i lageret uden relation til en ydre enhed. Forekomster i tabel 6-2 er erklæret i<br />

biblioteket og kan ses af klienten.<br />

Objektnavn Type<br />

cin<br />

cout<br />

cerr<br />

clog<br />

streampos<br />

streamoff<br />

istream<br />

ostream<br />

ostream<br />

ostream<br />

typedef<br />

typedef<br />

Tabel 6-2: Objekter i standardbiblioteket.<br />

Derudover indeholder biblioteket et stort antal interne variable og objekter, som er indkapslet i<br />

klasserne og kun kan ses af medlemsfunktionerne. Klassernes medlemsfunktioner bruges til at<br />

arbejde på strømme af forskellige arter, og brugen af dem afviger normalt kun i form af<br />

forskellige strømnavne og datatyper, der skal ud- eller indlæses.<br />

Der bruges overstyring af to bestemte operatorer i klasserne istream og ostream, nemlig<br />

de binære skifteoperatorer > (outputoperator). Denne operator er valgt,<br />

fordi den passer godt til abstraktionen "at hente data" og "at sende data" til en strøm. På den<br />

måde kan man ved operatorens orientering (højre eller venstre) se, hvor data kommer fra (strøm<br />

eller data) og hvor de går til. "Pilen peger" i den retning, data bevæger sig:<br />

str >> x; // læs objektet "x" fra strømmen "str"<br />

str


semantisk sammenhæng. Det kan hjælpe en smule at tænke på, at sætningerne, som beskrevet<br />

ovenfor, er ækvivalente til funktionskaldene<br />

str.operator>> (x);<br />

str.operator> x;<br />

ostream& d = str


6.2 STANDARD I/O<br />

Standard-I/O er betegnelsen for standardiserede metoder for udførelse af tegnbaseret input og<br />

output. Standardiseringen generaliserer over I/O på terminal (konsol), disk, printer- og<br />

kommunikationsporte, interproceskommunikation osv. Dette afsnit beskriver standardklasserne<br />

ostream og istream, hvordan de anvendes, hvilke metoder de indeholder og hvordan de<br />

relateres til hinanden.<br />

6.2.1 Output<br />

Den oftest benyttede outputmetode er en anvendelse af outputoperatoren på standardstrømmen<br />

cout. For eksempel vil<br />

#include <br />

void main () {<br />

cout


Output:<br />

Skov<br />

double n = 10;<br />

cout


cout.put ('S').write ("kovtur", 3).put ('\n');<br />

med det samme resultat. Der er ikke meget at gå fejl af i brugen af outputstrømme. Følgende<br />

eksempel viser imidlertid to hyppige fejltagelser.<br />

#include <br />

void main () {<br />

char s [] = "En streng";<br />

cout


emærke, at det kun er objektindhold, som læses ind, ikke selve objekterne, som skal erklæres og<br />

allokeres først. Derudover skal det bemærkes, at indlæsning altid er mere kompliceret end<br />

udlæsning, netop fordi man ikke er bekendt med udseendet af de data, der kommer ind.<br />

#include <br />

void main () {<br />

int x, y;<br />

cout > x >> y;<br />

cout "Adderet giver de "


#include <br />

void main () {<br />

char i, j;<br />

cin.get (i).get (j);<br />

}<br />

get() returnerer det aktuelle tegn som en heltalsværdi. Denne værdi er en int, så klienten<br />

kan se, om der er indlæst en valid værdi eller en fejl, for eksempel EOF. EOF er en konstant<br />

defineret i iostream.h, og er normalt -1, og betyder, at slutningen på inputstrømmen er<br />

nået. For at kunne skelne mellem en returneret værdi i char-intervallet og en fejlværdi, er den<br />

returnerede type således større end en char. En typisk anvendelse af dette er<br />

#include <br />

void main () {<br />

for (int c = cin.get (); c != EOF; c = cin.get ())<br />

cout.put (c);<br />

}<br />

Hvis dette program præsenteres for inputstrømmen<br />

D I K U<br />

vil løkken gennemløbes 7 gange, både for de alfabetiske tegn og for mellemrummene mellem<br />

dem.<br />

Medlemsfunktionen read() er modstykket til ostreams write(). read() læser et<br />

antal tegn ind i en buffer, hvor antallet og bufferens adresse er givet som parametre, og returnerer<br />

i øvrigt den aktuelle strøm:<br />

char Buffer [0x80];<br />

cin.read (Buffer, 0x80); // læs 128 tegn<br />

En lignende funktion er readline(), som ligesom read() læser et bestemt antal tegn<br />

ind i en buffer, men har den udvidelse, at et bestemt tegn i inputstrømmen også kan slutte den<br />

aktuelle læsning. Det kan bruges til at læse poster eller andre formaterede data af forskellige<br />

længder fra en strøm og samtidig undgå overløb. readline() har samme parameterliste som<br />

read() plus en underforstået tredie parameter, som er terminator-tegnet for indlæsningen. Hvis<br />

den ikke specificeres, er den underforståede værdi '\n'.<br />

getline() og read() bruges ofte sammen med en anden medlemsfunktion,<br />

gcount(), som returnerer antallet af læste tegn. Det er anvendeligt i forbindelse med<br />

gennemløb af den buffer, der er indlæst. En istream har også en underforstået værdi, som<br />

fortæller, om der er en fejl på strømmen. Med denne standardkonvertering og med for eksempel<br />

6.2.2 Input 463


ead() og gcount() kan en enkel bufferet søgerutine let implementeres:<br />

// eksempel 6-3<br />

// søgning i et istream-objekt<br />

#include <br />

#include <br />

typedef int bool;<br />

bool Search (istream& strm, char* searchString) {<br />

const Size = 0x80;<br />

char Buffer [Size];<br />

while (strm.read (Buffer, Size))<br />

if (!strncmp (Buffer, searchString, strm.gcount ())<br />

return 1;<br />

return 0;<br />

}<br />

Tre andre medlemsfunktioner i istream er væsentlige for arbejdet med at læse fra en strøm.<br />

putback() indsætter et tegn i starten af bufferen, som bliver det næste tegn, der indæses igen.<br />

peek() henter det næste tegn, ligesom get(), men lader det stå i bufferen, så det bliver det<br />

næste, der læses. Funktionen ignore() smider et antal tegn væk i strømmen, eller op til og<br />

med den første forekomst af et bestemt tegn. En anvendelse af disse tre medlemsfunktioner kan<br />

ses i følgende eksempel, som fjerner C++-kommentarer fra kildetekster:<br />

// eksempel 6-4<br />

// filtrering af kommentarer fra kildetekst<br />

#include <br />

void main () {<br />

int i, Size = 0x80;<br />

char Buffer [Size];<br />

while (cin.get (i))<br />

if (i == '/' && cin.peek () == '/') // kommentar?<br />

cin.ignore (Size, '\n'); // ja, fjern den<br />

else {<br />

cin.getline (Buffer, Size, '\n');<br />

cout


6.3 FORMATERING<br />

Både istream og ostream er afledt fra klassen ios, som arbejder generelt med I/O, og<br />

som formaterer tegn til specifikke formål. Begge klasser arver således et antal formateringsmetoder,<br />

som kan bruges til specialisering af strømklassernes opførsel. I eksempler som det<br />

ovenfor har vi ofte brug for at kunne kontrollere måden, hvorpå data formateres under ind- og<br />

udlæsning, for eksempel det anvendte talsystem, justering på skærmen og bredden af bufferen i<br />

strømmen.<br />

I alle stømobjekter findes et antal flag, der bestemmer, hvordan objekter konverteres til tegn<br />

når de ind- og udlæses. Disse flag er erklæret i ios-klassen, og har følgende betydninger, hvis<br />

de er sat:<br />

• ios::skipws vil hoppe over mellemrumstegn etc. under indlæsning fra en strøm med<br />

inputoperatoren. Ellers medtages mellemrummstegnene. Flaget er normalt sat.<br />

• ios::left, ios::right og bestemmer justeringen af udskrevne objekter. Kun ét<br />

af disse flag kan sættes på samme tid. Hvis left er sat, vil teksten venstrejusteres i en<br />

buffer, hvis længde er sat af width()-metoden og hvis tomme tegn er bestemt af<br />

fill()-metoden. Er right sat, vil teksten justeres op ad højremargen af bufferen.<br />

right er normalt sat.<br />

• ios::dec, ios::oct, ios::hex bestemmer det talsystem, som numeriske<br />

objekter udskrives eller indlæses i. Kun ét kan være sat ad gangen. Sættes for eksempel<br />

hex-flaget, vil alle heltalsvariable forventes i sekstentalssystemet. dec er normalt sat.<br />

• ios::showbase vil automatisk indsætte en indikator på det aktuelle talsystem før<br />

numeriske data indsættes i eller udskrives fra en strøm. For eksempel, hvis ios::hex<br />

er sat, vil alle tal, der sendes til cout automatisk efterfølge 0x.<br />

• ios::showpos vil automatisk indsætte et plustegn (+) før alle positive tal, ganske som<br />

negative tal har et minus-præfix.<br />

• ios::showpoint vil sørge for, at alle kommatal udskrives med et tvungent komma,<br />

hvad enten tallet er helt eller ej. Det har kun betydning for float og double, og er<br />

normalt ikke sat.<br />

• ios::uppercase vil sørge for, at alle alfabatiske indsættelser i strømmene bliver<br />

konverteret til versaler. For eksempel vil en hexadecimal notation hedde 0X.<br />

• ios::scientific og ios::fixed styrer formatet på kommatal. Hvis<br />

scientific er sat, vil alle kommatal udskrives og indlæses i formatet 1.2345E2,<br />

mens fixed vil udskrive det samme tal som 123.45. Normalt er ingen af flagene sat,<br />

og formatet vælges afhængig af tallets størrelse: hvis eksponenten er mindre end -4 eller<br />

6.2.2 Input 465


større end precision, bruges scientific, ellers bruges fixed.<br />

• ios::unitbuf vil automatisk tømme bufferen efter hver operation, det samme som<br />

nonbufferet I/O. Flaget er normalt ikke sat.<br />

Disse flag gemmes i enhver strøm i en bitvektor (en long), der kan sættes og nulstilles af<br />

klienten. Følgende medemsfunktioner arbejder på alle flagene på én gang:<br />

• flags() returnerer de aktuelle flag i strømmen. Funktionen bruges mest til at gemme<br />

den aktuelle status af flagene.<br />

• flags(long) sætter alle flagene, og returnerer den hidtidige status i strømmen.<br />

Funktionens anvendes hovedsageligt til at genskabe en status af alle flagene i en strøm.<br />

Udover disse to findes to flag-specifikke metoder, som sætter og nulstiller enkelte flag uden at<br />

have effekt på de andre. setf() sætter et bestemt flag i strømmen, mens unsetf() slår<br />

flaget fra. setf() og unsetf() kan kaldes med alle ovenstående flag-værdier, og vil<br />

returnere den aktuelle værdi før kaldet. Følgende eksempel viser de forskellige flags betydning<br />

samt brugen af de fire metoder:<br />

flag<br />

// eksempel 6-5<br />

// brugerdefinerede formateringer<br />

#include <br />

void main () {<br />

int i = 12345;<br />

double pi = 3.1415927;<br />

long origFlags = cout.flags (); // gem strømmens status<br />

cout.setf ( // sæt følgende flag:<br />

ios::hex | // hexadecimale heltal<br />

ios::showbase | // vis talsystemet<br />

ios::scientific | // potensnotation<br />

ios::uppercase // store bogstaver<br />

);<br />

cout


}<br />

Programmet skriver følgende output ved kørsel:<br />

i == 0x3039 pi == 3.141593e+00;<br />

i == 12345 pi == 3.141593<br />

Udover de enkelte flag findes et antal andre indkapslede værdier i strømmene, som beskriver<br />

forhold, der ikke kan udtrykkes som et binært valg. Disse værdier sættes til en bestemt skalær<br />

værdi, der har en bestemt betydning for formateringen. Hver værdi har en associeret<br />

medlemsfunktion, der både læser og skriver den aktuelle værdi. Disse funktioner er<br />

• width() sætter strømbufferens størrelse til et bestemt antal tegn, hvorefter strømmens<br />

indhold vil blive udlæst. Normalt er denne værdi 0, hvorved bufferen først udlæses, når<br />

den bliver tømt explicit. Ved at sætte width til en anden værdi, vil det være muligt at<br />

få en strøm til at være bekendt med størrelsen på en destinations-streng, der er allokeret<br />

af klienten, for at undgå overløb. For eksempel,<br />

#include <br />

void main () {<br />

char buffer [5];<br />

cin.width (sizeof (buffer));<br />

while (cin >> buffer) cout


Output:<br />

void main () {<br />

cout.fill ('0');<br />

cout.width (4);<br />

cout


er beskrevet i de forrige afsnit, er meget proceduralt orienterede og har en lidt triviel syntaks.<br />

Derfor findes et alternativ, nemlig brugen af særlige manipulatorer. En manipulator er en global<br />

funktion, der kan bruges som argument efter en cout-sætning eller et andet objekt af typen<br />

ostream. En manipulator er således mulig at kalde i samme sætning som egentlige<br />

udlæsninger af objekter. For eksempel,<br />

// eksempel 6-6<br />

// anvendelse af manipulatorer<br />

#include <br />

void main () {<br />

int i = 11;<br />

cout


• flush() vil tømme strømmens buffer og udlæse alle data med det samme.<br />

• setfill(c) vil sætte det aktuelle filler-tegn til t, hvilket svarer til et kald til<br />

strømklassens fill()-medlemsfunktion.<br />

• setw(n) vil sætte strømmens bufferbredde til n, hvilket svarer til at kalde<br />

strømklassens width()-medlemsfunktion.<br />

• setprecision(n) sætter præcisionen i udlæsninger af kommatal til n, hvilket er det<br />

samme som et kald til strømklassens precision()-medlemsfunktion.<br />

• setiosflags(n) sætter flagene i den pågældende strøm til n OR den aktuelle<br />

værdi, ækvivalent til et kald til klassens setf()-medlemsfunktion.<br />

• resetiosflags(n) nulstiller flagene i den pågældende strøm til n NAND den<br />

aktuelle værdi, ækvivalent til et kald til klassens unsetf()-medlemsfunktion.<br />

Manupulatorer gør ganske simpelt syntaksen bedre og kildeteksten mere koncis. I afsnit 6.6.2<br />

vises, hvordan manipulatorer kan brugerdefineres til specifikke formål og integreres i<br />

standardbiblioteket.<br />

6.3.2 Formatering uden I/O<br />

Objekter kan konverteres til tegn og omvendt uden at skulle skrives til eller læses fra en konkret<br />

I/O-strøm. En særlig strøm for in-core formatering, altså formatering direkte i lageret, varetager<br />

denne opgave. Klassen strstream (strengstrøm) er afledt fra iostream, men benytter en<br />

anden buffer, som både læser fra og skriver til lageret. Som for standard-I/O findes tre klasser.<br />

Konvertering af tegn fra en streng til et objekt varetages af klassen istrstream, konvertering<br />

den anden vej foretages med klassen ostrstream og begge veje klares af klassen<br />

strstream. Der er ingen instantieringer af disse klasser i standardbiblioteket, så klienten må<br />

selv erklære et objekt af den ønskede type. Klasserne er erklæret i headerfilen<br />

, og skal inkluderes af alle programmer, som bruger dem.<br />

Alle medlemsfunktioner, manipulatorer og overstyrede operatorer, der kan bruges på en<br />

iostream kan også bruges på en strstream. Her følger et eksempel på brugen af<br />

ostrstream:<br />

// eksempel 6-7<br />

// in-core formatering<br />

#include <br />

#include <br />

470 Formatering 6.3


void main () {<br />

ostrstream sstrm;<br />

sstrm.setf (ios::fixed | ios::showpos | ios::showpoint);<br />

sstrm


Output:<br />

cout > a >> b >> c;<br />

cout


float VersionCode;<br />

istrstream (Version, sizeof (Version)) >> VersionCode;<br />

// ...<br />

}<br />

Klassen strstream har både input- og outputfaciliteter, og har primær anvendelse som en<br />

ganske normal FIFO-buffer eller kø, med den udvidede mulighed for konvertering af objekter.<br />

6.4 STRØMSTATUS OG FEJLBEHANDLING<br />

Der kan opstå en fejl i en strøm, for eksempel hvis en outputstrøm ikke kan skrive til den<br />

associerede buffer, eller hvis en inputstrøm ikke kan konvertere bufferens aktuelle inhold til den<br />

ønskede type eller hvis bufferen er løbet tom. Enhver iostream kan implicit konverteres til en<br />

sanhedsværdi, ved brug af konverteringsmetoden til en void* og operator-overstyringen af !.<br />

Disse to konverteringer fortæller, om strømmens status er "god" eller "dårlig". Hvis en<br />

outputstrømstatus er dårlig, betyder det oftest, at der er problemer med en ekstern enhed - for<br />

eksempel en printer. Fejl på en inputstrøm er normalt resultatet af slutningen på data fra<br />

strømmen.<br />

For eksempel,<br />

#include <br />

void main () {<br />

if (cin) cout Buffer) wordCnt++; // indtil cin er<br />

"dårlig"<br />

cout


Det er muligt at få bedre besked om en eventuel fejl i en strøm. Enhver strøm har fem flag, der<br />

fortæller mere detaljeret om strømmens status. Disse flags navne og betydninger er:<br />

• ios::goodbit er sat, hvis strømmen ikke har fejl af nogen art.<br />

• ios::eofbit er sat, hvis en inputstrøm er tom og indikerer altså, at der ikke er flere<br />

tegn at læse. Det er altså mere end status end en fejl.<br />

• ios::failbit er sat, hvis den sidste indsættelse i en ostream eller sidste<br />

udlæsning fra en istream slog fejl, af årsager som beskrevet i starten af dette afsnit.<br />

• ios::badbit er sat, hvis en ind- eller udlæsning i en strøm ikke var lovlig. Denne fejl<br />

opstår for eksempel, hvis en ostrstream's buffer er fjernet med et kald til str()funktionen<br />

(se afsnit 6.3.2). Det er altså en reel indikation af en programmeringsfejl, og<br />

ikke en uventet ekstern hændelse.<br />

• ios::hardfail er sat, hvis en seriøs fejl er opstået i en strøm. Generelt er den<br />

forårsaget af en ekstern enhed, der slet ikke fungerer som den skal. Fejl af denne type er<br />

det normalt ikke muligt at gøre noget ved, og bør afstedkomme en afslutning af<br />

programmet med en fejlmeddelelse til brugeren.<br />

Disse flag læses af et antal funktioner, som associerer de relevante flag:<br />

• good() returnerer 1, hvis der ingen fejl er i strømmen og et 0, hvis en af ovenstående<br />

fejl er opstået. Værdien er den samme, som returneres implicit ved den underforståede<br />

konvertering til en void*.<br />

• bad() returnerer 0, hvis enten ios::badbit eller ios::hardfail er sat, ellers<br />

returneres 1. Værdien er den samme, som returneres implicit via den overstyrede<br />

operatorfunktion for !.<br />

• eof() returnerer 1, hvis flaget ios::eofbit er sat, ellers 0, og bruges til at teste<br />

for slutningen på en inputstrøm.<br />

• fail() returnerer 1, hvis enten ios::failbit, ios::hardfail eller<br />

ios::badbit er sat, ellers 0. Den anvendes især som modsætning til eof() for at<br />

se, om en opstået fejl kan rettes i programmet eller ej.<br />

• rdstate() returnerer alle flagene i en int.<br />

Flagene kan også skrives, hvis en klient har opdaget en fejl i en strøm, eller hvis en<br />

brugerdefineret datatype eller manipulator (se afsnit 6.6) ikke kan fortolke strømmens buffer<br />

ordentligt. Når en fejl explicit skrives i en strøm, vil senere brug af strømmen automatisk<br />

474 Strømstatus og fejlbehandling 6.4


esultere i den pågældende fejl. En fejl kan kun rettes op ved at nulstille det pågældende flag, der<br />

beskriver fejlen. Skrivefunktionerne er:<br />

• clear() vil nulstille alle flag i strømmen. Et valgfrit, underforstået parameter vil<br />

maske de bits fra, der ikke ønskes nulstillet.<br />

• setstate(n) vil sætte bitvektoren til den værdi, der angives som parameter. Denne<br />

funktion er protected, og kan kun kaldes af afledte klasser og medlemsfunktioner,<br />

inklusive brugerdefinerede operatorfunktioner og manipulatorer.<br />

Her er en simpel anvendelse af disse funktioner i et program, der tester inputstrømmens status:<br />

// eksempel 6-10<br />

// anvendelse af strømmenes statusflag<br />

#include <br />

void main () {<br />

char c;<br />

while (cin.get (c)) cout.put (c);<br />

if (cin.fail ())<br />

cerr


ind til et eksisterende file-handle<br />

char Buffer [100];<br />

ofstream testData3 (4, Buffer, sizeof (Buffer)); // printer<br />

// bind til et eksisterende file-handle, uden buffering<br />

ofstream testData4 (4, 0L, 0);<br />

// blot opret en filstrøm<br />

ofstream testData;<br />

Disse forskellige konstruktørkald tillader en filstrøm at blive åbnet enten ved specifikation af et<br />

filnavn plus eventuel modus (ios::out eller ios::app), en præallokeret buffer og dens<br />

længde, uden buffer eller blot som en filstrøm, der ikke er tøjret til en bestemt fil. Efter<br />

konstruktion af et filstrømobjekt kan det stort set bruges på samme måde som en iostream,<br />

idet objekter kan indlæses fra og udskrives til filen uden bekymring typen. For eksempel:<br />

#include <br />

#include <br />

void main () {<br />

ostream testData ("test.dat");<br />

testData


eksempel 6-11<br />

// læsning og skrivning af filer med fstream<br />

#include <br />

#include <br />

int main () {<br />

ofstream tabelFil ("output.dat");<br />

if (!tabelFil) {<br />

cerr


#include <br />

#include <br />

int main (int argc, char** argv) {<br />

if (argc != 2) return -1;<br />

char Buffer [100];<br />

ifstream nextWord (argv [1]);<br />

if (!nextWord) return -1;<br />

ifstream > Buffer) count++;<br />

cout


#include <br />

#include <br />

const fileCnt = 3;<br />

char* Filename [fileCnt] = {<br />

"data.hex", "data.oct", "data.dec"<br />

};<br />

void main () {<br />

ofstream outputFile;<br />

for (int i = 0; i < fileCnt; i++) {<br />

outputFile.open (Filename [i], ios::app);<br />

outputFile


• ios::noreplace betyder, at filen ikke må eksistere i forvejen. Hvis den gør, sættes<br />

strømmens badbit.<br />

• ios::text åbner filen i tekst-modus, dvs. tegn fortolkes under konverteringen, så<br />

sekvenser med \n\r eller \r\n oversættes til \n under indlæsning og omvendt<br />

under udlæsning. Dette er den underforståede fortolkning af strømme, og er i konstrast til<br />

ios::binary.<br />

• ios::binary åbner filen i binær modus, så alle tegn, der læses eller skrives<br />

fuldstændig reflekteres i strømmen. En \n\r-sekvens læses altså som to tegn. Det er<br />

nødvendigt at specificere ios::binary, hvis der skal arbejdes på binære data, da<br />

flaget har betydning for alle metoder, også put() og get().<br />

For eksempel, hvis en fil skal åbnes for output, men hvis programmet skal konfirmere en<br />

eventuel overskrivning, kan følgende flag benyttes:<br />

char Bekraeft;<br />

ofstream outputFil ("output.dat", ios::out | ios::noreplace);<br />

if (!outputFile) {<br />

cout > Bekraeft;<br />

if (Bekraeft == 'J' || Bekraeft == 'j')<br />

outputFil.open ("output.dat", ios::out | ios::trunc);<br />

}<br />

Undertiden er der brug for at specificere den position, hvor data læses fra en fil. Denne position<br />

beskrives med en long, hvis værdi er den byte-position, data befinder sig på. På næsten alle<br />

systemer er spændvidden på en long 32 bits, altså et godt stykke over fire gigabytes lineært<br />

adresserum. Vi kan spørge en strøm, hvor dens datapointer befinder sig relativt til starten af filen<br />

med<br />

long position = inputFil.tellg();<br />

og sætte den med<br />

inputFil.seekg (n);<br />

hvor n er den ønskede position. En valgfri parameter i seekg() bestemmer relativiteten i<br />

forhold til den ønskede slutposition: ios::beg, den underforståede værdi, søger fra starten af<br />

strømmen, ios::end søger fra slutningen og ios::cur fra den aktuelle position. Hvis den<br />

absolutte destination bliver en værdi mindre end 0 eller større end filens længde, sættes flaget<br />

eofbit i strømmen.<br />

For eksempel,<br />

480 Filer 6.5


#include <br />

#include <br />

void main () {<br />

istream inputFil ("input.dat");<br />

char Buffer [100];<br />

for (i = 0; i < 4; i++) {<br />

inputFil >> Buffer;<br />

cout


Som beskrevet i afsnit 6.1.1 findes kun overstyringer af ind- og udlæsningsoperatorerne for de<br />

fundamentale typer. Der er ingen indbygget mulighed for at udlæse en brugerdefineret type. Af<br />

den grund vil kode som<br />

class Complex;<br />

Complex c;<br />

cout > cplx.re >> c >> cplx.im >> c;<br />

// ... test for korrekt indlæsning herefter ...<br />

}<br />

De to funktioner, som i øvrigt fordi de rører ved de private data i Complex-objektet skal<br />

erklæres som friend-funktioner i Complex, vil blive kaldt, hvis klienten skriver kode som<br />

Complex c (5, 7);<br />

cout


Det er også muligt for en klient at skrive sin egen manipulator ved en simpel funktionserklæring<br />

af typen<br />

ostream& manipulator (ostream& strm) {<br />

// ...<br />

return strm;<br />

}<br />

Dermed kan et programmør- eller applikationsspecifikt sæt af manipulatorer defineres og bruges<br />

i strømudlæsninger for at gøre kildeteksten koncis. Følgende er et eksempel på en manipulator,<br />

der udlæser en sekvens for at slette skærmen på en ANSI-terminal:<br />

#define Escape = char(0x1B);<br />

ostream& cls (ostream& strm) {<br />

return o


Herefter kan klienten kalde manipulatoren colour efter de normale regler, blot med et<br />

parameter i parantes:<br />

cout


6.7 OPGAVER TIL KAPITEL 6<br />

1. Erklær en type Person med navn og alder, og overstyr operatorerne > for<br />

istream og ostream, så objekter af klassen kan ind- og udlæses på normal vis.<br />

2. Skriv en klasse DiskFile, som tillader læsning fra vilkårlige steder. Brug eventuelt<br />

indekserings-operatoren til formålet.<br />

3. Udvid DiskFile fra opgave 6-2, så den indekserer på Person-objekterne fra<br />

opgave 6-1 i stedet for på byteværdier.<br />

4. Ved hjælp af arv og polymorfi, skriv klassen fra opgave 6-3 om, så den kan indeksere<br />

vilkårlige objekter i filen.<br />

5. Skriv manipulatorer til (a) at kopiere en fil til strømmen, (b) at slette skærmen, (c) at<br />

skrive med kursiv skrift på printeren, (d) at forvente et "ja/nej" svar, (e) at indsætte tid og<br />

klokkeslet.<br />

6. Nedarv en klasse ww_ostream fra ostream, og implementér den virtuelle metode<br />

operator


Klassebiblioteket XCL<br />

På de følgende sider vil et komplet klassebibliotek blive præsenteret, med<br />

forskellige faciliteter for kombinationer af objekter, integration med klassiske<br />

strukturer og algoritmer, integration med standardbiblioteket, simpel bestandighed<br />

og naturligvis som fundament for objekt-orienteret udvikling. Biblioteket er et<br />

eksempel på design af biblioteker, og kan i vid udstrækning bruges til at lære at<br />

forstå de forskellige fordele og ulemper ved programkonstruktion af denne art.<br />

7.1 INTRODUKTION<br />

Klassebiblioteket XCL, Example Class Library, er en samling af cirka 20 klasser til generel<br />

databehandling. Der findes klasser til listebehandling, køer, stakke og kataloger med<br />

associationer mellem data. Der er både dynamiske og lineære lister, sorterede og tilfældigt<br />

arrangerede, indeksérbare og associative. Der er strenge og numeriske datatyper samt værktøj til<br />

integration af forskellige typer af objekter i containerklasser. Biblioteket gør naturligvis stor brug<br />

af polymorfi og arv, og indkapsler omhyggeligt de respektive implementationer i klasserne. Det<br />

kan bruges til enhver opgave, der indeholder trivielle administrative behandlinger og kan<br />

naturligvis udbygges efter behov. Da første udgave af denne bog udkom, var skabeloner og<br />

parametiserede typer endnu ikke en del af sproget, hvorfor template'r ikke tog del i XCL. I<br />

denne udgave beskrives en ny version af XCL, version 3.0, som benytter sig af skabelon-klasser<br />

som fundament for containerklasser.<br />

Vi har i de forrige kapitler allerede stiftet bekendtskab med flere af de containerklasser og<br />

aktive klasser, som indgår i XCL. Forskellen er blot, at de her alle er relateret til hinanden, og<br />

alle har en fælles grænseflade gennem almene baseklasser. Det er altså en god lektion og<br />

indføring i integration af datatyper, som allerede er skrevet eller defineret, men som skal indgå i<br />

en større sammenhæng. XCL er implementeret som ét hierarki, og alle klasser er på en eller<br />

anden måde subtyper af en og samme baseklasse.<br />

Som det kan ses i figur 7-1, er hierarkiet over klasserne i XCL opdelt i et par forgreninger, som<br />

kan benævnes containerklasser, sortérbare klasser og private værktøjsklasser. Der er kun fire<br />

abstrakte klasser, resten er aktive og umiddelbart brugbare.<br />

486 Opgaver til <strong>kapitel</strong> 7 6.7<br />

7


7.1.1 Funktionalitet i XCL<br />

Figur 7-1: Hierarkisk fordeling af klasserne i XCL.<br />

Klassebiblioteket er holdt lille, så det er overskueligt, men dog anvendeligt. Der er lagt stor vægt<br />

på, at de forskellige typer skal kunne blandes sammen af klienten, som skal have mulighed for at<br />

arbejde med polymorfi uden de store problemer, kunne arbejde med vektorer og lister som<br />

fundamentale datatyper (i tildelinger, additioner osv.) samt at kunne ind- og udlæse<br />

komplicerede datastrukturer til disk i et maskinuafhængigt, implementationsuafhængigt format.<br />

Til beskrivelsen af hver klasse følger også et eller flere eksempler på brugen af klassen. Som<br />

flere nye klasser introduceres, bliver deres sammenhæng åbenbar.<br />

7.1.2 Opbygning af klassebiblioteket<br />

XCLs abstrakte "superklasse", som er base for alle klasser i biblioteket, indeholder et antal rene<br />

virtuelle funktioner, som er implementeret i de aktive klasser længere nede. Disse funktioner gør<br />

det muligt at<br />

• Sammenligne to objekter,<br />

• Udskrive et objektindhold på en læselig måde,<br />

6.6.2 Brugerdefinerede manipulatorer 487


• Udlæse et objekt med typeinformation på en strøm,<br />

• Indlæse et objekt fra en strøm,<br />

• Kopiere et objekt,<br />

• Finde typen på et objekt og<br />

• Finde navnet på et objekt.<br />

Disse operationer har alle XCL-objekter til fælles. For containerklasser i XCL gælder, at de har<br />

en udvidet polymorf grænseflade, idet deres virtuelle funktioner muliggør at<br />

• Indsætte et objekt i en container,<br />

• Udtage et objekt fra en container,<br />

• Undersøge (se på) et objekt i en container og<br />

• Sammenlægge to conatinere.<br />

ud over den øvrige funktionalitet. De forskellige typer af containerklasser er skrevet, så de<br />

lægger et fundament for videre udvidelse. I toppen af hierarkiet findes en ikke-abstrakt<br />

baseklasse for alle containere, ContainerBase. Denne klasse bruges til at klistre<br />

parameteriserede og ikke-parameteriserede containerklasser sammen, så de har et minimum af<br />

dataelementer, de kan deles om. Klassen Container er arvet fra Object og definerer et<br />

stort antal rene virtuelle metoder og implementerer et par metoder, der er ens for alle containere.<br />

Der er desuden to parameteriserede container-baseklasser for henholdsvis hægtede lister og<br />

lineære lister, GenericList og GenericArray, som også arver fra ContainerBase<br />

og som instansieres under arv i de afledte klasser. Pointen er her, at de parameteriserede<br />

containerklasser skaber en frihed i instantieringer, fordi de ikke er polymorfisk afhængige af<br />

baseklasser med virtuelle metoder mens de polymorfe containerklasser kan blandes sammen af<br />

klienten og samtidig drage fordel af de parameteriserede klasser. Denne delegering af<br />

mekanikken bag containerne gør, at nye containere kan defineres med meget lidt besvær.<br />

Samtidig er alle instantieringer af de parameteriserede klasser gjort med samme type - en<br />

Object-pointer - så der er ikke tale om overforbrug af de parameteriserede klassers<br />

medlemsfunktioner.<br />

Containerklasser i XCL indeholder et begreb om placeringen de objekter, de besidder, og<br />

ladder denne skinne svagt igennem til klienten. Således kan en klient arbejde med "abstrakte<br />

pointere" til elementer i containerne på en meget transparent måde. Denne transparens er<br />

implementeret via en opfattelse af containerklassen som en række af objekter, som kan findes<br />

ved kald til medlemsfunktioner, der returnerer henholdsvis det første, næste og aktuelle element i<br />

containeren. På den måde kan alle containere gennemløbes og behandles i den kontekst, som den<br />

underliggende datastruktur repræsenterer.<br />

7.1.3 Hierarkiet og standardbiblioteket<br />

XCL arbejder sammen med standardbiblioteket iostream.h, altså version 2 af AT&Ts<br />

standardbibliotek, beskrevet i <strong>kapitel</strong> 6. Strømme bruges i alle ind- og udlæsningssituationer<br />

samt til at udskrive objekternes indhold. Af den grund rådes klienter af XCL også til at benytte<br />

488 <strong>Introduktion</strong> 7.1


strømbiblioteket. Der er absolut ingen maskin-, oversætter- eller miljøspecifikke detaljer i XCL,<br />

som vil kunne oversættes under alle systemer, der holder sig til C++ 3.0 eller det udkast, ANSI<br />

har lavet til sproget.<br />

I de følgende afsnit beskrives klassere én for én. I starten af hvert underafsnit opremses<br />

arvefølgen for den aktuelle klasse samt headerfilen, hvor klassens erklæring er foretaget. For<br />

eksempel,<br />

BaseKlasse <br />

AfledtKlasse <br />

betyder, at der kommer en forklaring af AfledtKlasse, som er arvet fra BaseKlasse<br />

som findes i de nævnte filer. Disse filer findes i deres helhed i appendiks F. I de tilfælde, hvor<br />

der anvendes multipel arv, gentages arvefølgerne for hver baseklasse, som i<br />

BaseKlasse1 <br />

AfledtKlasse <br />

BaseKlasse2 <br />

AfledtKlasse <br />

7.2 ABSTRAKTE KLASSER<br />

De abstrakte klasser i biblioteket er få, men vigtige. Den første klasse, Object er moderklasse<br />

til alle andre klasser i hierarkiet, og er grunden til, at de overhovede kan arbejde sammen. Den<br />

anden klasse, Container, er en generel grænseflade til en klasse, der indeholder objekter af<br />

andre klasser. Der er en polymorf container i dobbelt forstand, både fordi den tillader forskellige<br />

subtyper af Container at blive behandlet på samme måde og fordi en Container<br />

udelukkende arbejder på referencer til Object-objekter. Den tredie, Sortable, definerer en<br />

grænseflade for et sortérbart objekt og implementerer de fleste af metoderne for sammenligning<br />

af objekter på en større-end og mindre-end basis.<br />

Disse tre abstrakte klasser indeholder størstedelen af kompleksiteten i selve hierarkiet, fordi der<br />

generelt i XCL kun arbejdes på referencer eller pointere til disse klasser.<br />

7.2.1 Klassen Object<br />

Object <br />

Superklassen, som man kunne kalde den, er så abstrakt, som den kan blive: næsten udelukkende<br />

rene virtuelle funktioner og en beskyttet konstruktør. I Object erklæres alle de virtuelle<br />

metoder, der skal gælde for alle klasser i hierarkiet. Her er erklæringen på Object:<br />

class Object {<br />

7.1.3 Hierarkiet og standardbiblioteket 489


protected:<br />

Object () { };<br />

Object (const Object&) { };<br />

public:<br />

virtual ~Object () { };<br />

virtual void printOn (ostream& = cout) const = 0;<br />

virtual void readFrom (istream& = cin);<br />

virtual void dumpOn (ostream& = cout) const;<br />

virtual bool isEqual (const Object&) const = 0;<br />

virtual Class Type () const = 0;<br />

virtual Object& Copy () const = 0;<br />

virtual const char* Name () const = 0;<br />

};<br />

Klassen har ingen datamedlemmer, blot virtuelle funktioner. Det er altså en helt generel ramme<br />

for afledte klasser. De forskellige funktioner har meget præcist definerede og vigtige<br />

betydninger, og forklares én for én:<br />

Object () { };<br />

er den underforståede konstruktør, som kaldes for alle oprettelser af objekter uden samtidige<br />

initieringer af klasser i XCL. Den laver ikke meget, men er et udmærket sted at sætte breakpoints<br />

ind.<br />

Object (const Object&) { };<br />

er kopi-konstruktøren, som kopierer objekter. Det er vigtigt, at den afledte klasse, som<br />

indeholder en kopi-konstruktør kalder den tilsvarende kopi-konstruktør i sin(e) baseklasse(r) med<br />

det samme parameter. Parameteret vil automatisk blive konverteret.<br />

virtual ~Object () { };<br />

er den virtuelle destruktør. Den er virtuel, fordi der er andre virtuelle funktioner i Object, og<br />

for at undgå at klienten opretter et objekt af en afledt klasse og tildeler adressen herpå til en<br />

pointer til en baseklasse og derefter destruerer det, så den forkerte destruktør bliver kaldt (se<br />

afsnit 4.4.9).<br />

virtual void printOn (ostream& = cout) const = 0;<br />

er en udskrivningsmetode, som blot skriver objektets indhold pænt ud, helt i en sammenhæng<br />

som passer til objektets type. Med andre ord, udskriften må godt have et par ekstra mellemrum,<br />

linieskift eller andre fyldetegn, hvis det hjælper på forståelsen af objektets indhold. Et eksempel<br />

er, at et komplekst tal udskrives som (1,-1) uden at der er hverken paranteser eller komma i<br />

objektet.<br />

490 Abstrakte klasser 7.2


virtual void readFrom (istream& = cin);<br />

er en indlæsningsmetode, som læser et objekt ind fra en strøm. Strømmen skal være positioneret,<br />

således at objektets data står på den aktuelle plads og er klar til at blive indlæst i objektets data.<br />

Funktionen er ikke statisk, og arbejder altså på et allerede allokeret objekt, med den store fordel<br />

til følge, at den er virtuel. Det er altså muligt for en klient at indlæse indholdet af et objekt, for<br />

hvilken den egentlige type ikke er kendt. readFrom() arbejder sammen med dumpOn(),<br />

og disse to er de eneste normale metoder, der er implementeret for klassen Object.<br />

virtual void dumpOn (ostream& = cout) const;<br />

er en udlæsningsmetode, der er komplimentær til readFrom(). dumpOn() er<br />

implementeret for Object, og vil udlæse både objektets type og objektets indhold på<br />

strømmen. Grunden til, at typen er så vigtig at udlæse er, at readFrom()-funktionen (samt<br />

visse andre associerede funktioner i klassen ObjectManager) skal have en chance for at<br />

kunne identificere hvad de læser ind igen. Typen udlæses som en streng, navnet på objektets<br />

klasse, ved et kald til Name().<br />

virtual bool isEqual (const Object&) const = 0;<br />

er en sammenligningsoperation, der tillader to vilkårlige objekter at blive sammenlignet.<br />

Klienten bør dog ikke kalde isEqual() uden at være helt sikker på de egentlige typer, der<br />

arbejdes på, for de fleste implementationer af funktionen foretager en tvungen typekonvertering<br />

af parameteret. Der er erklæret en global sammenligningsoperator, som sikrer typeækvialens før<br />

den kalder isEqual().<br />

virtual Class Type () const = 0;<br />

returnerer objektets type. Class er en opremsning, hvori alle XCL-klasser erklæres som<br />

medlemsværdi, og således får en unik identifikation. Type() bruges blandt andet til<br />

sammenligning af objekter.<br />

virtual Object& Copy () const = 0;<br />

er en kopi-funktion, der allokerer en kopi af objektet og returnerer denne kopi til klienten som<br />

reference. Anvendelsen af denne funktion, som på en måde kan sammenlignes en kopikonstruktør,<br />

er at kunne kopiere en pointer til et polymorft objekt. Med kopi-konstruktøren, som<br />

ikke kan være virtuel, er det ikke muligt at skabe en kopi uden at kende til typen af objektet.<br />

Copy() kalder typisk objektets kopi-konstruktør for et nyallokeret objekt.<br />

virtual const char* Name () const = 0;<br />

returnerer blot objektets navn. Anvendelsen af denne funktion er primært henvendt til<br />

7.2.1 Klassen Object 491


administration af objekt-I/O (se afsnit 3.7.4 og 5.7), hvorved objektets typenavn bliver udlæst<br />

sammen med dets indhold. Grunden til, at et navn og ikke et nummer (for eksempel returværdien<br />

fra Type()) bruges er, at navnet er applikationsuafhængigt mens typenummeret kommer an på<br />

det aktuelle program.<br />

Med klassen Object som den gennemgående grænseflade er det muligt at skrive forskellige<br />

generelle funktioner og klasser. For eksempel sammenligningsoperatorer:<br />

bool operator== (const Object& o1, const Object& o2) {<br />

return o1.Type () == o2.Type () ? o1.isEqual (o2) : 0;<br />

}<br />

bool operator!= (const Object& o2, const Object& o2) {<br />

return ! (o1 == o2);<br />

}<br />

eller til integration med standardbibliotekets ind- og udlæsningsoperatorer, så disse gælder for<br />

alle objekter i XCL:<br />

ostream& operator> (istream& strm, const Object& o) {<br />

o.readFrom (strm);<br />

}<br />

Den næste abstrakte klasse, Container, benytter udelukkende pointere og referencer til<br />

Objecter i sin grænseflade. Denne lille klasse er således en fundamental platform for den<br />

videre udvikling af biblioteket.<br />

7.2.2 Klassen ContainerBase<br />

ContainerBase <br />

Denne klasse definerer minimums-fællestræk for alle containere i XCL. Den er abstrakt for<br />

klienter, fordi dens konstruktør er protected og bruges derfor kun som baseklasse fra bla.<br />

Container, GenericList og GenericArray. Klassens funktion er at holde rede på<br />

noget så fundamentalt som antallet af elementer i containeren samt det aktuelle element, som<br />

containeren på et givet tidspunkt arbejder på. Containere i XCL 3.0 har nemlig den facilitet, at de<br />

indeholder begrebet "aktuelt indholdt objekt", som gør det muligt for klienten at arbejde mere<br />

lineært på containere.<br />

Her er erklæringen af ContainerBase:<br />

492 Abstrakte klasser 7.2


class ContainerBase {<br />

protected:<br />

unsigned long elementCount; // størrelse på container<br />

unsigned long currentIndex; // aktuelt index i container<br />

ContainerBase (unsigned long size = 0)<br />

: elementCount (size), currentIndex (0) { }<br />

ContainerBase (const ContainerBase& other)<br />

: elementCount(other.elementCount), currentIndex(0) { }<br />

};<br />

Klassens to datamedlemmer, elementCount og currentIndex repræsenterer<br />

henholdsvis det aktuelle antal af elementer i containeren og indekset på det aktuelle. De er<br />

erklæret som unsigned long, og der kan således være et stort antal objekter i en container<br />

(typisk lidt over fire milliarder, mere præcist konstanten ULONG_MAX i ). De to<br />

konstruktører opretter henholdsvis en "tom" container og kopierer medlemmerne fra en anden<br />

container. Det er kun afledte klasser, der kan arbejde med ContainerBase, ikke klienter, og<br />

klassens funktion er da også at klistre specifikke containerklasser sammen længere nede i<br />

hierarkiet. De to datamedlemmer skal nemlig kunne bruges af søsterklasser, som begge er<br />

baseklasser til multipelt afledte containere, som for eksempel LinkedList.<br />

7.2.3 Klassen Container<br />

Object <br />

Container <br />

ContainerBase <br />

Container <br />

Den abstrakte containerklasse er baseklasse for alle containere i XCL-biblioteket. Container<br />

er multipelt afledt fra både Object og fra ContainerBase, og arver således en hel del rene<br />

virtuelle metoder fra Object samt to datamedlemmer fra ContainerBase.<br />

Her er erklæringen af Container (læg mærke til, at de rene virtuelle metoder, som den<br />

arver fra Object og ikke generklærer består som rene virtuelle metoder):<br />

class Container : public Object,<br />

public virtual ContainerBase {<br />

friend Iterator;<br />

protected:<br />

Container (unsigned long size = 0)<br />

: ContainerBase (size) { }<br />

Container (const Container& other)<br />

: ContainerBase (other), Object (other) { }<br />

public:<br />

7.2.2 Klassen ContainerBase 493


virtual ~Container () { }<br />

virtual void Insert (Object*) = 0;<br />

virtual Object* Retrieve () = 0;<br />

virtual unsigned Size () const;<br />

virtual Container& operator+= (Container&);<br />

virtual const Object& operator[]<br />

(const unsigned long) const = 0;<br />

virtual const Object* getFirst () const = 0;<br />

virtual const Object* getNext () const = 0;<br />

virtual const Object* getCurrent () const = 0;<br />

virtual void dumpOn (ostream& = cout) const;<br />

virtual void readFrom (istream& = cin);<br />

};<br />

Alle medlemsfunktioner, med undtagelse af de beskyttede konstruktører, er virtuelle. Der<br />

udvides således med 9 virtuelle funktioner ud over de 8, der arves fra Object, og der defineres<br />

ingen data. Her følger en forklaring på Containers medlemsfunktioner:<br />

Container (unsigned long size =0) : ContainerBase (size) { }<br />

opretter en "tom" container, uden indhold. Her kaldes Objects underforståede konstruktør<br />

automatisk og ContainerBases initierings-konstruktør eksplicit. Det er en protected<br />

konstruktør, fordi kun afledte klasser må specificere en størrelse ved konstruktionen.<br />

Container (const Container& other) :<br />

ContainerBase (other), Object (other) { }<br />

er kopi-konstruktøren, som initierer containeren med en anden container og kalder baseklassen<br />

Objects kopi-konstruktør.<br />

virtual ~Container () { }<br />

destruerer en container. Der er ingen data at destruere, fordi det eneste datamedlem er af typen<br />

auto og deallokeres af systemet. I de afledte klasser, som implementerer en egentlig container,<br />

skal destruktøren sørge for også eksplicit at deallokere eventuelle indeholdte objekter. Se afsnit<br />

5.6.1.<br />

virtual void Insert (Object*) = 0;<br />

indsætter data i containeren. Parameteren er objektet, der skal indsættes i containeren, en adresse<br />

på et Object. Placeringen af dette objekt i selve containeren kommer helt an på containerens<br />

natur - en kø, stak, mængde eller hvad det nu er. Den ikke-abstrakte, parametiserede klasse<br />

GenericList, som beskrives i afsnittet nedenfor om containerklasserne i XCL indeholder et<br />

"aktuelt element" efter hvilket Insert() vil indsætte det nye. Det væsentlige her er, at<br />

494 Abstrakte klasser 7.2


metoden overdrager et objekt fra klientens til containerens hænder.<br />

virtual Object* Retrieve () = 0;<br />

udtager et objekt fra containeren, og overgiver det til klienten. Det er den omvendte operation i<br />

forhold til Insert(). Placeringen af det objekt, der udtages afhænger igen af den egentlige<br />

afledte containerklasse. Når klienten kalder Retrieve() fjerner containeren objektet fra sig<br />

selv, og klienten får ansvaret for det.<br />

virtual const Object* getCurrent () const = 0;<br />

er en funktion, der finder det "første" element i containeren. Opfattelsen af dette første element<br />

afhænger af den afledte klasse, som implementerer funktionen. Det er et "kig" ind i en container<br />

uden at fjerne objektet fra containeren. Derfor returnerer funktionen en konstant objektreference,<br />

så klienten ikke kan manipulere med objektet. Hele funktionen er tillige konstant, og kan ikke<br />

ændre på de indeholdte data. Det sikrer konsistensen nedad i hierarkiet, og getCurrent()<br />

bliver en metode til blot at se ind i containeren uden hverken ansvar eller pligter.<br />

De to næste funktioner arbejder sekventielt på elementerne i containeren på en måde, der giver<br />

containeren en simpel lineær semantik. Begrebet "første element", "næste element" og "aktuelle<br />

element" implementeres med de tre medlemsfunktioner getFirst(), getNext() og<br />

getCurrent().<br />

virtual const Object* getFirst () const = 0;<br />

virtual const Object* getNext () const = 0;<br />

Det er de samme regler for konstante returværdier, der ligger til grund for disse to funktioner, så<br />

klienten kun kan læse dem og ikke skrive til dem.<br />

virtual unsigned long Size () const { return count; }<br />

returnerer størrelsen på containeren, altså antallet af elementer. Metoden er implementeret i<br />

Container som den eneste sammen med operator+=().<br />

virtual Container& operator+= (Container&);<br />

er en metode til at addere to containere, eller mere specifikt til at lægge en anden container<br />

sammen med den aktuelle. Metoden er implementeret i Container og benytter sig af<br />

Container::getCurrent(), Container::Insert() og Object::Copy() til at<br />

kopiere containeren.<br />

virtual const Object& operator[] (int) const = 0;<br />

har den samme betydning som getCurrent(), men med en bedre grænseflade.<br />

7.2.3 Klassen Container 495


Resten af Containers medlemmer er direkte arvet fra Object og er blot igen erklæret<br />

som virtuelle rene funktioner. Da en Container er abstrakt er der ingen grund til at<br />

implementere dem. Med den ny viden om Container-objekters opførsel kan vi for eksempel<br />

skrive en generel funktion til at undersøge, om en given Container er en delmængde af en<br />

anden, dvs. om alle objekterne i den ene også forekommer i den anden.<br />

bool subSet (Container& a, Container& b) {<br />

for (int i = 0; i < a.Size (); i++) {<br />

Object& searchFor = a [i];<br />

for (int j = 0; j < b.Size (); j++)<br />

if (searchFor == b [i]) break;<br />

if (j == b.Size ()) return 0;<br />

}<br />

return 1;<br />

}<br />

7.2.4 Klassen Sortable<br />

Object <br />

Sortable <br />

I Object findes en metode til sammenligning af objekter, isEqual(). I mange situationer,<br />

eksempelvis sortering eller søgning, er det vigtigt at kunne sammenligne objekter og få at vide<br />

hvilket som er "størst" fremfor blot en sandhedsværdi om lighedsforholdet. For at opnå at kunne<br />

sammenligne objekter på en større/mindre-end basis introduceres klassen Sortable som<br />

baseklasse for alle klasser, der skal have denne egenskab.<br />

Sortable indeholder kun én ny ren virtuel metode, som ganske simpelt tester om det<br />

aktuelle objekt er større end et givet andet sortérbart objekt. Klassens erklæring er<br />

class Sortable : public Object {<br />

public:<br />

virtual bool isGreater (const Sortable&) const = 0;<br />

};<br />

Medlemsfunktionen isGreater() sammenligner de to sortérbare objekter og returnerer en<br />

sandhedsværdi - sand, hvis *this er større end det angivne parameter, ellers falsk. Det<br />

interessante ved isGreater() i forbindelse med den nedarvede isEqual() er, at vi med<br />

disse to funktioner (og lidt logik) globalt kan overstyre alle de forskellige operatorer for<br />

sammenligning af objekter på forskellige måder:<br />

bool operator> (const Sortable& a, const Sortable& b) {<br />

return a.isGreater (b);<br />

}<br />

496 Abstrakte klasser 7.2


ool operator< (const Sortable& a, const Sortable& b) {<br />

return ! (a.isGreater (b) || a.isEqual (b));<br />

}<br />

bool operator>=(const Sortable& a, const Sortable& b) {<br />

return a.isGreater (b) || a.isEqual (b);<br />

}<br />

bool operator


aIterator, // gennemløb af containere<br />

aAssociation, // en association mellem objekter<br />

aDictionary, // et katalog over associationer<br />

};<br />

Hver ny klasse, der introduceres i hierarkiet, bør påføres denne liste - eller gives et vilkårligt,<br />

helst højt, nummer. Bemærk, at det ikke er en stavefejl når der optræder navne som<br />

aAssociation, men engelsk morfologi kommer trods alt i anden række. Udover disse typer<br />

indeholder headerfilen tre vigtige makrodefinitioner, nemlig<br />

DECLARE_AS_STOREABLE (class)<br />

som vil annoncere den aktuelle klasse i XCL-typesystemet, og tillade at objekter af klassen kan<br />

ind- og udlæses med typeinformation,<br />

DECLARE_TRIVIAL_FUNCTIONS (class)<br />

som erklærer de funktioner i klassen, som er helt trivielle og åbenlyse, men som dog ikke kan<br />

nedarves af andre årsager, for eksempel Copy() og Name(),<br />

DEFINE_TRIVIAL_FUNCTIONS (class)<br />

som implementerer de funktioner, som forrige makro erklærer. De to sidste makroer skal<br />

henholdsvis optræde i klasseerklæringen i headerfilen og i selve implementationsfilen. Hvis de<br />

ønskes implementeret som inline-funktioner, skal DEFINE_TRIVIAL_FUNCTIONS stå i<br />

klassens erklæring i stedet for DECLARE_TRIVIAL_FUNCTIONS.<br />

7.3.1 Klassen Nil<br />

Object <br />

Nil <br />

Nil er XCL-modstykket til C's NULL-pointer. Et objekt er nul, ulovligt, fejl eller hvad man har<br />

lyst til at kalde det, hvis det er lig en forekomst af Nil. I XCL er der kun én forekomst af<br />

klassen Nil, nemlig det statisk allokerede objekt nil (med lille forbogstav). Metoderne i<br />

Nil er skrevet, så de regner med, at der kun findes en enkelt forekomst og bekymrer sig i øvrigt<br />

ikke om adresser og indhold. Hvis klienten således erklærer et nyt Nil-objekt med et andet<br />

navn vil det være identisk med nil.<br />

Nil er det første eksempel på en aktiv, ikke-abstrakt klasse i XCL, men implementationerne<br />

af de virtuelle funktioner er ikke særlig rammende for en XCL-klasse idet Nil-klassen er meget<br />

enkel i funktion. Den skal bare gøre opmærksom på sin eksistens og ellers fylde så lidt som<br />

muligt. Da der ingen data er i et Nil-objekt, og heller ikke i et Object-objekt, som det arver,<br />

vil det blot bestå af en pointer til en virtuel tabel, hvilket ofte kan indeholdes i et register på<br />

498 Værktøjsklasser 7.3


maskinen. Her er Nil's erklæring og implementation:<br />

extern Nil nil; // den eneste forekomst<br />

class Nil : public Object {<br />

public:<br />

Nil () { }<br />

virtual ~Nil () { }<br />

virtual void printOn (ostream& strm = cout) const {<br />

strm > ws;<br />

}<br />

virtual bool isEqual (const Object& o) const {<br />

return bool (&o);<br />

}<br />

virtual Class Type () const { return aNil; }<br />

virtual Object& Copy () const { return nil; }<br />

virtual const char* Name () const { return "Nil"; }<br />

};<br />

Klassen Nil laver således ikke ret meget, men har sin anvendelse i situationer som<br />

konsistenscheck af objekter. Forestil dig for eksempel, at du udtager et objekt fra en tom<br />

Container:<br />

void getOb (Container& cont) {<br />

Object& o = *cont.Retrieve ();<br />

if (o == nil) cerr


eferencer eller to objektpointere, der sammenlignes. Det er på visse systemer med segmenteret<br />

lager (for eksempel IBM PC/AT) svært at foretage ordentlig pointeraritmetik, hvorfor følgende<br />

kode er uønskelig:<br />

bool Consistency (Object* o) {<br />

return o == &nil;<br />

}<br />

fordi den blot sammenligner pointerne. Brug i stedet<br />

bool Consistency (Object* o) {<br />

return *o == nil;<br />

}<br />

hvilket vil kalde den rette operator-funktion og, i anden række, binde dynamisk til de to objekters<br />

isEqual()-metoder. Denne regel gælder for sammenligning af alle typer af objekter i XCL.<br />

Typesystemet er hævet til så højt et plan på kørselstidspunktet, at gemen pointeraritmetik ikke<br />

slår til.<br />

7.3.2 Klassen ObjectManager<br />

ObjectManager <br />

classInfo <br />

Der er i XCL brug for en smule overordnet administration af objekterne. Denne administration er<br />

indkapslet som statiske metoder og medlemsdata i klassen ObjectManager, som indeholder<br />

noget af den "klister" der er nødvendig for visse af de avancerede funktioner i biblioteket - for<br />

eksempel at kunne indlæse et objekt fra en strøm uden at kende typen af objektet.<br />

ObjectManager har en central og enkel funktion, nemlig at løse det problem, der opstår,<br />

når et objekt af en given type skal allokeres, men hvor vi på oversættertidspunktet ikke kender<br />

typen. I ObjectManager findes en database over de forskellige ikke-abstrakte klassers navne<br />

tillige med adresser på særlige funktioner, som eksplicit allokerer en forekomst af den klasse.<br />

Denne information er gemt i en hægtet liste af classInfo-strukturer, som associerer et<br />

klassenavn med en eksplicit allokationsfunktion.<br />

Alt dette foregår selvfølgelig uden klientens indblanding. Alle klasser i systemet "annonceres"<br />

til ObjectManager før klientens program startes via statiske initieringer i de forskellige<br />

moduler, for eksempel som<br />

static Object* CreateSomething () {<br />

return new Something;<br />

}<br />

static bool initSomething = ObjectManager::Announce (<br />

500 Værktøjsklasser 7.3


"Something", CreateSomething<br />

);<br />

hvorved ObjectManager nu er klar over, at et objekt med navnet Something kan<br />

konstrures ved at kalde funktionen CreateSomething(). Faktisk er det lidt af<br />

oversætterens arbejde, vi her overtager, men C++ arbejder som bekendt slet ikke med navne, kun<br />

adresser og værdier, så et symbol som CreateSomething eksisterer ikke på<br />

kørselstidspunktet. Implementationen af disse to funktioner for en given klasse er ideen med<br />

makroen DECLARE_AS_STOREABLE, som ekspanderer til disse to (temmelig trivielle)<br />

funktioner givet et parameter, navnet på klassen. Denne makro forekommer derfor i de fleste<br />

moduler i XCL.<br />

Her er erklæringen på ObjectManager:<br />

class ObjectManager {<br />

static classInfo* classList; // liste over objekter<br />

public:<br />

~ObjectManager ();<br />

static Object* readPolymorphicObject (istream& = cin);<br />

static bool Announce (char*, PTOM);<br />

};<br />

Den anden statiske medlemsfunktion, readPolymorphicObject() benytter sig af<br />

informationen om de eksplicitte klassekonstruktioner til at indlæse et klassenavn, allokere en<br />

forekomst af denne klasse, kalde klassens readFrom()-funktion for at indlæse det fra<br />

strømmen og til sidst returnere det til klienten. Det tillader kode som<br />

Object* o = ObjectManager::readPolymorphicObject ();<br />

hvor o kan være af en hvilkensomhelst type, og, som vi skal se senere, kan være en meget<br />

kompleks datastruktur af kombinerede XCL-klasser.<br />

Destruktøren sørger for, at den hægtede liste af classInfo-strukturer med information om<br />

klassernes allokationsfunktioner bliver slettet (faktisk er det kun hægterne, der slettes - navnene<br />

er blot pointere til de samme konstanter, som bruges i de virtuelle Name()-funktioner).<br />

7.3.3 Klassen SharedObject<br />

Object <br />

SharedObject <br />

Selv om reglen om containerklasser siger, at en container "ejer" det objekt, det indeholder og<br />

derfor er ansvarlig for at destruere det, er der dog tidspunkter, hvor der må undtages fra denne<br />

regel. Et typisk eksempel er, hvis to containere skal deles om samme objekt. Hvad gør man så?<br />

Enten må den ene container skrives helt om (i en afledning) eller også må klienten lave lidt<br />

7.3.2 Klassen ObjectManager 501


akrobatik. XCL vælger det sidste, fordi det er det enkleste.<br />

Klassen SharedObject er både arvtager og klient af Object. Den indeholder en pointer<br />

til et Object, og alle de virtuelle funktioner, den arver fra Object føres direkte videre til det<br />

indkapslede objekt - med én undtagelse: destruktøren. Den er tom.<br />

class SharedObject : public Object {<br />

Object* ref;<br />

public:<br />

SharedObject (Object* o) : ref (o) { }<br />

SharedObject (Object& o) : ref (&o) { }<br />

virtual ~SharedObject () { }<br />

virtual void printOn (ostream& s = cout) const {<br />

ref->printOn (s);<br />

}<br />

virtual void readFrom (istream& s = cin) {<br />

ref->readFrom (s);<br />

}<br />

virtual void dumpOn (ostream& s = cout) const {<br />

ref->dumpOn (s);<br />

}<br />

virtual bool isEqual (const Object& o) const {<br />

return ref->isEqual (o);<br />

}<br />

virtual Class Type () const {<br />

return ref->Type ();<br />

}<br />

virtual Object& Copy () const {<br />

return ref->Copy ();<br />

}<br />

virtual const char* Name () const {<br />

return ref->Name ();<br />

}<br />

};<br />

Det tillader en komplet transparent indsættelse af et objekt i en container, men sikrer, at når<br />

containeren forsøger at destruere sit indhold, så vil SharedObject's destruktør sikre, at<br />

objektet forbliver intakt. Og så påhviler det naturligvis klienten at destruere objektet. Et<br />

eksempel (der benytter String-objekter fra afsnit 7.4.1) er:<br />

void Share (Contaier& c1, Container& c2) {<br />

String* s = new String ("Delt streng");<br />

c1.Insert (*new SharedObject (s));<br />

c2.Insert (*new SharedObject (s));<br />

}<br />

502 Værktøjsklasser 7.3


Når de to Containere, der bruges i funktionen her, deallokeres, vil deres kald til destruktøren<br />

i de objekter, de indeholder standse i SharedObjects destruktør.<br />

7.3.4 Klassen Iterator<br />

Object <br />

Iterator <br />

Iterationer, eller gennemløb, af containere på samme måde som en normal C-vektor eller liste er<br />

beskrevet i detaljer i afsnit 5.6.4. I XCL implementeres en Iterator-klasse, der virker på alle<br />

typer af containere, så længe de er arvet fra Container. En Iterator initieres af enten en<br />

anden Iterator eller en Container. I begge tilfælde vil den ny Itarator pege på en<br />

Container og begynde at indeksere den.<br />

Til dette formål vedligeholder Iterator en indeksværdi, som benyttes i kald til<br />

Container::getNext(), indtil indeksværdien kommer op på siden af<br />

Container::Size() for et givet container-objekt. Dette indeks optælles hver gang<br />

Iteratoren optælles via kald til operator++(). Den anden overstyrede operator,<br />

dereferencen (ikke multiplikation, som er binær), returnerer en konstant reference til det objekt,<br />

der står på den position i containeren, som iteratoren for øjeblikket refererer. For også med enkel<br />

syntaks at kunne teste en Iterators tilstand, specifikt om den stadig peger på valide data i<br />

containeren, kan en Iterator konverteres til en bool.<br />

Her er erklæringen af Iterator:<br />

class Iterator : public Object {<br />

Container* ref;<br />

int index;<br />

public:<br />

DECLARE_TRIVIAL_FUNCTIONS (Iterator);<br />

Iterator () : ref (NULL), index (0) { }<br />

Iterator (const Iterator& i) :<br />

ref (i.ref), index (i.index) { }<br />

Iterator (Container& c) : ref (&c), index (0) { }<br />

virtual ~Iterator () { }<br />

virtual bool isEqual (const Object&) const;<br />

virtual void printOn (ostream& = cout) const;<br />

const Object& operator* ();<br />

Iterator& operator++ (); // prefix<br />

Iterator& operator++ (int); // postfix<br />

operator bool ();<br />

bool More ();<br />

};<br />

7.3.3 Klassen SharedObject 503


Vi kan for eksempel løbe gennem en container på følgende måde:<br />

void printContainer (Container& c) {<br />

for (Iterator i = c; i; i++) cout


implementation af readFrom() og dumpOn(), derfor:<br />

void String::readFrom (istream& strm) {<br />

Sortable::readFrom (strm); // indlæs baseklassen<br />

if (len) delete sptr; // noget i forvejen?<br />

strm >> len; // længde på streng<br />

sptr = new char [len + 1]; // allokér ny streng<br />

strm.getline (sptr, len); // indlæs den<br />

}<br />

void String::dumpOn (ostream& strm) const {<br />

Sortable::dumpOn (strm); // udlæs baseklassen<br />

strm


Object& o = *ObjectManager::readPolymorphicObject (input);<br />

cout


Object <br />

Sortable <br />

Complex <br />

Denne klasse er næsten identisk med den gennemgående eksempelklasse fra <strong>kapitel</strong> 3, og<br />

indeholder derudover kun implementationer af virtuelle funktioner fra baseklasserne<br />

Sortable og Object.<br />

7.5 CONTAINERKLASSER<br />

Containerne i XCL er hierarkiets egentlige substans. Containerklasser er i det hele taget den<br />

grundpille i et objekt-orienteret system, som har størst betydning og anvendelse. I XCL er alle<br />

containerne afledt fra klassen Container, og er polymorfe både i anvendelse (de kan erstatte<br />

hinanden) og i indhold (de kan indeholde objekter af forskellige klasser på samme tid). I dette<br />

afsnit beskrives de seks containerklasser samt den private klasse Association, der arbejder<br />

sammen med nogle af containerne.<br />

Containerhierarkiet er udbygget en del i forhold til tidligere versioner af XCL, idet<br />

parameteriserede typer nu spiller aktivt ind.<br />

7.5.1 Klassen GenericList::GenericNode<br />

GenericList::GenericNode <br />

Alle containere, der bygger på hægtede lister, arbejder på hægter, der indirekte refererer de<br />

objekter, containeren indeholder. Hægter i XCL var tidligere gennemført med en generel klasse<br />

Node, som klienten havde i sit skop, men aldrig så, fordi det foregik gennem Object-pointere.<br />

Det gav en del spildtid i form af kald til funktioner, der blot videreførte disse kald. Nu er<br />

hægtearbejdet lagt ind i den parameteriserede klasse GenericList (som beskrives nedenfor)<br />

og ser ganske simpelt ud som følger:<br />

struct GenericList::GenericNode {<br />

GenericNode* next;<br />

Element* objptr;<br />

GenericNode (Element* obj) : objptr (obj), next (0) { }<br />

};<br />

Klassen GenericNode er indlejret i den parameteriserede klasse GenericList og<br />

indeholder ikke selv nogen form for mekanik hvad angår hægtning. Figur 7-2 er en illustration af<br />

GenericNodes måde at kæde forekomster sammen på.<br />

Et alternativ til denne private hægteklasse er en reel XCL-klasse, som klienten må arve fra, når<br />

objekter skal kædes ind i lister. Denne fremgangsmåde betyder imidlertid, at alle klasser, der skal<br />

hægtes, skal nedarves specielt og udfyldes med visse adminstrative funktioner, en arbejdsgang,<br />

7.4.2 Klassen Complex 507


der ikke altid er lige hensigtsmæssig. Den nuværende implementation skjuler helt mekanikken<br />

bag grænsefladen i GenericList.<br />

7.5.2 Skabelonklassen GenericList<br />

ContainerBase <br />

GenericList <br />

Denne klasse er en parametrisk definition af den mekanik, der ligger bag XCLs måde at arbejde<br />

med hægtede lister på. Det er en minimumsdefinition af en hægtet liste og udgør sammen med<br />

GenericArray det underliggende, maskinnære arbejde med fordeling, organisering og<br />

lokalisering af data i XCL. GenericList indkapsler typen GenericNode, som hægter de<br />

enkelte objekter sammen, og det er disse objekter, der er parameteret i instantieringen af klassen.<br />

template <br />

class GenericList : public virtual ContainerBase {<br />

public:<br />

struct GenericNode {<br />

GenericNode* next;<br />

Element* objptr;<br />

GenericNode (Element* obj)<br />

: objptr (obj), next (0) { }<br />

};<br />

protected:<br />

GenericNode* head; // begyndelsen<br />

GenericNode* cursor; // aktuel hægte<br />

GenericNode* trail; // følger cursor<br />

GenericNode* tail; // slutningen<br />

GenericList ();<br />

~GenericList ();<br />

void insertAt (Element*, GenericNode*);<br />

void Insert (Element*);<br />

Element* Retrieve ();<br />

Element* getCurrent() const;<br />

Element* getFirst ();<br />

Element* getNext ();<br />

};<br />

Der er fire datamedlemmer i GenericList, der alle er pointere til forekomster af<br />

GenericNode. head peger altid på den første hægte i listen og har værdien 0, hvis listen er<br />

tom. Denne pointer bruges til altid at kunne lokalisere den første hægte, hvilket er brugbart, når<br />

data skal indsættes i starten og nødvendigt, når hele listen skal gennemløbes i for eksempel<br />

destruktøren. tail peger på samme måde på den sidste hægte i listen og er især anvendelig,<br />

508 Containerklasser 7.5


når data skal sættes ind her. Det sparer, at listen gennemløbes hver gang, den sidste hægte skal<br />

findes. Klassen garanterer, at tail altid peger på den sidste hægte. Den pointer, der nok<br />

(implicit) benyttes oftest af klienten, er cursor, som peger på den aktuelle hægte i listen. Det<br />

betyder, at en hægtet liste altid har et objekt i fokus, som kan udtages eller undersøges. Der er tre<br />

funktioner, der specielt arbejder med denne pointer, og som beskrives nedenfor. Den sidste<br />

pointer er trail, som altid peger på den hægte, der kommer lige før cursor. Klienten ser<br />

aldrig denne pointer, som kun anvendes, når han fjerner objekter fra listen. Det er nemlig<br />

nødvendigt for GenericList at have information om både den forrige hægte i forhold til<br />

cursor og den hægte, der kommer lige efter cursor, fordi disse to skal hægtes sammen, når<br />

cursor-hægten fjernes. Adressen på den næste hægte findes inde i cursor-hægten<br />

(cursor->next), men den forrige hægtes adresse står ikke her. Derfor følger trail i<br />

cursors fodspor og giver os denne information. Alternativet ville være en dobbelthægtet liste,<br />

som ville tillade os at gennemløbe listen begge veje og som ville unødvendiggøre trail, men<br />

også optage en pointervariabel for hver hægte.<br />

Her følger en gennemgang af de interessante medlemsfunktioner i GenericList:<br />

void insertAt (Element*, GenericNode*);<br />

indsætter et element i listen på en angivet position. Ved at kalde insertAt() med for<br />

eksempel head som andenparameter kan objekter lægges ind på særlige steder. Her tages altså<br />

ikke hensyn til cursor-positionen, som iøvrigt ikke berøres. Det skal bemærkes, at<br />

insertAt() indsætter data i listen efter den hægte, der er angivet. Den eneste undtagelse er,<br />

hvis vi vil indsætte et element før den første hægte, altså head. I det tilfælde skal<br />

GenericNode-pointeren være 0. Så er alle muligheder dækket for indsættelse på alle<br />

positioner i listen.<br />

void Insert (Element*);<br />

indsætter ganske simpelt et element på cursor-positionen i listen og lader cursor pege på<br />

det ny element.<br />

Element* Retrieve ();<br />

fjerner et element fra listen og overgiver det til klienten. En GenericList vil automatisk<br />

fjerne alle hægter og elementer på hægterne, når den går ud af skop. Elementer, der udtages fra<br />

listen er det derfor klientens ansvar at delete. Hægten, dvs. forekomsten af GenericNode<br />

fjernes af Retrieve() umiddelbart før elementet returneres, så det skal klienten ikke<br />

bekymre sig om.<br />

De næste tre medlemsfunktioner returnerer den første, den aktuelle og den næste (efter den<br />

aktuelle) hægte på listen. Hvis der ikke er data på det sted, klienten ønsker at udtage information<br />

fra, returneres en NULL-pointer.<br />

Element* getFirst ();<br />

Element* getCurrent() const;<br />

7.5.2 Skabelonklassen GenericList 509


Element* getNext ();<br />

Figur 7-2 viser, hvordan GenericList hægter elementerne sammen, hvordan de fire pointere<br />

i ethvert GenericList-objekt opfører sig og hvordan indholdet af GenericNode ser ud,<br />

når de er hægtet sammen.<br />

510 Containerklasser 7.5


Figur 7-2: Den interne (fundamentale) repræsentation af hægtede lister i XCL.<br />

Det er klart, at det ikke er tilladt blot at tildele værdier til GenericLists pointervariable, idet<br />

de skal have en vis struktur. Medlemsfunktionerne i klassen sørger for konsistens mellem de fire<br />

pointere, så der ikke går noget galt - de skal altid ligge i den rækkefølge, som figur 7-2 illustrerer,<br />

dvs. logisk efter hinanden i kæden. Bemærk dog, at hvis cursor == head, så er trail<br />

== NULL, fordi der ikke er en forrige hægte, hvis head == NULL, så er alle de andre også<br />

NULL samt hvis der kun er én hægte i listen har både cursor og tail den samme værdi som<br />

head og trail værdien NULL.<br />

Der erklæres også to typenavne for at lette syntaken i erklæringer af GenericNode<br />

parameteriseret med Object,<br />

typedef GenericList ObjList;<br />

typedef GenericList::GenericNode ObjNode;<br />

7.5.3 Skabelonklassen GenericArray<br />

ContainerBase <br />

GenericArray <br />

Denne klasse er analog til GenericList i den forstand, at den implementerer<br />

minimumskravene for en container. Forskellen er, at denne klasse bygger på lineære lister i<br />

stedet for hægtede. Den lineære repræsentation kræver, at størrelsen på listen er kendt på<br />

instantieringstidspunktet og er derfor et parameter til konstruktøren. Klassens definition er som<br />

følger:<br />

template <br />

class GenericArray : public virtual ContainerBase {<br />

protected:<br />

Element** rep;<br />

GenericArray (const unsigned long);<br />

~GenericArray ();<br />

const Element* getCurrent () const;<br />

const Element* getFirst ();<br />

const Element* getNext ();<br />

};<br />

7.5.2 Skabelonklassen GenericList 511


Der er tale om en meget enkel repræsentation af en lineær liste, nemlig en vektor af pointere,<br />

som allokeres i konstruktøren. De fra ContainerBase arvede data definerer den aktuelle<br />

position samt den fysiske størrelse på containeren. De tre medlemsfunktioner getCurrent(),<br />

getFirst() og getNext() har akkurat samme funktion som de, der findes i<br />

GenericList, blot er der her ingen indre pointere.<br />

Klassen bruges som baseklasse (sammen med Container) for alle XCL-klasser, som<br />

bygger på lineære lister som den fundamentale datastruktur, for eksempel Array.<br />

7.5.4 Klassen LinkedList<br />

Object <br />

Container <br />

LinkedList <br />

ContainerBase <br />

GenericList <br />

LinkedList <br />

Den hægtede liste, som denne klasse repræsenterer, er sammen med Array den vigtigste<br />

containerklasse i XCL, fordi alle andre containere bruger implementationen i den til<br />

specialisering og rekomposition. LinkedList hægter Object-pointere på en liste, og<br />

arbejder således på alle typer af XCL-objekter. Den benytter sig af GenericList-klassen til<br />

at hægte objekterne sammen. Klassen implementerer de virtuelle funktioner i Container,<br />

udbygger med to funktioner til hurtig søgning i listen samt implementerer en tildelingsoperator<br />

og en additionsoperator.<br />

LinkedList arver multipelt fra GenericList og Container. Det er i selve arven,<br />

at klassen specificerer parameteret til skabelonklassen GenericList. På den måde får<br />

LinkedList fordelen fra både en parameteriseret klasse, hvis funktioner den kan anvende i<br />

den kontekst, som er krævet af Container, nemlig polymorfe Object-objekter. Det kan<br />

virke overkompliceret, men letter faktisk arbejdet med klasser, at der findes en balance mellem<br />

klasser, der er afhængige af et strengt, brugerdefineret typesystem og klasser, der er helt<br />

uafhængige og har en høj grad af parametisérbarhed.<br />

class LinkedList : public GenericList,<br />

public Container {<br />

public:<br />

DECLARE_TRIVIAL_FUNCTIONS (LinkedList)<br />

LinkedList () : GenericList (), Container () { }<br />

LinkedList (const LinkedList&);<br />

LinkedList& operator= (const LinkedList&);<br />

virtual LinkedList operator+ (const LinkedList&);<br />

virtual ~LinkedList () { };<br />

virtual void printOn (ostream& = cout) const;<br />

512 Containerklasser 7.5


virtual void dumpOn (ostream& = cout) const;<br />

virtual void readFrom (istream& = cin);<br />

virtual bool isEqual (const Object&) const;<br />

virtual void Insert (Object*);<br />

virtual Object* Retrieve ();<br />

virtual const Object* getCurrent () const;<br />

virtual const Object* getFirst () const;<br />

virtual const Object* getNext () const;<br />

virtual void goTo (const unsigned long);<br />

virtual const Object& operator[] (const unsigned long);<br />

virtual const unsigned long Locate (const Object&) const;<br />

virtual const Object* Locate (const unsigned long) const;<br />

};<br />

De fra Object arvede metoder implementeres på vektor-basis, dvs. de arbejder på alle<br />

indeholdte objekter efter tur, mens de fra Container arvede metoder benytter sig af indeksparameteret<br />

til at identificere positionen på en hægte i listen - om den er forrest, bagest eller et<br />

sted i midten. Da LinkedList-indeholdte objekter refereres helt dynamisk er det nemlig altid<br />

et spørgsmål om at traversere listen for at finde et bestemt objekt eller en bestemt indeksværdi.<br />

Denne traversering findes i de funktioner, som er arvet fra GenericList.<br />

De forskellige medlemsfunktioner fungerer som specificeret i baseklasserne, men da disse er<br />

abstrakte, har vi ikke haft mulighed for egentlige eksempler med containerklasser. Derfor følger<br />

nu nogle eksempler på anvendelser. LinkedList-objekter kan tildeles hinanden samt<br />

adderes:<br />

#include "linklist.h"<br />

#include "dstring.h"<br />

void main () {<br />

LinkedList komponister, forfattere, kunstnere;<br />

komponister.Insert (*new String ("Franz Liszt"));<br />

komponister.Insert (*new String ("Felix Mendelssohn"));<br />

komponister.Insert (*new String ("Edward Elgar"));<br />

forfattere.Insert (*new String ("Oscar Wilde"));<br />

forfattere.Insert (*new String ("Robert Burns"));<br />

forfattere.Insert (*new String ("William Shakespeare"));<br />

kunstnere = komponister + forfattere;<br />

cout


if (forfattere == komponister) cerr


associerer to objekter på en nøgle/værdi-basis, dvs. et objekt bruges som opslag og et andet som<br />

værdi. Med andre ord, et Object kan associeres til et andet Object, og begge to kan være<br />

en forekomst af enhver XCL-klasse, selv en containerklasse.<br />

På den måde har Association samme betydning for Dictionary, som<br />

GenericNode har for GenericList, blot genbruger både Association og<br />

Dictionary en stor del af funktionaliteten i de to andre klasser. Associations erklæring<br />

ser således ud:<br />

class Association : public Object {<br />

friend Dictionary;<br />

protected:<br />

Object* Key;<br />

Object* Value;<br />

Association () : Key (0), Value (0) { }<br />

Association (Object& a, Object& b) :<br />

Key (&a), Value (&b) { }<br />

Association (const Association&);<br />

public:<br />

DECLARE_TRIVIAL_FUNCTIONS (Association)<br />

virtual ~Association () { delete Key; delete Value; }<br />

Object* getKey () const { return Key; }<br />

Object* getValue () const { return Value; }<br />

virtual void printOn (ostream& = cout) const;<br />

virtual void dumpOn (ostream& = cout) const;<br />

virtual void readFrom (istream& = cin);<br />

virtual bool isEqual (const Object&) const;<br />

};<br />

Associationen er beskrevet på en dynamisk basis, idet pointerne til de to associerede objekter<br />

ikke ligger i objekterne selv, men i denne tredie klasse. De to ny medlemsfunktioner<br />

getKey() og getValue() leverer henholdsvis opslagsobjektet og værdiobjektet til<br />

klienten.<br />

Kun Dictionary kan allokere og destruere Associationer.<br />

7.5.6 Klassen Dictionary<br />

Object <br />

Container <br />

LinkedList <br />

Dictionary <br />

ContainerBase <br />

7.5.5 Klassen Association 515


GenericList <br />

LinkedList <br />

Dictionary <br />

Hvor LinkedList er en lineær indeksérbar liste udbygger Dictionary den med mulighed<br />

for opslag på indholdet af hægterne. Dictionary repræsenterer således en associativ liste,<br />

fordi klienten kan slå en værdi op på en anden værdi i stedet for blot en indeksværdi.<br />

Dictionary holder rede på objekternes placering, indhold og søgenøgler gennem forekomster<br />

af Association.<br />

Dictionary er afledt fra LinkedList og udbygger blot med to medlemsfunktioner til<br />

indsættelse og udtagning af en association. Den erklæres således:<br />

class Dictionary : public LinkedList {<br />

public:<br />

DECLARE_TRIVIAL_FUNCTIONS (Dictionary)<br />

Dictionary () : LinkedList () { }<br />

Dictionary (const Dictionary& from) : LinkedList (from) {}<br />

virtual void addAssociation (Object&, Object&);<br />

virtual Object* getAssociation (Object&);<br />

};<br />

Typiske anvendelser for et Dictionary er i situationer, hvor data skal lagres, søges efter og<br />

behandles efter deres værdier. Et trivielt eksempel er en symboltabel, der associerer en værdi<br />

med et navn. For eksempel,<br />

Output:<br />

void main () {<br />

Dictionary SymbolTable;<br />

SymbolTable.addAssociation (<br />

*new String ("en"), *new Complex (1));<br />

SymbolTable.addAssociation (<br />

*new String ("to"), *new Complex (2));<br />

SymbolTable.addAssociation (<br />

*new String ("tre"), *new Complex (3));<br />

String id = "to";<br />

cout


i en container og associeres kollektivt) har en vilkårlig betydning afhængigt af indholdet, som er<br />

bestemt på kørselstidspunktet. Objekter af typen Dictionary kan naturligvis ind- og udlæses<br />

på samme vis som LinkedList-objekter, og er i samme forstand begrænset bestandige.<br />

Objekter af forskellige typer kan indsættes vilkårligt i et Dictionary, hvilket betyder, at<br />

der kan forekomme både nøgler og værdier af alle slags XCL-klasser i samme Dictionary.<br />

Der er altså ikke tale om en nøgleværdi af statisk type, men en total frihed for klienten til at<br />

associere data i samme kontekst.<br />

Med Dictionary kan det også illustreres, hvordan der kan arbejdes indirekte på data i en<br />

container. I følgende kodefragment indsættes en LinkedList i et Dictionary, hvorefter<br />

der fyldes data i listen. Selv om listen "ejes" af Dictionary-objektet, er det altså muligt at<br />

manipulere med det direkte og uden problemer:<br />

Dictionary dict;<br />

dict.Insert (<br />

new String("A"),<br />

LinkedList* list = new LinkedList<br />

);<br />

list->Insert (new String ("B"));<br />

list->Insert (new String ("C"));<br />

cout


objekter på en kø. Klassen Queue vil udtage objekter (med Retrieve()) i samme<br />

rækkefølge som den indsætter dem (med Insert()), og repræsenterer således en form for<br />

FIFO-buffer, der er brugbar i processtyring, avancerede traverseringer af træstrukturer og grafer<br />

og som generelt værktøj til at gemme data af vejen.<br />

Queue kan genbruge næsten hele LinkedList, og udbygger kun med to funktioner for<br />

indsættelse og udtagning af data explicit fra henholdsvis slutningen og starten af listen. Grunden<br />

til disse to funktioners eksistens er, at en nedarvet klasse DeQueue indeholder de omvendte<br />

funktioner og således bliver komplet ved arv af disse to. Det tillader nemlig en DeQueue at<br />

bruges som en Queue i dennes sammenhæng. Queue har følgende erklæring:<br />

class Queue : public LinkedList {<br />

public:<br />

DECLARE_TRIVIAL_FUNCTIONS (Queue)<br />

Queue () : LinkedList () { };<br />

Queue (Queue& q) : LinkedList (q) { };<br />

virtual void insertBack (Object& o) { Insert (o); }<br />

virtual Object* retrieveFront () { return Retrieve (); }<br />

};<br />

og har faktisk ingen ikke-inline medlemsfunktioner ud over de trivielle par stykker, der genereres<br />

af makroerne.<br />

Her et eksempel på brugen af Queue:<br />

#include "queue.h"<br />

#include "dstring.h"<br />

void testContainer (Container& c) {<br />

String LVB [3] = { "Eroica", "Waldstein", "Emperor" };<br />

LVB.Insert (&s [0]);<br />

LVB.Insert (&s [1]);<br />

LVB.Insert (&s [2]);<br />

while (LVB.Size ()) cout


Waldstein<br />

Emperor<br />

7.5.8 Klassen DeQueue<br />

Object <br />

Container <br />

LinkedList <br />

Queue <br />

DeQueue <br />

ContainerBae <br />

GenericList <br />

LinkedList <br />

Queue <br />

DeQueue <br />

Denne klasse har akkurat samme funktion som Queue, blot er det muligt både at udtage og<br />

indsætte objekter i begge ender af køen (dequeue betyder double-ended queue, altså med hul i<br />

begge ender). Der arves fra Queue og udbygges med to metoder for henholdsvis indsættelse af<br />

data først i køen og udtagning af data sidst i køen, altså det omvendte af de udbyggede metoder i<br />

Queue:<br />

class DeQueue : public Queue {<br />

public:<br />

DECLARE_TRIVIAL_FUNCTIONS (DeQueue)<br />

DeQueue () : Queue () { };<br />

DeQueue (DeQueue& q) : Queue (q) { };<br />

virtual void insertFront (Object& o) {<br />

Insert (o, head);<br />

}<br />

virtual Object* retrieveBack () {<br />

cursor = tail; return Retrieve ();<br />

}<br />

};<br />

Et eksempel på anvendelsen af denne klasse er i visse sorteringsalgoritmer og søgninger med<br />

prioritet. Ligeledes kan en DeQueue være brugbar, når rekursivitet skal omdannes til<br />

iterationer. Lad følgende eksempel være en illustration af klassen:<br />

#include "dequeue.h"<br />

#include "dstring.h"<br />

7.5.7 Klassen Queue 519


void main () {<br />

char buf [100];<br />

int i = 0;<br />

DeQueue dq;<br />

while (cin >> buf) sq.Insert (new String (buf));<br />

while (sq.Size ()) {<br />

Object* o = (sq.Size () & 1) ?<br />

dq.retrieveFront () : dq.retrieveBack ();<br />

cout


Stack (Stack& s) : LinkedList (s) { };<br />

void Push (Object& o) { insertAt (o, NULL); }<br />

Object* Pop () { cursor = head; return Retrieve (); }<br />

virtual void Insert (Object&, int = LAST_ITEM);<br />

};<br />

Med henvisning til funktionen testContainer() i afsnit 7.5.7 kan vi kalde denne funktion<br />

med en Stack og lade den udskrive indholdet:<br />

Output:<br />

#include "stack.h"<br />

#include "dstring.h"<br />

void main () {<br />

Stack st;<br />

testContainer (st);<br />

}<br />

Emperor<br />

Waldstein<br />

Erioca<br />

7.5.10 Klassen Array<br />

Object <br />

Container <br />

Array <br />

ContainerBase <br />

GenericArray <br />

Array <br />

Hvor LinkedList er en dynamisk container er Array en statisk container. Objekter af<br />

typen Array skal erklæres med en størrelsesangivelse, og vil indeholde en lineær liste af<br />

Object-pointere. Bortset derfra er grænsefladen magen til LinkList på nær en ekstra<br />

indekseringsoperator. Array er afledt multipelt fra Container og fra GenericArray og<br />

samler ligesom LinkedList parametrisk polymorfi og horisontal, brugerdefineret polymorfi<br />

gennem disse to klasser.<br />

Array er hurtigere end LinkedList, fordi den ikke skal spilde tid med at finde data ved<br />

gennemløb af lister. Til gengæld er den mere pladskrævende, fordi den typisk vil være en del<br />

større end den har brug for at være. Den er henvendt til opgaver med lister af data, hvis antal er<br />

kendt fra starten. Indeksopslag er typisk log 2 hurtigere end tilsvarende i LinkedList.<br />

7.5.9 Klassen Stack 521


Klassen har følgende specifikation:<br />

class Array : public Container,<br />

public GenericArray {<br />

public:<br />

DECLARE_TRIVIAL_FUNCTIONS (Array)<br />

Array (const unsigned long = DEFAULT_CAP);<br />

Array (const Array&);<br />

virtual ~Array ();<br />

virtual void Insert (Object*);<br />

virtual Object* Retrieve ();<br />

virtual const Object* getCurrent () const;<br />

virtual const Object* getFirst () const;<br />

virtual const Object* getNext () const;<br />

virtual const Object& operator[] (const unsigned long);<br />

virtual Array operator+ (const Array&);<br />

virtual Object*& operator[] (const unsigned long);<br />

virtual void printOn (ostream& = cout) const;<br />

virtual void dumpOn (ostream& = cout) const;<br />

virtual void readFrom (istream& = cin);<br />

virtual bool isEqual (const Object&) const;<br />

};<br />

Den interessante nye funktion, som ikke findes i andre containerklasser i XCL, er<br />

operator[] som ikke-konstant funktion. Den returnerer en reference til en pointer, og kan<br />

dermed bruges på venstre side af lighedstegnet, eller som en lvalue:<br />

Array a (3); // 3 elementer<br />

a [0] = new String ("streng-objekt");<br />

a [2] = new String ("streng-objekt");<br />

cout


indeksering, der returnerer en nil-pointer, er derfor en tom eller ulovlig (eller hvordan man har<br />

lyst til at fortlkee det) reference.<br />

7.5.11 Samarbejde mellem containerklasser<br />

De fleste containerklasser i XCL kan arbejde sammen, så deres elementer kan kopieres mellem<br />

forskellige typer af containere. For eksempel kan en LinkedList og en Array adderes:<br />

#include "linklist.h"<br />

#include "array.h"<br />

#include "string.h"<br />

void main () {<br />

Array a (2);<br />

LinkedList l;<br />

l.Insert (new String ("A"));<br />

l.Insert (new String ("B"));<br />

a.Insert (new String ("C"));<br />

a.Insert (new String ("D"));<br />

l += a;<br />

cout


dette på er at gennemgå et eksempel. Antag, at et operativsystem skal administrere forskellige<br />

køer til printere. Der er en kø for hver printer, og køerne selv hægtes sammen i en liste. Vi får<br />

demed en grafstruktur, som minder om figur 7-2.<br />

Figur 7-3: Struktur over liste af printerkøer.<br />

Hver kø er en forekomst af klassen Queue, og alle køerne hægtes på et Dictionary, så de<br />

kan refereres associativt i listen af et printerovervågningsprogram (skal vi sige en proces i<br />

operativsystemet) med henvisning til printerens navn. Instantieringen af denne stuktur er simpel:<br />

Dictionary pQueues;<br />

pQueues.addAssociation (*new String ("LaserJet"), *new<br />

Queue);<br />

pQueues.addAssociation (*new String ("EpsonFX"), *new Queue);<br />

// osv...<br />

Vi kan indsætte data i køerne på en generel måde med en funktion,<br />

void putDataForPrint (String pName, Object& Data) {<br />

Queue* q = (Queue*) pQueues.getAssociation (pName);<br />

q->Insert (Data);<br />

}<br />

524 Objekt-I/O 7.6


Når en printer bliver fri, kan en anden simpel funktion ligeledes finde de data, der skal udskrives<br />

næste gang:<br />

Object* getDataForPrint (String pName) {<br />

Queue* q = (Queue*) pQueues.getAssociation (pName);<br />

return q->Retrieve ();<br />

}<br />

Nu antager vi, at systemet pludselig skal lukkes ned, men at printerkøerne skal gemmes til<br />

systemet startes igen - vi vil jo ikke miste udskrifterne. Det foregår med det enkle kollektive kald<br />

til printerkøernes udlæsningsmetoder:<br />

pQueues.dumpOn (outputStream);<br />

hvor outputStream er en normal ostream-forekomst. Hvad sker nu? Medlemsfunktionen<br />

Dictionary::dumpOn() har følgende implementation, som den arver fra LinkedList:<br />

void LinkedList::dumpOn (ostream& strm) const {<br />

Container::dumpOn (strm);<br />

for (ObjNode* p = head; p; p = p->Next ())<br />

p->objptr->dumpOn (strm);<br />

}<br />

Først kaldes altså baseklassens udlæsningsmetode Container::dumpOn(), som ser således<br />

ud:<br />

void Container::dumpOn (ostream& strm) const {<br />

Object::dumpOn (strm);<br />

strm


strømmen. Disse køer udlæses én for én i løkken, som kalder de enkelte køers udlæsningsmetoder.<br />

Queue arver samme dumpOn() fra LinkedList som Dictionary gør, og<br />

udlæser således ved primære kald til baseklassernes udlæsningsmetoder navnet på sig selv<br />

(Queue) samt antallet af elementer (data til print) i dem. For hver Queue, der udlæses,<br />

gennemløbes derfor også alle dens indeholdte objekter ét for ét og udlæses gennem deres<br />

respektive dumpOn().<br />

Lad os for enkelthedens skyld lagre teksten til udskrift som String-objekter i køerne. De to<br />

køer som erklæredes før fyldes nu op med tekst til print med følgende funktionskald:<br />

putDataForPrint ("LaserJet", *new String ("Første<br />

tekst..."));<br />

putDataForPrint ("EpsonFX", *new String ("Anden tekst..."));<br />

putDataForPrint ("LaserJet", *new String ("Tredie<br />

tekst..."));<br />

putDataForPrint ("EpsonFX", *new String ("Fjerde tekst..."));<br />

Nu indeholder printerkølisten to køer med hver to ventende objekter. Når de udlæses kaldes de<br />

forskellige klassers dumpOn()-metoder i den rækkefølge, de forekommer i figur 7-3. I venstre<br />

spalte vises navnene på de klasser, hvis dumpOn() kaldes. Læg mærke til, at det ikke<br />

nødvendigvis er den ovenstående klasse, der kalder metoden; den kaldende klasse står i højre<br />

spalte. I midten ses det output, den pågældende metode producerer. Det skal dog ikke tages som<br />

den rækkefølge, de udlæses i, for dumpOn()-metoderne kalder først sin klasses base og<br />

udlæser derefter egne data, så baseklassens output kommer altid før den afledte klasses.<br />

Det simple kald til Dictionary-klassens dumpOn()-metode vil således resultere i 36<br />

kald mellem de forskellige klasser i det komplekse pQueues-objekt, hvoraf de 14 er<br />

klientorienterede (et objekt kalder et andet objekt) og de 22 er kald i egen klasse (objektet kalder<br />

en metode i baseklassen). Det største administrative forbrug ligger altså i udlæsningen af<br />

nedarvede objekter, især i flere led, fordi alle baseobjekterne skal udlæses ét for ét mens<br />

klientorienterede kald kommer an på, hvordan objektet er repræsenteret.<br />

Det reelle output fra kaldet til Dictionary::dumpOn() bliver:<br />

Dictionary 2<br />

Association<br />

String 8 LaserJet<br />

Queue 2<br />

String 15 Første tekst...<br />

String 15 Tredie tekst...<br />

Association<br />

String 7 EpsonFX<br />

Queue 2<br />

String 14 Anden tekst...<br />

String 15 Fjerde tekst...<br />

526 Objekt-I/O 7.6


dumpOn() i klasse Udlæser (preorder) Kaldes af<br />

Dictionary<br />

Container<br />

Object<br />

Association<br />

Object<br />

String<br />

Sortable<br />

Object<br />

Queue<br />

Container<br />

Object<br />

String<br />

Sortable<br />

Object<br />

String<br />

Sortable<br />

Object<br />

Queue<br />

Container<br />

Object<br />

String<br />

Sortable<br />

Object<br />

String<br />

Sortable<br />

Object<br />

2<br />

Dictionary<br />

Association<br />

8 LaserJet<br />

String<br />

2<br />

Queue<br />

15 Første tekst...<br />

String<br />

15 Tredie tekst...<br />

String<br />

2<br />

Queue<br />

14 Anden tekst...<br />

String<br />

15 Fjerde tekst...<br />

String<br />

Klient<br />

Dictionary<br />

Container<br />

Dictionary<br />

Association<br />

Association<br />

String<br />

Sortable<br />

Association<br />

Queue<br />

Container<br />

Queue<br />

String<br />

Sortable<br />

Queue<br />

String<br />

Sortable<br />

Association<br />

Queue<br />

Container<br />

Queue<br />

String<br />

Sortable<br />

Queue<br />

String<br />

Sortable<br />

Tabel 7-1: Propagation gennem dumpOn()-metoder i komplekst objekt.<br />

Grunden til, at dumpOn() først kalder baseklassernes tilsvarende metoder før den udlæser<br />

egne medlemsdata bliver klar, når vi overvejer indlæsningsproceduren. Skal samme struktur<br />

læses af XCL igen, så den eksakte struktur bliver genskabt, er det nødvendigt med baseklassernes<br />

mere generelle information først og de afledte klassers mere specifikke information<br />

senere. Det nytter for eksempel ikke noget at udlæse en streng med indholdet først, dernæst<br />

længden og til sidst typen. Hvordan skal indlæsningskoden hitte ud af, at der er tale om en<br />

streng? Rækkefølgen må og skal være nedefter i klassehierarkiet - og faktisk minder det en hel<br />

del om normal konstruktion af afledte objekter, hvor base-klassens konstruktør altid kaldes før<br />

den afledte konstruktør eksekveres. Det afledte objekt afhænger af baseobjektets indhold, og kan<br />

derfor aldrig konstrueres eller initieres først.<br />

Indlæsning af objekter foregår på en fuldstændig spejlet måde i forhold til udlæsningen. Når et<br />

objekt skal indlæses, bruges den særlige klasse ObjectManager til automatisk identifikation<br />

af objekternes typer. Idet klienten ikke nødvendigvis selv kender til disse typer, generaliserer<br />

7.6 Objekt-I/O 527


XCL, så der altid skal indlæses polymorfe objekter. Med andre ord, der indlæses altid et objekt,<br />

der tildeles en Object-reference. ObjectManagers indlæsningsmetode hedder<br />

readPolymorphicObject, og har følgende enkle funktion:<br />

1. Først indlæses typenavnet, som er det første element på strømmen. ObjectManager<br />

har en liste over alle kendte indlæsbare klasser med associerede pointere til eksplicitte<br />

allokeringsfunktioner. På den måde kan den allokere en forekomst af objektet. Den<br />

eneste ydre forudsætning for dette er, at alle klasser, der skal være en del af det<br />

indlæsbare system skal annoncere sig selv til ObjectManager, hvilket automatisk<br />

gøres med makroen i headeren som hedder<br />

DECLARE_AS_STOREABLE.<br />

2. Når ObjectManager har allokeret en forekomst af objektet kaldes den virtuelle<br />

funktion readFrom() for dette objekt, og herfra indlæses det egentlige indhold.<br />

Denne funktion følger samme regler for udformning som dumpOn(), idet baseklassens<br />

readFrom() skal kaldes som det første. Ellers vil data på strømmen ikke blive indlæst<br />

i samme rækkefølge (i et komplekst objekt) som det var udlæst i. Afhængigt af, om<br />

objektet, der indlæses, er en forekomst af en mere eller mindre sammensat klasse<br />

allokeres og indlæses andre objekter med gentagne kald til<br />

ObjectManager::readPolymorphicObject(). Det er af den grund, at en<br />

containerklasse med et indhold af forskellige typer kan indlæses.<br />

At indlæse det aggregate objekt pQueues er altså et spørgsmål om et enkelt funktionskald til<br />

den statiske metode i ObjectManager,<br />

Dictionary& pQueue =<br />

(Dictionary&) *ObjectManager::readPolymorphicObject ();<br />

hvorefter objektet har præcis det samme indhold som det, der tidligere blev udlæst, inklusive alle<br />

indeholdte objekter og baseobjekter. Den løkke, som indlæser objekterne i containerklassens<br />

readFrom()-metode har følgende udformning:<br />

void LinkedList::readFrom (ostream& strm) {<br />

Container::readFrom (strm);<br />

int i = count; // antal objekter, der skal indlæses<br />

for (count = 0; i; i--)<br />

Insert (*ObjectManager::readPolymorphicObject (strm));<br />

}<br />

Containerklasserne kalder altså selv den statiske indlæsningsmetode, hvilket gør det muligt at<br />

indlæse objekter af enhver XCL-klasse i en container, selv andre containere. Og det er netop det,<br />

der sker, når pQueues skal genskabes; objektet er en liste af køer.<br />

Objekt-I/O i XCL er således meget robust og anvendelig. Det er især interessant, at de små ind-<br />

og udlæsningsmetode er få og små samt har en høj grad af genbrugelighed i hierarkiet.<br />

528 Objekt-I/O 7.6


Komplekse objekter, for eksempel store grafer eller tilstødende lister kan udlæses uden at skrive<br />

en eneste ny specialiseret funktion til formålet.<br />

7.7 UDVIDELSER OG TILPASNINGER<br />

Klassebiblioteket XCL er et fundament for programudvikling. Af den grund er det naturligt at<br />

udvide med nye klasser for en given applikation, særligt i erkendelse af, at det næsten<br />

udelukkende består af containerklasser og værktøjsklasser. Der er kun to egentlige aktive klasser,<br />

og deres anvendelse er tillige meget begrænset.<br />

Men der er visse regler, der skal følges, når hierarkiet udvides med nye klasser. De i afsnit 5.1<br />

beskrevne fremgangsmåder for introduktion af nye klasser i et klassebibliotek gælder fuldt ud for<br />

XCL, så det første spørgsmål er, om der findes en klasse der har en funktion som ligner den<br />

aktuelle klasse. Her er det vigtigst at se på de abstrakte klasser Object, Container og<br />

Sortable. Den ny klasse vil i mange tilfælde også kunne drage fordel af at være klient af en<br />

eksisterende XCL-klasse, for eksempel en container eller en hægte.<br />

Ud over disse forhold mellem en ny klasse og en eksisterende klasse skal en XCL-klasse<br />

indeholde et antal medlemsfunktioner, der tillader den at indgå i systemet på lige fod med alle<br />

andre klasser. Alle disse funktioner er virtuelle og opdeles i to grupper,<br />

1. de klassespecifikke metoder, som skal implementeres for enhver XCL-klasse, og<br />

2. de artsspecifikke metoder, som kan arves fra en baseklasse.<br />

De klassespecifikke metoder er:<br />

virtual Class Type () const;<br />

virtual Object& Copy () const;<br />

virtual const char* Name () const;<br />

Da de alle tre er meget trivielle implementeres de ved hjælp af de to makroer<br />

DECLARE_TRIVIAL_FUNCTIONS og DEFINE_TRIVIAL_FUNCTIONS, som henholdsvis<br />

erklærer funktionerne i klassens specifikation (den skal altså stå i klassedefinitionen i<br />

headerfilen) og implementerer dem. Begge makroer kaldes med klassens navn.<br />

Den anden gruppe, de artsspecifikke metoder, varierer lidt fra baseklasse til baseklasse<br />

afhængigt af, om den ny klasse er en Container eller en Sortable eller noget helt tredie.<br />

De rene virtuelle metoder i de abstrakte klasser kan findes i kildeteksten i appendiks F, så her<br />

gengives blot de vigtigste metoder fra Object-klassen. Disse er alle rene virtuelle metoder i<br />

Object, og skal implementeres for en nedarvet klasse. Dog kan de arves fra en mellemliggende<br />

klasse, der implementerer dem, hvis der ikke skal foretages ændringer eller udvidelser.<br />

Metoderne er:<br />

virtual void printOn (ostream& = cout) const;<br />

virtual void readFrom (istream& = cin);<br />

7.6 Objekt-I/O 529


virtual void dumpOn (ostream& = cout) const;<br />

virtual bool isEqual (const Object&) const;<br />

samt den virtuelle destruktør. Derudover skal klassen have en kopi-konstruktør, så den tvungne<br />

klassespecifikke Copy()-metode kan skabe en identisk kopi. Hvis klassen skal kunne ind- og<br />

udlæses via ObjectManager skal der tillige være en tom konstruktør.<br />

Hvis den ny klasse er abstrakt, kan de virtuelle metoder erklæres rene (sættes lig nul, se afsnit<br />

4.4) medmindre de allerede er implementeret i en baseklasse. Det tilfælde vil dog være atypisk.<br />

Ud over disse funktioner kan den ny klasse indeholde et vilkårligt antal nye metoder, både<br />

virtuelle og ikke-virtuelle. Der er også total frihed i overstyring af operatorer, medlemsfunktioner<br />

og konstruktører. Tildelingsoperatoren, der som bekendt ikke arves, bør normalt også<br />

implementeres.<br />

Her er et skelet for en XCL-klasse (udskift KLASSE med klassens navn og BASEKLASSE<br />

med baseklassens):<br />

class KLASSE : public BASEKLASSE {<br />

// datamedlemmer her<br />

protected:<br />

// eventuelle medlemsfunktioner og datamedlemmer her<br />

public:<br />

DECLARE_TRIVIAL_FUNCTIONS (KLASSE);<br />

KLASSE (/* ... */);<br />

KLASSE (const KLASSE&);<br />

virtual ~KLASSE ();<br />

virtual void printOn (ostream& = cout) const;<br />

virtual void readFrom (istream& = cin);<br />

virtual void dumpOn (ostream& = cout) const;<br />

virtual bool isEqual (const Object&) const;<br />

};<br />

Lad os tage et eksempel. Vi er interesserede i at benytte hierarkiet til konstruktion af et system,<br />

der holder rede på udlån af bøger i et bibliotek. Vi har brug for en klasse til at registrere lånernes<br />

navne, adresser og CPR-numre. Denne klasse arver vi fra Sortable, så det bliver muligt at<br />

indsætte objekter af klassen i sorterede lister, og samtidig gør vi den klient til klassen String,<br />

fordi de indeholdte data kan udtrykkes som sådanne. De forskellige metoder i ovenstående<br />

skeletklasse erstattes med det reelle indhold, der beskriver et Person-objekt. Her er<br />

specifikationen på klassen (headerfilen):<br />

// person.h<br />

#include "object.h"<br />

#include "dstring.h"<br />

class Person : public Sortable {<br />

530 Udvidelser og tilpasninger 7.7


String Fornavn, Efternavn, Adresse;<br />

String CPR_nummer;<br />

public:<br />

DECLARE_TRIVIAL_FUNCTIONS (Person)<br />

Person ();<br />

Person (const Person&);<br />

Person (const char*,const char*,const char*,const char*);<br />

virtual ~Person ();<br />

virtual void printOn (ostream& = cout) const;<br />

virtual void readFrom (istream& = cin);<br />

virtual void dumpOn (ostream& = cout) const;<br />

virtual bool isEqual (const Object&) const;<br />

virtual bool isGreater (const Sortable&) const;<br />

};<br />

Du undrer dig måske over, hvorfor funktionerne printOn(), readFrom(), dumpOn()<br />

og isEqual() ikke kan erklæres samtidig med Copy(), Type() og Name(). Svaret er,<br />

at disse funktioner ikke absolut skal erklæres i en klasse - de kan være nedarvet fra en baseklasse.<br />

En makro vil ikke kunne se dette, og derfår må de slavisk kopieres ind i klasseerklæringen. En<br />

anden vigtig makro i implementationen er den, der erklærer den aktuelle klasse som ind- og<br />

udlæsbart. Denne makro, DECLARE_AS_STOREABLE, skal blot skrives et sted i filen, udenfor<br />

funktioner og strukturerklæringer. En forklaring af makroens funktion følger nedenfor.<br />

Her er en simpel implementation af klassen:<br />

// person.cpp<br />

#include "person.h"<br />

DECLARE_AS_STORABLE (Person)<br />

DEFINE_TRIVIAL_FUNCTIONS (Person)<br />

// konstruér en "tom" personpost<br />

Person () : Fornavn (), Efternavn (),<br />

Adresse (), CPR_nummer () { }<br />

// kopiér en eksisterende Person ind i dette objekt<br />

Person (const Person& other) : Fornavn (other.Fornavn),<br />

Efternavn (other.Efternavn), Adresse (other.Adresse),<br />

CPR_nummer (other.CPR_nummer) { }<br />

// konstruér fra tre strenge / String-objekter<br />

Person (const char* f, const char* e, const char* a,<br />

const char* c) : Fornavn (f), Efternavn (e), Adresse (a),<br />

CPR_nummer (c) { }<br />

7.7 Udvidelser og tilpasninger 531


destruér en Person - tom destruktør!<br />

virtual ~Person () { }<br />

// udskriv en Person, pænt formateret<br />

virtual void printOn (ostream& strm) const {<br />

strm


Mere skal der ikke til; nu er Person et gyldigt medlem af XCL-hierarkiet, og kan benyttes af<br />

de globale funktioner, der kalder de virtuelle funktioner i objekterne. For eksempel,<br />

#include "person.h"<br />

void main () {<br />

// test printOn ()-funktionen...<br />

Person enMand ("J.","Hansen","Dalstrøget 7","111111-<br />

1111");<br />

Person enDame ("E.","Gallumbits","Eroticon 5", "?");<br />

cout


hvor CLASS er klassens navn. Hvis klassenavnet starter med en vokal, skriver du ikke "an" i<br />

stedet for "a". Denne konstant benyttes af den trivielle implementation af Type(), som<br />

returnerer denne værdi. Det gør det muligt at type-checke før sammenligning af objekter fra den<br />

globale funktion bool operator== (const Object&, const Object&).<br />

Makroen DECLARE_AS_STOREABLE, som er defineret i annoncerer<br />

automatisk klassen til ObjectManager. Den består af en statisk funktiosdefinition, som blot<br />

allokerer en forekomst af den aktuelle klasse og returnerer en Object-pointer til denne samt en<br />

statisk erklæring af en konstant, som tildeles returværdien af et kald til ObjectManager. På<br />

den måde kaldes ObjectManager før main() begynder, og det er dermed sikret, at alle<br />

klasser er kendt i ObjectManager når de skal indlæses og udlæses.<br />

De to funktioner er meget enkle og har dette skelet:<br />

static Object* createCLASS () {<br />

return new CLASS;<br />

}<br />

static bool initCLASS = ObjectManager::Announce (<br />

"CLASS", createCLASS<br />

);<br />

Den statiske funktion ObjectManager::Announce() modtager henholdsvis en<br />

karakterstreng, som er navnet på klassen samt en pointer til den funktion, der allokerer en<br />

forekomst af denne klasse. Det er alt, hvad systemet behøver af viden om den ny klasse for at<br />

indlemme den i det dynamiske typesystem under objekt-I/O.<br />

7.8 OPGAVER TIL KAPITEL 7<br />

1. Skriv en containerklasse Tree, som repræsenterer et generelt n-vejs træ. Klassen skal<br />

kunne indeholde ethvert ikke-abstrakt objekt i XCL (godt råd: nedarv fra en af de<br />

eksisterende containere).<br />

2. Skriv en klasse BinaryTree, som repræsenterer et binært træ og indeholder<br />

funktionalitet til traversering af træet. Retfærdiggør dit valg af grænseflade til klassen.<br />

3. Nedarv en ny klasse fra Tree i forrige opgave, og implementér særlige metoder til<br />

bearbejdning af det binære træ på særlige måder. Ideer: AVL-sortering, Huffmanorganisering.<br />

Skal hægterne laves om?<br />

4. Implementér en virtual funktion Hash() i alle XCL-klasserne, som returnerer en hashværdi<br />

for det aktuelle objekt. Fjern klassen Sortable og alle isGreater()metoderne.<br />

Omskriv den globale operator==(), så den kalder Hash() i stedet for<br />

isGreater(). Skal klassen SortedList modificeres for at kunne benytte den nye<br />

534 Udvidelser og tilpasninger 7.7


form for sammenligning af objekter?<br />

5. Implementér en mængdeklasse Set, som har den virkemåde, at den kun kan indeholde<br />

ét objekt med samme værdi på samme tid. Skriv medlemsfunktioner til at uddrage<br />

fællesmængder, foreningsmængder og til at sammenligne mængder. Benyt for eksempel<br />

en LinkedList som baseklasse, men husk, at rækkefølgen af elementerne i en<br />

mængden er ligegyldig - to mængder er ens, hvis de indeholder de samme elementer<br />

uanset rækkefølgen. Redegør for implementationen og beskriv overvejelserne for<br />

effektivitet.<br />

7.9 REFERENCER OG UDVALGT LITTERATUR<br />

Eksempler på design af klassebiblioteker er [Gorlen 87] og [Gorlen 90], som indeholder<br />

beskrivelser af NIH Class Library, der er blevet en standard mange steder. Der henvises i øvrigt<br />

til dokumentationen af særlige klassebiblioteker som Zortech C++ Tools, M++ Math Class<br />

Library, Screens++ User Interface Library, Classix, ObjectStore samt Borland International Data<br />

Structures (BIDS), som er et parameteriseret klassebibliotek, der følger med Borland's<br />

oversættersystemer til MS-DOS og OS/2.<br />

7.7 Udvidelser og tilpasninger 535


Fremtidsudsigter<br />

Denne bog beskriver C++ version 3.0, som den foreligger fra producenten<br />

AT&T. Indtil for nylig var det svært at finde oversættersystemer, der holdt sig til<br />

den. Sproget har udviklet sig fantastisk meget gennem de sidse fem år; det er<br />

meget sjældent at et nyt sprog, særligt på dette tidspunkt i udviklingen, får så<br />

umiddelbar en succes. Det har skyldes, at designerne at C++ har sørget for at<br />

udgive referencemanualer på et tidligt tidspunkt samt at de har inddraget<br />

udviklere verden over i overvejelserne om sprogets muligheder. Af samme grund<br />

har der været stor debat om sprogets faciliteter og eventuelle udvidelser. AT&T<br />

lancerede i 1992 version 3.0 af sproget, og det ser ud som om, at denne standard<br />

bliver mere eller mindre direkte accepteret af ANSI som en reel standardisering.<br />

Arbejdsgrupperne ANSI X3J11 og X3J16 forventer at være færdige med en<br />

færdig standard medio 1993. I dette appendiks findes en oversigt over de<br />

væsentligste foreslåede udviklinger i deres udkast.<br />

A.1 ANSI C++<br />

A<br />

ANSI, det amerikanske standardiseringsinstitut, arbejder med at skabe en fællesnævner for de<br />

forskellige krav, der findes til en eksisterende de facto standard. Der arbejdes med meget andet<br />

end computersprog og computerkoder, og samarbejdes i vid udstrækning med den internationale<br />

standardiseringsorganisation, ISO.<br />

ANSI er i en særlig kommite for standardisering af C++ i færd med at lægge sidste hånd på et<br />

dokument om sprogets udformning. Denne udformning er ikke nødvendigvis den samme som de<br />

eksisterende versioner af C++ fra AT&T eller andre, men forskellene vil være minimale. Et<br />

arbejdsdokument findes i [Ellis 90].<br />

A.2 Fejlbehandling<br />

Behandlingen af fejl, der opstår uventet på kørselstidspunktet, er blandt de største problemer i<br />

softwareudviklingen. Netop fordi der skal tages hensyn til fejl, der kun måske opstår, bliver<br />

programmerne ofte til store betingede strukturer, der ikke bidrager til den egentlig opgave, der<br />

536 Opgaver til <strong>kapitel</strong> 7 7.8


skal løses. I mange tilfælde er et ordentligt design den primære vej til eliminering af run-time<br />

fejl, men for de applikationer, der bygger kontrolsekvensen på I/O kan det være et mareridt.<br />

Sprogunderstøttet fejlbehandling er en elegant måde at komme dette til livs på. Den foreslåede<br />

struktur af en indbygget fejlbehandling (Exception Handling) i C++ består af en enkel metode til<br />

overførsel af kontrollen fra en del af programmet til en anden via særlige sprogkonstruktioner,<br />

der kaldes undtagelser. En undtagelse er en opstået situation, der bryder med programmets<br />

egentlige sekvens. Normalt gør vi dette med gentagne betingelser, der hverken gavner<br />

effektiviteten eller læsbarheden, men med undtagelser som et generelt begreb kan vi arbejde med<br />

fejl helt parallelt til det egentlige program. Vi kan implementere fejlbehandling på et<br />

hvilketsomhelst tidspunkt, fordi det egentlig ikke interfererer med den øvrige software.<br />

Fejlbehandling i C++ 2.1 består af særlige nøgleord til at sætte programmet i<br />

"undtagelsestilstand" og til at fange disse undtagelser på de rette steder i programmet. Det<br />

foregår på en kaldt/kalder-basis, hvor den kaldte funktion returnerer til kalderen med en eventuel<br />

undtagelse i baghånden. Kalderen kan så behandle undtagelsen og eventuelt igen sende den<br />

tilbage til sin egen kalder. I sammenhæng med nedarvede klasser, der kalder hinanden opefter<br />

eller nedefter i hierarkiet, betyder dette, at en fejl i en fjern klasse kan "boble op" til den øverste<br />

klasse mens der ryddes pænt op i alle subobjekterne i det komplekse objekt ét efter ét.<br />

Til formålet introduceres tre nye nøgleord, try, throw og catch. En kalder omkranser de<br />

kald, der eventuelt kan fejle, i en try-blok ("forsøg disse kald"). Den kaldte funktion kan<br />

derefter "kaste" undtagelser tilbage til kalderen med en throw-sætning, og kalderen kan<br />

"fange" disse med en eller flere catch-sætninger. Grunden til, at denne udformning er valgt er,<br />

at hvis den kaldte funktion ikke kaster en untagelse, så vil det ikke være en tidsstraf i<br />

programeksekveringen. Dette har været et primært mål i designet af C++ i alle leder og kanter,<br />

og fejlbehandling er ingen afvigelse. Hvis den kaldte funktion returnerer normalt, vil catchblokken<br />

blive sprunget over.<br />

Det er en meget simpel form for fejlbehandling. Det eneste, der sker er, at en fejl med<br />

sikkerhed vil blive fundet og kan behandles. Der er ingen avancerede faciliteter for automatisk at<br />

prøve at "redde" en undtaglese ved at starte på det sted, hvor den blev kastet fra. En throwsætning<br />

afslutter altid funktionen og den eneste mulighed er at foretage et nyt kald. Til gengæld<br />

kan en throw-sætning overføre vilkårlige informationer til kalderen, som med disse kan<br />

afgøre, hvad der skal foretages for at redde så meget som muligt - eller eventuelt afslutte<br />

programmet pænt og ordentligt.<br />

Et eksempel på et kast af en undtagelse er:<br />

throw "division med nul";<br />

Denne sætning vil afslutte funktionen ummidelbart og forsøge at finde en catch-sætning i den<br />

kaldende funktion, der har et argument af samme type som throw-sætningen. Her er det en<br />

char*, så<br />

catch (char* s) {<br />

cout


fanger og udskriver en undtagelse med en pointer til en char og udskriver den som<br />

fejlmeddelelse. Blokken i catch-sætningen skal følge en try-blok. I kalderens funktion<br />

bliver udformningen af en undtagelsesstruktur derfor noget i retning af:<br />

try {<br />

// funktioner, der kan kaste en undtagelse<br />

}<br />

catch (/* undtagelsestype */) { /* behandl */ }<br />

catch (/* undtagelsestype */) { /* behandl */ }<br />

// osv...<br />

Alle funktionskald i en try-sætning vil være genstand for et undtagelsescheck. Hvis en<br />

funktion kaster en undtagelse, vil try-blokken terminere (resten af funktionerne vil ikke blive<br />

kaldt), og en catch-blok, der passer til typen af undtagelse vil forsøges fundet og udført.<br />

Typesystemet i C++ er altså en integreret del af fejlbehandlingsstrukturen, og både fundamentale<br />

og brugerdefinerede typer kan kastes.<br />

Faktisk kan en throw-sætning ses som et slags funktionskald, og en catch-sætning som en<br />

funktionserklæring. De har blot en anden opførsel. Typisk vil en catch kun behandle en del af<br />

undtagelsen og så kaste den videre:<br />

catch (int errnum) {<br />

// deallokér dynamiske data i denne funktion etc.<br />

throw errnum;<br />

}<br />

Dermed kan alle funktioner rydde så meget op, som de kan (skal), og lade kalderen klare resten<br />

af problemet. Dermed undgås den besværlige administration af programmets status, især over<br />

dynamisk allokerede data, som ryddes op i særlige funktioner.<br />

Med brugerdefinerede typer kan fejlsystemet specialiseres. For eksempel,<br />

// fejltyper i systemet<br />

struct device_error { /* ... */ };<br />

struct math_error { /* ... */ };<br />

struct memory_error { /* ... */ };<br />

Disse fejl kan kastes på følgende måde:<br />

void g () {<br />

// ...<br />

if (timeout) throw device_error;<br />

// ...<br />

double* a = new double [400];<br />

if (a == NULL) throw memory_error;<br />

// ...<br />

538 Fremtidsudsigter A.0


}<br />

og ligeledes fanges med<br />

void f () {<br />

try {<br />

// ...<br />

g ();<br />

// ...<br />

}<br />

catch (device_error d) {<br />

// behandl fejl på en enhed<br />

}<br />

catch (math_error m) {<br />

// behandl matematisk fejl<br />

}<br />

catch (memory_error m) {<br />

// behandl lagerfejl<br />

}<br />

}<br />

Det gode ved denne måde at arbejde med fejlsituationer på, er at hvis ingen fejl opstår, så vil<br />

programmet ingenting foretage sig. Dermed kan et underliggende fejlbehandlende system bygges<br />

ind i programmet på en sådan måde, at det ikke vil lide tidsmæssigt under det. Med den form for<br />

fejlbehandling, der ikke er understøttet af sproget, men som må skrives i det, er der et stort<br />

forbrug i forbindelse med test af situationer og statusflag. Et ideelt standardbibliotek vil således<br />

indeholde throw-sætninger i stedet for at returnere særlige værdier i særlige situationer, sådan<br />

som for eksempel C-biblioteksfunktioner som malloc() og printf() gør.<br />

Det er en god idé at modtage reference- eller pointertyper i en catch-sætning, fordi det<br />

tillader at vi indbygger polymorfi i fejlsystemet. Der er ingen standard-formel for design af et<br />

fejlbehandlingsystem, men oplæget her giver mulighed for mange forskellige slags<br />

implementationer, ganske i C++'s natur. Overvej følgende måde at arbejde med fejl på, hvor en<br />

enkelt catch fanger forskellige underordnede fejl i samme sætning:<br />

struct math_error { /* ... */ };<br />

struct divide_by_zero : math_error { /* ... */ };<br />

struct overflow : math_error { /* ... */ };<br />

struct underflow : math_error { /* ... */ };<br />

void g () {<br />

// ...<br />

throw overflow;<br />

}<br />

A.2 Fejlbehandling 539


void f () {<br />

try {<br />

g ();<br />

}<br />

catch (math_error& m) {<br />

// alle slags matematiske fejl<br />

}<br />

}<br />

Her har kalderen, som fanger undtagelsen, en frihed til enten at fange en overordnet fejl (en<br />

baseklasse) eller en underordnet fejl (en afledt klasse). Hvis kalderen vil specialisere sig indenfor<br />

de forskellige typer af matematiske fejl, der er tale om her, er det blot et spørgsmål om at benytte<br />

disse typer i stedet:<br />

void f () {<br />

try {<br />

g ();<br />

}<br />

catch (overflow& m) {<br />

// overløbstilstand<br />

}<br />

}<br />

På den måde kan der med arv specificeres forskellige niveauer af fejl, som er naturlige i deres<br />

udformning og entydige i deres brug, når fejl skal boble op gennem funktionerne. Jo længere op,<br />

vi kommer, des mere generelle kan vi tillade os at være i vores catch-sætninger. Virtuelle<br />

funktioner spiller også en vigtig rolle her, da de kan bruges til en standardoprydning for fejlen.<br />

Læg mærke til, at catch-sætningerne gennemløbes i den rækkefølge, de står i kildeteksten.<br />

Det er også muligt at kaste og fange undtagelser uden information om typen:<br />

// kast en generel undtagelse<br />

throw;<br />

catch (...) {<br />

// alle typer af fejl fanges her<br />

}<br />

Fordi catch-sætningerne gennemløbes i rækkefølge, bør en catch(...) stå til sidst. Hvis<br />

en throw uden parameter står i en catch-sætning vil samme undtagelse dog blive kastet som<br />

den aktuelle catch modtager:<br />

catch (math_error& a) {<br />

// ...<br />

throw; // kaster en math_error-undtagelse<br />

540 Fremtidsudsigter A.0


}<br />

Der er en underforstået undtagelses-fanger, som automatisk oprettes, hvis der ikke specificeres<br />

en catch(...), som har udformningen<br />

catch (...) { throw; }<br />

Hvis der på det øverste niveau i programmet ikke findes en passende catch-sætning, vil den<br />

indbyggede funktion terminate() automatisk blive kaldt. terminate() kan anmodes<br />

om at kalde en brugerfunktion med et kald til set_terminate(), som modtager et<br />

parameter af typen void(*)(), altså pointer til void-funktion.<br />

Når en undtagelse kastes, deallokeres alle automatisk allokerede objekter, der er erklæret efter<br />

try-blokkens start. Dynamisk allokerede objekter er, som altid, programmørens ansvar. En<br />

undtagelse kan kastes i en konstruktør, altså i et objekt under konstruktion. Hvis en undtagelse<br />

kastes i en konstruktør i en baseklasse, er subobjekterne (i de afledte klasser) endnu ikke<br />

konstrueret, så destruktørne for disse vil ikke blive kaldt. Kun færdigtkonstruerede subobjekter<br />

vil blive destrueret.<br />

Det er muligt at specificere, hvilke undtagelser en given funktion er i stand til at kaste, hvilket<br />

er en måde at automatisere fejlbehandlingen på. Specifikationen på undtagelserne skrives efter<br />

funktionserklæringens navn og parameterliste, som i<br />

void funk () throw (dette, hint) {<br />

// ...<br />

}<br />

Dette betyder i klare termer, at funk() kan kaste (kaster) undtagelserne dette og hint<br />

videre fra en af funk() kaldt funktion til den funktion, der kaldte funk(). Dermed behøver<br />

vi ikke skrive try- og catch-blokke i funk(). Hvis en af funk() kaldt funktion kaster<br />

en undtagelse, som ikke står i funk()'s specifikation af tilladte undtagelser, vil funktionen<br />

unexpected() blive kaldt. unexpected() kan ligesom terminate() sættes til at<br />

kalde en brugerfunktion, her blot med et kald til set_unexpected(), som modtager samme<br />

parametertype som set_terminate() (se ovenfor).<br />

Erklæringen af funk() ovenfor vil generere denne underforståede kode, eller noget i<br />

lignende:<br />

void funk() throw (dette, hint) {<br />

try { /* alle sætninger i funktionen */ }<br />

catch (dette) { throw; }<br />

catch (hint) { throw; }<br />

catch (...) { unexpected (); }<br />

}<br />

Specifikationen af mulige undtagelser i en funktion har også muligheder indenfor statisk<br />

analyse, fordi den specificerer et forhold mellem funktioner. Hvis der ikke er en specifikation af<br />

A.2 Fejlbehandling 541


undtagelser, kan funktionen kaste alle slags, og hvis specifikationen udtrykker en tom liste af<br />

typer - throw() - er det ulovligt for funktionen at kaste undtagelser. Læg for øvrigt mærke til,<br />

at en funktion, som kaster en undtagelse af typen T rent faktisk er i stand til at kaste untagelser<br />

af enhver type afledt fra T.<br />

Det skal nævnes, at fejlbehandling, som er beskrevet i dette afsnit, er genstand for megen<br />

diskussion blandt udviklere. Mange oversættersystemer indeholder ikke faciliteten mens andre<br />

implementerer den i nedskåret grad. Microsoft's C/C++ 7.0 indeholder fejlbehandling, mens<br />

Borland's C++ 3.0 ikke gør. Det ser heller ikke ud til, at ANSI vil lade fejlbehandling være<br />

omfattet af standarden.<br />

A.3 Parameteriserede typer<br />

Ikke alle C++-oversættere indeholder faciliteter for parameteriserede typer, som beskrives i<br />

denne bog som en del af sproget. Microsoft's C/C++ 7.0 har for eksempel satset på andre<br />

faciliteter, mens Borland's oversætter bygger meget på skabeloner. Det ser dog alligevel ud til, at<br />

denne facilitet bliver medtaget i ANSI C++ og at AT&T C++ 3.0 har været meget populær netop<br />

af den grund.<br />

A.4 Typeidentifikation på kørselstidspunktet<br />

En mulig og for mange velkommen udvidelse af C++ har at gøre med entydig identifikation af<br />

typer under programmets kørsel. Som det ser ud nu, har kun oversætteren viden om de egentlige<br />

typer af objekter, der bruges polymorft, mens programmet, når det kører, må læne sig op af<br />

virtuelle funktioner. Problemet er, at polymorfien i C++'s objekt-orienterede muligheder kun<br />

virker opad i hierarkiet; man kan konvertere fra en afledt klasse til en baseklase. Hvis nu man har<br />

en pointer til en baseklasse, som måske er abstrakt, hvordan ved man så, hvilken klasse den<br />

egentlig peger på?<br />

Lad os tage et eksempel fra XCL-biblioteket. En gennemgående funktion i de fleste klasser<br />

sammenligner objekter, og ser nogenlunde således ud:<br />

bool Klasse::isEqual (const Object* obj) {<br />

return (Klasse*) obj == this);<br />

}<br />

Parameteren skal være af typen const Object*, fordi der er tale om en virtuel funktion, der<br />

skal være identis med baseklassernes erklæringer. Derfor bliver funktionen nødt til at foretage en<br />

tvungen typekonvertering fra en Object* ned til en Klasse* (Klasse er en eller anden<br />

ikke-abstrakt XCL-klasse) og sammenligne de to. Men hvordan ved isEqual(), om denne<br />

konvertering gik godt? Hvad nu, hvis obj faktisk var en helt anden type, så de to slet ikke<br />

542 Fremtidsudsigter A.0


kunne sammenlignes (hvordan sammenlignes en Complex og en String, for eksempel)?<br />

Forslaget til en ændring går ud på, at typekonverteringen skal resultere i en NULL-pointer, hvis<br />

konverteringen gik galt. Det er faktisk den slags manglende indbyggede typeidentifikation, som<br />

nødvendiggør de typiske isA()- og typeOf()-funktioner i de fleste C++-biblioteker. For<br />

eksempel,<br />

bool Klasse::isEqual (const Object* obj) {<br />

Klasse* kp = (Klasse*) obj;<br />

if (kp) // ok: obj var en Klasse-pointer<br />

return obj == this;<br />

else // ups: obj var IKKE en Klasse-pointer<br />

return 0;<br />

}<br />

Generelt hvad angår forslag til udvidelser af C++ henvises til de artikler og notater fra AT&T,<br />

der periodisk publiceres i tidsskrifter med objekt-orienteret tilknytning. Især The C++ Report,<br />

Journal of Object-Oriented Programming og Hotline on Object-Oriented Technology indeholder<br />

seneste nyheder om sprogets udvikling. Der må også især henvises til Bjarne Stroustrups The<br />

Annotated C++ Reference Manual [Stroustrup 91], som er et arbejdsdokument for ANSIkomiteen,<br />

der arbejder med udkastet til standardiseringen. Bogen udkommer snart i en anden<br />

udgave med et rationale for de forskellige forslag til udvidelser og ændringer, der er kommet til i<br />

løbet af de sidste to år.<br />

A.3 Parameteriserede typer 543


Gode råd til C-programmører<br />

C++ har en stor fordel i at være næsten kompatibelt med C. C-programmører kan<br />

uden videre foretage skiftet og begynde at benytte de ny faciliteter. Der skal ikke<br />

læres ny syntaks, regler for oversættelse, organisation af kildetekst eller<br />

udviklingsmiljø. Dog, som beskrevet i introduktionen, skal visse begreber aflæres<br />

- for eksempel globale og delte data - før der kan skrives objekt-orienterede<br />

programmer i sproget. Til gengæld er indlæringskurven for en C-programmør<br />

lettere i begyndelsen end for folk med en anden baggrund. I dette appendiks<br />

opremses et antal nøglepunkter, hvor C-programmører umiddelbart skulle kunne<br />

se en fordel i at bruge en bedre C.<br />

1. Brug const i stedet for #define<br />

const WC_PAINT = 1;<br />

const double PI = 3.1415927;<br />

const int EOF = -1;<br />

B<br />

Makroer benyttes overalt i konventionelle C-programmer. Den hyppigste anvendelse er til<br />

erklæring af konstanter. Ved i stedet at erklære konstanterne med const-nøgleordet kan de<br />

indgå i typesystemet og begrænses i skop, i modsætning til makroerne, som ikke har typer og er<br />

helt globale. Typeinformationen hjælper oversætteren i deduktion af ulovlige tildelinger,<br />

parameteroverførsler mv.<br />

Hvor der i C skrives<br />

#define WM_PAINT 1<br />

#define PI 3.1415927<br />

#define EOF -1<br />

skrives i C++ som<br />

544 Fremtidsudsigter A.0


Makroer har stadig anvendelse i C++, men kun i tilfælde, hvor indholdet af makroen er mere<br />

end en konstant. Når der for eksempel skal erklæres komplicerede makroer til substitution af<br />

egentlig kode, til at undgå gentagne gennemløb af headerfiler eller til erklæring af navne, der skal<br />

benyttes af oversætteren:<br />

#if !defined _OBJECT_H<br />

#define _OBJECT_H<br />

#define DECLARE_AS_STOREABLE(CLASS) \<br />

static Object* Create ## CLASS (void) { \<br />

return new CLASS; \<br />

} \<br />

static bool init ## CLASS = \<br />

ObjectManager::Announce (#CLASS, Create ## CLASS); \<br />

#define TARGET_SYSTEM OS_2_20<br />

#endif<br />

2. Brug ikke typedef<br />

I C er det kutyme at skrive kode som<br />

typedef struct _MONIN {<br />

USHORT cb;<br />

BYTE abBuffer [18];<br />

} MONIN;<br />

hvilket erklærer en type MONIN som en datastruktur. Det gøres for at undgå at skulle bruge<br />

struct, hver eneste gang strukturen bruges i programmet. I C++ har typer og variable samme<br />

navnerum, dvs. et strukturnavn indgår som et leksikalt element. Skriv i stedet:<br />

struct MONIN {<br />

USHORT cb;<br />

BYTE abBuffer [18];<br />

};<br />

hvorved oversætteren kan benytte strukturen (eller klassen) som et normalt typenavn. Brug dog<br />

stadig typedef til erklæring af typer, der har en meget lang og forvirrende syntaks, som for<br />

eksempel<br />

typedef int (*Handler) (const unsigned long&);<br />

A.3 Parameteriserede typer 545


hvis fordel i koden kan illustreres af forskellen mellem disse to funktionsprototyper:<br />

int (*) (const unsigned long&)<br />

set_new_handler (int (*) (const unsigned long&) = 0);<br />

Handler set_new_handler (Handler = 0);<br />

Læsbarheden vinder i den sidste.<br />

2. Brug inline-funktioner i stedet for makroer<br />

Makroer, der består af en mindre parametiseret stump kode, kan ofte med fordel konverteres til<br />

en inline-funktion, fordi den indgår bedre i typesystemet og er sikrere og mere konsistent i<br />

brug. Overvej<br />

#define max(x,y) ( (x) > (y) ? (x) : (y) )<br />

Hvad sker der, når vi skriver<br />

a = max (b++, --c);<br />

hvilket ekspanderes til noget i retning af<br />

a = ( (b++) > (--c) ? (b++) : (--c) );<br />

hvilket vist ikke er meningen? Desuden vil<br />

a = max ("A", "B");<br />

sandsynligvis kunne oversættes uden problemer, men vil være meningsløst og sikkert generere en<br />

fejl under kørslen. En inline-funktion er et godt alternativ, og bærer typeinformation med sig.<br />

Skal den bruges til forskellige typer, kan den overstyres eller skrives som en skabelonklasse:<br />

inline int max (int x, int y) { return x > y ? x : y; }<br />

Nu vil oversætteren med en meningsfuld fejl fange de ovenstående forkerte anvendelser af<br />

makroen. I de fleste tilfælde kan en makro erstattes af en inline-funktion (eller flere) uden at<br />

der skal ændres i andet kode.<br />

Makroer skal dog stadig bruges i stedet for inline-funktioner i de situationer, hvor det er en<br />

type eller et andet navn, der skal overføres til makroen som parameter.<br />

546 Fremtidsudsigter A.0


4. Erklær variabelnavne hvor de bruges første gang<br />

I C er det påkrævet, at alle variable (objekter) erklæres før den første sætning i funktionen,<br />

hvilket skaber en del moslen rundt i kildeteksten, når typen på en variabel et sted midt i en<br />

funktion skal identificeres. C++ tillader, at vi erklærer variablen hvor den bruges første gang, så<br />

bliver til<br />

void f () {<br />

int i;<br />

// ...<br />

for (i = 0; i < 100; i++) { /* ... */ }<br />

};<br />

void f () {<br />

// ...<br />

for (int i = 0; i < 100; i++) { /* ... */ }<br />

};<br />

Det gør det for det første lettere hurtigt at identificere objekters typer og sparer også på<br />

navnerummet, fordi lokale erklæringer (lejret ind i blokke) ikke forurener de ydre.<br />

5. Brug konstante referencer som parametre<br />

I C bruges pointere til datastrukturer ofte som parametre for at spare tid i funktionskald, da en<br />

pointer fylder langt mindre end en stor datastruktur. Der er dog tilfælde, hvor en funktion ikke må<br />

ændre i strukturens indhold, hvorfor den overføres som kopi. For eksempel,<br />

void f (struct INFO infoblock) { /* ... */ }<br />

Her er det bedre at erklære parameteren som en konstant pointer, hvilket sikrer at den ikke bliver<br />

ændret og samtidig at den overføres langt hurtigere:<br />

void f (const INFO* infoblock) { /* ... */ }<br />

Eller endnu bedre, en referenceparameter, hvilket lader klienten slippe for at bruge adresseoperatoren,<br />

når funktionen kaldes:<br />

void f (const INFO& infoblock) { /* ... */ }<br />

6. Brug statiske klasser i stedet for globale data<br />

A.3 Parameteriserede typer 547


Næsten alle C-programmer, der består af mere end en main()-funktion indeholder globale<br />

data. Globale data skaber rodede programmer, der er svære at ændre på (se afsnit 5.2), fordi de<br />

netop består af funktioner, der afhænger af data udenfor ethvert domæne. Brug i stedet en statisk<br />

klasse (afsnit 3.13 og 3.14) med en eventuel statisk konstruktør, og gå gennem klassens statiske<br />

metoder (med et kvalificeret typenavn) for at komme til de indkapslede data deri. Det er en<br />

modularisering, der indskrænker de globale data, og som også kan gælde for globale funktioner.<br />

For eksempel,<br />

class MemoryAllocator {<br />

static void** alloctable;<br />

static void (*ErrorHandler) (void);<br />

public:<br />

static int status;<br />

static void* Allocate (size_t);<br />

static void FreeUp ();<br />

static void heapWalk ();<br />

// ...<br />

};<br />

Kald til de statiske metoder giver en indgang til de statiske data, som nu ikke længere forurener<br />

det globale navnerum. For eksempel,<br />

char* pcBuf = (char*) MemoryAllocator::Allocate (1000);<br />

if (MemoryAllocator::status == 0) cerr


struct Node;<br />

// ...<br />

bliver til<br />

Node* n = (Node*) malloc (sizeof (Node));<br />

// ...<br />

free (n);<br />

struct Node;<br />

// ...<br />

Node* n = new Node;<br />

// ...<br />

delete n;<br />

new sikrer, at konstruktøren (konstruktørerne) for det nye objekt kaldes, og dermed, at objektet<br />

initieres korrekt - eller, hvis lageret ikke kunne findes, at det slet ikke bliver initieret.<br />

malloc() lægger derimod op til en del statuskontrol hos klienten.<br />

8. Brug i stedet for <br />

Selv om C++ fungerer med både C-biblitoteket stdio og det nye standardbibliotek i C++,<br />

iostream, bør det gamle bibliotek ikke benyttes. iostream har langt bedre faciliteter for I/O, fordi<br />

det er implementeret som et klassehierarki, som tillader, at klienten integrerer egne datatyper i<br />

I/O-biblioteket (se afsnit 6.6). Specifikt bør du bruge de overstyrede operatorer > i<br />

stedet for funktioner som scanf() og printf(), fordi de lader dig være ligeglad med<br />

specifikation af typen på de data, du vil ind- og udlæse. For eksempel,<br />

bliver til<br />

og<br />

bliver til<br />

printf ("%s er %d år gammel\n", navn, alder);<br />

cout


9. Brug konstruktører og destruktører<br />

I C har vi for vane at skrive en sekvens som<br />

void f () {<br />

unsigned whdl;<br />

// ...<br />

whdl = WinCreateWindow (0); // opret en forekomst<br />

WinInitialise (whdl) // initiér den<br />

// ...<br />

WinCloseDown (whdl); // fjern den<br />

}<br />

Her er tre funktionskald, som klienten må og skal huske at kalde, og tilmed i den rette<br />

rækkefølge. I C++ kan den eksplicitte allokationsfunktion erstattes af en new, som eventuelt<br />

overstyres af en klasse. Eller også kan konstuktører og destruktører sikre korrekt oprettelse og<br />

nedbrydning af objektet, så klienten ikke ved at glemme at kalde en initieringsfunktion får en<br />

banal run-time fejl.<br />

10. Brug overstyrede funktioner<br />

C-biblioteker har ofte forskellige implementationer af samme funktion, hvor de arbejder på hver<br />

sin type eller parameterliste. Der findes således flere funktioner til at tage den absolutte værdi af<br />

et tal, både til heltal og til kommatal. Disse funktioner har i C-biblioteket math.h navnene<br />

abs() og fabs(). Klienten skal huske hvilken funktion, der skal bruges, afhængig af typen,<br />

der skal gives med.<br />

I C++ kan dette overlades til oversætteren, som ved overstyring af funktionsnavnet skelner<br />

mellem to funktioner med identiske navne på parameterlisten. Begge funktioner hedder abs(),<br />

men tager henholdsvis en int og en double. Læg mærke til, at dette kun virker for<br />

parameterlister (og konstante medlemsfunktioner), men ikke for returværdier (se afsnit 2.6.9 og<br />

3.3.3). C++ skelner også, i modsætning til C++, mellem en unsigned int og en int. Hvis<br />

der således er to overstyrede funktioner<br />

unsigned int abs (unsigned int i);<br />

int abs (int i);<br />

vil oversætteren udtrykkeligt vælge den, der passer til parameteret.<br />

12. Brug virtuelle funktioner i stedet for switch-sætninger<br />

I C finder man tit switch-sætninger, der fylder flere sider. Disse typer af statiske<br />

550 Fremtidsudsigter A.0


sammenligninger gør det svært at udvide programmet, fordi afhængighederne af de forskellige<br />

værdier i en switch er spredt ud over hele programmet. En ofte benyttet teknik er at give en<br />

datastruktur et typefelt i en indeholdt union, og så teste på dette felt i switch-sætninger<br />

rundt om i programmet. For eksempel,<br />

struct command {<br />

enum _type { streng, heltal } type;<br />

union _cmd_types {<br />

char* s;<br />

int i;<br />

} cmd_type;<br />

};<br />

/* ... */<br />

void read (struct command* c) {<br />

switch (c->type) {<br />

case streng: /* ... */ break;<br />

case heltal: /* ... */ break;<br />

/* osv... */<br />

}<br />

}<br />

void write (struct command* c) {<br />

switch (c->type) {<br />

case streng: /* ... */ break;<br />

case heltal: /* ... */ break;<br />

/* osv... */<br />

}<br />

}<br />

/* osv... */<br />

På denne måde bliver koden, der arbejder på en bestemt type af strukturen spredt rundt i hele<br />

programmet, og ofte i forskellige moduler. Dermed er det svært at lokalisere dem (og fejlene i<br />

dem), fordi det ikke er entydigt, hvilke funktioner, der har adgang. En udvidelse af strukturen<br />

med en ny type involverer en opdatering af alle switch-sætninger i programmet, der tester på<br />

typefeltet.<br />

Et godt alternativ er at skrive strukturen om til en base-klasse og de forskellige konstanter, der<br />

beskriver typerne i opremsningen om til afledte klasser. Alle case-sætningerne skrives i stedet<br />

som virtuelle funktioner i alle klasserne (rene i baseklassen). Dermed isoleres al kode, der<br />

A.3 Parameteriserede typer 551


arbejder på en bestemt type i samme modul, og nye typer kan påhæftes uden at ændre koden i de<br />

eksisterende moduler. For eksempel,<br />

class command {<br />

public:<br />

virtual void read () = 0;<br />

virtual void write () = 0;<br />

};<br />

class streng_command : public command {<br />

char* s;<br />

public:<br />

virtual void read () { /* ... */ }<br />

virtual void write () { /* ... */ }<br />

};<br />

class heltal_command : public command {<br />

char* s;<br />

public:<br />

virtual void read () { /* ... */ }<br />

virtual void write () { /* ... */ }<br />

};<br />

// ...<br />

void read (command* c) {<br />

c->read ();<br />

}<br />

void write (command* c) {<br />

c->write ();<br />

}<br />

// osv...<br />

13. Brug anonyme unions<br />

unions i C har den ulempe, at typenavnet på den skal specificeres, hvis et medlem skal<br />

refereres. For eksempel,<br />

struct A {<br />

552 Fremtidsudsigter A.0


union U {<br />

int i;<br />

long l;<br />

char* c;<br />

} u;<br />

int t;<br />

};<br />

struct A a;<br />

a.u.l = 5000L;<br />

I C++ (også i ANSI C) kan en union være anonym (afsnit 2.8.3), hvilket sparer unødig tekst:<br />

struct A {<br />

union {<br />

int i;<br />

long l;<br />

char* c;<br />

};<br />

int t;<br />

};<br />

struct A a;<br />

a.l = 5000L;<br />

De tre medlemmer af den anonyme union har et skop, der svarer til den blok, hvori de står, i<br />

dette tilfælde altså A.<br />

13. Brug type-sikker lænkning<br />

På grund af C++ adminstration af overstyrede funktioner, kan der opstå konflikter i lænkningen<br />

til eksisterende C-biblioteker (og for den sags skyld til Pascal, Fortran eller assemblerbiblioteker).<br />

Konstruktionen for type-sikker lænkning sikrer, at det navn, der lænkes med ikke ændres af<br />

oversætteren. For eksempel,<br />

extern "C" long rand ();<br />

extern "C" {<br />

#include <br />

#include <br />

}<br />

A.3 Parameteriserede typer 553


14. Brug skabeloner<br />

Skabeloner og symbolske konstanter gør næsten makroer overflødige. Brug så vidt muligt<br />

skabelonklasser (parameteriserede typer) og skabelonfunktioner (parameteriserede funktioner)<br />

hvis der er den mindste mistanke om, at funktionens centrale datatype kan opfattes generisk, dvs.<br />

anvendes i forskellige uafhængige sammenhænge.<br />

En vektor med grænsecheck som den nedenfor er for eksempel centreret omkring en type, som<br />

rent faktisk slet ikke har noget med vektorens mekanik at gøre - og derfor kan den så at sige<br />

fjernes og gøres til et anonym parameter. Så selvom klassen er meget enkel kan den godt<br />

parameteriseres med fordel.<br />

template <br />

class Vector {<br />

T* rep;<br />

T ERROR;<br />

unsigned size;<br />

public:<br />

Vector (unsigned sz) { rep = new T [size = sz]; }<br />

~Vector () { delete rep; }<br />

T& operator[] (unsigned i) {<br />

return i < size ? rep [i] : ERROR;<br />

}<br />

};<br />

554 Fremtidsudsigter A.0


Andre Objekt-Orienterede Sprog<br />

C++ er naturligvis ikke det eneste objekt-orienterede system, der findes. Man kan<br />

heller ikke sige, at det er det bedste. Det er godt til visse formål, og forfatteren vil<br />

sige de fleste. Der er imidlertid andre OOP-systemer, sprog mv., som har fortrin<br />

på visse punkter frem for C++. Nogle af disse systemer gennemgås i dette<br />

appendiks. Alle produktnavne er registrerede varemærker hos deres respektive<br />

producenter.<br />

C.1 Hvornår er man objekt-orienteret?<br />

C<br />

Der skelnes normalt mellem rene OOP-systemer, som er designet objekt-orienteret helt fra<br />

bunden, og mellem en hybrid type system, som "låner" forskellige steder fra og som udbygger<br />

eksisterende sprog med objekt-orienterede faciliteter. De rene sprog har normalt en leksikal<br />

opbygning, der er meget forskellig fra de procedurale sprog. Hvad enten systemet er rent eller ej,<br />

er det ikke en determinant for, om det er et ægte objekt-orienteret system. Simula, som var det<br />

første ægte objekt-orienterede sprog, der blev udviklet i midten af 60'erne, var en udbygning af<br />

Algol.<br />

Spørgsmålet om hvilken type objekt-orienteret system er det bedste er kontroversielt. De fleste<br />

tilhængere af de rene sprog er utilfredse med hybridernes sammensætning og mener ikke, man<br />

kan klistre en OOP-overbygning på for eksempel procedurale sprog. Den anden vej rundt går<br />

argumentet på, at de rene sprog oftest er fortolkede, og derfor for langsomme til "rigtige<br />

programmer". Situationen er da også den, at de rene sprog mest bruges i forskninssammenhæng<br />

mens hybriderne bruges i produktion og udvikling. Smalltalk og LISP er eksempler på fortolkede<br />

sprog. Fortolkede sprog har større objekt-orienterede muligheder, fordi et subsystem konstant kan<br />

kontrollere polymori. Der er dermed ingen overstættertid - programmet kan eksekveres<br />

umiddelbart - og der er større muligheder for fejlfinding og hjælp på kørselstidspunktet.<br />

En ulempe ved de rene OOP-sprog er, at det tager tid at lære syntaksen og opbygningen at<br />

kende, fordi sproget sandsynligvis følger en helt anden fremgangsmåde end de mere udbredte. De<br />

tvinger også programmøren op på den objekt-orienterede hest. Spørgsmålet går altså også på, om<br />

man som programmør ønsker frihed til at vælge eller blande paradigmer. Friheden gør trods alt<br />

programmerne mindre objekt-orienterede. C++ er et hybrid-sprog, fordi det skal være<br />

kompatibelt med C, og fordi det skal indeholde statisk binding.<br />

Ud over de følgende beskrivelser af andre systemer kan nævnes LOOPS (en LISP-variant),<br />

A.3 Parameteriserede typer 555


Objective-C (en anden hybrid med C som fundament), Clu, ML, Simula og til dels Ada.<br />

C.2 Pascal-varianter<br />

I 1988 lanceredes to udvidede Pascal-systemer med objekt-orienterede overbygninger til MS-<br />

DOS systemer. Microsoft og Borland brugte hver sin implementation af et simpelt klassesytem til<br />

indkapsling af data og funktioner. Microsofts QuickPascal låner fra Smalltalk mens Borlands<br />

Turbo Pascal låner fra C++.<br />

Disse sprog er, hvad man kan kalde minimale OOP-sprog, fordi de understøtter netop de basale<br />

krav til et sådant system: indkapsling, arv og polymorfi. De har ingen mulighed for<br />

parametisering, adgangskontrol, skopkontrol, underforstået konstruktion og destruktion, multipel<br />

arv, abstrakte klasser eller overstyring. De er imidlertid gode til at lære grundprincipperne i<br />

objekt-orienteret programmering og programkonstruktion, og har, på grund af Pascal-sprogets<br />

læresprogsagtige syntaks og omfang, en enklere og mere læselig definition af klasser (som iøvrigt<br />

i disse sprog kaldes objekttyper).<br />

Her er et eksempel på en klasseerklæring i Turbo Pascal:<br />

type<br />

obj = OBJECT<br />

constructor init (i: integer); { konstruktør }<br />

destructor down; { destruktør }<br />

procedure doit (var j: integer); { medlemsprocedure }<br />

integer value;<br />

end;<br />

{ medlemsdata }<br />

C.3 Eiffel<br />

Eiffel er et dedikeret sprog til professionel softwareudvikling, som har sin baggrund fra et seriøst<br />

forskningsprojekt, ledet af Bertrand Meyer. Eiffel er et rent objekt-orienteret system, men er trods<br />

det ikke et fortolket sprog. Systemet har visse udvidede sprogunderstøttede faciliteter så som<br />

begrebet forudsætninger (preconditions) og følgevirkninger (postconditions), en status i alle<br />

objekter og en bedre definitiona af multipel arv end den, der findes i andre objekt-orienterede<br />

systemer. For eksempel kan der i Eiffel defineres to abstrakte klasser, som med en meget simpel<br />

multipel afledning kan samles i en virkende klasse. Abstrakte klasser hedder i Eiffel afventende<br />

klasser (deferred classes) og deres ikke-implementerede virtuelle funktioner afventende rutiner<br />

(deferred routines). Eiffel indeholder fuld support for parametiserede typer og for omdøbning af<br />

tvetydige navne.<br />

556 Fremtidsudsigter A.0


Her er et eksempel med en hægtet liste fra Eiffel:<br />

class LINKED_LIST [T] -- parametiseret af T<br />

inherit -- arver fra LIST<br />

LIST [T]<br />

redefine first -- omdøbning<br />

feature<br />

first: T; -- indkapslede data<br />

first_element: LINKABLE [T];<br />

active, previous, next: like first_element;<br />

value: T is -- en rutine<br />

require<br />

not offleft;<br />

not offright;<br />

-- forudsætninger...<br />

do -- følgevirkninger...<br />

Result := active.value;<br />

end;<br />

C.4 Smalltalk<br />

Smalltalk er mere end et objekt-orienteret sprog, det er et komplet objekt-orienteret<br />

udviklingsmiljø. Det er designet hos Xerox i deres Palo Alto Research Center (PARC) med<br />

Adele Goldberg som banekvinde og er en slags gudfar for de fleste OOP-systemer (og en masse<br />

andet: Apple Lisa og Macintosh's brugergrænseflade, som igen blev inspiration for Windows,<br />

GEM, X osv., var en løs implementation af Smalltalk-miljøet). Dette miljø tjener både som<br />

udviklingsmiljø og som brugergrænseflade. Det er i sig selv objekt-orienteret med vinduer, popup<br />

menuer og alle de kendte små ting og sager på den grafiske skærm. Smalltalk er skrevet i<br />

Smalltalk, og et komplet klassebibliotek, som også har været inspiration for talrige C++<br />

klassebiblioteker, er også en standard del af systemet.<br />

Smalltalk er et fortolket sprog, hvilket gør det meget fleksibelt, men gør det også begrænset i<br />

anvendelse for kommerciel softwareudvikling. Flyrbarheden er lav, fordi Smalltalk-miljøet ikke<br />

findes til ret mange maskintyper og operativsystemer, dog både UNIX, OS/2 Presentation<br />

Manager, MS-Windows og MS-DOS.<br />

Her er et uddrag af en Smalltalk-klasse:<br />

class ObjectStream<br />

superclass Object<br />

instance variables stream alreadyTraced<br />

instance creation<br />

A.3 Parameteriserede typer 557


pathName: aString<br />

^self new pathName: aString<br />

instance initialization<br />

pathName: aString<br />

stream := File pathName: aString<br />

querying<br />

atEnd<br />

^stream atEnd<br />

initializeTracing "konstruktør"<br />

alreadyTraced := IdentityDictionary new<br />

finalizeTracing "destruktør"<br />

alreadyTraced := nil<br />

Klassebiblioteket XCL i <strong>kapitel</strong> 7 er løst inspireret af standardbiblioteket i Smalltalk-miljøet, da<br />

klasser som Dictionary, Object, Sortable, Container og så videre ligger i dette.<br />

C.5 Actor<br />

Actor er et meget specialiseret system til udvikling af grafiske applikationer i MS-Windows og<br />

OS/2, og lægger sig tæt op af dette system. Det er en krydsning mellem Pascal og Smalltalk,<br />

benytter kun dynamisk binding og kommer med et færdigt klassebibliotek. Idet MS-Windows og<br />

OS/2 Presentation Manager i sig selv er objekt-orienteret (vinduer, menuer, undermenuer,<br />

rammer, forskellige slags ikoner osv.) er Actor en glimrende ramme for at gøre det komplicerede,<br />

hændelsesdrevne programmeringsarbejde lettere.<br />

558 Fremtidsudsigter A.0


Objekt-Orienteret Ordbog<br />

D<br />

Abstrakt datatype (eng. Abstract Data Type): en datatype, hvis egenskaber og interne<br />

repræsentation i form af data- og kontrolstrukturer samt grænseflade til klienten er defineret og<br />

implementeret af designeren af typen. Også kaldet bruger-defineret datatype eller klasse, da der<br />

er en vis begrebsforvirring i sammenhæng med termet abstrakt klasse.<br />

Abstrakt klasse (eng. Abstract Class): en klasse, der ikke kan instantieres. Grunden til dette kan<br />

være (a) at klassen har beskyttede eller private konstruktører og (b) at klassen indeholder virtuelle<br />

metoder.<br />

ADT, se abstrakt datatype.<br />

Afledt klasse (eng. Decendant Class eller Derived Class): en klasse, der arv egenskaber fra en<br />

baseklasse og kan benytte sig af disse egenskaber som om, egenskaberne var erklæret direkte i<br />

den. Også kaldet en underordnet klasse.<br />

Aktiv klasse: en klasse, der kan instantieres.<br />

Arv (eng. Inheritance): En egenskab i objekt-orienterede sprog, der definerer en klasses direkte<br />

overtagelse af en andet klasses egenskaber til eget brug. Egenskaberne er både data og metoder<br />

samt adgangskriterier for disse.<br />

Baseklasse (eng. Ancestor Object eller Base Object): et objekt fra hvilket en eller flere andre -<br />

afledte - klasse er nedarvet. Baseklassen repræsenterer generelle, fælles træk i de afledte klasser<br />

og kan erstatte dem i de fleste situationer.<br />

Beskyttede medlemmer: den del af klassen, der er utilgængelig for klienten (som må gøre sig<br />

uafhængig af den og gå gennem den offentlige del), men som er tilgængelig for nedarvede klasser<br />

(som har fordel af tilgangen).<br />

Bestandighed (eng. Persistence): et forhold ved et eller flere objekter i et system, som tillader at<br />

systemet ind- og udlæser objektindhold på sekundært lager med eller uden klientens indblanding.<br />

Binding: Den proces, der definerer bestemmelsen af adressen (placeringen) af et bestemt navn i<br />

computerens lager (data- eller funktionsnavn). Dette kan ske på flere forskellige tidspunkter,<br />

A.3 Parameteriserede typer 559


enten af oversætteren under oversættelsen (statisk binding), af programmet selv under kørslen<br />

(dynamisk binding) eller af operativsystemet under administration af virtuelt lager (virtuel<br />

binding). Undertiden kaldes binding alternativt for lænkning, hvis administrationen foretages af<br />

operativsystemet fremfor programmet selv.<br />

Dataabstraktion (eng. Data Abstraction): De teorier, der ligger bag udviklingen og arbejdet med<br />

abstrakte datatyper og indkapsling. Dataabstraktion er i vidt omfang en generel<br />

programmeringsteknik.<br />

Destruktør (eng. Destructor): En bestemt funktion, som er medlem af en klasse. Destruktøren<br />

kaldes implicit ved slutningen af objektets skop, og bruges bla. til deallokeriong af lager.<br />

Dynamisk binding (eng. Dynamic Binding eller Late Binding): Når adressen på et navn (data-<br />

eller funktionsnavn) bestemmes under kørslen af programmet (ved run-time), nærmere bestemt<br />

lige før referencen foretages. Også kaldet sen binding. Det tillader, at programmet har større<br />

frihed i valg af slutadresse end tilfældet er med modstykket, tidlig binding.<br />

Forekomst (eng. Instance): En forekomst er en fysisk tilstedeværende variabel eller objekt af en<br />

bestemt type, for eksempel er float en type og a kan være en forekomst af float efter<br />

deklarationen float a.<br />

Grænseflade (eng. Interface) er den specifikation, en klasse har i den offentlige del, dvs. de<br />

funktioner og data, der er tilgængelige for klienten. Resten af klassens definition (den indkapslede<br />

del) er ikke tilgængelig og klienten tvinges til at være uafhængig af denne. Dermed opnås en<br />

frihed i klassen til at ændre på den interne repræsentation og implementation af henholdsvis data<br />

og metoder.<br />

Indkapsling (eng. Encapsulation) er den teknik, der samler data og funktioner i samme skop og<br />

dermed relaterer dem til hinanden.<br />

Instans, se forekomst.<br />

Instantiering (eng. Instantiation): Den proces at skabe en forekomst af en klasse med et objekt<br />

som resultat. Dette gøres normalt eksplicit med en erklæring, men kan dog også foregå implicit<br />

under returneringer, i sammensatte udtræk og i funktionskald.<br />

Konstruktør (eng. Constructor): En bestemt funktion, som er offentligt medlem af et objekt.<br />

Konstruktøren kaldes implicit ved starten af objektets skop, og bruges til bla. lagerallokation og<br />

initiering. Den sikrer, at der aldrig findes objekter, der ikke er korrekte.<br />

Medlem (eng. Member): En funktion eller en dataerklæring, som står i en klasse, anses for<br />

medlem af klassen.<br />

Metode (eng. Method): En funktion, som tilhører en klasse, dvs. som er defineret indenfor<br />

560 Fremtidsudsigter A.0


klassen deklaration. En metode kaldes med reference til en forekomst af klassen. Den klassiske<br />

terminologi refererer til, at man i teorien "sender et objekt en besked" ved et kald til en metode i<br />

objektets klasse.<br />

Multipel Arv (eng. Multiple Inheritance): Arv, hvor en klasse har mere end én baseklasse og<br />

således kombinerer to eller flere klassers indhold.<br />

Klasse, se abstrakt datatype.<br />

Klient (eng. Client) er den person, der benytter en klasse. Der skelnes på en klient/udbyder-basis<br />

mellem den eller de personer, der skriver en klasse og den eller de, der benytter den i<br />

applikationer eller videreudviklinger/specialiseringer. Klienten kan også kaldes brugeren af<br />

klassen.<br />

Klassehierarki (eng. Class Hierarchy): en acyklisk graf af klasser, nedarvet fra hinanden i to<br />

eller flere lag. Et klassehierarki består af komplekst relaterede klasser, ordnet efter stigende<br />

specialisering. Flere afledte klasser kan arve fra samme baseklasser ligesom en afledt klasse kan<br />

have flere baseklasser.<br />

Modul (også kaldet oversættelsesenhed) er produktet af en enkelt oversættelse. Flere moduler<br />

kan lænkes sammen til et eksekverbart program. Hvert modul indeholder egne private data og<br />

funktioner samt andre, der eksporteres til andre moduler. Moduler kan også samles i biblioteker,<br />

som gør programudviklingen lettere.<br />

Offentlige medlemmer: den del af klassen, som er tilgængelig for klienten, og som udgør<br />

klassens grænseflade.<br />

OODBMS: Forkortelse for Object-Oriented Database Management System.<br />

Overordnet klasse, se baseklasse.<br />

Overstyring (eng. Overloading) er (a) en funktion, som deler navn med andre funktioner med<br />

afvigende parameterlister og (b) en operator, der gives en udvidet semantisk betydning for en<br />

abstrakt datatype.<br />

Polymorfe objekter: Objekter, der indeholder virtuelle metoder og således kan bruges polymorft<br />

i klientens referencer.<br />

Polymorfi (eng. Polymorphism, af græsk, mange former). En egenskab ved objekt-orienteret<br />

programmering, der via arv, virtuelle metoder og dynamisk binding tillader, at samme syntaks og<br />

reference kan bruges på forekomster af objekter af forskellige typer, så længe de har et fælles<br />

overordnet objekt. Ofte (fejlagtigt) kaldet "polymorfisme" på dansk.<br />

Private medlemmer: den del af klassen, som ikke kan ses af klienten grundet en erklæring i<br />

A.3 Parameteriserede typer 561


klassens definition. De private medlemmer er skjult fra klienten.<br />

Sen binding, se dynamisk binding.<br />

Statisk binding (eng. Static Binding eller Early Binding): Når adressen på et navn (data- eller<br />

funktionsnavn) bestemmes på oversættertidspunktet og skrives statisk i objektkoden. Også kaldet<br />

tidlig binding.<br />

Statisk klasse: en klasse, der kun indeholder statiske data og metoder, og som ikke er bestemt for<br />

instantiering. En statisk klasse erklæres i reglen også abstrakt.<br />

Skabelon (eng. Template): en klasse eller funktion, som i sig selv ikke er aktiv, og som kun kan<br />

instantieres i en eller flere andre typers kontekst. Også kaldet parameterisering.<br />

Skop (eng. Scope) er det område, et objekt (a) lever i, dvs. fra dets konstruktion til dets<br />

destruktion og (b) det område, som objektet er tilgængelig i i referencer. Kaldes undertiden også<br />

for virkefelt.<br />

Strøm (eng. Stream): Det sted, hvorfra data kommer eller går. Traditionelt er en inputstrøm de<br />

konceptuelle inddata til et program, mens outputstrømmen er uddata. Disse begreber indkapsles i<br />

C++ i dedikerede strømklasser til standard-, filbaseret og andet I/O.<br />

Tidlig binding, se statisk binding.<br />

Underordnet objekt, se afledt klasse.<br />

Virtuel metode (eng. Virtual Method eller Virtual Function): En metode i en klasse, hvis binding<br />

kan være dynamisk. Et kald til en virtuel funktion i en baseklasse vil i realiteten blive<br />

omdirrigeret til en implementation af funktionen i en afledt klasse, såfremt objektet, den kaldes<br />

for, er af en anden (afledt) type. I C++ findes desuden rene virtuelle metoder, som ikke har en<br />

implementation.<br />

Virtuel klasse (eng. Virtual Class) er en overordnet klasse i et multipel afledt objekt, som kun<br />

forekommer én gang i forekomster af klassen skønt den findes to steder i klassens arvemateriale.<br />

562 Fremtidsudsigter A.0


Bibliografi<br />

[Aho 83] A.V. Aho, J.E. Hopcroft og J.D. Ullman: Data Structures and Algorithms.<br />

Addison-Wesley, 1983.<br />

[ANSI 89] ANSI: Standard for the Programming Language C, American National<br />

Standards Institute 1989.<br />

[Barnes 82] J.G.P. Barnes: Programming in Ada. Addison-Wesley, 1982.<br />

[Berry 88] John Berry: C++ Programming. The Waite Group, 1988.<br />

[Beck 89] Kent Beck og Ward Cunningham: A Laboratory for Teaching Object-<br />

Oriented Thinking, i SIGPLAN Notices, oktober 1989.<br />

[Booch 91] Grady Booch: Object-Oriented Design with Applications,<br />

Benjamin/Cummings 1991.<br />

[Coggins 89] James M. Coggins og Gregory Bollela: Managing C++ Libraries,<br />

SIGPLAN Notices, juni 1989.<br />

[Coplien 92] James O. Coplien: Advanced C++ Programming Styles and Idioms,<br />

Addison-Wesley 1992.<br />

[Dijkstra 68] Edsgar W. Dijkstra: Go To Statement Considered Harmful. i<br />

Communications of the ACM, marts 1968.<br />

[Ellis 90] Margaret A. Ellis og Bjarne Stroustrup: The Annotated C++ Reference<br />

Manual. Addison-Wesley, 1990.<br />

[Fisher 91] Charles N. Fischer og Richard J. LeBlanc, Jr.: Crafting a Compiler with<br />

C. Benjamin/Cummings Publishing Company, 1991.<br />

[Goldberg 83a] Adele Goldberg og David Robson: Smalltalk-80 The Language and Its<br />

Implementation. Addison-Wesley, 1983.<br />

[Goldberg 83b] Adele Goldberg: The Influence of an Object-Oriented Language on the<br />

Programming Environment, i Proceedings of the 1983 ACM Computer<br />

Science Conference, februar 1983, Florida.<br />

A.3 Parameteriserede typer 563<br />

E


[Gorlen 90] Keith E. Gorlen, Sanford M. Orlow og Perry S. Plexico: Data Abstraction<br />

and Object-Oriented Programming in C++. John Wiley & Sons, 1990.<br />

[Gorlen 87] Keith Gorlen: An Object-Oriented Class Library for C++ Programs.<br />

Software Practice and Experience, december 1987.<br />

[Horowitz 78] Ellis Horowitz og Sartaj Sahni: Fundamentals of Computer Algorithms.<br />

Computer Science Press, 1978.<br />

[Horowitz 84] Ellis Howrowitz and John Munson; An Expansive View of Reusable Software, i<br />

IEEE Trans. on Software Engineering, 5. 1984.<br />

[Johnson 88] Ralph Johnson og Brian Foote: Designing reusable classes in C++.<br />

Journal of Object-Oriented Programming, juni/juli 1988.<br />

[Kernighan 88] Brian W. Kernighan og Dennis M. Richie: The C Programming Language, 2.<br />

edition. Prentice-Hall 1988 (ANSI C).<br />

[Kirkpatrick 81] S. Kirkpatrick og E. Stoll: A Very Fast Shift-Register Random Number<br />

Generator, i Journal of Computational Physics, 40, 1981.<br />

[Kirslis 88] Peter A. Kirslis: A Style for Writing C++ Classes, AT&T, Denver,<br />

Colorado 1988.<br />

[Koenig 88] Andrew Koenig: An Example of Dynamic Binding in C++. Journal of<br />

Object-Oriented Programming, august/september 1988.<br />

[Koenig 89a] Andrew Koenig og Bjarne Stroustrup: Exception Handling for C++.<br />

Proceedings of the C++ Conference, Massachusetts, november 1989.<br />

[Koenig 89b] Andrew Koenig og Bjarne Stroustrup: C++: As Close as Possible to C -<br />

But No Closer, i C++ Report, juli 1989.<br />

[Ladd 90] Scott Robert Ladd: C++ Techniques and Applications. M&T Books<br />

1990.<br />

[Lippman 88] Stanley Lippman og Barbara Moo: C++: From Research to Practice.<br />

Proceedings of the USENIX C++ Conference, Colorado, oktober 1988.<br />

[Lippman 89] Stanley B. Lippman: C++ Primer. Addison-Wesley, 1989.<br />

[Lippman 89b] Stanley B. Lippman: What is this? The C++ Report, marts 1989.<br />

[Lippman 91] Stanley B. Lippman: C++ Primer, 2nd. edition, Addison-Wesley 1991.<br />

[Liskov 88] Barbara Liskov: Data Abstraction and Hierarchy, i SIGPLAN Notices,<br />

maj 1988.<br />

[Meyer 88] Bertrand Meyer: Object-Oriented Software Construction. Prentice-Hall<br />

1988.<br />

[Pratt 85] Terrence W. Pratt: Programming Languages: Design and<br />

Implementation, 2nd. edition. Prentice-Hall 1985.<br />

[Russo 88] V. F. Russo og S. M. Kaplan: A C++ Interpreterfor Scheme, i<br />

Proceedings of the C++ Workshop, oktober 1988.<br />

[Sedgewick 88] Robert Sedgewick: Algorithms, 2nd. edition. Addison-Wesley 1988.<br />

[Snyder 86] Alan Snyder: Encapsulation and Inheritance in Object-Oriented<br />

Programming Languages, i SIGPLAN Notices, november 1986.<br />

[Strang 86] John Strang: Programming with Curses, Reilly & Associates 1986.<br />

[Stroustrup 86a] Bjarne Stroustrup: The C++ Programming Language. Addison-Wesley<br />

1986.<br />

[Stroustrup 86b] Bjarne Stroustrup: Adding Classes to C: An Exercise in Language<br />

564 Fremtidsudsigter A.0


Evolution. Software Practice & Experience, august 1986.<br />

[Stroustrup 87a] Bjarne Stroustrup: Multiple Inheritance for C++. Proceedings of the<br />

EUUG Conference, Helsinki, maj 1987.<br />

[Stroustrup 87b] Bjarne Stroustrup: What Is Object-Oriented Programming? Proceedings<br />

of the USENIX C++ Workshop, Santa Fe, november 1987.<br />

[Stroustrup 87c] Bjarne Stroustrup: The Evolution of C++ 1985 to 1987. Proceedings of<br />

the USENIX C++ Workshop, Santa Fa, november 1987.<br />

[Stroustrup 87d] Bjarne Stroustrup: Type-safe Linkage for C++. Proceedings of the<br />

USENIX C++ Workshop, Santa Fe, november 1987.<br />

[Stroustrup 88] Bjarne Stroustrup: Parameterized Types for C++. Proceedings of the USENIX<br />

C++ Conference, Colorado, oktober 1988.<br />

[Stroustrup 91] Bjarne Stroustrup: The C++ Programming Language, 2nd. Edition, Addison-<br />

Wesley 1991.<br />

[Tracz 88a] William Tracz: Software Reuse: Emerging Technology, IEEE Computer<br />

Society Press 1988.<br />

[Tracz 88b] William Tracz: Software Reuse Myths, i ACM SIGSOFT (Software<br />

Engineering Notes), januar 1988.<br />

[Ungar 87] David Ungar og Randall Smith: Self: The Power of Simplicity, i<br />

SIGPLAN Notices, december 1987.<br />

[Whitehead 10] A. N. Whitehead og B. A. Russell: Principia Mathematica, Cambridge<br />

University Press 1910.<br />

[Wirth 76] Niklaus Wirth: Algorithms + Data Structures = Programs. Prentice-Hall<br />

1976.<br />

[Wirth 82] Niklaus Wirth: Programming in Modula-2. Springer-Verlag, 1982.<br />

A.3 Parameteriserede typer 565


Kildetekst<br />

// array.h<br />

//<br />

// specifikation på statisk allokeret klasse for gruppering<br />

// af objekter.<br />

//<br />

// (c) 1991, 1992 Maz Spork<br />

#if !defined _ARRAY_H<br />

#define _ARRAY_H<br />

#include "containr.h"<br />

#include "genlist.h"<br />

class Array : public Container,<br />

public GenericArray {<br />

public:<br />

DECLARE_TRIVIAL_FUNCTIONS (Array)<br />

Array (const unsigned long = DEFAULT_CAP);<br />

Array (const Array&);<br />

virtual ~Array ();<br />

virtual void Insert (Object*);<br />

virtual Object* Retrieve ();<br />

virtual const Object* getCurrent () const;<br />

virtual const Object* getFirst () const;<br />

virtual const Object* getNext () const;<br />

virtual const Object& operator[]<br />

(const unsigned long) const;<br />

virtual Array operator+ (const Array&);<br />

virtual Object*& operator[] (const unsigned long);<br />

virtual void printOn (ostream& = cout) const;<br />

virtual void dumpOn (ostream& = cout) const;<br />

virtual void readFrom (istream& = cin);<br />

virtual bool isEqual (const Object&) const;<br />

};<br />

#endif<br />

566 Fremtidsudsigter A.0<br />

F


assoc.h<br />

//<br />

// specifikation af en associations-klasse<br />

//<br />

// (c) 1991, Maz Spork<br />

#if !defined _ASSOC_H<br />

#define _ASSOC_H<br />

#include "object.h"<br />

class Association : public Object {<br />

public:<br />

Object* Key;<br />

Object* Value;<br />

DECLARE_TRIVIAL_FUNCTIONS (Association)<br />

Association () : Key (0), Value (0) { }<br />

Association (Object& a, Object& b)<br />

: Key (&a), Value (&b) { }<br />

Association (Association&);<br />

virtual ~Association () { delete Key; delete Value; }<br />

virtual void printOn (ostream& = cout) const;<br />

virtual void dumpOn (ostream& = cout) const;<br />

virtual void readFrom (istream& = cin);<br />

virtual bool isEqual (const Object&) const;<br />

};<br />

#endif<br />

A.3 Parameteriserede typer 567


complex.h<br />

//<br />

// specifikation af en kompleks numerisk klasse<br />

//<br />

// (c) 1991, Maz Spork<br />

#if !defined _COMPLEX_H<br />

#define _COMPLEX_H<br />

#include "sortable.h"<br />

class Complex : public Sortable {<br />

double re, im;<br />

public:<br />

DECLARE_TRIVIAL_FUNCTIONS (Complex)<br />

Complex () : re (0), im (0) { }<br />

Complex (const Complex& other)<br />

: re (other.re), im (other.im) { };<br />

Complex (const double& r, const double& i = 0)<br />

: re (r), im (i) { }<br />

virtual ~Complex () { }<br />

virtual void printOn (ostream& = cout) const;<br />

virtual void dumpOn (ostream& = cout) const;<br />

virtual void readFrom (istream& = cin);<br />

virtual bool isEqual (const Object&) const;<br />

virtual bool isGreater (const Sortable&) const;<br />

friend double abs (const Complex&);<br />

friend double norm (const Complex&);<br />

friend double real (const Complex&);<br />

friend double imag (const Complex&);<br />

friend Complex operator+<br />

(const Complex&, const Complex&);<br />

friend Complex operator-<br />

(const Complex&, const Complex&);<br />

friend Complex operator*<br />

(const Complex&, const Complex&);<br />

friend Complex operator/<br />

(const Complex&, const Complex&);<br />

operator double () { return norm (*this); }<br />

};<br />

#endif<br />

568 Fremtidsudsigter A.0


containr.h<br />

//<br />

// specification på abstrakt klasse for gruppering<br />

// af objekter<br />

//<br />

// (c) 1991, Maz Spork<br />

#if !defined _CONTAINR_H<br />

#define _CONTAINR_H<br />

#include "object.h"<br />

#include "contbase.h"<br />

const DEFAULT_CAP = 64; // underforstået størrelse<br />

class Container : public Object,<br />

public virtual ContainerBase {<br />

friend Iterator;<br />

protected:<br />

Container (unsigned long size = 0)<br />

: ContainerBase (size) { }<br />

Container (const Container& other)<br />

: ContainerBase (other), Object (other) { }<br />

public:<br />

virtual ~Container () { }<br />

virtual void Insert (Object*) = 0;<br />

virtual Object* Retrieve () = 0;<br />

virtual unsigned Size () const {<br />

return ContainerBase::elementCount;<br />

}<br />

virtual Container& operator+= (const Container&);<br />

virtual const Object& operator[]<br />

(const unsigned long) const = 0;<br />

virtual const Object* getFirst () const = 0;<br />

virtual const Object* getNext () const = 0;<br />

virtual const Object* getCurrent () const = 0;<br />

virtual void dumpOn (ostream& = cout) const;<br />

virtual void readFrom (istream& = cin);<br />

};<br />

#endif<br />

A.3 Parameteriserede typer 569


contbase.h<br />

//<br />

// baseklasse for alle containere af alle typer<br />

//<br />

// (c) 1992, Maz Spork<br />

#if !defined (_CONTBASE_H)<br />

#define _CONTBASE_H<br />

class ContainerBase {<br />

protected:<br />

unsigned long elementCount; // størrelse på container<br />

unsigned long currentIndex; // aktuelt index i container<br />

ContainerBase (unsigned long size)<br />

: elementCount (size), currentIndex (0) { }<br />

ContainerBase ()<br />

: elementCount (0), currentIndex (0) { }<br />

ContainerBase (const ContainerBase& other)<br />

: elementCount (other.elementCount), currentIndex (0) {}<br />

};<br />

#endif<br />

570 Fremtidsudsigter A.0


dequeue.h<br />

//<br />

// implementation af en generel dobbelthægtet kø<br />

//<br />

// (c) 1991, Maz Spork<br />

#if !defined _DEQUEUE_H<br />

#define _DEQUEUE_H<br />

#include "queue.h"<br />

class DeQueue : public Queue {<br />

public:<br />

DECLARE_TRIVIAL_FUNCTIONS (DeQueue)<br />

DeQueue () : Queue () { };<br />

DeQueue (DeQueue& q) : Queue (q) { };<br />

virtual void insertFront (Object* o) { Insert (o); }<br />

virtual Object* retrieveBack () { return Retrieve (); }<br />

};<br />

#endif<br />

A.3 Parameteriserede typer 571


dict.h<br />

//<br />

// specifikation af en katalog-klasse<br />

//<br />

// (c) 1991, Maz Spork<br />

#if !defined _DICT_H<br />

#define _DICT_H<br />

#include "linklist.h"<br />

#include "assoc.h"<br />

class Dictionary : public LinkedList {<br />

public:<br />

DECLARE_TRIVIAL_FUNCTIONS (Dictionary)<br />

Dictionary () : LinkedList () { }<br />

Dictionary (Dictionary& from) : LinkedList (from) { }<br />

virtual void addAssociation (Object&, Object&);<br />

virtual Object* getAssociation (Object&);<br />

};<br />

#endif<br />

572 Fremtidsudsigter A.0


dstring.h<br />

//<br />

// specification på en streng-klasse<br />

//<br />

// (c) 1991, Maz Spork<br />

#if !defined _STRING_H<br />

#define _STRING_H<br />

#include "sortable.h"<br />

class String : public Sortable {<br />

protected:<br />

char* sptr;<br />

unsigned len;<br />

public:<br />

DECLARE_TRIVIAL_FUNCTIONS (String)<br />

String () : sptr (NULL), len (0) { }<br />

String (const char*);<br />

String (const String&);<br />

virtual ~String ();<br />

virtual bool isGreater (const Sortable&) const;<br />

virtual bool isEqual (const Object&) const;<br />

virtual void printOn (ostream& = cout) const;<br />

virtual void dumpOn (ostream& = cout) const;<br />

virtual void readFrom (istream& = cin);<br />

String& operator= (const String&);<br />

operator const char* const () const { return sptr; }<br />

unsigned Length () const { return len; }<br />

bool operator () (const String) const;<br />

String toLower () const;<br />

String toUpper () const;<br />

void Set (const char);<br />

friend String operator+ (const String&, const String&);<br />

};<br />

#endif<br />

A.3 Parameteriserede typer 573


genlist.h<br />

//<br />

// specifikation på<br />

// en generisk hægtet liste, implementeret som skabelon,<br />

// og en generisk lineær liste, implementeret som<br />

// skabelon.<br />

//<br />

// (c) 1992, Maz Spork<br />

#include "contbase.h"<br />

template <br />

class GenericList : public virtual ContainerBase {<br />

public:<br />

struct GenericNode { // privat struktur<br />

GenericNode* next;<br />

Element* objptr;<br />

GenericNode (Element* obj)<br />

: objptr (obj), next (0) { }<br />

};<br />

protected:<br />

GenericNode* head;<br />

GenericNode* cursor; // aktuel hægte<br />

GenericNode* trail; // følger cursor<br />

GenericNode* tail;<br />

void insertAt (Element*, GenericNode*);<br />

GenericList ()<br />

: head (0), cursor (0), trail (0), tail (0) { }<br />

~GenericList ();<br />

void Insert (Element*);<br />

Element* Retrieve ();<br />

Element* getCurrent () const {<br />

return cursor->objptr;<br />

}<br />

Element* getFirst () {<br />

trail = 0, cursor = head, currentIndex = 0;<br />

return cursor->objptr;<br />

}<br />

Element* getNext () {<br />

if (!cursor || !cursor->next) return 0;<br />

trail = cursor,<br />

cursor = cursor->next, currentIndex++;<br />

return cursor->objptr;<br />

}<br />

574 Fremtidsudsigter A.0


};<br />

template <br />

class GenericArray : public virtual ContainerBase {<br />

protected:<br />

Element** rep;<br />

GenericArray (const unsigned long);<br />

~GenericArray ();<br />

const Element* getCurrent () const {<br />

return rep [currentIndex];<br />

}<br />

const Element* getFirst () {<br />

return rep [currentIndex = 0];<br />

}<br />

const Element* getNext () {<br />

return rep [++currentIndex];<br />

}<br />

};<br />

// prototype nødvendig for følgende typedef's<br />

class Object;<br />

// anvendelige navne for parametiserede typer<br />

typedef GenericList ObjList;<br />

typedef GenericList::GenericNode ObjNode;<br />

// destruktør for generisk liste, fjerner hægter og<br />

// objekter<br />

template <br />

GenericList::~GenericList () {<br />

for (GenericNode* p = head; p;) {<br />

GenericNode* temp = p;<br />

p = p->next;<br />

delete temp->objptr;<br />

delete temp;<br />

}<br />

}<br />

// indsæt element i listen på given hægteposition<br />

template // protected medlemsfunktion<br />

void GenericList::insertAt (<br />

Element* elem, GenericNode* pos) {<br />

ObjNode* temp = cursor;<br />

cursor = pos; // præservér aktuel position<br />

A.3 Parameteriserede typer 575


}<br />

Insert (elem);<br />

cursor = temp;<br />

// indsæt hægte i listen på "aktuel" cursorposision<br />

template <br />

void GenericList::Insert (Element* elem) {<br />

ContainerBase::elementCount++;<br />

GenericNode* newNode = new GenericNode (elem);<br />

if (head) {<br />

if (cursor)<br />

newNode->next = cursor->next,<br />

cursor->next = newNode,<br />

cursor = newNode;<br />

else<br />

newNode->next = head, head = cursor = newNode;<br />

}<br />

else<br />

cursor = head = newNode, trail = 0;<br />

if (!newNode->next) tail = newNode;<br />

}<br />

// hent fra listen på "aktuel" cursorposition<br />

template <br />

Element* GenericList::Retrieve () {<br />

if (!cursor) return &nil;<br />

elementCount--;<br />

if (trail)<br />

trail->next = cursor->next; // opdater listen<br />

else head = cursor->next;<br />

Element* elem = cursor->objptr;<br />

delete cursor;<br />

cursor = trail ? trail->next : head;<br />

return elem;<br />

}<br />

// medlemsfunktioner i generisk lineær liste<br />

// konstruktør, allokerer et pointerarray<br />

// de individuelle elementer allokeres eller tildeles<br />

// af subklasserne<br />

template <br />

GenericArray::GenericArray<br />

(const unsigned long size)<br />

576 Fremtidsudsigter A.0


: ContainerBase (size) {<br />

rep = new Element* [size];<br />

}<br />

// destruktør, deallokerer array'et<br />

template <br />

GenericArray::~GenericArray () {<br />

delete rep;<br />

}<br />

A.3 Parameteriserede typer 577


iterator.h<br />

//<br />

// specifikation på en klasse til gennemløb af container<br />

//<br />

// (c) 1991, Maz Spork<br />

#if !defined _ITERATOR_H<br />

#define _ITERATOR_H<br />

#include "containr.h"<br />

class Iterator : public Object {<br />

Container* ref;<br />

unsigned long index;<br />

public:<br />

DECLARE_TRIVIAL_FUNCTIONS (Iterator)<br />

Iterator () : index (0), ref (NULL) { }<br />

Iterator (const Iterator& i)<br />

: ref (i.ref), index (i.index) { }<br />

Iterator (Container& c) : ref (&c), index (0) { }<br />

virtual ~Iterator () { }<br />

virtual bool isEqual (const Object&) const;<br />

virtual void printOn (ostream& = cout) const;<br />

const Object& operator* ();<br />

Iterator& operator= (const Iterator&);<br />

Iterator& operator= (Container&);<br />

Iterator& operator= (const unsigned long);<br />

Iterator& operator++ (int); // prefix<br />

Iterator& operator++ (); // postfix<br />

operator bool ();<br />

bool More ();<br />

};<br />

#endif<br />

578 Fremtidsudsigter A.0


linklist.h<br />

//<br />

// specification på en hægtet liste-klasse<br />

//<br />

// (c) 1991, 1992 Maz Spork<br />

#if !defined _LINKLIST_H<br />

#define _LINKLIST_H<br />

#include "genlist.h"<br />

#include "containr.h"<br />

class LinkedList : public GenericList,<br />

public Container {<br />

public:<br />

DECLARE_TRIVIAL_FUNCTIONS (LinkedList)<br />

LinkedList () : GenericList (), Container () { }<br />

LinkedList (const LinkedList&);<br />

LinkedList& operator= (const LinkedList&);<br />

virtual LinkedList operator+ (const LinkedList&);<br />

virtual ~LinkedList () { };<br />

virtual void printOn (ostream& = cout) const;<br />

virtual void dumpOn (ostream& = cout) const;<br />

virtual void readFrom (istream& = cin);<br />

virtual bool isEqual (const Object&) const;<br />

virtual void Insert (Object*);<br />

virtual Object* Retrieve ();<br />

virtual const Object* getCurrent () const {<br />

Object* o = GenericList::getCurrent ();<br />

return o ? o : &nil;<br />

}<br />

virtual const Object* getFirst () const {<br />

Object* o = GenericList::getFirst ();<br />

return o ? o : &nil;<br />

}<br />

virtual const Object* getNext () const {<br />

Object* o = GenericList::getNext ();<br />

return o ? o : &nil;<br />

}<br />

virtual void goTo (const unsigned long);<br />

virtual const Object& operator[] (unsigned long) const;<br />

virtual const unsigned long Locate(const Object&) const;<br />

virtual const Object* Locate(const unsigned long) const;<br />

};<br />

A.3 Parameteriserede typer 579


#endif<br />

580 Fremtidsudsigter A.0


object.h<br />

//<br />

// specifikaion af rod-klassen Object, fejl-klassen Nil<br />

// samt I/O-klassen ObjectManager<br />

//<br />

// (c) 1991, Maz Spork<br />

#if !defined _OBJECT_H<br />

#define _OBJECT_H<br />

#include <br />

// fremad-referencer til de fire klasser i denne fil<br />

class Nil;<br />

class Object;<br />

class classInfo;<br />

class ObjectManager;<br />

// anvendelige navne for afledte typer<br />

typedef int bool; // boolsk værdi<br />

typedef Object* (*PTOM) (); // pointer til funktion<br />

const TRUE = 1;<br />

const FALSE = 0;<br />

// klassebeskrivelser for ikke-abstrakte klasser<br />

enum Class {<br />

aNil, // fejl-objekt<br />

aLinkedList, // hægtet liste<br />

aSortedList, // sorteret hægtet liste<br />

aStack, // stak (først-ind-sidst-ud-buff)<br />

aQueue, // kø (først-ind-først-ud-buffer)<br />

aDeQueue, // dobbelt-endt kø<br />

aArray, // en lineær liste<br />

aComplex, // komplekst tal<br />

aString, // streng<br />

aIterator, // gennemløb af containere<br />

aAssociation, // en association mellem to obj<br />

aDictionary, // et katalog over associationer<br />

};<br />

// statisk klasse til definition af ind- og udlæselige<br />

// objekter<br />

class ObjectManager {<br />

A.3 Parameteriserede typer 581


static classInfo* classList;<br />

public:<br />

~ObjectManager ();<br />

static Object* readPolymorphicObject (istream& = cin);<br />

static bool Announce (char*, PTOM);<br />

};<br />

// rod-objektet for alle klasser<br />

class Object {<br />

protected:<br />

Object () { };<br />

Object (const Object&) { };<br />

public:<br />

virtual ~Object () { };<br />

virtual void printOn (ostream& = cout) const = 0;<br />

virtual void readFrom (istream& = cin);<br />

virtual void dumpOn (ostream& = cout) const;<br />

virtual bool isEqual (const Object&) const = 0;<br />

virtual Class Type () const = 0;<br />

virtual Object& Copy () const = 0;<br />

virtual const char* Name () const = 0;<br />

};<br />

// det "tomme" objekt - bruges i fejl og termineringer (kun<br />

// én forekomst)<br />

extern Nil nil;<br />

// fejl-klasse: bruges til identifikation af ulovligt<br />

// objekt<br />

class Nil : public Object {<br />

public:<br />

Nil () { }<br />

virtual ~Nil () { }<br />

virtual void printOn (ostream& strm = cout) const {<br />

strm > ws;<br />

}<br />

virtual bool isEqual (const Object& o) const {<br />

return bool (&o);<br />

}<br />

virtual Class Type () const { return aNil; }<br />

virtual Object& Copy () const { return nil; }<br />

582 Fremtidsudsigter A.0


virtual const char* Name () const { return "Nil"; }<br />

};<br />

// globale metoder til sammenligning af objekter<br />

bool operator== (const Object&, const Object&);<br />

bool operator!= (const Object&, const Object&);<br />

// globale metoder til udskrivning og indlæsning af<br />

// objektindhold<br />

ostream& operator> (istream&, const Object&);<br />

// makro til registrering af klasser med I/O-kapabilitet<br />

#define DECLARE_AS_STOREABLE(CLASS) \<br />

static Object* Create ## CLASS () { \<br />

return new CLASS; \<br />

} \<br />

static bool init ## CLASS = ObjectManager::Announce ( \<br />

#CLASS, Create ## CLASS \<br />

);<br />

// makro til erklæring/implementation af trivielle<br />

// medlemsfunktioner<br />

#define DECLARE_TRIVIAL_FUNCTIONS(CLASS) \<br />

virtual Class Type () const; \<br />

virtual Object& Copy () const; \<br />

virtual const char* Name () const;<br />

// makro til implementation af trivielle medlemsfunktioner<br />

#define DEFINE_TRIVIAL_FUNCTIONS(CLASS) \<br />

Class CLASS ## ::Type () const { \<br />

return a ## CLASS; \<br />

} \<br />

Object& CLASS ## ::Copy () const { \<br />

return *new CLASS (*this); } \<br />

const char* CLASS ## ::Name () const { \<br />

return #CLASS ; \<br />

}<br />

#endif<br />

A.3 Parameteriserede typer 583


queue.h<br />

//<br />

// specifikation af en generel enkelthægtet kø<br />

//<br />

// (c) 1991, Maz Spork<br />

#if !defined _QUEUE_H<br />

#define _QUEUE_H<br />

#include "linklist.h"<br />

class Queue : public LinkedList {<br />

public:<br />

DECLARE_TRIVIAL_FUNCTIONS (Queue)<br />

Queue () : LinkedList () { };<br />

Queue (Queue& q) : LinkedList (q) { };<br />

virtual void insertBack (Object* o) {<br />

LinkedList::Insert (o);<br />

}<br />

virtual Object* retrieveFront () {<br />

return Retrieve ();<br />

}<br />

};<br />

#endif<br />

584 Fremtidsudsigter A.0


shared.h<br />

//<br />

// specifikation af deleligt objekt mellem flere<br />

// containerklasser<br />

//<br />

// (c) 1991, Maz Spork<br />

#if !defined _SHARED_H<br />

#define _SHARED_H<br />

#include "object.h"<br />

// et "delt" objekt, opnået gennem delegering og filtrering<br />

// af destruktør<br />

class SharedObject : public Object {<br />

Object* ref;<br />

public:<br />

SharedObject (Object* o) : ref (o) { }<br />

SharedObject (Object& o) : ref (&o) { }<br />

virtual ~SharedObject () { }<br />

virtual void printOn (ostream& s = cout) const {<br />

ref->printOn (s);<br />

}<br />

virtual void readFrom (istream& s = cin) {<br />

ref->readFrom (s);<br />

}<br />

virtual void dumpOn (ostream& s = cout) const {<br />

ref->dumpOn (s);<br />

}<br />

virtual bool isEqual (const Object& o) const {<br />

return ref->isEqual (o);<br />

}<br />

virtual Class Type () const { return ref->Type (); }<br />

virtual Object& Copy () const { return ref->Copy (); }<br />

virtual const char* Name () const {<br />

return ref->Name ();<br />

}<br />

};<br />

#endif<br />

A.3 Parameteriserede typer 585


sortable.h<br />

//<br />

// specifikation på et sortérbart objekt<br />

//<br />

// (c) 1991, Maz Spork<br />

#if !defined _SORTABLE_H<br />

#define _SORTABLE_H<br />

#include "object.h"<br />

class Sortable : public Object {<br />

public:<br />

virtual void dumpOn (ostream& = cout) const;<br />

virtual void readFrom (istream& = cin);<br />

virtual bool isGreater (const Sortable&) const = 0;<br />

};<br />

bool operator> (const Sortable&, const Sortable&);<br />

bool operator< (const Sortable&, const Sortable&);<br />

bool operator>=(const Sortable&, const Sortable&);<br />

bool operator


sortlist.h<br />

//<br />

// specifikation på en sorteret liste<br />

//<br />

// (c) 1991, Maz Spork<br />

#if !defined _SORTLIST_H<br />

#define _SORTLIST_H<br />

#include "linklist.h"<br />

#include "sortable.h"<br />

class SortedList : public LinkedList {<br />

public:<br />

enum SortOrder { // indlejret opremsning<br />

AscendingOrder, DescendingOrder, randomOrder<br />

};<br />

private:<br />

SortOrder order;<br />

public:<br />

DECLARE_TRIVIAL_FUNCTIONS (SortedList)<br />

SortedList (SortOrder o = AscendingOrder)<br />

: order (o) { }<br />

SortedList (SortedList& o)<br />

: LinkedList (o), order (o.order) { }<br />

virtual void Insert (Object*);<br />

};<br />

#endif<br />

A.3 Parameteriserede typer 587


stack.h<br />

//<br />

// specifikation på en generel stak (FILO-buffer)<br />

//<br />

// (c) 1991, Maz Spork<br />

#if !defined _STACK_H<br />

#define _STACK_H<br />

#include "linklist.h"<br />

class Stack : public LinkedList {<br />

public:<br />

DECLARE_TRIVIAL_FUNCTIONS (Stack)<br />

Stack () : LinkedList () { };<br />

Stack (Stack& s) : LinkedList (s) { };<br />

void Push (Object* o) { insertAt (o, NULL); }<br />

Object* Pop () { cursor = head; return Retrieve (); }<br />

virtual void Insert (Object* o) { insertAt (o, head); }<br />

};<br />

#endif<br />

588 Fremtidsudsigter A.0


array.cpp<br />

//<br />

// implementation af statisk allokeret klasse for<br />

// gruppering af objekter.<br />

// Array-klassen indeholder pointere til objekter.<br />

//<br />

// (c) 1991, 1992 Maz Spork<br />

#include "array.h"<br />

DECLARE_AS_STOREABLE (Array)<br />

DEFINE_TRIVIAL_FUNCTIONS (Array)<br />

// konstruér en Array med fast størrelse, angivet som<br />

// tvungen parameter<br />

Array::Array (const unsigned long size)<br />

: Container (size), GenericArray (size) {<br />

elementCount = size;<br />

for (unsigned long i = 0; i < elementCount; i++)<br />

rep [i] = &nil;<br />

}<br />

// kopi-konstrutør: Array til Array<br />

Array::Array (const Array& other)<br />

: Container (other),<br />

GenericArray (other.elementCount) {<br />

for (unsigned i = 0; i < elementCount; i++)<br />

rep [i] = &(other.rep [i]->Copy ());<br />

currentIndex = 0;<br />

}<br />

// læg to Array-objekter sammen<br />

Array Array::operator+ (const Array& other) {<br />

Array ret = *this;<br />

ret += other;<br />

return ret;<br />

}<br />

// destruér Array, deallokerer alle objekter ikke lig nil<br />

Array::~Array () {<br />

for (unsigned i = 0; i < elementCount; i++)<br />

if (*rep [i] != nil) delete rep [i];<br />

}<br />

A.3 Parameteriserede typer 589


indsæt et objekt i et Array på en (eventuelt<br />

/// uspecificeret) position<br />

void Array::Insert (Object* o) {<br />

if (*rep [currentIndex] != nil)<br />

delete rep [currentIndex];<br />

rep [currentIndex] = o;<br />

}<br />

// fjern et objekt fra et Array, klienten har nu objektet<br />

Object* Array::Retrieve () {<br />

if (currentIndex >= elementCount) {<br />

cerr = elementCount) return &nil;<br />

return GenericArray::getCurrent ();<br />

}<br />

// næste objekt<br />

const Object* Array::getNext () const {<br />

if (currentIndex + 1 >= elementCount) return &nil;<br />

return GenericArray::getNext ();<br />

}<br />

// første objekt<br />

const Object* Array::getFirst () const {<br />

if (!elementCount) return &nil;<br />

return GenericArray::getFirst ();<br />

}<br />

// bedre syntaks for getCurrent, returnerer konstant<br />

// pointer<br />

const Object& Array::operator[]<br />

(const unsigned long pos) const {<br />

if (pos < elementCount) return *rep [pos];<br />

cerr


eturn nil;<br />

}<br />

// anden version, returnerer reference til pointer, kan<br />

// bruges som lvalue<br />

Object*& Array::operator[] (const unsigned long pos) {<br />

if (pos < elementCount) return rep [pos];<br />

cerr


assoc.cpp<br />

//<br />

// implementation af en associations-klasse<br />

//<br />

// (c) 1991, Maz Spork<br />

#include "assoc.h"<br />

DECLARE_AS_STOREABLE (Association)<br />

DEFINE_TRIVIAL_FUNCTIONS (Association)<br />

// opret association fra anden association<br />

Association::Association (Association& from) {<br />

Key = &from.Key->Copy ();<br />

Value = &from.Value->Copy ();<br />

}<br />

// sammenlign to associationer<br />

bool Association::isEqual (const Object& other) const {<br />

return ((Association&)other).Key == Key &&<br />

((Association&)other).Value == Value;<br />

}<br />

// udskriv en association<br />

void Association::printOn (ostream& strm) const {<br />

strm dumpOn (strm);<br />

}<br />

// indlæs en association<br />

void Association::readFrom (istream& strm) {<br />

Object::readFrom (strm);<br />

Key = ObjectManager::readPolymorphicObject (strm);<br />

Value = ObjectManager::readPolymorphicObject (strm);<br />

}<br />

592 Fremtidsudsigter A.0


complex.cpp<br />

//<br />

// implementation af en kompleks numerisk klasse<br />

//<br />

// (c) 1991, Maz Spork<br />

#include // matematiske funktioner & konstanter<br />

#include "complex.h"<br />

DECLARE_AS_STOREABLE (Complex)<br />

DEFINE_TRIVIAL_FUNCTIONS (Complex)<br />

// abs() er en friend-funktion for at tillade bedre syntaks<br />

double abs (const Complex& c) {<br />

return sqrt (norm (c));<br />

}<br />

// norm er en friend-funktion for at tillade bedre syntaks<br />

double norm (const Complex& c) {<br />

return c.re * c.re + c.im * c.im;<br />

}<br />

// returnér reel del af det komplekse tal (friend-funktion)<br />

double real (const Complex& c) {<br />

return c.re;<br />

}<br />

// returnér imaginær del af det komplekse tal<br />

// (friend-funktion)<br />

double imag (const Complex& c) {<br />

return c.im;<br />

}<br />

// sammenlign to komplekse tal for størrelsesforhold<br />

bool Complex::isGreater (const Sortable& o) const {<br />

Complex& other = (Complex&) o;<br />

return abs (*this) > abs (other);<br />

}<br />

// sammenlign to komplekse tal for lighed<br />

bool Complex::isEqual (const Object& other) const {<br />

return ((Complex&)other).re == re &&<br />

((Complex&)other).im == im;<br />

}<br />

A.3 Parameteriserede typer 593


følgende fire udfører addition, subtraktion,<br />

// multiplikation og division<br />

Complex operator+ (const Complex& c1, const Complex& c2) {<br />

return Complex (c1.re + c2.re, c1.im + c2.im);<br />

}<br />

Complex operator- (const Complex& c1, const Complex& c2) {<br />

return Complex (c1.re - c2.re, c1.im - c2.im);<br />

}<br />

Complex operator* (const Complex& c1, const Complex& c2) {<br />

return Complex (<br />

c1.re * c2.re - c1.im * c2.im,<br />

c1.re * c2.im + c1.im * c2.re<br />

);<br />

}<br />

Complex operator/ (const Complex& c1, const Complex& c2) {<br />

return Complex (<br />

(c1.re * c2.re + c1.im * c2.im) / norm (c2),<br />

(c1.im * c2.re - c1.re * c2.im) / norm (c2)<br />

);<br />

}<br />

// udskriv et komplekst tal<br />

void Complex::printOn (ostream& strm) const {<br />

strm


containr.cpp<br />

//<br />

// implementation af abstrakt klasse for gruppering af<br />

// objekter<br />

//<br />

// (c) 1991, 1992 Maz Spork<br />

#include "containr.h"<br />

// metode til at lægge to containere sammen<br />

Container& Container::operator+= (const Container& other) {<br />

for (const Object* o = other.getFirst ();<br />

*o != nil; o = other.getNext ()) {<br />

Insert (&o->Copy ());<br />

currentIndex++;<br />

}<br />

return *this;<br />

}<br />

// metode til at udlæse en container - udlæser blot tæller<br />

void Container::dumpOn (ostream& strm) const {<br />

Object::dumpOn (strm);<br />

strm elementCount;<br />

}<br />

A.3 Parameteriserede typer 595


dequeue.cpp<br />

//<br />

// specifikation på en generel dobbelt kø (med hul i<br />

// begge ender)<br />

//<br />

// (c) 1991, Maz Spork<br />

#include "dequeue.h"<br />

DECLARE_AS_STOREABLE (DeQueue)<br />

DEFINE_TRIVIAL_FUNCTIONS (DeQueue)<br />

596 Fremtidsudsigter A.0


dict.cpp<br />

//<br />

// implementation af en katalog-klasse, indeholdende<br />

// associationer<br />

// mellem objekter.<br />

//<br />

// (c) 1991, Maz Spork<br />

#include "dict.h"<br />

DECLARE_AS_STOREABLE (Dictionary)<br />

DEFINE_TRIVIAL_FUNCTIONS (Dictionary)<br />

// tilføj en association: et opslagsobjekt og<br />

// en associeret værdi<br />

void Dictionary::addAssociation (Object& a, Object& b) {<br />

Insert (new Association (a, b));<br />

}<br />

// find en association til en given nøgle<br />

Object* Dictionary::getAssociation (Object& searchKey) {<br />

for (ObjNode* n = head; n; n = n->next) // lineært!<br />

if (*((Association*) n->objptr)->Key == searchKey)<br />

return ((Association*) n->objptr)->Value;<br />

return &nil;<br />

}<br />

A.3 Parameteriserede typer 597


dstring.cpp<br />

//<br />

// implementation af an dynamisk strengklasse<br />

//<br />

// (c) 1991, Maz Spork<br />

#include // strengmanipulationer<br />

#include // is...(), to...() osv.<br />

#include "dstring.h"<br />

DECLARE_AS_STOREABLE (String)<br />

DEFINE_TRIVIAL_FUNCTIONS (String)<br />

// opret en String fra C-streng<br />

String::String (const char* s) {<br />

if (s && *s) {<br />

len = strlen (s);<br />

sptr = new char [len + 1];<br />

strcpy (sptr, s);<br />

}<br />

else sptr = NULL, len = 0;<br />

}<br />

// opret en String fra anden String<br />

String::String (const String& other) : len (other.len) {<br />

sptr = new char [len + 1];<br />

strcpy (sptr, other.sptr);<br />

}<br />

// dealloker en String<br />

String::~String () {<br />

if (len) delete sptr;<br />

}<br />

// sammenlign to strenge for størrelse<br />

bool String::isGreater (const Sortable& o) const {<br />

String& other = (String&) o;<br />

return strcmp (sptr, other.sptr) > 0;<br />

}<br />

// sammenlign to strenge for lighed (*helt* ens)<br />

bool String::isEqual (const Object& o) const {<br />

String& other = (String&) o;<br />

return len == other.len && !strcmp (sptr, other.sptr);<br />

598 Fremtidsudsigter A.0


}<br />

// udskriv en String<br />

void String::printOn (ostream& strm) const {<br />

strm


String String::toUpper () const {<br />

String temp = *this;<br />

strupr (temp.sptr);<br />

return temp;<br />

}<br />

// konvertér til små bogstaver<br />

String String::toLower () const {<br />

String temp = *this;<br />

strlwr (temp.sptr);<br />

return temp;<br />

}<br />

// søg efter substreng (fx. "CD" i "ABCDEF")<br />

bool String::operator () (const String searchFor) const {<br />

return strstr (sptr, searchFor) != NULL;<br />

}<br />

// sæt alle tegn i strengen til bestemt værdi<br />

void String::Set (const char c) {<br />

strnset (sptr, c, len);<br />

}<br />

600 Fremtidsudsigter A.0


iterator.cpp<br />

//<br />

// implementation af en klasse til gennemløb af container<br />

//<br />

// (c) 1991, 1992 Maz Spork<br />

#include "iterator.h"<br />

// metode til optælling af et gennemløbs-objekt (postfix)<br />

Iterator& Iterator::operator++ () {<br />

ref->getNext ();<br />

return *this;<br />

}<br />

// metode til optælling af et gennemløbs-objekt (pretfix)<br />

Iterator& Iterator::operator++ (int) {<br />

ref->getNext ();<br />

return *this;<br />

}<br />

// returnér det aktuelle objekt, som gennemløberen peger på<br />

const Object& Iterator::operator* () {<br />

return *ref->getCurrent ();<br />

}<br />

// returnér en sandhedsværdi afhængig af, om enden er nået<br />

bool Iterator::More () {<br />

return index < ref->Size ();<br />

}<br />

// samme som forrige, men bedre syntaks<br />

Iterator::operator bool () {<br />

return More ();<br />

}<br />

// sammenlign to gennemløbere<br />

bool Iterator::isEqual (const Object& other) const {<br />

return index == ((Iterator&) other).index<br />

&& ref == ((Iterator&) other).ref;<br />

}<br />

// udskriv en gennemløber på den aktuelle position<br />

void Iterator::printOn (ostream& strm) const {<br />

strm operator[] (index)<br />

A.3 Parameteriserede typer 601


}<br />


linklist.cpp<br />

//<br />

// implementation af en hægtet liste klasse<br />

//<br />

// (c) 1991, Maz Spork<br />

#include "linklist.h"<br />

DECLARE_AS_STOREABLE (LinkedList)<br />

DEFINE_TRIVIAL_FUNCTIONS (LinkedList)<br />

// at finde en hægte ved lineær traversering af listen<br />

const Object* LinkedList::Locate<br />

(const unsigned long index) const {<br />

unsigned long i = index;<br />

for (ObjNode* c = head; i-- && c; c = c->next);<br />

return c ? c->objptr : &nil;<br />

}<br />

// læg to LinkedList'er sammen<br />

LinkedList LinkedList::operator+<br />

(const LinkedList& other) {<br />

LinkedList ret = *this;<br />

ret += other;<br />

return ret;<br />

}<br />

// find indekset på en specifik hægte ved en nøgle<br />

const unsigned long LinkedList::Locate<br />

(const Object& toFind) const {<br />

unsigned long i = 0;<br />

for (ObjNode* c = head;<br />

*c->objptr != toFind && c;<br />

c = c->next)<br />

i++;<br />

return i;<br />

}<br />

// gå til hægte nr og lad dette være den aktuelle<br />

void LinkedList::goTo (const unsigned long index) {<br />

unsigned long i = index;<br />

for (ObjNode* c = head; i-- && c; c = c->next);<br />

cursor = c ? c : head;<br />

}<br />

A.3 Parameteriserede typer 603


kopi-konstruktør, genererer en reel kopi af<br />

// eksisterende liste<br />

LinkedList::LinkedList (const LinkedList& from)<br />

: Container (from), GenericList () {<br />

for (ObjNode* c = from.head; c; c = c->next)<br />

Insert (&c->objptr->Copy ());<br />

getFirst (); // nulstil ny liste<br />

}<br />

// tildeling af anden liste<br />

LinkedList& LinkedList::operator=<br />

(const LinkedList& from) {<br />

ObjNode* temp, * cursor = head;<br />

while (cursor) {<br />

temp = cursor->next;<br />

delete cursor->objptr;<br />

delete cursor;<br />

cursor = temp;<br />

}<br />

for (cursor = from.head; cursor; cursor = cursor->next)<br />

Insert (&cursor->objptr->Copy ());<br />

getFirst ();<br />

return *this;<br />

}<br />

// indsættelse af et objekt på listen<br />

void LinkedList::Insert (Object* o) {<br />

GenericList::Insert (o);<br />

}<br />

// at fjerne et objekt på listen<br />

Object* LinkedList::Retrieve () {<br />

if (!Size ()) return &nil;<br />

return GenericList::Retrieve ();<br />

}<br />

// indekseret reference i listen, relativt index<br />

// fra første element<br />

const Object& LinkedList::operator[]<br />

(const unsigned long index) const {<br />

604 Fremtidsudsigter A.0


if (head == NULL || index >= elementCount) return nil;<br />

return *Locate (index);<br />

}<br />

// sammenligning af to lister<br />

bool LinkedList::isEqual (const Object& o) const {<br />

bool result;<br />

for (ObjNode* p = head, *q = ((LinkedList&) o).head; p;<br />

p = p->next, q = q->next)<br />

if (*p->objptr != *q->objptr) return 0;<br />

return 1;<br />

}<br />

// udskrivning af liste på strøm<br />

void LinkedList::printOn (ostream& strm) const {<br />

for (ObjNode* p = head; p; p = p->next)<br />

strm objptr next)<br />

p->objptr->dumpOn (strm);<br />

}<br />

// indlæs en hægtet liste<br />

void LinkedList::readFrom (istream& strm) {<br />

Container::readFrom (strm);<br />

int i = elementCount;<br />

for (elementCount = 0; i; i--)<br />

Insert (ObjectManager::readPolymorphicObject (strm));<br />

}<br />

A.3 Parameteriserede typer 605


object.cpp<br />

//<br />

// implementation af rod-klassen Object, fejl-klassen Nil<br />

// og I/O-klassen ObjectManager.<br />

//<br />

// (c) 1991, Maz Spork<br />

#include "object.h"<br />

#include "string.h"<br />

// initiér statiske data i ObjectManager. Tabellen<br />

// createAddress indeholder adresser på funktioner, som<br />

// explicit allokerer objekter af forskellige typer, og<br />

// returnerer en polymorf reference. Det tillader<br />

// ObjectManger at allokere objekter på basis af en<br />

// klasseidentifikation, og derefter kalde<br />

// indlæsningsmetoder i det pågældende objekt.<br />

classInfo* ObjectManager::classList = NULL;<br />

// følgende erklæring sikrer ordentlig oprydning i globale<br />

// data<br />

static ObjectManager cleanup;<br />

// det globale nil-objekt (det 'tomme' objekt)<br />

Nil nil;<br />

// Annoncer nil-objektet som "gennemsigtigt" udlæseligt<br />

static Object* CreateNil () { return &nil; }<br />

static bool initNil = ObjectManager::Announce<br />

("Nil", CreateNil);<br />

// privat klasse til administration af eksplicit allokering<br />

// af objekter<br />

class classInfo {<br />

friend ObjectManager; // bruges kun herfra !<br />

char* Name; // klassens navn<br />

PTOM allocFunc; // klassens allokator<br />

classInfo* next; // næste klasse...<br />

classInfo (char* s, PTOM f, classInfo* n)<br />

: Name (s), allocFunc (f), next (n) { }<br />

};<br />

606 Fremtidsudsigter A.0


udlæsning af et objekt gennem en ostream (uden<br />

// typeinformation)<br />

ostream& operator> (istream& strm, Object& obj) {<br />

obj.readFrom (strm);<br />

return strm;<br />

}<br />

// udlæsning af et objekt genne en ostream (med<br />

// typeinformation)<br />

void Object::dumpOn (ostream& strm) const {<br />

strm


classList = new classInfo<br />

(className, pointer, classList);<br />

return 1;<br />

}<br />

// læs et objekt fra en istream (statisk metode)<br />

Object* ObjectManager::readPolymorphicObject<br />

(istream& strm) {<br />

char className [0x20];<br />

strm >> className;<br />

for (classInfo* c = classList; c; c = c->next)<br />

if (!strcmp (c->Name, className)) break;<br />

if (!c) return &nil;<br />

Object* empty = (c->allocFunc) ();<br />

empty->readFrom (strm);<br />

return empty;<br />

}<br />

// dealloker alle objekter, der blev allokeret med<br />

// statiske "annonceringer"<br />

ObjectManager::~ObjectManager () {<br />

classInfo* cursor = classList;<br />

while (cursor) {<br />

classInfo* temp = cursor;<br />

cursor = cursor->next;<br />

delete temp;<br />

}<br />

}<br />

608 Fremtidsudsigter A.0


queue.cpp<br />

//<br />

// implementation af en generel enkelthægtet kø<br />

//<br />

// (c) 1991, Maz Spork<br />

#include "queue.h"<br />

DECLARE_AS_STOREABLE (Queue)<br />

DEFINE_TRIVIAL_FUNCTIONS (Queue)<br />

A.3 Parameteriserede typer 609


shared.cpp<br />

//<br />

// implementation af deleligt objekt mellem<br />

// containerklasser<br />

//<br />

// (c) 1991, Maz Spork<br />

#include "shared.h"<br />

610 Fremtidsudsigter A.0


sortable.cpp<br />

//<br />

// implementation af abstrakt sortérbart objekt<br />

//<br />

// (c) 1991, Maz Spork<br />

#include "sortable.h"<br />

// kald den virtuelle isGreater()-metode for at undersøge<br />

// forholdet<br />

bool operator> (const Sortable& a, const Sortable& b) {<br />

return a.isGreater (b);<br />

}<br />

// mindre end er det samme som ej større end og ej ens<br />

bool operator< (const Sortable& a, const Sortable& b) {<br />

return ! (a.isGreater (b) || a.isEqual (b));<br />

}<br />

// større end eller lig med (lige ud ad landevejen)<br />

bool operator>= (const Sortable& a, const Sortable& b) {<br />

return a.isGreater (b) || a.isEqual (b);<br />

}<br />

// mindre end eller lig med er det samme som ej større end<br />

bool operator


sortlist.cpp<br />

//<br />

// implementation af en sorteret liste<br />

//<br />

// (c) 1991, Maz Spork<br />

#include "sortlist.h"<br />

DECLARE_AS_STOREABLE (SortedList)<br />

DEFINE_TRIVIAL_FUNCTIONS (SortedList)<br />

// indsæt objekt i listen på den rette plads<br />

// afhængig af sortering<br />

void SortedList::Insert (Object* obj) {<br />

ObjNode* a = head, *b = 0; // after og before<br />

if (order == AscendingOrder)<br />

while (a && *(Sortable*) obj ><br />

*(Sortable*) a->objptr)<br />

b = a, a = a->next;<br />

else // == DescendingOrder<br />

while (a && *(Sortable*) obj <<br />

*(Sortable*) a->objptr)<br />

b = a, a = a->next;<br />

insertAt (obj, b);<br />

}<br />

612 Fremtidsudsigter A.0


stack.cpp<br />

//<br />

// implementation af en generel stak<br />

//<br />

// (c) 1991, Maz Spork<br />

#include "stack.h"<br />

DECLARE_AS_STOREABLE (Stack)<br />

DEFINE_TRIVIAL_FUNCTIONS (Stack)<br />

A.3 Parameteriserede typer 613


# makefile-eksempel for XCL.<br />

# lib er dit biblioteksprogram, cc er din oversætter.<br />

# -c betyder: oversæt uden at lænke.<br />

xcl.lib: array.obj assoc.obj complex.obj containr.obj \<br />

contbase.obj dequeue.obj dict.obj dstring.obj \<br />

iterator.obj linklist.obj object.obj shared.obj \<br />

sortable.obj sortlist.obj stack.obj queue.obj<br />

.cpp.obj:<br />

lib xcl +array +assoc +complex +containr \<br />

+contbase +dequeue +dict +dstring \<br />

+iterator +linklist +object +shared \<br />

+sortable +sortlist +stack +queue<br />

cc -c {$


Stikordsregister<br />

, 112<br />

, 110<br />

, 27<br />

, 246<br />

, 61<br />

, 237<br />

, 62<br />

#define, 16, 85<br />

#elif, 88<br />

#else, 16, 88<br />

#endif, 88<br />

#error, 89<br />

#if, 16, 88<br />

#ifdef, 88<br />

#ifndef, 88<br />

#include, 84<br />

A<br />

Abstrakte<br />

datatyper design af, 226<br />

datatyper anvendelser for, 247<br />

datatyper, 16<br />

klasser, 337<br />

datatyper og fejlbehandling, 234<br />

Abstraktion<br />

af data, 4<br />

funktionel, 3<br />

Abstraktionsmekanismer, 473<br />

Abstraktionsniveauer, 117<br />

Actor, 599<br />

Ada, 14, 118, 134<br />

A.3 Parameteriserede typer 615


Addition, 39<br />

Adgang, 135<br />

Adgangskontrol, 416<br />

Administrative metoder, 411<br />

Adresse, 28, 90<br />

effektiv, 93<br />

Adresserum, 91<br />

ADT, 16<br />

Afhængighed af implementationen, 134<br />

Afstand, 19<br />

Aggregate klasser, 466<br />

Algol, 7<br />

Algoritmers transparens, 439<br />

Alias, 55<br />

Allokering, 82<br />

af vektor, 83<br />

Analyse, 182, 183<br />

af domæne, 471<br />

Anonyme<br />

datastrukturer, 73<br />

klasser, 142<br />

ANSI C++, ii, 7, 573<br />

ANSI C, 7, 51, 106<br />

Anvendelser for ADT'er, 247<br />

Applikation, 3<br />

Applikationsdomæne, 472<br />

Aritmetisk if/else, 46<br />

Aritmetiske operatorer, 39<br />

Arv, 267, 286<br />

og lageradministration, 445<br />

og genbrug, 476<br />

problemer med, 316<br />

rationale, 396<br />

og tvetydigheder, 301<br />

Arvesystemer, 268<br />

ASCII, 27<br />

ASCIIZ, 94<br />

AssocArray, 250<br />

Associativ liste, 250<br />

Associativitet, 47<br />

Attributter, 390<br />

auto, 81<br />

Automatiske variable, 81<br />

616 <strong>Introduktion</strong> 1


B<br />

BCPL, 19<br />

Bearbejdende metoder, 412<br />

Beskyttelse, 134<br />

Bestandige objekter, 456<br />

Bestandighed, 455<br />

administration af, 460<br />

implementation af, 457<br />

Betinget oversættelse, 16, 88<br />

Bibliografi, 607<br />

Biblioteker, 119<br />

Big-endian, 22<br />

Bindingstidspunkter, 321<br />

Bitfelter, 72<br />

Bitvis kopiering, 169, 190<br />

Bitvise operatorer, 40<br />

break, 67<br />

Brugerdefineret<br />

manipulatorer, 506<br />

lageradministration, 237<br />

typekonvertering, 204<br />

Burns, Robert 228<br />

C<br />

C, 134<br />

vs.C++, 583<br />

løsning med, 123<br />

ANSI, 7<br />

Call-by-explicit-reference, 57<br />

Call-by-implicit-reference, 57<br />

Call-by-reference, 53, 107<br />

Call-by-value, 53, 54, 107<br />

case, 68<br />

catch, 574<br />

cerr, 111<br />

cfront, 13<br />

char, 26<br />

cin, 111, 492<br />

class, 136<br />

clog, 111<br />

1.6 Organiseringen af denne bog 617


Cobol, 175, 246<br />

CommonView, 468<br />

Containere gennemløb af, 454<br />

Containerklasser, 448<br />

continue, 67<br />

cout, 111, 490<br />

Crash, 83<br />

CustMemMgr, 446<br />

D<br />

Dangling pointer, 83<br />

Data hiding, 119<br />

Datastrukturer, 70<br />

med pointermedlem, 101<br />

fremad-referencer, 78<br />

indlejrede, 74<br />

anonyme, 73<br />

polymorfe, 333<br />

transparens, 439<br />

Datatyper<br />

abstrakte, 16<br />

fundamentale,16<br />

Dataabstraktion definition, 4<br />

Deallokering af vektorer, 175<br />

default, 70<br />

Dekomposition, 425<br />

Delegering, 407, 473<br />

delete, 82<br />

Delhierarkier, 349<br />

Delte objekter, 445<br />

Dereference, 90<br />

og pointere, 91<br />

af pointer, 55<br />

af medlemsdata, 79<br />

Design<br />

af abstrakte datatyper, 226<br />

balance, 473<br />

kompositorisk, 402<br />

Destruktion af afledte objekter, 307<br />

Destruktør, 164<br />

statiske, 172<br />

618 <strong>Introduktion</strong> 1


i afledte klasser, 308<br />

Differentialprogrammering, 6, 442<br />

Dijkstra, Edsgar, 325<br />

Disk-I/O, 508<br />

Diskriminerende union, 136<br />

Division, 39<br />

Dokumentation, 481<br />

Domæneanalyse, 471<br />

double, 26<br />

Dummy-funktion, 163<br />

Dyadiske operatorer, 39<br />

Dynamisk<br />

lageradministration, 82<br />

binding, 267<br />

binding som indkapslings, 325<br />

strenge, 229<br />

objekter, 174<br />

E<br />

EBCDIC, 27<br />

Effektiv adresse, 93<br />

Eiffel, 597<br />

Eiffel, 468<br />

Ejerforhold i containere, 448<br />

Ekspansion af makroer, 16, 84<br />

Eksplicit typekonvertering, 36, 207<br />

Ekstern lænkning, 80<br />

Eksterne medlemsfunktioner, 129<br />

Ellipsis, 61<br />

Entitetsplanet, 403<br />

enum, 34<br />

Evalueringsrækkefølge, 47, 62<br />

Evolution, 480<br />

Exception handling, 574<br />

extern, 81, 246<br />

F<br />

Fejlbehandling, 574<br />

i abstrakte datatyper, 234<br />

Figur, 278<br />

1.6 Organiseringen af denne bog 619


Figurfortegnelse, xiv<br />

Filbaseret I/O, 112<br />

Filer, 508<br />

Firkant, 278<br />

Flersidet arv, 345<br />

float, 26<br />

Flydende komma, 27<br />

Flytbarhed, 16, 389<br />

for, 66<br />

Formatering af standard-I/O, 496<br />

Fornuft og makroer, 87<br />

Fortegnsoperator, 39<br />

Fortran, 175, 246<br />

Forwarding, 473<br />

Fremad-reference, 51<br />

friend-funktioner, 188<br />

fstream, 507<br />

Funktion, 48<br />

overstyret, 59<br />

kaldte, 50<br />

rekursiv, 59<br />

returværdier fra, 57<br />

pointer til, 99<br />

inline, 62<br />

kald af, 53<br />

kaldende, 50<br />

administrative, 411<br />

som medlemmer, 77<br />

typekonverterende, 206<br />

bearbejdende, 412<br />

status, 412<br />

Funktionider, 443<br />

Funktionsbiblioteker, 388<br />

Funktionskald, 53<br />

Funktionskaldsoperator, 196<br />

overstyring af, 196<br />

G<br />

Garbage collection, 83<br />

Genbrug, 469<br />

vs. software-bjærgning, 470<br />

og granularitet, 477<br />

620 <strong>Introduktion</strong> 1


vs. software-overlevering, 470<br />

og klasser, 387<br />

og standarder, 469<br />

og arv, 476<br />

Genbrugsmekanismer, 469<br />

Generalitet, 424<br />

Genericitet, 423<br />

rationale, 423<br />

Gennemløb af containere, 454<br />

Globale variable, 82<br />

goto, 67<br />

Granularitet, 477<br />

Grænseflade, 119<br />

dualisme, 435<br />

til klienten, 435<br />

til afledte klasser, 437<br />

rationale, 434<br />

blød, 322<br />

H<br />

Headerfiler, 15<br />

inkludering af, 84<br />

organisering af, 81<br />

Heap, 82<br />

Hello World, 154<br />

Hexadecimale konstanter, 33<br />

Hierarkier, 287<br />

I<br />

Identifikation af entiteter, 478<br />

Idiom, 11<br />

if/else, 63<br />

Implementationsafhængighed, 134<br />

Implementationsdomæne, 472<br />

Implicit typekonvertering, 208<br />

Indeksoperatoren<br />

og lvalues, 195<br />

overstyring af, 193<br />

Indkapslede typeerklæringer, 140<br />

1.6 Organiseringen af denne bog 621


Indkapsling, 133, 267<br />

klassebaseret, 138<br />

klasse- vs. objektbaseret, 138<br />

objektbaseret, 138<br />

typebaseret, 138<br />

eksempel, 133<br />

og dynamisk binding, 325<br />

Indlejring, 75<br />

datastrukturer, 74<br />

klasser, 140<br />

Informationsskjul, 119<br />

Inholdsfortegnelse, iv<br />

Initiering<br />

af referencer, 106<br />

vs. tildeling, 170<br />

af objekter, 154<br />

af multipelt afledte objekter, 363<br />

af polymorfe klasser, 335<br />

af statiske medlemmer, 143<br />

med andet objekt, 169<br />

Initieringslister, 161<br />

Initieringsrækkefølge, 366<br />

Inkludering af headerfiler, 84<br />

Inkremental udvikling, 384<br />

inline-funktioner, 62<br />

Instansplanet, 403<br />

int, 26<br />

Intern lænkning, 80<br />

Interne medlemsfunktioner, 129<br />

ios, 496<br />

Isotrope klasser, 320<br />

Iteration af containere, 454<br />

Iterator, 196<br />

K<br />

Kaldekonventioner, 244<br />

Kanonisk klassestruktur, 415<br />

Kildetekst organisering af, 392<br />

Klassebaseret indkapsling, 138<br />

Klassebegrebet, 127<br />

Klassebiblioteker, 464<br />

Klassehierarkier, 287<br />

622 <strong>Introduktion</strong> 1


Klasser<br />

skabeloner, 221<br />

og pointere, 216<br />

samspil mellem, 226<br />

indlejrede, 140<br />

som processer, 439<br />

og genericitet, 387<br />

som byggesten, 350<br />

relationer mellem, 403<br />

og egalitet, 285<br />

anonyme, 142<br />

og skop, 136<br />

kongruens, 397<br />

kendetegn, 390<br />

Klassifikationer, 128<br />

Klient, 4, 119<br />

vs. udbyder, 470<br />

løs, 408<br />

Klog pointer, 185<br />

Kommentarer, 18<br />

Komplekse klasser, 410<br />

Kompleksitetsskjul, 385<br />

Kompositorisk design, 402<br />

Konstante<br />

objekter,146<br />

medlemsfunktioner, 148<br />

medlemsfunktioner og overstyring, 150<br />

pointere, 104, 105<br />

Konstanter<br />

literale, 28<br />

symbolske, 33<br />

oktale, 33<br />

hexadecimale, 33<br />

Konstruktion af afledte objekter, 303<br />

Konstruktører, 155<br />

statiske, 172<br />

og konvertering, 208<br />

i afledte klasser, 305<br />

kopiering, 170<br />

Kontrolstrukturer, 63<br />

Konvertering, 206<br />

implicit, 36<br />

eksplicit, 36<br />

Koordinat, 270<br />

1.6 Organiseringen af denne bog 623


Kopi-konstruktør, 170<br />

Kopiering<br />

af objekter, 169, 190<br />

af strukturer, 72<br />

af polymorfe objekter, 343<br />

medlemsvis, 169<br />

problemer med, 170<br />

Kreativitet, 389<br />

Krop, 48<br />

Kvalificerede navne, 141<br />

L<br />

Lageradministration, 80<br />

og arv, 445<br />

brugerdefineret, 237<br />

Lagringsklasser, 81<br />

Leksikalt skop, 142<br />

Levetid af objekter, 456<br />

Lineær<br />

programmering, 2<br />

arv, 288<br />

semafor, 444<br />

Linie, 278<br />

LinkedList, 247<br />

Liskov, Barbara, 476<br />

Liskovs substitutionsprincip, 476<br />

Litteratur, 607<br />

Little-endian, 22<br />

Logiske operatorer, 41<br />

long, 26<br />

Lvalue, 23<br />

Lænker, 14<br />

Lænkning<br />

type-sikker, 246<br />

ekstern, 80<br />

intern, 80<br />

Løs klient, 408<br />

M<br />

624 <strong>Introduktion</strong> 1


main(), 52<br />

Makroekspansioner, 16, 84<br />

Makroer<br />

og udviklingsmiljøet<br />

og fornuft, 87<br />

Manipulatorer, 500<br />

brugerdefinerede, 506<br />

Medlemmer af strukturer, 71<br />

Medlemmer i funktioner, 77<br />

Medlemsfunktioner, 129<br />

overstyrede, 131<br />

eksterne, 129<br />

private, 138<br />

konstante, 148<br />

interne, 129<br />

Medlemsreferencer, 72<br />

underforståede, 131<br />

Medlemsvariable statiske, 143<br />

Medlemsvis kopiering, 169<br />

Metaklasse, 373<br />

Midlertidige objekter, 192<br />

Mingelering, 60, 245<br />

Modul eksempel 146<br />

Modula-2, 118, 134, 256, 266<br />

Moduler, 15, 139<br />

Modulus, 39<br />

Modulærprogrammering, 4, 388, 442<br />

Monadiske operatorer, 39<br />

MS-DOS, 16, 115<br />

Multipel arv, 345<br />

med relaterede baseklasser, 354<br />

typekonvertering under, 366<br />

med uafhængige baseklasser, 347<br />

tvetydigheder ved, 351<br />

og polymorfi, 369<br />

Målfunktion, 50<br />

N<br />

Navne kvalificerede, 141<br />

Navnemingelering, 245<br />

Nedbrydning<br />

1.6 Organiseringen af denne bog 625


af objekter 154<br />

af multipelt afledte objekter, 363<br />

af polymorfe klasser, 336<br />

Nedtælling, 39<br />

Negation, 40<br />

new, 82<br />

NIHCL, 468<br />

O<br />

Objekt konstant, 146<br />

Objekt-I/O, 242<br />

Objekt-orienterede kendetegn, 265<br />

Objekt-orienteret<br />

rationale, 2<br />

rationale, 265<br />

ordbog, 601<br />

paradigme, 266<br />

terminologi, 268<br />

analyse, 183<br />

indkapsling, 138<br />

Objekter<br />

delte, 445<br />

og ejerforhold, 448<br />

statiske, 172<br />

kopiering af, 169<br />

nedbrydning af, 154<br />

initiering af, 154<br />

midlertidige, 192<br />

vektorer af, 176<br />

kopiering af, 190<br />

levetid, 456<br />

Oktale konstanter, 33<br />

OODBMS, 462<br />

Operator<br />

fortegns, 39<br />

særlig, 45<br />

skift, 40<br />

relationel, 40<br />

, 514<br />

aritmetisk, 39<br />

triadisk, 39<br />

626 <strong>Introduktion</strong> 1


itvis, 40<br />

tildeling, 42<br />

reference, 42<br />

logisk, 41<br />

dyadiske, 39<br />

monadisk, 39<br />

Operator-funktioner, 179<br />

Operatorer<br />

oversigt over, 39<br />

overstyring af, 178<br />

vs. operanter, 21<br />

præcedensproblemer, 261<br />

Opremsninger, 34<br />

Optælling, 39<br />

Ordbog, 601<br />

Organisering<br />

af lager, 22<br />

af kildetekst, 392<br />

af klasser, 395<br />

OS/2 238, 246<br />

Overstyrede<br />

funktioner, 59<br />

medlemsfunktioner, 131<br />

Overstyring<br />

af funktioner vs.<br />

underforståede parametre, 161<br />

og konstante medlemsfunktioner, 150<br />

problemer med, 205<br />

af new per klasse, 239<br />

af delete, 186<br />

af funktionskaldsoperator, 196<br />

af særlige operatorer, 185<br />

rationale, 180<br />

af relationelle operatorer, 183<br />

af operatorfunktioner, 197<br />

af binære operatorer, 183<br />

i traditionelle sprog, 178<br />

retningslinier for, 186<br />

af new, 186<br />

i klasser, 187<br />

af monadiske operatorer, 182<br />

af fortegnsoperatorer, 182<br />

af indeksoperatoren, 193<br />

af operatorer, 178<br />

1.6 Organiseringen af denne bog 627


af tildelings-operatorer, 184<br />

af dyadiske operatorer, 183<br />

i standardbiblioteket, 198<br />

Oversættelse<br />

betinget, 88<br />

Oversættelsesenheder, 15<br />

P<br />

Parameteriserede typer, 218, 476<br />

Parameterisering, 218<br />

eksempler på, 218<br />

Parametre<br />

underforståede, 132<br />

afledte typer, 53<br />

underforståede, 60<br />

i konstante funktioner, 149<br />

referencetype, 107<br />

Pascal, 86, 118, 175, 246, 256<br />

Per-klasse overstyringer, 239<br />

Permanente objekter, 456<br />

Persistens, 455<br />

Pointer, 90<br />

reference til, 107<br />

og genericitet, 99<br />

typekonvertering af, 103<br />

og vektor, 95<br />

initiering af, 91<br />

til medlemmer, 79<br />

i datastrukturer, 101<br />

dereference af<br />

og void, 103<br />

til konstant lager, 104<br />

advarsler, 100<br />

konstant, 105<br />

til medlemsfunktion, 217<br />

til pointer, 98<br />

og nul, 102<br />

til funktion, 99<br />

til medlemsdata, 216<br />

og dereferencer, 91<br />

og virtuelle funktioner, 322<br />

til objekter, 172<br />

628 <strong>Introduktion</strong> 1


Pointeraritmetik, 91<br />

Pointertyper, 28<br />

Polymorfe<br />

objekter kopiering af, 343<br />

klasser nedbrydning af, 336<br />

klasser, 319<br />

containere, 449<br />

klasser initiering af, 335<br />

datastrukturer, 333<br />

Polymorfi<br />

og genbrug, 422<br />

programmørkontrolleret, 461<br />

og virtuelle baseklasser, 376<br />

i detaljer, 330<br />

og multipel arv, 369<br />

rationale, 269<br />

Postfix-optælling, 181<br />

Prefix-optælling, 181<br />

private, 134<br />

Private medlemsfunktioner, 138<br />

Problemer<br />

med arv, 316<br />

med virtuelle baseklasser, 378<br />

med switch, 327<br />

Programeksempler, xvii<br />

Programmering<br />

lineært, 2<br />

struktureret, 3<br />

generelt, 14<br />

med containerklasser, 451<br />

differentialt, 6<br />

objekt-orienteret, 6<br />

modulært, 4<br />

protected, 299<br />

Prototyper, 51<br />

Præcedens, 47<br />

Præcision, 27<br />

Præprocessering, 15, 84<br />

public, 134<br />

Punkt, 278<br />

Q<br />

1.6 Organiseringen af denne bog 629


QuickPascal, 596<br />

R<br />

Random, 254<br />

Reference<br />

transparent, 463<br />

Referenceoperator, 42<br />

Referencer, 29, 106<br />

initiering af, 106<br />

til objekter, 172<br />

underforståede, 131<br />

som parametre, 107<br />

til medlemmer, 72<br />

til pointer, 107<br />

register, 27<br />

Regneoperatorer, 39<br />

Rekomposition, 425<br />

Rekursion, 59<br />

Relationelle operatorer, 41<br />

Relationer mellem klasser, 403<br />

Rene virtuelle metoder, 339<br />

Repræsentation af virkeligheden, 385<br />

Reserverede navne, 20<br />

Returværdier, 57<br />

Reverse engineering, 470<br />

Robust kode, 477<br />

Run-time typeidentifikation, 580<br />

Rvalue, 23<br />

S<br />

Samarbejde med andre sprog, 244<br />

Samspil mellem klasser, 226<br />

Semaforstyring, 119<br />

Semantisk katalog, 429<br />

Semantiske udvidelser, 34<br />

Set, 256<br />

short, 26<br />

signed, 27<br />

Sikkerhed, 127<br />

Simple klasser, 410<br />

630 <strong>Introduktion</strong> 1


Simula, 7<br />

sizeof, 46<br />

Skabeloner, 221<br />

Skalérbare typer, 118<br />

Skifteoperatorer, 40<br />

Skop, 50<br />

og klasser, 136<br />

leksikalt, 142<br />

opløsning, 46<br />

Smalltalk, 14, 266, 597<br />

Softwaredesign definition, 381<br />

Standard-I/O, 16<br />

Standard-input (cin), 112<br />

Standard-output (cout), 111<br />

Standardbiblioteket, 482<br />

static, 81<br />

Statiske<br />

konstruktører, 172<br />

medlemsvariable initiering af, 144<br />

variable, 81<br />

destruktører, 172<br />

objekter, 172<br />

funktioner, 145<br />

medlemsvariable, 143<br />

Statusmetoder, 412<br />

Stream, 110<br />

Stroustrup, Bjarne, 13<br />

strstream, 502<br />

struct, 70, 136<br />

Struktureret programmering, 3<br />

Strøm, 110<br />

Strømme, 16<br />

Strømstatus, 505<br />

Stuktureret analyse, 182<br />

Subtraktion, 39<br />

Svartider, 55<br />

Swift, Johnathan, 22<br />

switch, 68<br />

Særlige operatorer, 45<br />

Sætninger, 21<br />

T<br />

1.6 Organiseringen af denne bog 631


Tabelfortegnelse, xvi<br />

template, 222<br />

Temporære<br />

objekter, 456<br />

forekomster, 193<br />

Terminologi<br />

objekt-orienteret, 268<br />

i denne bog, 9<br />

this, 145, 200<br />

throw, 574<br />

Tildeling vs. initiering, 170<br />

Tildelingsoperator, 42<br />

Tilfældige tal, 254<br />

Tilgang, 135<br />

fra klientkode, 294<br />

Tilgang under arv, 291<br />

Token pasting, 87<br />

Tomme klasser, 410<br />

Top-down analyse, 182<br />

Topologi, 467<br />

Transformation, 270<br />

Translatør, 13<br />

Transparent reference, 463<br />

Triadisk operator, 39<br />

try, 574<br />

Turbo Pascal, 596<br />

Tvetydigheder<br />

under arv, 301<br />

ved multipel arv, 351<br />

Type-cast, 207<br />

Type-sikker lænkning, 246<br />

Type-ækvivalens, 286<br />

Typebaseret indkapsling, 139<br />

typedef, 109<br />

Typeforhold, 141<br />

Typekonvertering, 207<br />

eksplicit, 207<br />

implicit, 208<br />

og konstruktører, 208<br />

i flere led, 209<br />

eksplicit, 36<br />

implicit, 36<br />

brugerdefineret, 204<br />

regler for, 37<br />

632 <strong>Introduktion</strong> 1


af pointere, 103<br />

under arv, 308<br />

under multipel arv, 366<br />

Typer<br />

fundamentale, 26<br />

parameteriserede, 218<br />

pointere, 28<br />

associationer med, 128<br />

brugerdefinerede, 35<br />

skalérbare, 118<br />

vektorer, 28<br />

forhold mellem, 91<br />

referencer, 28<br />

U<br />

Udbyder, 119<br />

Udtryk, 21<br />

Udvidelsesmuligheder, 125<br />

Udvikling, 479<br />

inkremental, 384<br />

miljø, 14<br />

miljø og makroer, 88<br />

Ulovlige pointerreferencer, 185<br />

Underforståede<br />

parametre i konstruktører, 160<br />

parametre, 60<br />

medlemsreferencer, 131<br />

parametre, 132<br />

værdier i objekter, 211<br />

union, 72, 136<br />

diskriminerende, 136<br />

UNIX, 16, 115<br />

M4, 424<br />

unsigned, 28<br />

V<br />

Variable<br />

automatiske, 81<br />

globale, 82<br />

statiske, 81<br />

Variansdomæne, 472<br />

1.6 Organiseringen af denne bog 633


Vektor, 28<br />

type af, 93<br />

gennemløbning af, 92<br />

indeksering i, 92<br />

gennemløbning af, 92<br />

af char, 94<br />

initiering af, 94<br />

størrelse på, 94<br />

i flere dimensioner, 95<br />

allokering af, 93<br />

deallokering af, 175<br />

og pointer, 95<br />

af objekter, 176<br />

Virkefelt, 50<br />

Virtualitet, 269<br />

Virtuelle<br />

baseklasser og polymorfi, 376<br />

baseklasser, 356<br />

funktioner og klienten, 412<br />

funktioner, 322<br />

funktioner i detaljer, 328<br />

baseklasser problemer med, 378<br />

void, 52<br />

og pointere, 103<br />

W<br />

while/do, 65<br />

X<br />

XCL, 520<br />

kildetekst, 611<br />

udvidelser og tilpasninger, 564<br />

generelle datatyper, 538<br />

værktøjsklasser, 531<br />

containerklasser, 541<br />

abstrakte klasser, 522<br />

634 <strong>Introduktion</strong> 1


Æ<br />

Ækvivalens, 286<br />

1.6 Organiseringen af denne bog 635


C++, dataabstraktion og objekt-orienteret programmering<br />

Copyright © 1991, 1993 by Maz Spork and Polyteknisk Forlag<br />

1. udgave, 1. oplag 1991<br />

2. udgave, 1. oplag 1993<br />

Omslag: Helle Johnsen<br />

Kopiering fra denne bog kun tilladt i overensstemmelse med overenskomst mellem<br />

Undervisningsministeriet og Copy-Dan.<br />

Bogen er sat, monteret og illustreret af forfatteren og trykt i offset hos AiO Tryk as,<br />

Odense.<br />

Printed in Denmark 1993<br />

ISBN 87-502-0739-3<br />

Polyteknisk Forlag<br />

Anker Engelundsvej 1<br />

DK-2800 Lyngby<br />

42 88 14 88<br />

FAX 42 88 11 67<br />

Polyteknisk Forlag fralægger sig ethvert ansvar for fejl og mangler i bogens<br />

indhold samt følgevirkninger af sådanne fejl og mangler, selvom bogens fulde<br />

indhold er kontrolleret før tryk. Det fulde ansvar for enhver privat og<br />

erhvervsmæssig benyttelse af de i bogen fremlagte oplysninger påhviler derfor<br />

læseren.<br />

636 <strong>Introduktion</strong> 1

Hooray! Your file is uploaded and ready to be published.

Saved successfully!

Ooh no, something went wrong!