Föreläsning 9: Förprocessorn och stora program Nu ska vi studera hur vi skriver stora program, vi ska inte skriva så stora program, men vi ska illustrera de verktyg som finns för att skapa en bra struktur och bra struktur är precis det som behövs när man ska skriva stora program. Ni kommer inte att behöva göra detta i laborationerna, men i hemtentan till TEN2 måste ni använda dessa tekniker. Vi inleder med en kort genomgång av förprocessorn men sedan tittar vi på hur vi delar upp ett programs källkod i mindre delar. 14.1 Hur förprocessorn fungerar Att skapa ett körbart program kallas ibland för att bygga programmet (build). Att bygga ett program kan delas in i två huvudsteg: kompileringen och länkningen. Vid kompileringen översätts all källkod till körbar maskinkod (som ibland också kallas objektkod vars filer slutar på.o). Denna maskinkod organiseras i moduler (delar). Kompileringen resulterar alltså i ett antal kodsnuttar. Dessa kodsnuttar måste sättas samman med andra kodsnuttar för att få det slutliga körbara programmet. Det är detta som kallas länkning, man säger att man länkar ihop kod till ett körbart program. Men kompileringen har också flera delsteg. I det första steget behandlas all källkod av förprocessorn som bland annat hämtar alla inkluderingsfiler och expanderar de makron vi gjort. (Alltså om vi skrivit #define MAX 10 i källkoden så ersätts alla förekomster av MAX med 10 i den fil där makrot MAX förekommer.) Vi ska här studera hur denna förprocessor fungerar. 14.2 Olika förprocessordirektiv De flesta förprocessordirektiv faller i tre kategorier: makron, filinkluderingar (som vi nämnt ovan) och villkorlig kompilering. Vi ska använda oss av alla dessa tre slag av direktiv. Det finns en del annat också, men det lämnar vi utanför denna kurs. Vi pratar inte något vidare om filinkludering, vi nöjer oss bara med att konstatera att om det ges ett inkluderingsdirektiv i en källkodsfil så klistras den filen in där av förprocessorn efter den har behandlats klart (alltså efter makron har expanderats i den etc.) 14.3 Makron Vi har redan studerat makron i flera exempel genom kursen. Vi har många gånger sett konstruktioner som #define MAX 10 för att få ett program att kunna vara lättanpassligt till andra förutsättningar, med ovanstående makro kan vi lätt byta 10 mot 100 för att få ett program som är anpassat till 100 som maxantal av vad det nu är som programmet hanterar. Givetvis förutsätter detta att textsträngen MAX finns överallt där den behövs så att en ändring slår igenom i alla delar i programmet. Vi kommer inte att göra så mycket mer makron än att definiera olika tal på det här viset och möjliggöra en snabbanpassning av programmet. Om ni vill kan ni läsa avsnitt 14.3 själva, men vi kommer inte att göra så mycket mer med makron än just definiera tal. Vid några tillfällen kanske vi kommer att specificera makron på kommandoraden som vi använder för att kompilera ett program eller en programmodul, men vi berättar om den tekniken då istället. En annan anledning till att vi inte ska tala så mycket om makron är att det i själva verket är väldigt lätt. Om vi undervisar en massa om något som är lätt så skapar det lätt förvirring. Tänk bara så här: makron är ett sätt att byta ut texten MAX mot ett tal i hela programmet, programmet beror sedan på vad värdet på MAX är. Förstås finns det möjlighet att göra detta med flera och/eller andra texter, MAX var förstås bara ett exempel. Och det är en rent textmässig ersättning som sker, innan kompileringen, texten MAX byts mot 10, eller 100 eller vad man nu vill ha. Det finns mycket stora möjligheter att skapa många andra makron, men det kan vi studera senare när vi inhämtat mer programmering. johnnyp@kth.se Sidan 1 av 15
14.4 Villkorlig kompilering Villkorlig kompilering (alltså att vissa avsnitt av programkoden ingår i kompileringen eller inte) möjliggörs av en del förprocessordirektiv som kan styra vad som skickas till kompilatorn. Man styr det med speciella villkorsdirektiv som bland andra #if, #elif, #endif etc. Men innan vi kastar oss över detta ska vi titta på två begrepp. Operatorer och direktiv Ett direktiv inleds med #, vi har sett #include och #define, vi ska snart se mera. Men för att ge ett direktiv flexibilitet införs även operatorer som kan styra mer detaljer kring hur ett direktiv fungerar. Vi ska inte se på detta så mycket, men ibland poängtera detta. Vi går nu vidare och studerar de direktiv som vi kommer att behöva. Grundläggande direktiv De första direktiv vi studerar är #if och #endif. En intressant teknik är att skriva #define DEBUG 1 för att poängtera att vi nu kompilerar en debug-version av ett visst program. I programmet kan vi senare skriva kod av typen #if DEBUG printf( Value of i: %d.\n, i); printf( Value of j: %d.\n, j); #endif och eftersom DEBUG har värdet 1 så inkluderas de båda printf()-satserna i programmet, och då kompileras de förstås in i det körbara programmet. Om DEBUG däremot sätts till 0 (lätt att bara ändra i koden) så inkluderas inte printf()-satserna i det som skickas till kompilatorn. På detta sätt uppnår vi villkorlig kompilering, genom att styra om viss kod överhuvudtaget kommer med i kompileringen. Direktiven #ifdef och #ifndef frågar om ett visst makro är definerat. Vi skulle kunna uppnå samma resultat som ovan på ett alternativt sätt genom att skriva #define DEBUG och längre ner i programmet #ifdef DEBUG printf( Value of i: %d.\n, i); printf( Value of j: %d.\n, j); #endif Vi behöver då inte ens ha ett värde associerat med DEBUG, det räcker med att den är definerad med #define. Man kan lätt ändra i koden genom att kommentera bort direktivet #define DEBUG och senare ska vi se hur man kan styra värden på makron från kommandoraden som vi använder för att kompilera ett program. Det ger styrmöjligheter att kompilera olika varianter av ett program. Det finns också else-if- och else-konstruktioner i samband med detta, ni kan läsa om dessa saker själva. Läs speciellt avsnittet om Uses of Conditional Compilation. Det är ingenting som vi kommer att använda mycket i den här kursen, men ni ska känna till att möjligheterna finns. Avsnitt 14.5 Olika direktiv, lämnas till självstudier. Det är inga saker som är absolut nödvändiga för att genomföra kursen, men, återigen, det ingår i en allmänbildning som programmerare att känna till att dessa möjligheter finns. johnnyp@kth.se Sidan 2 av 15
Kapitel 15 Att skriva stora program Det här är ett nytt avsnitt i kursen, vi har inte haft detta avsnitt förut, men vi gör ett försök att ändra på examinationsformen i år så att vi kan få med detta avsnitt. Det är ett viktigt avsnitt som ger oss möjligheter att hantera stora program på ett bra sätt. Stora program är annars mycket svårhanterliga och vi löser detta genom att dela upp ett stort program i lämpligt valda delar, så kallade moduler eller bibliotek. Vi har faktiskt redan använt oss av detta, vi har alltid hanterat program som är uppbyggda av flera delar, bara vi skriver #include <stdio.h> så tar vi en en annan del i vårt program, den delen är ett så kallat bibliotek som innehåller funktioner för in- och utmatning av data. Funktionen printf() ingår här och utan den skulle vi inte kunna göra så mycket alls i nuläget. Det finns flera fördelar med att dela upp ett program i lämpliga delar. När vi grupperar funktioner som hör ihop (som till exempel alla in- och utmatningsfunktioner i stdio) så förtydligar detta strukturen på programmet och hjälper oss att ha en översikt över hela programmet. Varje del (som kallas källkodsfil) kan kompileras separat, det sparar tid om programmet måste ändras mycket och det är ofta fallet när man utvecklar ett stort program. Om vi grupperar funktioner i lämpliga delar (som ofta kallas bibliotek) så kan dessa bibliotek ofta återanvändas i flera andra program. Vi kan ju observera att vi ofta återanvänder biblioteket stdio i varje C-program som vi skrivit hittills. Återanvändning är en mycket stor fördel eftersom återanvänd kod ofta är mycket bra. Biblioteket stdio hör nog till ett av världens mest testade programvaror (eftersom det ingår i så många program). 15.1 och 15.2 Källkodsfiler (.c) och headerfiler (.h) Grundstenarna för att åstadkomma uppdelning av ett stort program i mindre delar är källkodsfiler och headerfiler. Vi får leva med språkmixen här, header är ett engelskt ord för rubrik, men en översättning som stämmer bättre med betydelsen i C-programmering skulle vara översikt eller sammanfattning. Vi kan anse att en headerfilen innehåller en översikt eller en sammanfattning. Hittills har vi arbetat med en fil som innehåller funktionen main() och den filen har slutat på.c. Och det har varit hela vårt program, som vi själva har skrivit. Vi har kallat den för källkodsfil och det ska vi fortsätta med (eftersom boken kallar den för det. Egentligen kan man också kalla headerfilen för källkodsfil). Men till källkodsfilen som innehåller main() har vi också ofta (för att inte säga alltid) knutit biblioteket stdio (som vi inte har skrivit) och vi har gjort det genom att använda oss av ett inkluderingsdirektiv, som refererar till en headerfil som ger en sammanfattning/översikt av stdio, vi har många gånger skrivit #include <stdio.h>. Målet med detta kapitel är att vi ska lära oss skriva egna bibliotek (eller moduler), alltså egna headerfiler tillsammans med källkodsfiler, och därmed lära oss att dela upp ett program i flera delar. Vi börjar genom att titta på ett typiskt program som vi sett det tidigare, en källkodsfil, med en main()-funktion som anropar ett antal funktioner. Funktionerna ska operera på strängar som tillhandahålls av main()-funktionen. Vi observerar då att våra strängfunktioner är ganska likartade och därför ska vi samla dem i ett litet bibliotek så att vårt program blir uppdelat i två delar: huvudprogrammet och minibiblioteket. När vi studerat denna grundläggande uppdelning ska vi införa ytterligare ett minibibliotek och se hur vi kan kombinera flera bibliotek. Vi kommer också att ibland kalla bibliotek för moduler. Ordet modul är en mer generell term än ordet bibliotek och kan betyda bibliotek eller på annat sätt en del som passar ihop med andra delar (moduler.) Huvudprogrammet kan också kallas en modul. johnnyp@kth.se Sidan 3 av 15
Här är programmet som det ser ut från början: #include <stdio.h> #include <string.h> #include <time.h> #include <stdlib.h> void reverse(char *s) char tmp; int i, length = strlen(s); for(i=0;i<length/2;i++) tmp=s[i]; s[i]=s[length-1-i]; s[length-1-i]=tmp; void sort(char *s) char tmp; int i,j, length = strlen(s); for(j=i+1;j<length;j++) if(s[i]>s[j])tmp=s[i];s[i]=s[j];s[j]=tmp; void mix(char *s) char tmp; int i,j, length = strlen(s); srand(time(0)); for(j=i+1;j<length;j++) if(rand()%2==0)tmp=s[i];s[i]=s[j];s[j]=tmp; int main(int argc, char *argv[]) int i; char reversed_string[20], sorted_string[20], mixed_string[20]; strcpy(reversed_string,argv[1]); reverse(reversed_string); printf("reversed string: %s.\n", reversed_string); strcpy(sorted_string,argv[1]); sort(sorted_string); printf("sorted string: %s.\n", sorted_string); strcpy(mixed_string,argv[1]); mix(mixed_string); printf("mixed string: %s.\n", mixed_string); return 0; och en provkörning: $./a.out johnny Reversed string: ynnhoj. Sorted string: hjnnoy. Mixed string: yhnjno. Vi har alltså möjlighet att med detta program reversera, sortera eller blanda tecknena i en sträng. Vi ska nu börja fundera över hur vi kan dela upp detta program i två moduler: huvudprogrammet och funktionerna som opererar på strängar. Vi börjar med att beskriva den headerfil vi vill skapa. johnnyp@kth.se Sidan 4 av 15
Headerfilen ska innehålla funktionsprototyperna, vi kallar den stringx.h och vår första version av den ser ut så här: void reverse(char *s); void sort(char *s); void mix(char *s); Alltså bara en uppräkning av funktionsprototyperna. (Vi tänker oss namnet stringx på vårt bibliotek och det står för extra strängfunktioner.) Headerfilen innehåller ingen information om hur dessa funktioner ska fungera, den information ska vi lägga undan i källkodsfilen stringx.c. Vi ger filens innehåll nedan: #include <stdio.h> #include <string.h> #include <time.h> #include <stdlib.h> #include "stringx.h" void reverse(char *s) char tmp; int i, length = strlen(s); for(i=0;i<length/2;i++) tmp=s[i]; s[i]=s[length-1-i]; s[length-1-i]=tmp; void sort(char *s) char tmp; int i,j, length = strlen(s); for(j=i+1;j<length;j++) if(s[i]>s[j])tmp=s[i];s[i]=s[j];s[j]=tmp; void mix(char *s) char tmp; int i,j, length = strlen(s); srand(time(0)); for(j=i+1;j<length;j++) if(rand()%2==0)tmp=s[i];s[i]=s[j];s[j]=tmp; Nu har vi separerat ut alla strängfunktioner från vårt ursprungliga program. Vi har nu två filer, en headerfil (.h) och en källkodsfil (.c) och vi ska nu hantera dem som en modul. I källkodsfilen skriver vi #include "stringx.h" för att också ta in headerfilen vid kompilering. Det är inte nödvändigt i detta exempel, men det kommer att vara nödvändigt i framtiden, så vi tar som vana att inkludera headerfilen i källkodsfilen. Vi går nu över och studerar hur vårt huvudprogram ska se ut när vi bygger dess körning på vår stringx. johnnyp@kth.se Sidan 5 av 15
Så här ser huvudprogrammet ut: #include <stdio.h> #include <string.h> #include "stringx.h" int main(int argc, char *argv[]) int i; char reversed_string[20], sorted_string[20], mixed_string[20]; strcpy(reversed_string,argv[1]); reverse(reversed_string); printf("reversed string: %s.\n", reversed_string); strcpy(sorted_string,argv[1]); sort(sorted_string); printf("sorted string: %s.\n", sorted_string); strcpy(mixed_string,argv[1]); mix(mixed_string); printf("mixed string: %s.\n", mixed_string); return 0; Vi ser att vi behöver inkludera stringx.h ovan, efter stdio.h och string.h. Och enda anledningen till att vi inkluderar string.h är för att vi anropar strcpy() i huvudprogrammet, hade vi inte haft anropet till strcpy() i main() hade vi sluppit också den. Det blir alltså väsentligt mindre inkluderingsfiler att hålla reda på vilket är ett uttryck för att vi skapar mer ordning och reda. Om ett program blir mer lättöverskådligt så är det ett mycket bra tecken. Vi ser också att vi inkluderar stringx.h på ett annat sätt än stdio.h och string.h, vi använder citationstecken istället för spetsparenteserna (< och >.) Anledningen är att vi skrivit stringx.h själva och lagt den precis brevid den källkodsfil som inkluderar den, då ska man ha citationstecken. Spetsparenteserna (< och >) används normalt för att inkludera filer som hör till själva systemet, i ett UNIX-system letar förprocessorn i katalogen /usr/include/ när man anger en inkluderingsfil med spetsparenteser. Undersök gärna innehållet i den katalogen om ni vill. Innan vi går in på hur man kompilerar och länkar det här programmet ska vi studera ett beroende mellan filerna. Vi har alltså tre filer, stringx.h, stringx.c och test_stringx.c. Vi kan rita följande beroendeschema över dessa filer: Vi ser här att att test_stringx, alltså vårt huvudprogram, inte alls behöver biblioteken time och stdlib. På samma sätt behöver inte stringx.c veta någonting om biblioteket stdio. Vidare är kopplingen mellan huvudprogrammet och string-biblioteket ganska svag, därför har vi valt en grå färg på det strecket i figuren. Att minska antalet beroende mellan olika moduler i ett programmeringsproblem är mycket mycket värdefullt. Vi kommer att förstå mer av det längre fram i utbildningen, men nu kan vi få en skymt av vad det betyder om vi ser på hur enkelt allt blev. johnnyp@kth.se Sidan 6 av 15
Vi bygger ett körbart program med hjälp av dessa filer i tre steg. Först kompilerar vi de båda modulerna stringx.c och test_stringx.c separat, utan att länka. Med gcc-kompilatorn gör vi det med växeln -c, så här: gcc -c stringx.c gcc -c test_stringx.c Dessa kommandon får till följd att det skapas så kallade objektkodsfiler, alltså byggstenar för det slutliga körbara programmet. I det här fallet heter dessa objektkodsfiler stringx.o respektive test_stringx.o. För att nu slutligen länka ihop filerna till ett körbart program använder vi gcc utan växeln -c. Vi lägger också på växeln -o för att kunna välja namn på den slutliga körbara filen. Så här: gcc -o test_stringx test_stringx.o stringx.o Vi utläser det sista kommandot så här, tag filerna test_stringx.o och stringx.o, länka ihop dem och skapa det körbara programmet med namnet test_stringx. Vi kan nu köra programmet och en testkörning visas nedan: $./test_stringx johnny Reversed string: ynnhoj. Sorted string: hjnnoy. Mixed string: yhnojn. Alltså, som förut, programmet har ju inte ändrats i sin funktion, men vi har gjort väsentliga förändringar i programmets uppbyggnad i och med att det nu består av två moduler, huvudprogrammet och vårt extra nyskrivna lilla bibliotek, stringx (med endast tre funktioner i). Så långt är allt gott och väl, vi har lyckats dela upp ett program i två delar eller moduler som vi också säger. Det blir tyvärr lite mer komplicerat när man har flera moduler och de inkluderar varandra på ett komplicerat sätt. Beroendeschemat ovan illustrerade inte det. Men, betrakta följande situation: Headerfiler kan inkludera andra headerfiler och i situationen beskriven av figuren ovan inkluderar vårt huvudprogram headerfiler till två bibliotek, (som heter bibliotek_1.h och bibliotek_1.h) men båda dessa bibliotek inkluderar, i sin tur, en tredje gemensam headerfil. Om man bara använder inkluderingsdirektiven som illustrerat ovan så kommer koden som finns i fil_som_alla_inkluderar.h att inkluderas i huvudprogrammet två gånger, en gång för varje inkluderingsdirektiv som finns i bibliotek_1.h respektive bibliotek_1.h. Det kommer att ge ett kompileringsfel, kod får inte förekomma mer än en gång. Vi behöver i det här läget införa en mekanism som ser till att kod inte förekommer flera gånger. Det uppnås med hjälp av villorlig kompilering som åstadkoms med hjälp av förprocessorn. Det finns här ett klassiskt sätt johnnyp@kth.se Sidan 7 av 15
att författa en headerfil och vi utvidgar våran stringx.h så att den följer det sättet. Då skriver vi på följande sätt i stringx.h: #ifndef STRINGX_H #define STRINGX_H void reverse(char *s); void sort(char *s); void mix(char *s); #endif Vad betyder detta? Jo, först frågar vi om makrot STRINGX_H är definerat. Om det inte är definerat så ingår allting fram till #endif. Det första som sker då är att makrot STRINGX_H defineras, visserligen till ingenting, men poängen med att definiera STRINGX_H är att vi inte ska inkludera det som står mellan #ifndef och #endif två gånger. Om förprocessorn inkluderar stringx.h flera gånger i samma kompilering så kommer alltså den egentliga koden, som bara består av void reverse(char *s); void sort(char *s); void mix(char *s); att förekomma en enda gång. Vi slipper då dubblerad kod och kan kompilera programmet utan bekymmer. Förprocessordirektiv som skrivs på detta sätt brukar kallas för guards, alltså vakter, som vaktar så att inte kod inkluderas mer än en gång. Det makro som man definerar för att utföra ett test på (ovan STRINGX_H) brukar få ett namn som är filens namn i stora bokstäver och punkten bytt mot ett understrykningstecken. Filen stringx.h får alltså en guard med makrot STRINGX_H. Vi kommer kanske inte att uppleva detta, men vi tar för vana att lägga till guards på alla headerfiler från och med nu. 15.3, 15.4 Flera bibliotek, stora program och make-filer Vi ska studera ett väsentligt mindre exempel än vad boken studerar. Vi tänker oss att vi vill skriva ett spionprogram som hanterar kryptering för att kunna skicka hemliga meddelanden till andra. Vi skriver då en till liten modul som heter crypt som innehåller möjligheter att kryptera och dekryptera meddelanden. Biblioteket crypt ska, liksom huvudprogrammet. Vi får då följande relationer mellan de olika modulerna: Detta diagram är en skiss över relationerna mellan modulerna, den beskriver inte hur vi ska inkludera filer. Vi har inte ett behov av guards i det här fallet, men vi lägger in guards i headerfilerna ändå. En mycket intressant aspekt här är att vi återanvänder stringx när vi skriver crypt. Det här är en väsentlig del av god mjukvaruhantering. Det finns ingen anledning att skriva johnnyp@kth.se Sidan 8 av 15
om stringx om den redan finns och är utprovad. Därför återanvänder vi den. Vi studerar nu detaljerna i de tre moduler som utgör vårt program, huvudprogrammet (som kallas spion) samt crypt respektive stringx. Eftersom vi redan studerat stringx tittar vi på crypt respektive spion. Vi börjar med crypt. Headerfilen ser ut så här: #ifndef CRYPT_H #define CRYPT_H void caesar_crypt(char* str, int steps); void caesar_decrypt(char* str, int steps); void caesar_crypt_with_reverse(char* str, int steps); void caesar_decrypt_with_reverse(char* str, int steps); #endif Vi har alltså fyra funktioner som opererar på strängar. De första funktionen, caesar_crypt(), krypterar en sträng enligt Caesarkryptot som innebär att alla A byts mot B, alla B byts mot C osv till alla Z som byts mot A. Detta är Caesarkryptot med ett steg. Man går två (eller flera) steg också (och byta alla A mot C, alla B mof D osv.) Hur många steg man går bestäms av parametern steps som man skickar in tillsammans med strängen som ska krypteras som skickas in i parametern str. Funktionen caesar_decrypt() gör det som caesar_crypt() gör, fast baklänges, så om vi krypterat en sträng med caesar_crypt() och säg 8 steg, så får vi tillbaka strängen om vi gör caesar_decrypt() på resultatet med 8 steg. De andra två funktionerna i minibiblioteket, caesar_crypt_with_reverse() och caesar_decrypt_with_reverse(), gör precis samma saker som de andra två, men vi lägger också till en reversering av strängen, det vill säga, i ett (patetiskt) försök att förbättra kryptot vänder vi också på strängen när vi krypterar den för att vi kanske får för oss att det blir säkrare då. Det intresssanta här är inte kryptering, det intressanta här är att vårt lilla bibliotek av fyra funktioner kan återanvända stringx. Vi behöver inte, i detta bibliotek, skapa en funktion som vänder på strängar, för att kunna vända på en sträng återanvänder vi alltså stringx därav inkluderingen av stringx.h i crypt.h. De här funktionerna är förstås ganska ointressanta, det är ingen som använder sig av Caesarkryptot eftersom det är så lätt att knäcka (med statistik), det enda skälet till att vi använder dem är för att illustrera hur man skapar programmoduler. make-filer Då vi utvecklar programvara som består av flera filer som ska kompileras i rätt ordning behöver vi, då vi vändrar och vill provköra en ändring, utföra följande kommandon vid en kommandoprompt: gcc -c stringx.c gcc -c crypt.c gcc -c spion.c gcc -o spion spion.o crypt.o stringx.o De första tre kommandona, anropen till gcc med växeln -c, skapar de tre objektkodfilerna, stringx.o, crypt.o och spion.o. När dessa är klara kan man bygga ihop hela programmet genom att länka, det gör vi också med gcc, fast med växeln -o, så att vi får ett program som heter spion som vi ser sist. Om vi nu ska ändra lite i säg stringx.h som inkluderas av både crypt och spion så måste alltså alla filer kompileras om. För att automatisera byggandet av ett program johnnyp@kth.se Sidan 9 av 15
som består av flera filer använder man i UNIX/POSIX-världen av ett verktyg som heter make. Det betyder göra på engelska (förstås), men kan också betyda tillverka. Med verktyget make kan vi ange beroenden mellan filer och ge ett schema till make som anropar gcc i den ordning och på det sätt som behövs. Schemat som styr beteendet av make-verktyget kallas för en make-fil. Den måste ha namnet Makefile eller makefile och läggs enklast brevid de filer den ska påverka. Så här ser make-filen ut för vårt exempel ovan: spion: spion.o crypt.o stringx.o gcc -o spion spion.o crypt.o stringx.o spion.o: spion.c crypt.h stringx.h gcc -c spion.c stringx.o: stringx.c stringx.h gcc -c stringx.c crypt.o: crypt.c crypt.h stringx.h gcc -c crypt.c clean: rm *.o rm *~ Denna fil heter som sagt Makefile och när den är närvarande brevid de källkodsfiler den ska påverka kan man bara skriva make vid en kommandoradsprompt så räknar make själv ut vilka anrop som behövs till gcc för att bygga det slutgiltiga programmet. Ni får själva läsa er till exakta detaljer om make, använd det definitivt från och med nu. Information hiding, att dölja information Ovan har vi ett sammansatt program med strukturen given av skissen som vi såg förut: Men hur ser detta ut rent kodmässigt? En viktig aspekt är att vi har headerfiler som bara deklarerar de funktioner som finns i en tillhörande källkodsfil (.c-fil) och att headerfilen inte innehåller upplysningar om hur funktionerna är skrivna i detalj. Faktiskt vill vi dölja denna information så att vi inte får så mycket att hålla reda på. Headerfilen ska presentera de funktioner som finns, gärna ge kommentarer om hur man anropar dem, men själva funktionen ska finnas i.c-filen. I början av huvudprogrammet, spion.c, står det bara: #include <stdio.h> #include <string.h> #include "stringx.h" #include "crypt.h" johnnyp@kth.se Sidan 10 av 15
och i dessa headerfiler finns bara funktionsprototyperna, stringx.h till exempel, hade innehållet void reverse(char *s); void sort(char *s); void mix(char *s); Att dölja information om detaljer ger programmeraren en styrka, för hon/han slipper tänka på precis hur de funktioner fungerar som hon/han använder sig av. Det är också en styrka för oss att vi slipper funder på procis hur printf() fungerar, vi kan bara tänka på det vi vill skriva ut. Huvudprogrammet Eftersom vi inte behöver grubbla över hur biblioteken stringx och crypt fungerar (ni kan läsa detaljerna själva, de finns i slutet av föreläsningen) så räcker det att studer huvudprogrammet. Vi ser på källkoden till spion.c: #include <stdio.h> #include <string.h> #include "stringx.h" #include "crypt.h" #define QUIT 0 #define REVERSE 1 #define NEWSTRING 2 #define CAESAR_CRYPT 3 #define CAESAR_DECRYPT 4 #define CAESAR_CRYPT_WITH_REVERSE 5 #define CAESAR_DECRYPT_WITH_REVERSE 6 int main(int argc, char *argv[]) char buf[2], str[20]; strcpy(str,argv[1]); int choice = -1; while (choice!= QUIT) printf("==============\nstring: %s.\n\n", str); printf("menue: \n" "1. Reverse.\n" "2. Enter new string.\n" "3. Caesar crypt.\n" "4. Caesar decrypt.\n" "5. Caesar crypt with reverse.\n" "6. Caesar decrypt with reverse.\n" "0. Quit.\n\n> "); scanf("%d", &choice); gets(buf); switch(choice) case QUIT: break; case REVERSE: reverse(str); break; case NEWSTRING: printf("new string: "); gets(str); break; johnnyp@kth.se Sidan 11 av 15
case CAESAR_CRYPT: int steps; printf("steps: "); scanf("%d", &steps); gets(buf); caesar_crypt(str,steps); break; case CAESAR_DECRYPT: int steps; printf("steps: "); scanf("%d", &steps); gets(buf); caesar_decrypt(str,steps); break; case CAESAR_CRYPT_WITH_REVERSE: int steps; printf("steps: "); scanf("%d", &steps); gets(buf); caesar_crypt_with_reverse(str,steps); break; case CAESAR_DECRYPT_WITH_REVERSE: int steps; printf("steps: "); scanf("%d", &steps); gets(buf); caesar_decrypt_with_reverse(str,steps); break; Uppbyggnaden av huvudprogrammet är först ett par inkluderingar, sedan ett antal makron (så vi kan skriva till exempel REVERSE i koden och veta att vi menar menyvalet då en användare vill vända på en sträng.) Sedan följer main() som bara innehåller en jättestor loop som kör tills användaren gör menyvalet QUIT. I loopen får användaren hela tiden göra olika val, vi läser in vilket val användaren gör med satserna scanf("%d", &choice); gets(buf);. Observera att vi alltså läser in ett heltal i heltalsvariabeln choice, men vi gör omedelbart efter en inläsning av en sträng med gets() i variabeln buf. Det finns en speciell anledning till att vi gör detta. Då vi blandar inmatning av så kallade primitiva datatyper, int, float, double, char och liknande, med strängar, (ovan str med inmatningen printf("new string: "); gets(str);) så kan vi komma i otakt. När vi läser in värdet i choice ovan, så lagras det tal som vi matar in i variabeln choice, det är inte något konstigt med det. MEN: returtangenttryckningen lagras också i en buffert som väntar på att bli läst. Därför gör vi, omedelbart efter scanf("%d", &choice); inmatningen gets(buf);. Detta tömmer bufferten. Om vi inte gör den extra inmatningen (gets(buf);) och gör menyvalet NEWSTRING, alltså att vi vill mata in en ny sträng, ja, då kommer satserna printf("new string: "); gets(str); att resultera i att str blir den tomma strängen. Vi kommer att uppleva detta som att vårt program hoppar över inmatningen av str, men det är inte sant, programmet läser bara en gammal returtangenttryckning som låg i en inmatningsbuffert. Knepet är alltså att tömma inmatningsbufferten helt innan vi går vidare. Det här är förstås väldigt klumpigt och konstgjort. Om ni tycker att det här verkar vansinnigt så är ni i gott sällskap, det tycker jag också. Anledningen att det funderar så här är att vi egentligen inte ska skriva konsolprogram, som riktiga programmerare vill vi ha fönster och menyer och så vidare. Men det kommer senare, i senare kurser så slipper vi det här konstgjorda terminalfönstret. johnnyp@kth.se Sidan 12 av 15
Vi kan nu provköra programmet och det ger oss en möjlighet att kryptera ord och vända och vrida på resultatet som vi vill. Samtidigt testar vi då de funktioner som finns i våra bibliotek, stringx och crypt. Här är en provkörning:./spion JOHNNY ============== String: JOHNNY. Menue: 1. Reverse. 2. Enter new string. 3. Caesar crypt. 4. Caesar decrypt. 5. Caesar crypt with reverse. 6. Caesar decrypt with reverse. 0. Quit. > 1 ============== String: YNNHOJ. Menue: 1. Reverse. 2. Enter new string. 3. Caesar crypt. 4. Caesar decrypt. 5. Caesar crypt with reverse. 6. Caesar decrypt with reverse. 0. Quit. > 3 Steps: 1 ============== String: ZOOIPK. Menue: 1. Reverse. 2. Enter new string. 3. Caesar crypt. 4. Caesar decrypt. 5. Caesar crypt with reverse. 6. Caesar decrypt with reverse. 0. Quit. > 6 Steps: 1 ============== String: JOHNNY. Menue: 1. Reverse. 2. Enter new string. 3. Caesar crypt. 4. Caesar decrypt. 5. Caesar crypt with reverse. 6. Caesar decrypt with reverse. 0. Quit. > 0 johnnyp@kth.se Sidan 13 av 15
Nedan ges den fullständiga källkoden till biblioteken crypt och stringx. CRYPT: $ cat crypt.h #ifndef CRYPT_H #define CRYPT_H void caesar_crypt(char* str, int steps); void caesar_decrypt(char* str, int steps); void caesar_crypt_with_reverse(char* str, int steps); void caesar_decrypt_with_reverse(char* str, int steps); #endif $ cat crypt.c #include <string.h> #include "stringx.h" #include "crypt.h" void caesar_crypt(char* str, int steps) int i, length = strlen(str); str[i] -= 65; str[i] += steps; str[i] %= 26; str[i] += 65; void caesar_decrypt(char* str, int steps) int i, length = strlen(str); str[i] -= 65; str[i] -= steps; if(str[i]<0)str[i]+=26; str[i] %= 26; str[i] += 65; void caesar_crypt_with_reverse(char* str, int steps) reverse(str); caesar_crypt(str,steps); void caesar_decrypt_with_reverse(char* str, int steps) reverse(str); caesar_decrypt(str,steps); johnnyp@kth.se Sidan 14 av 15
STRINGX: $ cat stringx.h #ifndef STRINGX_H #define STRINGX_H void reverse(char *s); void sort(char *s); void mix(char *s); #endif $ cat stringx.c #include <stdio.h> #include <string.h> #include <time.h> #include <stdlib.h> #include "stringx.h" void reverse(char *s) char tmp; int i, length = strlen(s); for(i=0;i<length/2;i++) tmp=s[i]; s[i]=s[length-1-i]; s[length-1-i]=tmp; void sort(char *s) char tmp; int i,j, length = strlen(s); for(j=i+1;j<length;j++) if(s[i]>s[j])tmp=s[i];s[i]=s[j];s[j]=tmp; void mix(char *s) char tmp; int i,j, length = strlen(s); srand(time(0)); for(j=i+1;j<length;j++) if(rand()%2==0)tmp=s[i];s[i]=s[j];s[j]=tmp; johnnyp@kth.se Sidan 15 av 15