Kapitel 1: Introduktion [kapitel]
Kapitel 1: Introduktion [kapitel]
Kapitel 1: Introduktion [kapitel]
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