Laboration i C

Syftet med den här laborationen är att lära sig programmeringspråket C. Labben är tänkt att vara klar på tre veckor med all felsökning man kan tänkas behöva göra. Labbtillfällena är hjälp och redovisningstillfällen, det mesta arbetet ska göras utanför labbtid. Förutom labbtillfällena finns det allmänhandledningen där en ensam hjälphandledare handleder studenter på flera kurser.

För att få tillgång till C-kompilatorn och övriga verktyg behöver du addera modulen gcc och gnu. Om du gjort course join tilpro09 module list.

Läs om pekare

Läs på Ted Jensens tutorial om pekare i C. Länken finns på hemsidan under länkar. A. Svara på följande frågor.

Vad menas i kapitel två med den här raden? ptr = &my_array[0];
Vad är skillnaden mellan de två variablerna?

Vad betyder 3[a]?

Vad är skillnaden mellan *(*(multi + row) + col) och multi[row][col]?

Innan du programmerar

Parallellt med att läsa Ted Jensens pekartutorial ska du göra följande för att lära dig mer om C och dess verktyg.

In och utmatning

Skriv in följande enkla program i en textfil med namnet mitt_program.c

#include <stdio.h>

int main() {
    printf("Hej vad heter du? ");
    char str [5];
    scanf("%s", str);
    long a;
    printf("Hur gammal är du? ");
    scanf("%d", a);
    printf("Hej %s du har endast 65 - %d år kvar till pensionen\n", str, a);
}
Kompilera programmet med
gcc -o mitt_program mitt_program.c
Programmet funderar inte särskilt bra. Ändra argumentet till det andra scanf så att man matar in åldern till variabeln a's adress.

Prova att köra programmet några gånger. Vad händer om du skriver in ett namn som är mycket längre än vektorn str?

Skriva över stacken

Alla lokala variabler lagras på stacken. Du ska nu prova att skriva ett program som skriver en annan variabel. Först ska du ta reda på var dina variabler lagras.

Spara undan ditt nuvarande program i en annan fil. Skriv ett nytt huvudprogram i filen mitt_program.csom allokerar fem heltal av olika storlekar. Skriv ut dess adresser och rita en bild av hur stacken ser ut med adresser och initialvärden.

Deklarera tre teckenvektorer enligt nedan. Rita upp hur stacken ser ut.

    char str1 [10] = "str1";
    char str2 [11];
    char str3 [12] = "str3";
Låt användaren mata in ett svar till str2. B. Hur ska man svara för att påverka andra variabler?

Hårdkodning

Vektorns str längd är hårdkodad. Det kan man åtgärda på två sätt. Det gamla sättet (ANSI C) definierar man ett makro, t.ex. #define STR_LENGTH 5. I nya c-standarden kan man deklarera en konstant istället const int max_storlek = 30. Nackdelen med den #define är att den är potentiellt farlig eftersom preprocessorn inte tar hänsyn till C-syntax.

Ändra på hårdkodningen. I fortsättningen ska du se till att ingen hårdkodning förekommer i dina program.

Preprocessor

#define är ett preprocessorkommando. Ett annat är #include som vi använde för att få tillgång till standard I/O (input/output) funktionalitet. C. På vilket sätt ändrar preprocessorn din källkod innan den körs av kompilatorn?

Statiska variabler

Deklarera två statiska variabler. En statisk variabel i C är filglobal. Man kan jämföra dem med instansvariabler i en klass (om filen är klassen) eller med klassvariabler i en klass.

Skriv ut variablernas värden och adress.

Externa moduler och variabler

Skriv en fil som heter modul1.c med följande rad:
    const int MAXLENGD = 13;
Kompilera filen till maskinkod genom att skriva
gcc -c modul1.c
Det enda denna maskinkod gör är att allokera heltalet MAXLENGD. Använd detta tal i ditt föregående program mitt_program.c. Skriv raden
    extern const int MAXLENGD;
i mitt_program.c. Ändra i main så att värdet och adressen till MAXLENGD skrivs ut. För att kompilera mitt_program.c måste du länka in objektfilen modul1.o. Det kan du göra genom att t.ex. lägga till modulen när du kör gcc.
gcc -o mitt_program mitt_program.c modul1.o
D. Vad händer om tar bort ordet extern?

Makefile

För att bygga större projekt med flera delprogram använder man programmet make. TAB-tangenten har en speciell betydelse i makefiler, vilken?

Prova att skriva en Makefile med följande rader.

mitt_program: modulen mitt_program.c
	gcc -o mitt_program mitt_program.c modul1.o

modulen: modul1.c
	gcc -c modul1.c
Kör make flera gånger, E. vad händer??

Programmet make kommer att kontrollera om filstämpeln för mitt_program är äldre än källkoden men tyvärr är den här makefilen korkat skriven. Ändra den sista regeln, döp om modulen till modul1.o på två ställen i Makefile. Om modul1.c är nyare ska båda filerna kompilera men om enbart mitt_program.c är nyare ska modul1.c inte behöva kompilera om. Prova flera gånger, använd touch för att förnya filstämpeln (t.ex. touch modul1.c)

Den sistnämnda egenskapen är väldigt viktig. Om man ändrar i enbart en fil vill man inte att hela projektet ska kompileras. Stora projekt kan ta ett par timmar att kompilera.

Manualsidor

På alla unix system finns manualsidor om C. Vissa kommandon finns både i C och i operativsystemet. Då måste man förutom kommandot också ange vilket avsnitt i manualen man vill titta i. Prova läsa mansidorna för scanf, printf, gcc genom att i terminalfönstret skriva:
man M 3 scanf
man M 3 printf
man gcc
Dessa man-sidor finns även på nätet. Prova googla efter dem. Läs om flaggor som man kan ge till kompilatorn gcc. F. Vad betyder flaggorna -g och -Wall?

Det är inte bara c-kommandon man kan läsa man-filer om. Det går även att läsa om flera unix-kommandon. Prova man touch. Läs även om olika sätt att kopiera strängar.

Var allokerar malloc

Utöka ditt program med två strängar som allokeras på heapen.
    char * p1 = malloc(sizeof(char[13]));
    char * p2 = malloc(sizeof(char[13]));
Ge dem era tilltalsnamn som värden. Skriv ut värdena och adresserna. Prova om du kan skriva över den ena strängen genom att kopiera till den andra.

Funktionspekare

Skriv en funktion
void foo() {
    printf("Nu körs foo\n");
}
Deklarera en funktionspekare
    void (*funktionspekare)() = foo;
och skriv ut dess adress. Prova skriv ut adressen till main också.

Debugger

Det finns en debugger gdb som man kan använda för att stega igenom sin kod. Du kan läsa om den på nätet (google gdb tutorial) eller man-sidorna. Du måste kompilera med flaggan -g för att kunna debugga ditt program. Prova stega igenom ditt program med debuggern. Det finns olika sätt att stega, dels stega över funktionsanrop (gå till nästa rad) och dels stega in i funktioner.

Jämför om adresserna blir likadana om du kompilerar med -g eller inte. Somliga kompilatorer stoppar om sina variabler ("padding") med lite extra utrymme när man kompilerar med debugflaggan på.

Jämför om det blir skillnad om du kompilerar med optimering påslaget. Gör nya regler i Makefile: mitt_program_debug och mitt_program_optimerad.

Ett alternativ för att felsöka utan debugger är att använda spårutskrifter.

Formaterad utskrift

Du har redan läst manualsidan om printf. Ibland är det lurigt.
    long long s = 11;
    printf("Variabel\t\t Värde\t\t Adress\n");
    printf("s \t\t\t %d \t\t %d\n", s, &s);
G. Varför blir värdet och adressen konstig?

Gör om dina utskriftssatser så att programmet skriver ut en snyggt formaterad tabell sorterad på minnesadresserna. Alla dina variabler inklusive funktionspekarna ska vara med (bör vara minst ett dussin). Programmet ska i princip bara innehålla flera printf-satser

Variabel          Adress            Värde
=========================================================
...
H. Åt vilket håll växer stacken? Deklarera en array (int vek[] = {1, 2, 3, 5, 7}. Skriv ut adresser och värden på enskilda element i en array I. Åt vilket håll växer en array på stacken?

Dags att programmera

Nu är det dags att programmera. Vi ska börja med att göra en länkad lista i C precis som vi tidigare gjort i Python.

Sammansatta datatyper

För att göra den länkade listan behövs en struct. Den motsvarar klasser i Python men man kan inte lägga in metoder utan bara medlemsvariabler i den. Den struktur som behövs för den länkade listan skulle kunna se ut så här:
struct post {
    char name[30];
    int tel;
    struct post * next;
};
För att göra typen lite enklare att skriva kan man använda typedef struct post Post. Då kommer Post att vara identiskt med struct post

Dynamisk minnesallokering

Fördelen med den länkade listan är att man inte behöver specificera en maxlängd vilket vi var tvungna att göra med vektorn. Varje gång vi lägger in ett nytt element allokerar vi en ny Post på heapen med malloc
Post * p = (Post *) malloc(sizeof(Post));

...

p -> next = (Post *) malloc(sizeof(Post));
Eftersom det inte finns någon automatisk skräphanterare i C så måste vi manuellt frigöra minnet med free om vi skulle vilja ta bort posten under programmets gång.

Strukturering .h och .c filer

Gör två filer, lista.h och lista.c. Låt första raden i implementationsfilen (c-filen) inkludera headerfilen (h-filen) men se till att använda en include guard

Deklarera struct post och två funktioner i header-filen. Den ena funktionen ska ta en Post som parameter och sätta in i listan. Den andra ska ta bort en Post ur listan och frigöra minnet. Repetera hur du skulle ha skrivit i python för stack eller kö. Implementera funktionerna i c-filen.

Skapa en ny fil som test_lista.c som inkluderar lista.h. Skriv en main-metod som skriver ut en enkel meny där användaren kan välja mellan att 1) sätta in namn och telefon i listan, 2) ta bort ett namn från listan, 3) söka efter namn, 4) skriva ut hela listan, 5) avsluta. Använd en slinga som i princip ser ut så här:

    printf("Hej och välkommen till testlistaren\n");
    int menyval = 1;
    while (menyval > 0 && menyval <= 5) {
        skrivMeny();                     // skriver ut menyn på skärmen
        scanf("%d", &menyval);
        ...
Observera att när man läser in ett heltal från tangentbordet måste man skicka adressen till heltalet som argument till scanf! Vad händer annars?

Kompilera programmet med gcc -o testlista testlista.c lista.c

Makefile

Skriv en makefile för ditt projekt. Om man enbart ändrar i testlista.c ska inte lista.c behöva kompileras om.

Programmera en hashtabell

Nu är det dags att göra hashtabellen. Som hashfunktion ska en korkad algoritm implementeras. Givet en sträng så returneras siffersumman av de olika bokstävernas ASCII-värden modulo hashvektorns storlek.

För den intresserade finns betydligt bättre hashfunktioner som Robert Jenkins har skrivit men de kan vara svårtydda innan man har lärt sig C.

Skriv hashtabellsimplementationen i två filer hash.h, hash.c och skriv sedan ett nytt huvudprogram i en ny fil. Hashtabellsimplementationen ska använda sig av den länkade listan som krocklista. Utöka din Makefile så att allting kan byggas, även testprogrammet till listan.

Låt testprogrammet läsa flera namn och telefonnummer från en fil när programmet startar. Använd fopen för att öppna filen för läsning och fscanf för att läsa in från filen. Programmet behöver inte kunna spara till filen.

Programmet ska ha en meny där man kan lägga in och plocka ut folk ur hashtabellen precis som för listan. Så här ska det kunna se ut:

Hej och välkommen till kom-ihåg, vad vill du göra?
1) Lägga in ny kontakt
2) Ta bort kontakt
3) Leta upp kontakt
4) Avsluta
>1
Vad heter din nya kompis?  Kalle
Ange telefonnummer?        123131

Kalle är lagrad

Hej och välkommen till kom-ihåg, vad vill du göra?
1) Lägga in ny kontakt
2) Ta bort kontakt
3) Leta upp kontakt
4) Avsluta
...

Innan du redovisar

Repetera dina kunskaper och tänk efter om du kan svara ja på följande:


Se så superbt ............................................ skålar .............................. den .................




Extrauppgift

För de som tycker labben var för lätt kan ni skissa på följande användning av en länkad lista. Deklarera ett stort utrymme. Dela upp minnet i många lika stora delar. Låt början på varje del vara en pekare till nästa del. Då har ni en länkad lista med ett maximalt antal element och en fix storlek på innehållet. Skriv funktionerna put, get och isFull för din länkade lista.

Ovanstående lista kan användas för att allokera små objekt. Större objekt kan använda malloc. Det är en beprövad teknik om man har ett program som har många små objekt med kort livslängd.