Kapitel 2. Funktioner, modularitet og relevante teknikker

Resume: Hello-world programmer isolerer og afprøver en speciel teknik. For at konstruere og lære nye teknikker er det en god ting at kunne isolere hver eneste lille funktionalitet i det nye program, man ønsker at bygge. Denne teknik kan man kalde terrasse-teknik.

Et C-program består af en serie definitioner af eksterne objekter. Et eksternt objekt kan være en funktion eller en datadefinition, d.v.s. en variabel. For at få overblik over en opgave fra begyndelsen af, er det godt at vide, hvordan man opdeler en opgave i mindre dele. Ikke blot hvilke funktionaliteter, som lader sig isolere, men også en idé om, hvordan man bærer sig ad med at opdele et program i mindre programdele. Det er hovedemnet for dette kapitel.

En opdeling forudsætter, at der er mulighed for at koble forskellige moduler sammen igen på et senere tidspunkt ved hjælp af libraries, header filer og make filer. Det vil nærværende kapitel give nogle eksempler på.

I C er funktioner grundlaget for opdelingen af programmer i mindre dele. Som Kernighan siger: funktioner gør det muligt at genbruge kode, gør det muligt, at en programmør bygger videre på en anden programmørs arbejde.

Det er vel at mærke sådan at forstå, at jeg, i mit nye program, kan includere al funktionalitet fra noget, som er skrevet i forvejen uden at jeg skal ændre i eller kopiere fra den oprindelige kode.

Grundlaget for genbrug er som sagt funktionen, men ved hjælp af separat kompilering, statiske variable og evt. dynamisk allokering af mere hukommelse kan denne teknik forfines, således at man reelt set råder over objektorienterede metoder. Det bedst kendte eksempel er ANSI C bibliotekets Input Output del, som bygger på begrebet en FILE. Man kan erklære en variabel af typen FILE* uden at ane, hvad den indeholder. Det varierer også fra system til system. Men fælles for alle FILE* er, at man kan åbne dem, fopen() , læse fra dem fread() og skrive til dem fwrite() finde en position, lukke igen og meget andet.

2.1. ANSI prototyper og modularitet

I dette kapitel ser vi på hvordan man opdeler en lille opgave i en beregningsdel og en hoveddel. Undervejs eksperimenteres med funktioner og prototyper.

Derefter ses på filter programmer, som er programmer, der læser noget input og transformerer dette til en bestemt slags output. For et givet input vil output altid være det samme.

Dette princip udstrækkes til et forholdsvis kompliceret program, som parser sgml tags og formaterer lidt på en sgml tekst. Endelig er der forslag til mange øvelser undervejs, og den interesserede læser vil sikkert ikke kunne lade være med at eksperimentere yderligere. God fornøjelse!

Men allerførst skal vi lige se på, hvilken betydning returværdien fra en funktion kan have på et helt andet program, nemlig en shell eller kommandofortolkeren. Vi bruger et minimal program til at isolere fænomenet.

2.1.1. Et brohoved.

Forhåbentlig kender du Kernighan & Ritchies bog The C Programming Language. Det første kapitel, den berømte "tour" gennem C sproget, starter med et program, der skriver "Hello, World!" på en uddataenhed (altså en skærm eller lignende).[1]

Det program kunne man jo så udnævne til stamfaderen for en hel kategori. "Hello-world" programmer isolerer en feature og afprøver, hvordan den virker. Et "hello-program" skal helst kunne køre, selv om vi nogen gange nøjes med at skrive en funktion for at se, om oversætteren accepterer den syntaks, vi anvender.

Et "hello-program" kan være et, som skriver noget på skærmen, eller det kan hente dato-information, så kan vi få bekræftet, om dato funktionerne opfører sig, som vi forventer, eller ej. Vi kunne kalde det for et minimalprogram. Vi kunne også sige, at vi isolerer de features i C sproget, som vi ønsker at lære/undersøge/afprøve. Metoden er vigtig at lære. Det er en ekstremt nyttig metode; med den kan man løse flere problemer, end man kan med en debugger.

2.1.2. Returværdi fra en funktion

Det, vi skal i gang med nu, er at undersøge, hvordan funktioner afleverer data til hinanden, og hvordan C sproget gør det lettere at lave sådan nogle "kasser", ofte kaldet black box, om hvilken man ved, at den kan dit og dat, og at den er helt uafhængig af resten af vores program.

Et helt grundlæggende "Hello-world" program er et, som simpelt hen afslutter med det samme! Sådan et kommer her:

Eksempel 2-1. HELLO - statuskode


/* frame.c Minimalt program til afproevning af statuskode. */

int main()
{
    return 0;
}

/* end of file frame.c */

Program-source, kildeteksten består af 8 linjer, hvis man tæller kommentarer og tomme linjer med. Aller øverst er der en kommentar, som fortæller kort hvad meningen med programmet er. En kommentar startes med "/*" og slutter med "*/".

Programmet består af en definition af ét eksternt objekt, nemlig en funktion, som har navnet "main". Parenteserne efter main fortæller, at main er et objekt af typen funktion. Parenteserne kaldes derfor "funktions-operator" [2] Selve koden i main er indrammet i krøllede parenteser, braces. Koden består af kun én sætning, eller statement , nemlig

return 0;

return er en specifikation af, at funktionen skal aflevere noget til den, som har bedt om at få udført funktionen (har kaldt den.)

return er et reserveret ord , d.v.s. et ord, som oversætteren er født med at kende. C sproget har 32 reserverede ord. (se Afsnit A.1 .)

Nullet er et "udtryk", (aritmetisk udtryk) med en talværdi. Vi kunne også have skrevet return 234 eller return 7000143. I dette tilfælde vil det dog være klogt at holde talværdien under 256.

Eksempler på andre expressions: kroner = timer * timeloen; hvor det forudsættes at kroner, timer og timeloen er variable, som indeholder fornuftige værdier. Et expression, som afsluttes med semikolon, kaldes et statement.

Hvis der er flere statements i en funktion, udføres de i rækkefølge, oppefra nedefter.

Et kald til en funktion, som f.eks. flg.: abs(-5); er også et expression, i dette tilfælde med værdien 5. Kald til en funktion vil ofte returnere en variabel af typen heltal, integer , og en integer i et expression kan erstattes af et kald til en funktion, som returnerer en integer.

Det er en konvention, at kørsel af et program, som benytter standardbiblioteket (med bl.a. læse- og skrivefunktioner) begynder med funktionen main. Når man kommer til slutningen af denne funktion, slutter programmet her med at returnere en statuskode til styresystemet. Denne statuskode bruger man til at markere om programmet blev afbrudt af en fejl (og lignende). Hvis et program slutter uden et "return <expression>;" er det sjusk.

Kernighan & Ritchie dropper return-sætningen en del gange i bogen, men det er faktisk sjusk alligevel! De gør det selvfølgelig fordi det er lettere at forklare et program, hvori der kun er de nødvendigste linjer.

Statuskoden bør fortælle, om programmet kunne køre uden fejl, (dvs. uden fejl, der er påført af ydre omstændigheder, som f.eks. at en datafil mangler). Det er altså programmørens mulighed for at sende et signal om at "alt er vel" eller "her opstod en fatal fejl".

Hvis programmøren vil fortælle systemet, at der var en fejl, skrives simpelthen:

return 255;

Tallet kan i Unix-kommandofortolker-sammenhæng læses i variabelen $?, som kan styre processtrømmen i et Unix-skalprogram (en shell) .[3] Øvelse: Ret, så programmet returnerer 117 og se, om du kan udskrive systemvariablen $? med kommandoen

dax@pluto$ echo $?
 0
dax@pluto$

Det lille program ovenfor kunne oversættes/compileres [4] med flg. kommando:

dax@pluto$ gcc frame.c -o frame 
dax@pluto$

Derefter kan det køres fra current directory (i det aktuelle katalog, eller sagt på en tredie måde, fra det bibliotek, som vi står i [5] ) med en kommando som:

dax@pluto$ ./frame 
dax@pluto$

eller, hvis din PATH-systemvariabel ender på ':'

dax@pluto$ frame 
dax@pluto$

Eksemplet lider imidlertid af en alvorlig skavank, vi kan jo næsten ikke se, om programmet rent faktisk kører. Det laver jo ikke noget! Derfor tilføjer vi en lille output kommando:

Eksempel 2-2. Skriv message på standard output.


/* frame2.c Skriv til stdout og afslut. */

#include <stdio.h>

int main()
{
    puts("Hello! Programmet frame2 kører nu...");
    return 0;
}

/* end of file frame2.c */

Her er flere ting, som er værd at lægge mærke til. Dels et include direktiv, d.v.s. en kommando, som fortæller oversætteren, at den skal læse en fil, der hedder stdio.h. Når filnavnet står i vinkler , så betyder det, at oversætteren skal lede der, hvor systemet normalt har sine filer med erklæringer, "include filerne". På Unix, Linux og andre systemer er det /usr/include, der gennemsøges først.

Oversætteren finder den pågældende file og læser den. Den indeholder kun type erklæringer.

Den erklæring, som vi skal bruge, ser ud som følger:

extern int puts (const char *__str);

Den kunne dog også have set enklere ud:

int puts (char *message);

Det kaldes en prototype. Denne gør det muligt for oversætteren at tjekke, at funktionskald vil fungere efter hensigten.

Prototypen "int puts(char*)" fortæller, at puts er en funktion, som returnerer en integer og forventer at få en character pointer som argument.

En prototype for vores main (som burde findes i en af glibc - systemets header filer) ville hjælpe oversætteren med at kontrollere, om vi overholdt interface mellem vores main og bibliotekets startup procedure. [6] Denne prototype ville se sådan ud:

int main(int argc, char *argv[], char *env[]);

Det er ikke nødvendigt at angive et navn, en identifier, på argumenterne, kun typen skal angives når vi skriver en prototype.

int main(int, char *[], char *[]);

Men det er oplagt at finde navne, som giver læseren en hjælp til at forstå meningen med funktionen. Argc står for argument count, argv for argument vector, en liste med alle parametre fra det program, som har startet vores program op; env står for systemvariable (eng. environment variables), her kan vi aflæse brugerpreferencer og ligenende. Når vi ikke bruger parametrene til main, kan vi nøjes med at skrive main(), altså en tom funktions-parentes.

Eksempel 2-3. En character pointer


/* frame3.c Demonstration af character pointer. */

#include <stdio.h>

int main()
{
    char *charpointer = "Hallo! Programmet frame3 kører nu...";
    puts(charpointer);
    return 0;
}

/* end of file frame3.c */

char *charpointer er erklæring af en variabel. Variabelen er en character pointer, det vil sige en adresse variabel. Den initialiseres på samme source linje, som den erklæres. Det er simpelthen en praktisk skrivemåde. Det svarer til:

main()
{
    char * charptr;              /* charptr er en variabel */
    charptr = "Hallo etc... ";

En literal string, "Hallo etc... "; er ikke en variabel, men er en besked til oversætteren om at initialisere et dataområde med den tekst, som vi nu ønsker os. For at kunne bruge teksten skal vi enten gemme adressen på den (altså cptr = "Hallo etc...") eller også give adressen på denne string til den funktion, som skal bruge den: puts("Hallo etc..");

En gengivelse af dette system af RAM-adresser og indhold kunne tegnes som en reol. Her er det en reol, hvor hver hylde har et indhold, der enten kan være 4 bytes eller 4 bogstaver, eller én adresse (32 bits).

        Adresse               
        eller
        hylde-nr.      Indhold

                    ~             ~
                    :             :
                    +-------------+
                    |             |
                    |  o !   P    |
           800440   +-------------+
                    |             |
                    |  H a l l    |
           800436   +-------------+
                    |             |
                    |             |
                    +-------------+
                    |             |
                    :             :
                    :             :
                    :             :
                    |             |
           120808   +-------------+  charptr er en adressevariabel,
                    |             |  som indeholder adressen
                    |  <800436>   |  på en string, "Hallo! Prog...etc."
 charptr:  120804   +-------------+
                    |             |
                    |             |
           120800   +-------------+
                    |             |
                    |             |
           120796   +-------------+
                    :             :
                    :             :
                    |             |
                    :             :
                    :             :
                    |             |

Det, der skrives på skærmen, er tekst, som bogstav for bogstav ligger i programmets data-del.

Lad os nu gå over til beregningsprogrammer. Hvis de ikke har noget input, er de en slags "hello" programmer, fordi de isolerer de aritmetiske operationer. Selv om vi begynder med simple eksempler, kan det sandelig godt være nyttige programmer. Den oprindelige idé om computere var jo blot at de skulle beregne ting, som var trælse at beregne i hånden, planetbaner, skatteopgørelser og lignende ;-)

Slutbemærkning:

[1]

Hvis du ikke kender bogen "The C Programming Language" af Kernighan & Ritchie, og hvis du ikke er en øvet C programmør, så vil jeg anbefale, at du køber eller låner den og bruger nogle uger til at arbejde kapitel 1 igennem - lav så mange af de ekstra øvelser, som du kan nå. Nærværende kapitel er en ikke en erstatning for den oprindelige "tour", men et supplement. Man kalder det for learning by doing eller deduktiv spiralpædagogik; vi udleder, hvordan C sproget fungerer ved at prøve det mange gange og ved at gøre øvelser lidt sværere hver gang.

Hvis Kernighan & Ritchie bogen også forekommer for vanskelig - den er nemlig heller ikke for helt grønne begyndere - så er der nogle andre introduktionsbøger, som kan guide dig igennem grundlæggende øvelser i programmering, f.eks. "Practical C" fra O'Reilly.

Hvis du har lidt gåpå-mod, kan du måske alligevel klare dig ved at bladre/klikke dig om til appendiks A i denne bog; dér gives en oversigt over C sprogets fire forskellige bestanddele, datatyper, operatorerne, flow-konstruktionerne - og om opdeling af programmer i moduler.

K&R bogen giver imidlertid flere eksempler end jeg har med i Appendiks A, og forklarer variationer på programmerne, variationer, som er rigtig gode til at få én igang med selv at forsøge. Det kan du selvfølgelig også gøre med eksemplerne i denne bog.

Det specielle ved Kernighan & Ritchies programeksempler i Kapitel 1 af den berømte bog er, at programmerne er nyttige. På en kommandolinje kan de bruges med det samme til endda ret fornuftige og realistiske ting. Hvis du ikke er fortrolig med kommandolinjesyntaks, så kan du finde eksempler i bogen »Linux – Friheden til at lære Unix«. Det er en god idé at eksperimentere lidt med de simpleste Unix-programmer, inden du går videre. Prøv f.eks. date, cal, uptime, id, who, finger, "echo hej", strings /usr/bin/ls, file *, ls, pwd, cd, du, df, man gcc, man ld, man ld.so, ldd /usr/bin/ls osv. (klassiske unix kommandoer).

[2]

Tabellen Eksempel A-5, viser parenteserne "()" øverst, fordi bindingen mellem identifier og () er stærkere end bindinger mellem andre operatorer.

[3]

I Microsoft miljøer som "errorlevel", der kan bruges af if-sætninger i batch-filer.

[4]

Generer det, hvis jeg staver engelske computer-udtryk på engelsk?

[5]

Se "Friheden til at programmere" afsnittet om C sproget, hvis du har brug for lidt mere indføring i, hvordan man bruger kommandolinjen.

[6]

I glibc2x: se efter filerne ./sysdeps/elf/start.S og ./sysdeps/generic/libc-start.c