Om du känner att du är stressad och har ont om tid -- kan det ibland vara en god idé att pröva att gå direkt på funktionspekare, dvs. hoppa över början.
Skapa ett program str.c
.
Som du säkert minns är C:s strängtyp char *
. Det betyder en
adress till en plats i minnet där det finns lagrat ett eller
fler tecken. Det är i de flesta avseenden ekvivalent med typen
char []
som avser en array av ett eller fler tecken. Strängar
innehåller ingen information om hur långa de är -- istället
används ett speciellt tecken som ett stopptecken: det s.k.
null-tecknet som skrivs '\0'
.
Om vi skriver char *str = "Guy Maddin, Winnipeg";
har vi skapat
en sträng vars innehåll är 21 tecken. Om vi skriver detta som en
array: ['G', 'u', 'y', ' ', 'M', 'a', 'd', 'd', 'i', 'n', ',', ' ', 'W', 'i', 'n', 'n', 'i', 'p', 'e', 'g', '\0']
. Det enda sättet att ta reda på hur lång
en sträng är är att "loopa igenom den" och leta efter null-tecknet.
Implementera en funktion string_length()
som tar som argument en
sträng och returnerar ett heltal som avser hur många tecken som
var i strängen. Du kan t.ex. använda en loop och en
heltalsvariabel som räknare för att lösa uppgiften. För varje varv
i loopen, gå ett steg längre "till höger" i arrayen, och leta
efter ett '\0'
-tecken. Antalet gångna steg är strängens längd.
Testa ditt program genom att jämföra dess output med strlen()
som finns i string.h
. Du kan t.ex. skriva så här:
int main(int argc, char *argv[])
{
if (argc < 2)
{
printf("Usage: %s words or string", argv[0]);
}
else
{
for (int i = 1; i < argc; ++i)
{
int expected = strlen(argv[i]);
int actual = string_length(argv[i]);
printf("strlen(\"%s\")=%d\t\tstring_length(\"%s\")=%d\n",
argv[i], expected, argv[i], actual);
}
}
return 0;
}
Observera att för att skriva ut "
inuti en sträng så måste vi
"escape:a dem", dvs. skriva dem som \"
. Tecknet \t
står för
tab.
Exempelkörning:
> gcc -Wall str.c
> ./a.out hej hopp
strlen("hej")=3 string_length("hej")=3
strlen("hopp")=4 string_length("hopp")=4
>
Ovan kan vi se att strlen()
och string_length()
ger samma svar
för de ord vi skickade in vilket ger förhoppning om att
string_length()
är korrekt implementerad.
Analogt med hur vi har använt funktionen getchar()
för att läsa
in tecken för tecken kan vi använda funktionen putchar()
för att
skriva tecken för tecken. Om jag t.ex. har en sträng s
kan jag
skriva putchar(s[3])
för att skriva ut dess fjärde tecken.
- Implementera funktionen
print()
som tar en sträng och skriver ut den på terminalen (med hjälp avputchar()
) fast utan avslutande radbrytning. - Implementera
println()
-- en egen motsvarighet tillputs()
, som tar en sträng och skriver ut den på terminalen (med hjälp avprint()
) med en avslutande radbrytning. Jämför medputs()
i ditt program för att kontrollera att du gör rätt. - Lägg till
print()
ochprintln()
iutils
-biblioteket!
Du är bekant med rekursionsbegreppet sedan PKD. Rekursion i C
fungerar i stort sett likadant. I labb 1 var en extrauppgift att implementera ett
program som skrev ut tal ur Fibonacci-serien på ett imperativt
sätt. Nedan följer ett liknande program, som bara skriver ut det
sista talet i serien och inte varenda tal på vägen dit. Målet med
denna uppgift är att skriva om fib()
-funktionen så att den blir
rekursiv. Efter koden nedan kommer ett exempel som visar hur man
kan skriva en annan imperativ funktion med rekursion.
#include <stdio.h>
#include <stdlib.h>
/// Den intressanta delen av programmet
int fib(int num)
{
int ppf = 0; // the two given fib values
int pf = 1;
for (int i = 0; i < num; ++i)
{
int tmp = pf;
pf = ppf + pf;
ppf = tmp;
}
return pf;
}
/// Den ointressanta main()-funktionen
int main(int argc, char *argv[])
{
if (argc != 2)
{
printf("Usage: %s number\n", argv[0]);
}
else
{
int n = atoi(argv[1]);
if (n < 2)
{
printf("fib(%d) = %d\n", n, n);
}
else
{
printf("fib(%s) = %d\n", argv[1], fib(n));
}
}
return 0;
}
Nedanstående funktion tar in en array av heltal samt längden på arrayen och adderar talen i arrayen och returnerar summan.
long sum(int numbers[], int numbers_siz)
{
long result = 0;
for (int i = 0; i < number_siz; ++i)
{
result += numbers[i];
}
return result;
}
En rekursiv motsvarighet kan skrivas utifrån insikten att summan
av en serie av N tal är lika med det första talet + summan av
resterande tal. Alltså, om vi vill beräkna sum([1, 2, 3, 4])
så kan vi räkna ut den som 1 + sum([2, 3, 4])
, och så vidare,
där basfallet är sum([]) = 0
, alltså summan av alla tal i en
tom array är 0.
Så här skulle vi kunna skriva C-kod som fungerar på detta sätt:
long rec_sum(int numbers[], int numbers_siz, int index)
{
if (index < numbers_siz)
{
return numbers[index] + rec_sum(numbers, numbers_siz, index + 1);
}
else
{
return 0;
}
}
Om vi använder rec_sum
för att summera arrayen [1, 2, 3]
kommer vi att göra följande steg (förenklat):
rec_sum([1, 2, 3]) = return 1 + rec_sum([2, 3])
rec_sum([2, 3]) = return 2 + rec_sum([3])
rec_sum([3]) = return 3 + rec_sum([])
rec_sum([]) = return 0
Vid (4) har vi nått slutet av vår rekursion och vi kan "gå tillbaka"
och räkna ut att svaret på (3) är 3 + 0
och därmed blir svaret på
(2) 2 + 3 + 0
och (1) 1 + 2 + 3 + 0
. Som man kunde förvänta sig.
Eftersom vi inte kan "skala av element från arrayen" använder vi
en extra parameter index
som stiger tills den når numbers_siz
.
Varje rekursionssteg har alltså tillgång till hela arrayen men
börjar titta från index
. Vid startanropet skall index
vara 0.
Vi kan nu implementera sum()
i termer av rec_sum()
:
long sum(int numbers[], int numbers_siz)
{
return rec_sum(numbers, numbers_siz, 0);
}
Nu återgår vi till uppgiften: skriv om fib()
-funktionen i
programmet längst upp på sidan så att den är rekursiv.
Ledning: Vi upprepar den rekursiva definitionen av Fibonacci-serien från labb 1:
fib(1) = 1
fib(2) = 1
fib(i) = fib(i-1) + fib(i-2) om i > 2
En något mer C-lik pseudokod skulle vara:
fib(i) = 1 om i = 0
1 om i = 1
fib(i-1) + fib(i-2) annars
Senare under kursen skall vi diskutera problemet med ändligt
utrymme på stacken och djup rekursion. Ett anrop till rec_sum()
med en väldigt stor array kommer sannolikt att krascha programmet
om inte kompilatorn är smart nog att översätta rekursionen till
en loop -- mer om detta senare alltså.
Testa att ditt program är korrekt genom att jämföra dess resultat med resultatet från ditt imperativa program.
Pekare är som bekant adresser till platser i datorns minne där
data finns lagrat. Vi har ännu inte stiftat någon djup bekantskap
med dem men vi har sett att strängar i C är pekare (char *
), och
att vi skickat in pekare till int
:ar i scanf()
. Nu skall vi
stifta bekantskap med pekare till funktioner. Detta motsvarar
högre ordningens funktioner som du använt i Haskell, t.ex. när du
skickat in en funktion och en datasamling till map
.
C:s syntax för funktionspekares typer är fruktansvärd och det är därför brukligt att man skapa ett typalias -- dvs. skapar ett beskrivande namn på en krånglig typ som man sedan kan använda istället för dess krångliga motsvarighet. Syntaxen för ett typalias är så här:
typedef existerande_typ nytt_namn;
Exempel på typalias som inte berör funktionspekare:
string_t
nedan!
typedef char * string_t;
typedef unsigned int age_t;
Här har vi skapat två typalias: string_t
kan nu användas överallt
som en synonym för char *
och vi har angivit att typen age_t
är
ett icke-negativt heltal. Suffixet _t
är en namnkonvention.
Just typaliaset string_t
är ett alias vi skall undvika eftersom
det avviker från alla andra C-program och därför blir förvirrande
för någon som läser din kod, även om string_t
förmodligen har
mindre kognitiv belastning än char *
för en ny C-programmerare.
Låt oss nu använda typedef
-nyckelordet för att definiera ett
typalias för en funktion. Det är invecklat, men inte speciellt
svårt när man väl har lärt sig läsa koden:
typedef int(*int_fold_func)(int, int);
Koden ovan definierar typen int_fold_func
som en funktion som
tar som argument två int
:ar och returnerar en int
. I Haskell
skulle typen skrivas Int -> Int -> Int
. Asterisken *
framför
namnet int_fold_func
ovan är det som gör det hela till en
pekare. Denna skall dock inte vara med i namnet.
Om jag har en funktion add(...)
och vill skicka en pekare till
den funktionen skriver jag alltså add
. Funktionens namn utan
parenteser och argument. Om jag skriver add(2,2)
är det ju
ett helt vanligt anrop till funktionen och det som skickas
in är resultatet!
Nu kan vi använda int_fold_func
som en datatyp och deklarara
t.ex. en "left fold" (som du kanske minns från PKD). Om du inte minns hur en left fold fungerar -- försök
att räkna ut det från C-koden nedan!
/// En funktion som tar en array av heltal, arrayens längd och
/// en pekare till en funktion f av typen Int -> Int -> Int
int foldl_int_int(int numbers[], int numbers_siz, int_fold_func f)
{
int result = 0;
// Loopa över arrayen och för varje element e utför result = f(result, e)
for (int i = 0; i < numbers_siz; ++i)
{
result = f(result, numbers[i]);
}
return result;
}
Låt oss skriva en funktion som adderar två tal:
int add(int a, int b)
{
return a + b;
}
Eftersom add()
är en funktion av typen Int -> Int -> Int
kan
den användas tillsammans med foldl_int_int
. För att skicka med
en funktion som en parameter skriver du bara funktionens namn, i
detta fall alltså add
.
Uppgift: Skriv om sum()
-funktionen ovan med hjälp av
foldl_int_int()
och add()
. Du behöver inte ändra i vare sig
foldl_int_int()
eller add()
.
Skapa en ny fil med ett lämpligt namn, t.ex. experiment.c
med
en tom main()
-funktion. Skriv all kod för denna uppgift här,
kompilera ofta och fundera på hur du kan skriva kod i
main()
-funktionen som testar/kör den kod som du skriver.
Vi kommer att vilja använda kod som du har skrivit tidigare.
Välj själv om du vill inkludera utils.h
och kompilera med
gcc experiment.c utils.c
eller om du vill kopiera in all
kod som behövs i experiment.c
.
Med hjälp av funktionspekare skall vi nu skriva en generell inläsningsrutin som ger ytterligare abstraktion vid inläsning. Den fångar det generella mönstret för inläsning:
- Skriv ut frågan (t.ex. "mata in ett tal")
- Läser in svaret
- Kontrollerar att svaret är på rätt format (t.ex. att det är ett tal) och går tillbaka till 1. igen vid behov
- Konverterar det till rätt format (t.ex. från
"42"
till42
) - Returnerar resultatet
För att göra detta skall vår nya ask_question()
-funktion ha tre
parametrar:
- Frågan i form av en sträng (
char *
) - En pekare till en funktion som kontrollerar att svaret har rätt format
- En pekare till en funktion som konverterar en sträng till något annat format
Typen på kontrollfunktionen skall vara char * -> bool
, dvs. den tar emot
en sträng och returnerar sant eller falskt.
Typen på konverteringsfunktionen skall vara char * -> answer_t
--
vilket introducerar en ny typ som vi ännu inte har sätt, nämligen
typen answer_t
som är en s.k. union:
typedef union {
int i;
float f;
char *s;
} answer_t;
Typen answer_t
avser ett värde som antingen är en int
eller
en float
eller en sträng (char *
). Om val
är en variabel av
typen answer_t
så kan jag tilldela den ett heltal via val.i = 42
och läsa det på motsvarande sätt ... = val.i
, eller en
sträng via val.s = ...
och ... = val.s
. Namnen i
, f
och s
har jag valt i min definition av answer_t
och kan bytas ut mot
andra namn om man så önskar.
Uppgift! Använd typedef
för att definiera typerna
check_func
och convert_func
med typerna ovan. Utgå från
exemplet från int_fold_func
.
Ett exempel på en kontrollfunktion är den is_number()
som du
redan skrivit (labb 1) och som tar in en sträng och returnerar true eller
false beroende på om strängen kan konverteras till ett tal. En
kontrollfunktion som kontrollerar att en sträng inte är tom kan se
ut så här:
/// Hjälpfunktion till ask_question_string
bool not_empty(char *str)
{
if (strlen(str) > 0)
{
return true;
}
else
{
return false;
}
}
Eller, mer kompakt och bättre:
/// Hjälpfunktion till ask_question_string
bool not_empty(char *str)
{
return strlen(str) > 0;
}
Ett exempel på en funktion som går att använda som
konverteringsfunktion är funktionen atoi()
som vi har använt
förut. Den fungerar eftersom atoi()
returnerar ett heltal som så
att säga är en delmängd av answer_t
.
Dock: Om man försöker skicka in atoi
som argument till en
funktion vars motsvarande parameter är convert_func
kommer
kompilatorn att klaga eftersom answer_t
och int
inte är samma
typ. Det kan man lösa med hjälp av en typomvandling (eng. type
cast):
atoi // har typen char * -> int
(convert_func) atoi // har typen char * -> answer_t
Uppgift! Nu är det dags att skriva funktionen ask_question()
med signaturen:
answer_t ask_question(char *question, check_func check, convert_func convert)
Inuti denna funktion avser variablerna check
och convert
funktioner som kan anropas check(str)
etc.
- Skapa en buffert av lämplig längd
- I en loop
- skriv ut frågan
- läs in en sträng med din egen
read_string()
-funktion - Använd
check
för att kontrollera att det du läst in är korrekt och terminera loopen
- Använd
convert
för att konvertera resultatet och returnera det
Med hjälp av din generella ask_question()
-funktion är det nu busenkelt
att definiera nya. T.ex. kan vi definiera ask_question_int()
så här:
int ask_question_int(char *question)
{
answer_t answer = ask_question(question, is_number, (convert_func) atoi);
return answer.i; // svaret som ett heltal
}
Om du har skrivit en is_float()
på en tidigare labb kan du
använda den för att definiera ask_question_float()
. Dock -- för
att göra detta måste vi skapa en funktion som skapar ett
answer_t
från en double
:
answer_t make_float(char *str)
{
answer_t a; // skapa ett oinitierat answer_t-värde
a.f = atof(str); // gör det till en float, via atof
return a; // returnera värdet
}
Eller, mer kompakt (skapar ett nytt answer_t
-värde) -- via en syntax
som vi skall behandla i mer detalj på nästa labb:
answer_t make_float(char *str)
{
return (answer_t) { .f = atof(str) };
}
Här skriver vi ask_question_float()
utan den onödiga variabeln
answer
bara för att visa att det också är möjligt (notera .f
på slutet!).
double ask_question_float(char *question)
{
return ask_question(question, is_float, make_float).f;
}
För att göra ask_question_string()
måste vi tyvärr gå händelserna
i förväg en aning och använda en funktion, strdup()
vars funktion
är svårt att förklara med den begränsade del av C som vi har tittat
på hittills. Den inlästa strängen i din ask_question()
är ju redan
en sträng, så man kan tycka att man kunde returnera den rakt av, men
så är inte fallet!
Skriv så här längst upp i filen så länge. Senare kommer vi att skriva en egen version av denna kod:
extern char *strdup(const char *);
Det är nämligen så att din inläsningsbuffert (iallafall om du har
använt en array, som är det enda vi gått igenom hittills) är knuten
till den omslutande funktionen -- och efter att funktionen är klar
kan minnet där texten lästes in komma att återanvändas när som helst.
Vi måste därför skapa en kopia av strängen som är fri att skickas
vart som helst. Detta görs med hjälp av funktionen strdup()
som
finns i string.h
och som duplicerar en sträng. Exemplet nedan
skapar en kopia och skriver ut både kopian och originalet:
char *original = "foo bar baz!"
char *kopia = strdup(original);
printf("%s\n%s\n", original, kopia);
Nu kan vi skriva klar ask_question_string()
så här:
char *ask_question_string(char *question)
{
return ask_question(question, not_empty, (convert_func) strdup).s;
}
Nu är det dags att uppdatera det generella biblioteket utils
.
Kopiera in dina funktioner och typalias dit. Där bör nu ligga
följande funktioner (åtminstone), i någon ordning:
- Typen
answer_t
- Typen
check_func
- Typen
convert_func
- Deklarationen
extern char *strdup(const char *);
int read_string(char *buf, int buf_siz)
bool is_number(char *str)
bool is_float(char *str)
ochanswer_t make_float(char *)
(inte obligatoriska)bool not_empty(char *str)
answer_t ask_question(char *question, check_func check, convert_func convert)
char *ask_question_string(char *question)
int ask_question_int(char *question)
double ask_question_float(char *question)
(inte obligatorisk)
Själva definitionen av funktionerna (med koden i) skall ligga i
utils.c
. Funktionsprototyperna (t.ex. bool is_number(char *str);
), extern...
samt typedef:arna skall ligga i utils.h
.
Senare under kursen skall vi diskutera mer ingående hur man skapar bibliotek/moduler, placering av funktioner och definitioner, inkapsling och synlighet.
Om du har gjort allt rätt kan du kompilera om ditt Gissa
Talet-program från föregående labb mot ditt nya utils
-bibliotek:
> gcc -Wall utils.c guess.c
Du borde inte få några varningar vid kompilering (såvida du inte fick det förut -- vilket du inte borde ha gjort!) och om du kör programmet igen skall det fungera precis som förut.
Verifiera att så är fallet och fixa till eventuella buggar!
En funktion som är vanlig i moderna strängbibliotek är funktionen
trim()
(aka strip()
) som tar bort "skräptecken" i början och
slutet av en sträng. Exakt vad ett skräptecken är naturligtvis
subjektivt, men låt oss definiera det som "whitespace" -- enligt
definitionen av isspace()
i ctype.h
. (Använd gärna man isspace
för att ta reda på mer.)
Du skall skriva funktionen trim()
som tar in en sträng och
helt enkelt tvättar bort allt "skräp".
trim(" hej ") => "hej"
trim(" h ej ") => "h ej"
trim(" hej \n") => "hej"
Funktionens signatur:
char *trim(char *str);
Förslag till implementation:
- Ta reda på det första tecknet från vänster, S, som inte är skräp (
!isspace()
) - Ta reda på det första tecknet från höger, E, som inte är skräp (
!isspace()
) - Kopiera varje tecken från S till och med E till början av
str
- Skriv in ett
'\0'
-tecken efter det sista flyttade tecknet - Returnera
str
Lägg till trim()
i utils
-biblioteket!
Du kan testa att ditt program är korrekt med hjälp av följande testprogram:
#include "utils.h"
int main(void)
{
char str1[] = " hej ";
char str2[] = " h ej ";
char str3[] = " hej\t ";
char str4[] = " hej\t \n";
char *tests[] = { str1, str2, str3, str4 };
for (int i = 0; i < 4; ++i)
{
print("Utan trim: '");
print(tests[i]);
print("'\nMed trim: '");
print(trim(tests[i]));
println("'\n");
}
return 0;
}
Sä här skall en körning av programmet se ut:
Utan trim: ' hej '
Med trim: 'hej'
Utan trim: ' h ej '
Med trim: 'h ej'
Utan trim: ' hej '
Med trim: 'hej'
Utan trim: ' hej
'
Med trim: 'hej'
(En modifierad version av detta program var nyligen på ett kodprov.)
Alternativ implementation:
- Gå genom strängen från vänster till höger, och så länge enbart skräptecken hittats, gör ingeting
- Från det att första skräptecknet hittats, kopiera varje tecken som passerats till början av strängen (första gången till position 0, andra till position 1, osv.) -- kopiera även skräptecken. Första gången ett skräptecken kopieras efter att ett eller flera icke-skräptecken har kopierats, kom ihåg platen P för det skräptecknet (vi är bara intresserade av det sista -- högraste -- P:t)
- När du når slutet på strängen, skriv
\0
i P