|
IT-Programmet,
Tema 1 i termin 4:
TTIT61 Processprogrammering och Operativ System/Concurrent Programming and Operating Systems/Den C/C++ som behövs för NachosFöljande är en liten introduktion till C och i viss liten mån C++, författad för studenter som skall laborera med Nachos och som har en bakgrund i Java. Introduktionen är på intet sätt komplett, eller ens fullt detaljerad i de delar som beskrivs.Innehåll
Enkla datatyper,
globala och lokala variabler
Ett C-program består i princip av tre saker: |
void arrCopy(int a[], |
void arrCopy(int *a, |
void arrCopy(int *a, |
I alternativ A använder vi array-syntax och allt blir tydligt och klart. I alternativ B skriver vi det som kompilatorn översätter alternativ A till - dvs vi vet att a inehåller adressen till ett (eller flera) heltal, och att i en array så placeras alla elementen ut i sekvens efter varandra i minnet vilket betyder att första talet ligger direkt där a pekar, andra talet ligger "ett snäpp längre fram"... dvs (a+1) skall alltså bli addressen till andra elementet i arrayen a. Att detta fungerar beror på att C-kompilatorn alltid adderar med storleken på det element som pekas ut. Därför är det viktigt att vi typar pekarna - det blir ju skillnad om vi har char*+1 eller float*+1 eftersom dessa typer tar så olika stor plats. Detta brukar kallas pekararitmetik och är rätt speciellt för C.
I alternativ C har vi den kod som en spelprogrammerare eller annan person som räknar varje slösad CPU-cykel som ett nederlag skulle skriva. För varför skall vi hela tiden plocka fram ett i och addera till ett fast a eller b - vi kan ju låta dessa pekare/addresser succesivt stegas upp genom arrayerna - det blir nog två assemblerinstruktioner mindre inne i for-loopen! ...kanske? Vi skall dock strax se hur detta kan effektiviseras ytterligare!
VARNING NR.1
Det är nu dags för en varning. Vad händer om vi i
for-looparna ovan kontrollerar mot
i<=len istället för som det står nu? Jo, vi
kommer att även pilla på en plats i minnet som ligger efter
det sista elementet i repektive array. Vi har inget skyddsnät som
"vet" hur stor arrayen är, var den börjar och slutar och att
vi håller oss däremellan. Många C-kompilatorer kan
fås att generea sådan extrakod för att utföra
sådana kontroller - men då måste man explicit
säga till att sådant skall kontrolleras.Strängar
En teckensträng som "kalle" är i C (och C++) inget annat än en array med plats för sex stycken tecken - inte fem! Kompilatorn lägger nämligen alltid till ett sjätte tecken, och detta 'tecken' är alltid talet 0. Vi kan få in det själva genom att skriva:
char *str = "två\0 strängar" ;Här har vi, med hjälp av tecknet '\', angett att vi vill stoppa in teckenkoden 0 och inte ascII-koden för skrivtecknet '0'. Som en konvention så tolkas alltid '\t' som ett tab-tecken, '\n' som ett radslutstecken osv (det finns ytterligare några sådana).
Varför avsluta med en nolla? Jo, det betyder ju även 'falskt', och allt som inte är 0 är ju 'sant'. Om vår arraykopieringsfunktion i alternativ C ovan vore till för strängar skulle vi alltså kunna skippa argumentet len och titta på när strängen är slut:
void strCopy(char *str1, char *str2) {
while(*str2++ = *str1++);
}
Att på detta sätt lita på att det står ett
speciellt värde i slutet av datamängden gör att vi kan
spara in någon operation i varje varv i loopen vilket för
stora data sparar märkbart med tid. I detta fall slipper vi
räkna upp ett i hela
tiden, och vi använder retur-värdet av tilldelningen för
att styra while-loopen. Den markör vi placerar sist i
datamängden kalls för sentinell,
och tekniken att sätta ut sådana används flitigt i
effektiva sorteringsalgoritmer och liknande.Och nej, det saknas ingen kod i exemplet ovan. Det som händer är att först hämtas värdet som str1 pekar på, sedan räknas pekaren upp. Därefter tilldelas platsen som str2 pekar på det nyss beräknade värdet. Sen räknas pekaren str2 upp ett steg. Och hela tilldelningsuttrycket returnerar det värde som tilldelades - vilket om det var 0 får while-loopen att stanna.
VARNING NR.2
I den alternativa lösningen så är det fortfarande bara längden på första strängen som det tas hänsyn till. Om mottagarsträngen egntligen inte har plats för alla tecknen och användaren av denna förväntar sig att den skall innehålla t.ex. max 10 tecken, så litar ju denna användare på att det står ett '\0' på plats nummer 11. Och det kan vi ju ha förstört med vår kopieringsfunktion. Någonstans på en helt annan plats i ert program kommer detta fel troligen att ställa till med problem....Knappa t.ex. in följande program och se vad det skriver ut:
#include <stdio.h>Visst, det är exterm-fult och fel att använda en konstantsträng (som buff1) som en teckenbuffert och sedan ändra dess innehåll. Men det illustrerar problemet - om man skriver som man skall (dvs char buff1[4];) så uppstår inte problemet i just detta program och vi har ingen aning om var i minnet vi skrev över...
char buff1[] = "Fun";
char buff2[] = "World Is Not Big Enoug";
char* strCopy(char *s1, char *s2) {
while(*s2++ = *s1++);
return s2;
}
void main() {
printf("%s %s\n", strCopy("Hello - The", buff1), buff2);
}
Måste jag använda pekararitmetik?
Om ni nu skall skriva en funktion som kopierar tecken från en strängbuffert till en annan, så kommer den troligen att vara deklarerad som:void moveChars(char *s1, char *s2); /* Copy chars from s1 to s2 */...eller som:
char* moveChars(char *s1, char *s2); /* Copy chars from s1 to s2, return s2 */I det senare fallet förväntas även att s2 (som ju är en pekare/adress/referens!) returneras av funktionen, vilket är användbart ibland.
När ni nu skall skriva definitionen för denna funktion så kan ni välja array-syntaxen istället - det var ju ekvivalenta uttryckssät:
void moveChars(char s1[], char s2[]) {
int i=0;
while(s1[i]) { /* Vi delar upp vårt uttryck och testar här explicit... */
s2[i] = s1[i]; /* ...och här kopierar vi endast... */
i++; /* ...och här räknar vi upp. Totalt sett mer läsbart! */
}
}
...och ni kan arbeta med index och slipper fundera på om *s1++ betyder (*s1)++ eller *(s1++) och liknande
struligheter. C++ -- Klasser, metoder och annat
Vid införandet av C++ bestämde man sig för att utöka (inte någonstans ändra) syntaxen för C. De gammla structarna finns kvar, men är detsamma som klassdefinitioner med bara publika medlemsvariabler. Dessutom kan man nu stoppa in metoder i en struct - de kommer alla att vara publika.Men, man har även lagt till det reserverade ordet class för att deklarera klasser - för er Java-hackers borde det kännas igen eftersom Java har skapats som en uppstädad och förbättrad variant av C++.
Ni finner en lite utförligare snabbgenomgång av hur klasser deklareras (och används) här. Att använda dem ser ut som i Java; men fungerar bara nästan som i Java (där vi nästan alltid arbetar med referenser, dvs pekare). I C/C++ fungerar det som med structar:
MyThing m;I första fallet får vi alltså en variabel m som verkligen innehåller en hel instans av klassen MyThing. Men se upp! Om ni anropar en funktion eller metod med denna som argument så kopieras den och ni får en ny instans inne i funktionen/metoden! På nästa rad som ser så Java-lik ut (om det inte vore för '*') så får vi en pekare till en MyThing instans. Och vi kan naturligtvis med olika konstruktorer skapa den på olika sätt med parametrar etc.
MyThing *mp = new MyThing();
Om vi nu vill komma åt data (medlemsvariabler) och metoder i instansen m, så skriver vi:
m.width = 5; /* Pilla på en publik instansvariabel */När vi har en pekare till objektet måste vi dereferera (följa pekaren) innan själva metoden/variabeln kan kommas åt (precis som om vi hade en pekare till en struct):
m.computeSomething("now"); /* Anropa en metod i objektet */
(*mp).width = 5;Att parenteserna behövs har att göra med att prioriteten på operatorerna egentligen är skapad för att göra något annat än det vi vill just nu. Men då detta är en vanlig användning finns en alternativ syntax:
(*mp).computeSomething("later");
mp->width = 5;
mp->computeSomething("nice");
New och Delete - att allokera eller inte allokera...
Från tid till annan behöver man någon mellanvariabel med en storlek man inte känner till i förväg. Då kan man skapa den i runtime (dvs under körning) och sedan se till att den slängs bort direkt efteråt igen (när vi inte längre behöver den): void doSomething(int size) {
1 char *tmp1 = new char[1024]; // Oftast onödig, använd rad 2 istället
2 char tmp2[1024];
3 char *tmp3 = new char[size];
4 MyStuff *tmp4 = new MyStuff(); // Oftast onödig, använd rad 5 istället
5 MyStuff tmp5;
...
På rad 1 skapar vi en array med en fast storlek. Men vi skapar
den på ett sådant sätt att vi själva måste
ta bort den mha följande rad:delete[] tmp1;Har vi använt [] när vi skapade en sak med new, så måste vi göra det när vi tar bort objektet också! Glömmer vi att ta bort den, eller
tar bort den på fel sätt kan de mest mystiska fel uppstå senare i programmet. Eftersom storleken på vår variabel är känd i förväg kan vi dock oftast skriva som på rad 2 istället, och slipper då problemet med delete.
På rad 2 så låter vi variablen skapas automatisk i vår procedur, och utrymmet för den kommer också att försvinna automatiskt när vi lämnar proceduren (eller metoden). Mycket användbart för alla temporära data med fast storlek, om de inte är väldigt stora förståss...
På rad 3 har vi inget val - vi känner inte till storleken i förväg. Glöm inte att anropa delete på rätt sätt!
Rad 5 är en variant på rad 2, vi skapar ett objekt på plats. Det kräver dock att det finns en konstruktor utan argument som kan användas. Vill man initiera något mer så får man stoppa in en metod för detta och anropa den i början på proceduren:
tmp5.init("initiala data");
Rad 4 behövs alltså bara i undantagsfall, och måste
senare åtföljas av ett anrop till delete:delete tmp2;...vilket ni slipper om ni skriver som på rad 5.
Spårutskrifter,
macron och debugger
Det är ofta lurigt att felsöka ett program om man inte vet
vad som händer. Ett sätt att ta reda på det är att
på olika ställen i programmet skriva ut små
meddelanden. Om vi håller oss till vanlig C så
använder man t.ex. printf()
för att skriva ut saker: printf("En sträng med argumentplatser som %d och %s och ett radslut:\n", 47, "procent-s");
Första argumentet är en vanlig textsträng, men i den kan
man ha symboler (oftast en bokstav med ett % framför) som skall
bytas ut mot andra värden som heltal, flyttal, andra strängar
etc. Värdena hämtas från argumenten som kommer efter
textsträngen, i exemplet ovan först heltalet 47 och
därefter en annan sträng. Radslut måste man stoppa in
själv. Resultatet blir:En sträng med argumentplatser som 47 och procent-s och ett radslut:I man-sidan för ett Unix-system kan man för printf() läsa mer om alla formatsymboler, men %d för heltal och %s för strängar är de ni behöver mest. Man kan även ange att utskriften skall ske med tex minst 6 tecken, då skriver man %6d och får alla talen högerjusterade med så många blanktecken före att det blir en utskrift om totalt minnst 6 skrivtecken (själva talet kan ju behöva mer än 6 tecken!).
I C++ kan vi använda allt från C men dessutom skriva ut till terminalen (strömen cout) med hjälp av operatorn << på följande sätt:
cout << "en sträng" << integerVariable << 56,6 << eol;Hur gör vi om vi vill stänga av utskrifterna under en demo och bara ha dom på när vi felsöker? Svaret är att i C (och C++) kan vi utnytja den pre-processor som tolkar alla #-kommandon i kodfilerna till detta. Om vi t.ex. skriver följande rad i början av en fil:
#define TRACE1(msg) printf("Inside file XXX: %s\n", msg)
...så kan vi använda detta macro i vår kod:blabla();...och pre-processorn kommer automatiskt att expandera macrot (ersätta det med den text vi skrev i definitionen) och kompilatorn kommer egentligen att kompilera följande kod:
TRACE1("mellan bla och blä");
bläblä();
blabla();En onödig omväg kan tyckas, men vad händer om vi i början av filen sedan ändrar så att det står:
printf("Inside file XXX: %s\n", "mellan bla och blä");
bläblä();
// #define TRACE1(msg) printf("Inside file XXX: %s\n", msg)
#define TRACE1(msg)
Jo, då (vid nästa kompilering) byts ju alla våra anrop till TRACE
ut mot en tom rad! Och vi slipper alla utskrifter!Ett varningens ord bara, ni kan ju vilja köra en debugger på Nachos-koden. Debuggern ser ju inte macron (det ni skrivit), den ser det som har blivit när alla macron ersats med riktig kod. Har man knepiga macron kan det bli knepigt attse vad koden gör...
Avslutningsvis...
Så - det var allt för nu. I Nachos-labbarna kommer ni med största sanolikhet inte att själva skapa nya klasser, det är bra nog om ni använder de befintliga på rätt sätt. Därför avslutas denna guide här - kommer du/ni/vi på att något mer borde stå här - tveka inte att lämna synpunkter och bidrag till kursledningen.Peter Loborg, Linköping 2004.
Sidansvarig: Sergiu Rafiliu
Senast uppdaterad: 2005-01-27
