TDIU16 Process- och operativsystemprogrammering
C-introduktion
Introduktion till C
Här finns det exempel vi gick igenom på föreläsningen. Exemplet börjar med ett C++-program som vi
stegvis skriver om till C. Programmet innehåller klassen sorted_list
, som
representerar en sorterad sekvens av heltal i en std::vector
. Med hjälp av exemplet
kommer vi alltså se hur vi implementerar allt detta i ren C i en stil som matchar det som används
i Pintos.
All kod finns här. Filerna är döpta main0-9
och live0-9
för vart och ett av stegen nedan. All kod kan kompileras på Unix med
kommandot make
.
Notera: Den här sidan visar den tänkta lösningsgången. Den kan därför avvika något från det som presenterades på föreläsningen. Det viktiga ska dock stämma överens.
Steg 0
Filerna live0.cpp
, live0.h
och main0.cpp
innehåller
originalprogrammet skrivet i C++. I live0.h
finns klassen sorted_list
,
som representerar en sorterad sekvens av heltal. Det maximala antalet heltal som ryms anges som
parameter till konstruktorn. Om listan blir överfull kastas ett
undantag. Filen main0.cpp
innehåller ett huvudprogram som sätter in talen 100-1 i
listan och skriver ut de 40 första i en tabell med 10 kolumner. Då kommer talen 1-40 att skrivas
ut eftersom listan automatiskt sorterar innehållet.
Steg 1
Första steget mot ett program som kompilerar i C. Eftersom cout
är något som bara
finns i C++ börjar vi med att byta ut cout
mot motsvarigheten printf
i
filen main1.cpp
. Se
exempelvis cppreference.com för en mer
detaljerad beskrivning av hur formatsträngar fungerar. De kan även användas till scanf
.
Steg 2
Eftersom C inte har klasser som i C++ börjar vi nu att göra om klassen sorted_list
i live2.h
till en struct
. Medlemsfunktioner är inte heller något som C
har stöd för, så vi flyttar ut dem ur klassen och lägger dem globalt i stället. Skillnaden mellan
en medlemsfunktion och en icke-medlemsfunktion i C++ är (förutom att den får tillgång till privata
medlemmar) att medlemsfunktioner automatiskt får en extra första
parameter this
. Eftersom vi inte använder medlemsfunktioner måste vi lägga till den
parametern själva. Vi kallar den här för me
, eftersom this
är reserverat
i C++. Vi skulle kunna använda this
som variabelnamn i C sedan.
Vi måste också göra något med konstruktorn och destruktorn i klassen. Det vanligaste är att
hantera dem som om de vore vanliga medlemsfunktioner vid namn create
och destroy
. Vi flyttar alltså ut dem ur klassen och får två funktioner som
heter list_create
och list_destroy
. Eftersom de funktionerna nu är
vanliga funktioner måste användaren av klassen själv vara noggran med att
anropa list_create
när en lista skapas list_destroy
när en lista ska
förstöras. Eftersom de är vanliga funktioner kan kompilatorn inte hjälpa till med detta.
Notera att vi hanterar publika och privata medlemsfunktioner lite olika. Publika funktioner
deklarerar vi i headerfilen och privata funktioner deklarerar vi bara som static
i
implementationsfilen. Att deklarera funktioner och variabler som static
innebär att
andra kompileringsenheter (dvs. andra C-filer) inte kommer att komma åt symbolen, vilket är
motsvarigheten till private
i C.
Den statiska variabeln num_moves
har också flyttats ut ur
vår struct
. Den har deklarerats med nyckelordet extern
för att indikera
att det bara är en deklaration och ingen definition. Skulle extern
utelämnas så
skulle varje C-fil få sin egen instans av variabeln, vilket länkaren sannolikt klagar
på. Variabeln definieras sedan i live2.cpp
.
Steg 3
Referenser är inte heller något som finns i C. Vi måste använda pekare i stället. I detta steg
byter vi därför ut alla referenser till pekare. I funktionsdeklarationen kan vi byta
ut &
mot *
, och sedan måste vi uppdatera alla ställen där parametern
eller variabeln används. Vi måste dereferera pekaren varje gång den används för att den ska bete
sig som en referens. Vi måste också lägga till ett &
när vi skickar in en varabel
som en referens. Vi vill exempelvis ersätta me
med (*me)
i koden. Notera
att me->foo
är ett smidigare sätt att skriva (*me).foo
.
Steg 4
std::vector
är inte heller något som finns i C. Vi måste implementera den
funktionaliteten själva med hjälp av dynamisk minnesallokering och arrayer i
stället. En vector
är mer eller mindre en pekare till ett array och en storlek, så vi
kan ersätta vector
variabeln i vår struct
med en pekarvariabel och ett
heltal (en vector
håller också koll på sin kapacitet, men det utnyttjar vi inte i det
här exemplet, så vi ignorerar det).
För att allokera minne skulle vi kunna använda new int[max]
, men eftersom vi vill
skriva C-kod använder vi i stället malloc(max*sizeof(int))
, eller
bättre calloc(max, sizeof(int))
, som ser till att vi inte får overflow i
multiplikationen och initierar minnet till 0. Eftersom både malloc
och calloc
returnerar en void *
måste vi i C++ explicit omvandla pekaren
till rätt typ med en C-cast (int *)calloc(max, sizeof(int))
. Detta behövs inte i C,
så vi kan ta bort den koden senare. Notera att vi behöver inkludera cstdlib
för att
få tillgång till malloc
och calloc
. Minne frigörs med
funktionen free()
.
En annan sak som vi måste tänka på här är hur vi itererar igenom vår data. I list_add
användes iteratorer, vilket inte heller finns i C. Som tur är kan vi använda pekare på samma sätt,
eftersom iteratorer är byggda för att efterlikna pekarmanipulation i C.
I C och C++ är det möjligt att modifiera vad en pekare pekar på med hjälp av vanliga aritmetiska
operationer. Vi kan exempelvis ta en pekare och säga p + 1
för att få en pekare till
det som ligger precis efter *p
i minnet. Hur långt fram uttrycket p + 1
hoppar i minnet beror på vad p
pekar på. Det enklaste är att tänka sig
att p
pekar på ett element i en array, och att p + 1
ger nästa element
oavsett hur stort varje element. p + 1
kommer alltså peka på olika ställen
om p
har typen int *
eller char *
eftersom sizeof(int) != sizeof(char)
. Pekare av typen void *
går alltså
inte att manipulera på detta sättet, även om vissa kompilatorer tillåter det ändå.
Varför fungerar pekarmanipulation på detta sättet? Jo, idén är att det ska vara smidigt att arbeta
med just arrayer. Det ser man ganska tydligt om man tittar närmare på hur arrayer fungerar i C. Om
vi deklarerar en array, int p[10];
, så kan vi utan problem tilldela p
till en variabel av typen int *q = p;
. Arrayer är alltså bara en pekare till första
elementet (det finns små skillnader, exempelvis med sizeof
och flerdimensionella
arrayer, men vi hoppar över dem nu). Det gör att både variablerna p
och q
kan användas som arrayer. Det är till och med så att p[1]
är
ekvivalent med *(p + 1)
.
Allt detta gör att vi kan använda pekare som iteratorer i C. Detta är inte så förvånande
egentligen eftersom iteratorer är byggda för att efterlikna pekare i C. En iterator till början av
en vektor motsvaras av en pekare till första elementet, och en iterator till slutet motsvarar en
pekare till elementet efter sista elementet i vårt array, det vill
säga &data[max]
eller ekvivalent data + max
. Vi kan stega pekarna
med hjälp av ++
och --
eller genom vanlig aritmetik. Vi kan därför se
att ändringarna i list_add
i live4.cpp
är minimala.
Steg 5
Undantag finns inte heller i C, så vi kan inte kasta undantag i list_add
och list_get
. I list_add
kan vi helt enkelt lägga till ett returvärde
för att indikera om allt gick bra eller inte. Egentligen borde vi kontrollera returvärdet
i main6.c
, men eftersom vi vet att alla insättningar kommer att gå bra struntar vi i
det.
I list_get
kan vi inte göra på samma sätt eftersom funktionen redan returnerar
något. I det här fallet är det ett heltal som kan vara precis vad som helst. Alltså kan vi inte ha
ett speciellt returvärde som visar att list_get
misslyckades. Skulle vi returnera en
pekare i stället så skulle vi kanske kunna använda NULL
för det. Ett annat alternativ
skulle vara att returnera resultatet som en utparameter och låta funktionen returnera
en bool
i stället. Eftersom felen som kan uppstå i list_get
tyder på
programmerarfel låter vi programmet terminera genom att anropa abort
. Man skulle
också kunna använda assert
, vilket fungerar på liknande sätt.
Steg 6
Nu har vi kommit så långt att vi nästan kan kompilera koden med C-kompilatorn. Det är bara några
få detaljer kvar. Först och främst måste vi ändra vilka headerfiler vi
inkluderar. Namnen cstdlib
är för C++. I C heter filerna stdlib.h
i
stället. Vi måste också inkludera stdbool.h
för att kunna använda bool
.
Till slut kommer en språklig skillnad: vi kan inte längre använda sorted_list
för att
referera till vår struct
. C har olika namnrymder för typer, struct
s
och union
s. Vår struct
heter alltså struct sorted_list
eftersom det skulle kunna finnas något annat som heter union sorted_list
eller
bara sorted_list
. Om man verkligen inte vill skriva struct
framför
namnet varje gång kan man använda typedef struct sorted_list sorted_list
för att
skapa ett alias. Efter det kan vi använda C-kompilatorn för att kompilera vårt program!
Notera: Det går att använda struct foo
även i C++ för att skilja på exempelvis
en struct
och en funktion. Det behövs exempelvis om man ska anropa
systemanropet stat
eftersom man då behöver en struct stat
som parameter.
Utöver det behöver vi göra ett par omskrivningar i main.c
för att få koden att
kompilera utan varningar. Först och främst måste vi deklarera print_first
som static
eftersom det är en privat funktion. Utöver det måste vi explicit
säga att main
inte tar några parametrar. I C betyder main()
:
funktionen main
finns, men jag orkar inte berätta vilka parametrar den tar. I stället
måste vi skriva main(void)
.
Steg 7
Antag nu att vi vill bryta ut innehållet i while
loopen inuti list_add
till en funktion. Jag vill då kunna modifiera pekaren into
inifrån funktionen. Om jag
bara skickar den som en parameter så kommer jag få en kopia av pekaren, vilket inte räcker. Jag
skulle vilja ha en referens till pekaren (int *&into
), men det går inte eftersom
referenser inte finns i C. Här kan jag använda samma teknik som i steg 3: jag kan skicka en pekare
till pekaren som parameter, det vill säga int **into
. Då kan jag komma åt
originalpekaren med (*into)
som tidigare, och talet vid pekaren
med *(*into)
(även om det inte behövs). Det finns ingen begränsning på hur många
pekarindirektioner som kan användas, men använder man mer än två så är det ofta ett tecken på att
man bör tänka om.
Steg 8
Ända fram till nu har variablerna i struct sorted_list
varit publika. Här visar vi
hur man kan göra för att skapa privata variabler i C. Mycket likt funktioner så vill vi flytta
deklarationen av sorted_list
till C-filen. Vi måste dock ha en framåtdeklaration
(eng. forward declaration) i headerfilen så att kompilatorn vet att typen finns. Det här
gör att det bara är funktioner i live8.c
som vet hur sorted_list
ser ut,
och därmed bara den koden som kan komma åt medlemmarna.
Den här lösningen har en liten nackdel i main8.c
. Eftersom vi inte längre kan
allokera struct sorted_list
på stacken (varför?) måste vi använda malloc
i live8.c
, vilket vi gärna undviker om vi kan eftersom vi nu måste komma ihåg att
frigöra mer minne. Att göra datamedlemmar privata på detta viset är alltså en avvägning mellan hur
viktigt det är att gömma data och hur smidig koden ska vara att använda.
Steg 9
Här har vi också lagt till möjligheten att ange ett predikat till sorteringen. Det visar på hur funktionspekare fungerar i C (och C++).
Sidansvarig: Filip Strömbäck
Senast uppdaterad: 2023-03-13