2.3. ANSI prototyper og modularisering

Opdelingen i funktioner er grundlaget for strukturering af opgaverne, som man måtte være ved at løse.

Funktioner er den teknik, som gør det muligt at genbruge kode, sådan at man ikke behøver at begynde fra bunden hver gang, men kan bygge videre på andres arbejde.

Der er ikke noget problem, der er så stort, at det ikke kan deles op i mindre :-)

2.3.1. Kursomregning

For at illustrere, hvordan man trækker en beregning ud af et program, kunne vi bruge en beregning så simpel som x = a + b.

Skal eksemplet være en lille smule realistisk foreslår jeg imidlertid, at vi i stedet laver en kursomregningsfunktion. Man kan forestille sig, at der er skal så mange beregninger til (f.eks. en tabel i en udskrift eller priser på en faktura) at det er en stor lettelse at få udført beregningen i en funktion.

Det er forhåbentlig indlysende, at en "euro-funktion" ville kunne indgå i bankers udbetalingsautomater, i faktureringsprogrammer etc.etc.

Lad os derfor benytte kursomregning til at forske i den teknik, som kaldes modularisering. Det endelige mål er en omregningsfunktion i et bibliotek af forskellige financielle funktioner. Man må forestille sig valutaveksling med og uden gebyr og afrunding etc. Dette er så den rå omregning til brug for rapporter eller lignende. Det er simpelt hen en funktion, som får vores kroner og "afleverer" dollar (eller Euro). Udgangspunktet er en tilretning af Eksempel 2-5, så vi kan afprøve funktionen mens vi skriver på den. For at gøre det mere overskueligt, har jeg imidlertid valgt at sløjfe prompt-input delen; den kan man evt. selv tilføje efter mønsteret i Eksempel 2-5.

Jeg må indrømme, at jeg efter at have skrevet dette her afsnit syntes, at det var lidt rigeligt langt! Grunden til, at jeg lader det stå er, at jeg har set så mange programmører, der havde svært ved at forstå mekanikken i funktionskald. Først når man forstår hvad der sker i computeren under et funktionskald kan man udnytte C sprogets fulde styrke.

2.3.1.1. Beregning som del af main-koden

Eksempel 2-6. Dollar omregning, spaghetti [1] version.

/* dollar0.c Input Kroner, beregn Dollar. UDGANGSPUNKT. */

#include <stdio.h>

int main()
{
    int kurs = 865;
    int kroner = 100;
    int resultat;

    resultat = kroner * 100 / kurs;
    printf("Kroner %d giver Dollar %d\n", kroner, resultat);
    return 0;
}
/* end of file dollar0.c */

Først trækker vi beregningen ud af programmet og lægger den i en funktion, som vi kalder kr2dollar.

2.3.1.2. En ANSI prototype

Eksempel 2-7. Dollar omregning med beregning i funktion.

/* dollar1.c Input kroner, kald int kr2dollar(int) */

#include <stdio.h>

/* vi erklærer nu en prototype for vores funktion. En prototype kan
 * kendes på, at der efter funktionsparentesen er et semikolon - ikke
 * nogen braces, som ville signalere starten af en kodeblok.
 */

int kr2dollar(int);
int kurs = 865;

int main()
{
    int kroner = 100;
    int resultat;

    resultat = kr2dollar(kroner);
    printf("Kroner %d giver Dollar %d\n", kroner, resultat);
    return 0;
}

int kr2dollar(int kr)
{
    return kr * 100 / kurs;
}

/* end of file dollar1.c */

Bemærk, at main står øverst i programmet. C inviterer til top - down programmering. Vi kan kalde kr2dollar uden at have nogen som helst idé om, hvordan vi vil implementere den. Selvfølgelig er programmet ikke færdigt, før end vi har skrevet den sidste kode, men i nødsfald kan man somme tider klare sig med en forsimplet udgave - eller en stub, en tom funktion - der, hvor man ikke har skrevet al koden.

Men funktionen kr2dollar er erklæret inden den anvendes, det er linjen lige neden under #include direktivet. Erklæringen er en slags forklaring til oversætteren af, hvad det er for en funktion. Den bevirker, at oversætteren opretter en entry i en symboltabel, så den kan slå op, hvad "kr2dollar" er for noget, næste gang den forekommer i kildeteksten.

Derfor ved oversætteren, hvad type der kommer ud af funktionen. Det kunne være, at det var en flydendetals dims i stedet for et heltal. (Ja forresten, det synes du nok, at det burde være! Det ville være rart med flydende tal for at få decimaler på, se Eksempel 2-10. Men strengt taget kunne vi få en mere præcis beregning ved at anvende 64-bits integers til at repræsentere 100-dele øre. For den avancerede: Prøv det! Og husk at indsætte et komma på det rigtige sted, når du skriver det ud.)

kr2dollar() består af KUN et return statement. Godt nok skal der regnes lidt, før end return værdien er klar, det er jo selve ideen i funktionen.

I almindelig stenalder C kunne man nøjes med at kalde funktionen uden at forklare oversætteren, at det var en funktion, der returnerede en integer. Det kaldes "implicit integer" regelen. [2]

2.3.1.3. Modulariseret udgave af beregningen

Nu skiller vi beregnings funktionen ud, så den ligger i en fil for sig selv - den er på vej til at blive en del af vores "financial library" (;-).

Desuden lader vi variabelen "resultat" udgå, for vi kan jo bare anbringe funktionskaldet der, hvor resultatet skal skrives.

Eksempel 2-8. Dollar omregning, modul version.

/* dollar2.c ask for Kroner and call int kr2dollar(int) */

#include <stdio.h>
#include <stdlib.h>

int kr2dollar(int);

int main()
{
    int kroner = 100;

    printf("Kroner %d giver Dollar %d\n", kroner, kr2dollar(kroner));
    return 0;
}
/* end of file dollar2.c */

Som det kan ses, har vi klippet de nederste 4 linjer ud, hvor funktionen kr2dollar var defineret. Den står nu i en fil, som vi kalder kr2dollar.c:

Eksempel 2-9. kr2dollar modul.

/* kr2dollar.c - beregn dollar ud fra kroner */

int kr2dollar(int kr)
{
    int kurs = 865;
    return kr * 100 / kurs;
}
/* end of file kr2dollar.c */

Kursen er ikke mere tilgængelig i main, vi har isoleret den, så den kun kan ses i funktionen, som omregner. Det er en primitiv udgave af et udmærket princip.

Det ville være fint, hvis vi skrev en funktion, som hentede kursen fra en pålidelig kilde, f.eks. en eller anden nationalbank på internettet. Når vi så skulle bruge kursen, kunne vi kalde denne funktion.

De to filer kan oversættes på flere forskellige måder:

Enten:

gcc -Wall dollar2.c kr2dollar.c -o omregning

Eller:

gcc -Wall -c dollar2.c
gcc -Wall -c kr2dollar.c
gcc dollar1.o kr2dollar.o -o omregning

Eller:

gcc -Wall -c kr2dollar.c
ar -rv libfinans.a kr2dollar.o
gcc -Wall -c dollar2.c
gcc -Wall dollar1.o -L./ -lfinans

Læg lige mærke til, at vi har genereret en bibliotek file med en meget simpel kommando, ar -rv libfinans.a <objectfile> ...

Hvis vi skulle glemme prototypen for denne simple beregning, så vil der ikke opstå fejl i dette eksempel. Det skyldes, at vi stadig har regelen om implicit integer, når vi skriver standard C programmer. [3]

Med GNU C-oversætteren vil man dog få en advarsel: "implicit declaration of function `kr2dollar'". Det betyder simpelthen, at oversætteren har opdaget, at vi kalder kr2dollar, men ikke kan finde den i typetabellen. Oversætteren antager at funktionen returnerer en integer. Man får kun denne warning, hvis man anvender -Wall (Warning level, give us ALL warnings).

2.3.1.4. Modul med return type double

Lad os nu prøve at definere kr2dollar() som en funktion, der returnerer en double. Prøv nogle eksperimenter med programmet. Der er vist nogle forslag.

Eksempel 2-10. Dollar omregning, double version.

/* dollar3.c bed om Kroner og call double kr2dollar(double) */

#include <stdio.h>
#include <stdlib.h>

double kr2dollar(double);  /* prøv at udelade denne her! */


int main()
{
    double kroner = 100;

    printf("Kroner %10.2f giver Dollar %10.2f\n", kroner, kr2dollar(kroner));
    return 0;
}
/* end of file dollar3.c */

I ovenstående eksempel er det nødvendigt, at der erklæres en prototype for kr2dollar. Hvis prototypen udelades, vil gcc, ligesom i forrige eksempel, stadig kun give en warning, og endda kun under forudsætning af, at -Wall anvendes!

$$\
gcc -Wall -c dollar3x.c # version uden prototype for kr2dollar():

dollar3x.c: In function `main':
dollar3x.c:16: warning: implicit declaration of function `kr2dollar'
dollar3x.c:16: warning: double format, different type arg (arg 3)

Og her kommer så den anden fil med funktionen, som foretager omregning med double precision floating point parameteren kr.

Eksempel 2-11. kr2dollar, return type double, module.

/* kr2dollar.c - beregn dollar ud fra kroner, double */


double kr2dollar(double kr)
{
    return kr / 8.65; 
}
/* end of file kr2dollar.c */

Oversættelse uden prototype vil som sagt alligevel resultere i en objektfil, som kan linkes med vores nye finans-bibliotek uden at man får en fejlmeddelelse. Men når man kører programmet, kan man se, at det ikke regner rigtigt, uha uha.

Når man oversætter et sådant program, vil oversætteren opfatte retur værdien fra en ikke erklæret funktion som en integer. Denne vil typisk være placeret i det primære register. På Intel x86 register EAX eller EBX. Oversætteren kan ikke kontrollere, om den kaldte funktion placerer sin retur værdi dér. Det er jo et helt andet modul, og måske endda et modul, som ikke er skrevet endnu.

Oversætteren "ser", at den retur værdien fra den funktion, som antages at være en integer, skal afleveres (her som argument til printf). Hvis returværdien er en integer, ligger den i EAX registeret. Derfor skriver compileren en instruktion, som skubber værdien af EAX op på applikations-stakken. Printf er, også i denne sammenhæng, lidt speciel, fordi den ikke aner, hvilke argumenter den får, før end den har læst format-specifikationen. Hvis vi skulle personificere printf, så ville den sige: "Jeg skal skrive et double precision floating point tal ud, ergo må der ligge sådan et på stakken. Det tager jeg!" Og det gør den så, den tager 8 bytes fra stakken uden at kunne kontrollere, om de rent faktisk repræsenterer en double eller en integer.

Det, som jeg prøver på at demonstrere, er konsekvensen af, at ANSI-C specifikationen ikke omfatter et krav om typekontrol under link-processen. Derfor er det nyttigt at tage notits af alle warnings.

Det er specielt vanskeligt med printf(3) , fordi det er tilladt at aflevere så mange parametre efter format-specifikationen, som man har lyst til, af de typer, som man har brug for. Det er vildt anarki, siger nogen, men det er uhyre praktisk. Med printf kan man formatere komplicerede rapporter med ét printf-statement, hvor det i C++ kan ende med mange linjers kompliceret kode, som skal styre forskellige skjulte interne variable i cout funktionens talkonvertering.

2.3.1.5. Header filer

For at automatisere processen med prototyper er det skik og brug at man laver en header fil til hvert projekt, som man har i gang. I vores minimal eksempel her:

Eksempel 2-12. Header fil for kr2dollar

/* File: dollar3.h, prototypes for finans-program ... */

double kr2dollar(double kroner);

Denne fil kan includeres i både der, hvor funktionen skal anvendes, og der, hvor den defineres (programmeres). Det giver jo kontrol med tingene.

Man får filen med ved at skrive: #include "dollar3.h" Bemærk, at der er anvendt double quotes om filnavnet fordi denne fil ligger i current directory.

NB! Der er en lignende regel, som tillader, at en (global) integer kan defineres 2 gange. Det er straks mere farligt - for tænk nu hvis det ikke var meningen - og det kan ikke tillades i C++.

Slutbemærkning:

[1]

Spaghetti er en derogativ betegnelse for en lang, uoverskuelig liste med programmeringsinstruktioner. (Eller er det noget andet? ;-)

[2]

Det hænger sammen med, at der alle funktioner i de aller første C-oversættere returnerede en integer. Funktioner, som returnerer doubles er stadig i mindretal.

[3]

Reglen kan være meget praktisk for den erfarne programmør, som i visse situationer kan gøre et program lidt mere læseligt, fordi der er mindre "støj".