3.2. Konkrete og abstrakte datatyper

Bemærk at dette afsnit er under omredigering, og at der er gentagelser af oplysninger fra forrige afsnit. Hvis du har kommentarer, ris eller ros eller forslag, så er du velkommen til at kontakte mig (eller andre SSLUG'er). Min adresse har tidligere været vist lidt forkert, derfor en gentagelse: donald_j_axel@get2net.dk

Konkrete og abstrakte datatyper nævnes ofte i samme åndedrag som objekt-orienterede sprog, men man har også stor glæde af at vide noget om disse emner, når man programmerer i C.

Er C et objektorienteret sprog? Hvorfor er det ikke objektorienteret, når et C program er en række definitioner af eksterne objekter?

Det korte svar er, at almindelig C ikke har virtuelle funktioner. Desuden er der ikke support for generisk algoritme programmering. Det er imidlertid interessant at vide, at C sproget som sådan ikke forhindrer programmøren i at tænke objektorienteret.

Hvorfor så ikke bruge C++ og glemme alt om C? Der er flere svar på dette spørgsmål. C oversætteren er stadig mere effektiv end C++, selv om C++ teoretisk set burde generere kode af samme størrelse og effektivitet. Et andet svar er, at C er sjovere og mere læseligt end C++, men det er måske ikke alle, der er enige i den betragtning. Hvis man vil forstå, hvad der sker i C++, er det en fordel at forstå C sproget fuldt ud. [1]

Eksempel 3-8. Kompilering af samme program med C og C++ oversættere

Læg mærke til, at der benyttes flag -O for optimering, -s for at fjerne symboltabellerne, således at programmet kun kommer til at bestå af maskininstruktioner og dynamisk link - information. Programfilen bliver lidt mindre for C oversætteren, men ikke meget. Programmet er så lille, at resultatet kun må opfattes som en strømpil. Prøv med nogle af de større programmer - og se, om det er muligt at rette lidt i kildeteksten, så C++-oversætteren accepterer program kildeteksten!

Endelig bemærkes, at man får versionen af oversætter-systemet frem ved kommandoen gcc -v eller g++ -v . Man må ikke sammenligne programfiler genereret med fx. gcc 2.8.1 med programfiler, som er genereret med gcc 2.95.2 eftersom der kan være meget stor forskel på oversætterens håndtering af alignment, optimering m.v.


/fri $ls -lo cirkle1.c
-rw-r--r--   1 root          361 Apr 30 00:11 cirkle1.c
/fri $gcc -Wall cirkle1.c -O -s -o cgg1
/fri $ls -lo cgg1
-rwxr-xr-x   1 root         3028 Apr 30 00:11 cgg1
/fri $gcc -v
Reading specs from /sources/gcc/bin-2.95.2/lib/gcc-lib/i586-pc-linux-gnu/2.95.2/specs
gcc version 2.95.2 19991024 (release)
/fri $g++ -Wall cirkle1.c -O -s -o cxx1
/fri $ls -lo cxx1
-rwxr-xr-x   1 root         3176 Apr 30 00:12 cxx1
/fri $g++ -v
Reading specs from /sources/gcc/bin-2.95.2/lib/gcc-lib/i586-pc-linux-gnu/2.95.2/specs
gcc version 2.95.2 19991024 (release)
/fri $

Hvis man skriver store programmer som fx. et operativsystem eller et GUI library, så er det en fordel at kunne tænke og arbejde på højt niveau. Derfor er det nyttigt at skrive "objektorienteret" også når man arbejder med "almindelig-C" programmering. Senere i dette kapitel vil vi sammenligne to udgaver af en linked liste, den ene skrevet i C og den anden i C++. Så kan du selv dømme. Men aller først lidt introduktion om objekter, konkrete og abstrakte datatyper.

De fleste sprog har nogle mekanismer, som er rigtigt objektorienterede, nemlig håndteringen af forskellige numeriske typer.

Vi kan have en integer i en variabel og gange den med en float og lægge resultatet i en double uden at compileren gider fortælle, at der skal konverteres. Taber vi præcision ved at konvertere fra double til integer, vil de fleste compilere give en warning, men de konverterer dog.

Det er egentlig objektorientering i en nøddeskal. Definer din algoritme (fx. addition) og sørg for, at den kan håndtere forskellige data, d.v.s. objekter, på en passende måde. Det er lidt vanskeligt at skrive operatorfunktionerne på en sådan måde, at de kan klare alle situationer, fx. både fortegns-minus og subtraktions-minus, ofte kaldet unært og binært minus. Som en øvelse i objekt-orienteret tankegang kan man prøve at definere en struct, som skal repræsentere brøker, som fx. 2/3, der jo ikke er det samme som 0.6667. I "almindeligt" C vil man bruge funktionskald til at udføre aritmetiske konverteringer og operationer, og det kunne nemt komme til at se lidt klodset ud, som for eksempel her:


f()
{
    struct broek andel;
    struct broek afgift;
    struct broek *b_bogpris;

    andel.t = 1;
    andel.d = 3;
    b_bogpris = new_broek(360,1);
    broek_multiply(&afgift,b_bogpris,&andel); /* ikke kønt */
    free(b_bogpris);

    /* se filerne bogpris1.c bogpris2.cxx for hele source.  */
}

I C++ er det muligt at erklære en variabel af typen broek som fx. nedenstående eksempel, og derefter benytte de tilhørende operationer udtrykt ved de i forvejen kendte operatorer:


broek andel(1,3);
int bogpris = 360;
broek afgift = bogpris * andel;  /* ret nemt at læse */

Det forudsætter selvfølgelig, at man har defineret typen broek og tilhørende funktioner for operatorerne på en passende måde!

En sådan brugerdefineret datatype kaldes somme tider en konkret datatype, i modsætning til en abstrakt datatype. Den konkrete datatype har ingen "virtuelle funktioner" sådan som den abstrakte har det. Læs videre:

Betegnelsen abstrakt datatype (ADT) bruges somme tider om alt, hvad der kan indkapsles, men den mest rimelige anvendelse er nu den, som Stroustrup angiver i sine forskellige bøger om C++. Hvis vi skal skrive en hardware driver til en grafisk device (en som er i stand til at tegne prikker på en angivet position) så kan vi definere en klasse "figur" - men for at opnå bedste hastighed ønsker vi, at måden, den tegnes på, er optimeret for henholdsvis cirkel og rektangel. C++ supporterer den slags konstruktioner:

Vi kan i C++ definere en generel klasse, "figur", som omfatter de grundlæggende egenskaber ved figur og tillige de operationer, som hører sammen med den. Nogle af disse operationer kan vi endnu ikke sige nøjagtigt hvordan vi ønsker implementeret. Hvis vi gjorde, ville det ikke være optimalt.


struct figur {  // man kan skrive class i.st.f. struct 
    public:
        figur(){;}
        virtual void tegn_figur() =0; // =0 er nødvendigt
};

Derefter definerer vi subklasser, det vil sige klasser, som overtager (arver) egenskaberne fra den generelle klasse. Sådan en klasse kaldes også specialiseret. Det kunne være cirkel, som jo er mere specialiseret end "figur". Denne subklasse skal definere en brugbar funktion til at udføre handlingen "tegn_figur".


struct cirkel : figur {
    public:
    void tegn_figur(){
        printf("Tegner figur/cirkel...\n");
    }
};

En funktion i klassen "figur", som erklæres at være virtuel, kan ikke kaldes, den eksisterer jo i virkeligheden kun, mens programmet kompileres. Det vil give oversættelses-fejl, hvis de nedarvende klasser ikke definerer en (rigtig) funktion. [2]

I C kan man ikke få compileren til at hjælpe sig med at "huske", at man skal skrive en optimeret, håndgribelig funktion som tegner cirkel-figuren.

For at fortsætte parallellen med de numeriske typer, int, double, long double etc., kan man sige, at de alle nedstammer fra en abstrakt datatype "tal". I "tal-klassen" er der virtuelle funktioner for addition, subtraktion etc. De er virtuelle, fordi de ikke eksisterer for klassen "tal", men kun for de specialiserede klasser, heltal, decimalbrøk og så videre.

Eksempel 3-9. Talklasse - en abstraktion


                 +-------------+
                 |             |
                 |    tal-     |
                 |   klasse    |
                 |             |
                 +------+------+
                        |
                        |
                        |
                        |
                        |
       +----------------+---------------+--------------+---------+
       |                |               |              |         |
       |                |               |              |         |
       |                |               |              |         |
       |                |               |              |         |
       |                |               |              |         |
   +---+--+        +----+----+    +------------+   +-------+  +------+
   | hel  |        | decimal |    | dobbelt    |   | posi- |  | char |
   | tal  |        | brøk    |    | precision  |   | tive  |  +------+
   +------+        +---------+    | decimalbrøk|   | heltal|
                                  +------------+   +-------+

For heltals-objekter skal oversætteren realisere operationen med heltal maskin-instruktioner, men hvis der derimod er tale om decimalbrøker skal oversætteren realisere operationen ved hjælp af flydende-komma maskin-instruktioner.

C sproget er ikke kun en "fattig" udgave af C++. C sproget bliver til stadighed påvirket af udviklingen indenfor bl.a. C++ og tager de bedste ting, som er fremkommet, med i standarden, se evt. appendix C, C99. C og C++ moduler kan sættes sammen. C sproget vil stadig blive valgt, hvor størrelse og hastighed er vigtigst, for eksempel i kerne-moduler og drivere. Derfor er det vigtigt at kunne håndtere højniveau programmering i C, og det er det, som det efterfølgende afsnit handler om.

I de efterfølgende afsnit vil vi bruge betegnelsen "abstrakte datatyper" mere løseligt om alt, hvad der kan indkapsles. Vi stiller os tilfreds med, at implementering af funktionaliteten kan udskiftes uden at brugere af datatypen skal ændre deres kode.

Slutbemærkning:

[1]

Anvendelse af objektorienterede fremgangsmåder er somme tider ikke sagligt begrundet (ifølge Stroustrup; jeg kommer til at skylde et sidetal i "The C++ Programming Language". Det er fx. ikke hensigtsmæssigt at opbygge et grafisk library som ét stort klassehierarki; det indskrænker mulighederne.

[2]

Det giver ikke en oversætter fejl, men en link fejl i c++ 2.95.2 hvis man glemmer =0; efter en virtuel funktion i baseklassen.