För många Amiga-ägare som vill komma vidare från Amiga-BASIC är programmeringsspråket C ett intressant alternativ. C är det språk med vilket en stor del av Amigans systemmjukvara utvecklades, t ex Graphics-biblioteket och Intuition-mjukvaran, som tillhandahåller rullgardinsmenyer, fönster och resten av Amigas användargränssnitt. C har också använts för att utveckla många viktiga tillämpningsprogram på Amiga, såsom ritprogram, animeringsprogram, ordbehandlare, musikprogram, databaser och t o m snabba spel av arkadtyp.
C har emellertid rykte om sig att vara "svårt", för att inte säga krävande! I denna del av Kickstart-guiden ska vi titta på vad C är och hur det skiljer sig från mer välbekanta språk, som BASIC. Vi börjar med att diskutera C:s starka och svaga sidor. Sedan tittar vi på några aspekter av C i mer detalj.
De som är högnivåprogrammerare av naturen brukar inte ha så mycket till övers för C. T o m jämfört med BASIC är C ganska svagt, särskilt när det gäller stränghantering. Det tenderar också att vara kryptiskt och utnyttja mycket pekare, vilket är variabler som innehåller adresser till minnesutrymmen där andra variabler lagras (mer om detta senare). Detta är något som assemblerprogrammerare är vana med, men det är inget man väntar sig finna i ett högnivåspråk. Med rikligt bruk av pekare kan verkligen ytterst svårbegriplig C-kod åstadkommas.
C är även svagt när det gäller typkontroll. I C, liksom i Pascal, måste man deklarera alla variabler innan de används och tala om vilken typ de har - heltal, långt heltal, flyttal, tecken, sträng eller vad det nu kan vara. I Pascal måste en variabel av en viss typ förbli av denna typ. C är emellertid "svagt typat", dvs det är möjligt att i en handvändning ändra typen med något som kallas för typomvandling. Detta kan även leda till förvirring och är något annat som högnivåprogrammerare brukar invända mot - vi rekommenderar att de tittar lite närmare på t ex Modula-2, som förmodligen är ett mycket lämpligare alternativ för dem.
Hur ligger det då till med lågnivåprogrammerare, som tidigare programmerat i assembler på 8-bitars datorer som Commodore 64. Till en början brukar de med denna bakgrund tycka att C är motbjudande och oföredragsamt. Deras första ansträngningar resulterar i otaliga felmeddelanden och varningsmeddelanden som helt klart är oriktiga ("pekarna pekar ej på samma objekt") och så slutligen förfäras de av storleken av den "objektmodul" som erhålls från ett program som helt klart är trivialt - om du inte ser upp kan du mycket väl stå där med över 12KB kod för ett program som enbart skriver ut ditt namn på bildskärmen. Assemblerprogrammerare kommer förmodligen tycka att det är enklare att lära sig 68000-processorn och programmera Amiga med assembler, som de kommer att finna kraftfull, men i det stora hela ganska välbekant.
Är det då någon mening med att lära sig C, när så mycket talar emot? I många fall är svaret på frågan "Ja, helt klart!". Den som vill skriva arkadspel eller någon annan tillämpning, där huvudintresset är att så snabbt som möjligt manipulera bitar i olika bitplan (bitplan är det närmaste man kan komma "skärm-RAM" på en Amiga), bör nog lära sig 68000-assembler. Den som vill skriva ett bokföringspaket bör överväga att lära Modula-2. För den som vill ha både snabb exekvering och en del av ett högnivåspråks fördelar, är C en bra kompromiss. Det är ett slags "medelväg" eller ett "medelnivåspråk". Dvs om du behöver snabbhet, samtidigt som du programmerar "seriöst", och riskerar att stå där med mycket kod med komplicerade inbördes samband, då kommer du att finna att C har följande fördelar:
Om du kommer till C med bakgrund som programmerare BASIC eller assembler på en 8-bitars dator, då kommer du att upptäcka att mycket är annorlunda.
Det första du måste vänja dig
med är att C kompileras och sedan länkas. Jämfört med
att bara inskriva ett program och sedan skriva run,
eller t o m köra en tvåpass-assembler, måste betydligt
fler stadier genomgås. Vi ska senare titta på dessa i detalj,
men i stora drag går det till på följande sätt:
C utvecklades som ett systemprogrammeringsspråk och utformades för att förenkla och effektivisera skrivning av sådant som operativsystem. Det bygger på språket BCPL, som aldrig fick samma popularitet, även om det fortfarande har sina entusiastiska anhängare. Både BCPL och C har använts för att skriva operativsystem. BCPL användes för att skriva Tripos och C utvecklades parallellt med Unix. Alla dessa system är relevanta för Amiga: AmigaDOS är skrivet i BCPL och baseras på Tripos, medan större delen av ROM-kärnan (bortsett från de lägsta nivåerna i Exec) är skriven i C och utvecklades på Unix-system.
Unix utvecklades ursprungligen på en PDP-11. C fick därför vissa inslag som utformades för att ge effektiv kompilerad kod vid användning av PDP-11:s instruktionsuppsättning. Denna instruktionsuppsättning har haft ett stort inflytande på utformningen av mikroprocessorer, inklusive 68000-processorn, vilket förklarar den popularitet som C fått pga den (jämförelsevis) effektiva kodgenereringen. Det faktum att C utformades på detta sätt förklarar även dess brister som ett högnivåspråk betraktat, liksom en del språkliga säregenheter. Till dessa hör öknings- och minskningsoperatorerna, som förekommer i satser som
nextchar = string[index++];
vilken tar fram nästa tecken i en sträng och sedan ökar index, vilket har en direkt motsvarighet i 68000-processorns instruktionsuppsättning i form av indirekt adressering via adressregister med efterinkrementering, dvs
MOVE.B (An)+,Dn
vilket flyttar byten i minnescellen vars adress finns i register An
till dataregistret Dn och inkrementerar An
till att peka på nästa byte. Liknande säregenheter är existensen av operatorer
som "+=" (value += 4;,
vilket är liktydigt med value = value
+ 4;), samt det faktum att tilldelningsuttryck
har värden: Värdet är resultatet av tilldelningen, dvs det
som står före likhetstecknet. Eftersom detta värde kommer
att ligga i ett register kan kompilatorn lika gärna utnyttja det!
Denna säregenhet utnyttjas ofta för att ge något ett värde
och i samma sats testa detta, t ex i den typiska satsen
if ((buffer=AllocMem(1000,0)) == 0) printf("Minne saknas\n");
Notera skillnaden mellan "=", som betyder tilldelning
och här används för att sätta en variabel buffer till det värde
som AllocMem() returnerar, och "==",
som är likhetsoperatorn och här utnyttjas för att kontrollera
om bufferten blir noll. Att glömma detta är ett vanligt C-nybörjarfel.
Den kanske viktigaste aspekten av C betraktad som en "super-assembler" är språkets faciliteter för strukturer och pekare. En C-pekare är nästan (men inte helt) samma sak som en maskinadress. Satsen
struct Node *ln_Succ;
deklarerar att ln_Succ
är en pekare till en struktur som heter Node,
dvs den innehåller en minnesadress där det finns en Node.
Pekaren är inte riktigt samma sak som en maskinadress, ty satsen
ln_Succ++;
ökar inte ln_Succ med en byte (eller ett ord eller ett
långord), utan med storleken av strukturen Node.
Av detta skäl (bland andra) är C-kompilatorer mycket kinkiga
när det gäller att få reda på vilka slags objekt
som pekare pekar på och ger ifrån sig irriterande varningsmeddelanden
om man glömmer klargöra detta.
En struktur är en samling av olika
datatyper som C understödjer, inkluderande andra strukturer och pekare.
T ex definieras Node-strukturen
struct Node {d
struct Node *ln_Succ; /* pekare till nästa nod */
struct Node *ln_Pred; /* pekare till föregående nod */
UBYTE ln_Type; /* typ - teckenlös byte */
BYTE ln_Pri; /* prioritet - byte med tecken */
char *ln_Name; /* pekare till nodnamn */
};
En Node är i själva verket en standardstruktur som all systemmjukvara
i Amigan utnyttjar. Den definieras i en h-fil, eller headerfil,
(exec/nodes.h), som bör följa med C-kompilatorn. Headerfilen kan inkluderas vid
kompileringen med ett #include-direktiv. Därefter kan man
deklarera Node-strukturer för eget bruk med
struct Node mynode;
eller deklarera en pekare till en Node med
struct Node *mypointer;
Sedan kan man plocka fram någon del av strukturen, t ex nodprioriteten, direkt med
mypriority = mynode.ln_Pri;
eller indirekt med
priority = mypointer->ln_Pri;
Återigen finns ett mycket nära samband mellan denna C-facilitet och ett adresseringssätt hos 68000-processorn, i detta fall indirekt adressering via adressregister med förskjutning:
MOVE.B d16(An),Dn
Här pekar adressregistret An
på en struktur. Offseten d16
läggs till denna adress för att "peka ut" en byte som
ligger på detta avstånd från strukturens början
och denna byte kopieras till dataregistret Dn.
Strukturer och pekare är centrala i C-programmering. Detta är
anledningen till att vi nämnt dem redan här i introduktionen.
Vi ska senare detaljstudera hur de fungerar.
Följande är ett klassiskt litet C-program:
/* ett klassiskt exempel */
main()
{
printf("Hejsan Amiga\n");
}
Programmet utnyttjar en standardfunktion, printf(),
för att utmata textsträngen "Hejsan
Amiga", följd av ett radbyte, representerat
av escape-sekvensen "\n".
I programmet definieras den enda parameterlösa funktionen main().
Det är en C-konvention att exekveringen överförs till en
funktion vid namn main()
när startoperationerna slutförts. Andra saker som bör noteras
är att /* och */ innesluter
kommentarer, att klamrar, { och }, innesluter
funktioner och att semikolon används som satsavslutare. Att glömma
semikolon är ett vanligt nybörjarfel. Radslut har ingen speciell
mening i ett C-program och i själva verket behandlas ett godtyckligt
antal blank-, tabb- och radslutstecken efter varandra som ett enda blanktecken.
Detta ger stor frihet för layouten av ett C-program. De regler för
indentering (radindragning) som tillämpas i denna bok är emellertid
relativt standardmässiga och rekommenderas för att öka programs läsbarhet.
För att kunna följa ett lite mer komplicerat exempel som utnyttjar en av de inbyggda funktionerna i Amigas Kickstart-ROM är det nödvändigt att veta lite grann om Amigas bibliotek (eng libraries).
Ett bibliotek är väsentligen en samling
rutiner som är åtkomliga via en hopptabell, liknande Commodore
64:ans "kärna" (eng kernel). Amiga skiljer sig dock från
64:an i det att den har ett stort antal bibliotek som rör olika aspekter
av datorn - exempel som vi här ska använda är Exec-biblioteket,
som hanterar avbrott, multiuppdragskörning osv, Intuition-biblioteket,
som tillhandahåller ett standardmässigt användargränssnitt
och DOS-biblioteket
som ger tillgång till de funktioner som AmigaDOS tillhandahåller.
En annan avgörande skillnad gentemot datorer som 64:an är att
Amiga-biblioteken inte nödvändigtvis behöver ligga på
en viss fast minnesadress, dvs innan ett bibliotek utnyttjas är det
nödvändigt att ta reda på var det finns.
Följande är ett enkelt exempel som utnyttjar Intuition-biblioteket. Det gör så att bildskärmen snabbt blinkar sex gånger.
/* Skärm-blinkning med Intuition */
#include <exec/types.h>
extern APTR OpenLibrary(char *, LONG);
extern VOID CloseLibrary(APTR /* eg: struct Library * */);
extern VOID DisplayBeep(struct Screen *)
extern VOID Delay(ULONG);
APTR IntuitionBase; /* Pekare till Intuition-biblioteket */
main()
{
int i;
IntuitionBase = OpenLibrary("intuition.library", 29L);
if (IntuitionBase != 0) { /* Om det lyckades ... */
for (i=0; i<6; i++) { /* utför då sex... */
DisplayBeep(0); /* skärmblinkningar */
Delay(5); /* och vänta... */
}
CloseLibrary(IntuitionBase); /* Stäng sedan */
} /* end if */
} /* end funktion main() */
Ovanstående exempel är betydligt komplexare än det tidigare och innehåller några C-aspekter som vi ska diskutera närmare senare. Några förklaringar kan dock vara på sin plats redan på detta stadium:
#include
i början är ingen C-programsats - lägg märke till avsaknaden
av ett avslutande semikolon. Det är ett kommando till en del av kompilatorn
som kallas "makro-preprocessorn" (kortare preprocessorn)
och medför att headerfilen exec/types.h,
med deklarationer av vissa standarddatatyper, såsom APTR
och VOID, inkluderas i programmet. Det är praxis att
skriva makrodefinitioner med stora bokstäver.#include följs av en extern-sats,
som ger kompilatorn viss information om de funktioner som används
i modulen, men definieras på annat håll - i detta fall i biblioteksfilen
amiga.lib. Här använder vi en funktion,
OpenLibrary(), som returnerar en adresspekare
(APTR) och tre funktioner CloseLibrary(),
DisplayBeep() och Delay(),
som inte returnerar någonting alls (VOID).IntuitionBase, av typen APTR,
där vi ska lagra en pekare som talar om var Intuition-biblioteket
finns i minnet. Namn på globala variabler utmatas till objektfilen
så att IntuitionBase blir tillgänglig för den rutin
i amiga.lib som behöver den.main()
startar med en annan variabeldeklaration, en "lokal variabel"
kallad i, som används som räknare inne i funktionen.
Variabler som på detta sätt deklareras inne i funktioner kallas
"automatiska variabler" (eller "auto-variabler"),
eftersom de skapas automatiskt när funktionen anropas och försvinner
när funktionen lämnas.intuition.library, med ett versionsnummer större eller
lika med 29. OpenLibrary() är självt en rutin från
Exec-biblioteket, men vi behöver inte öppna Exec-biblioteket,
eftersom standardstartmodulen öppnar Exec- och DOS-biblioteken åt oss.OpenLibrary().
lyckas returnerar den en pekare som talar om för oss var intuition.library.
finns i minnet. Om operationen misslyckas returneras noll. Resten av programmet
är därför beroende av att detta anrop inte returnerar noll
(!= betyder "inte lika med").for-konstruktionen
är mycket kraftfull i C och innehåller specifikationer för
loopinitiering (i=0),
upprepningsvillkor (i < 6)
och vad som ska göras mellan varje varv (i++,
dvs öka i).DisplayBeep() med parametern 0,
vilket betyder att vi säger åt Intuition att blinka hela bildskärmen
i stället för en specifik "skärm". Funktionen
DisplayBeep() ingår i själva verket i amiga.lib
och den nödvändiga koden inhämtas vid länkningen. Vad
rutinen utför är att först initiera 68000-processorns register
på lämpligt sätt och sedan bestämma vilken adress
som ska anropas, genom att lägga en offset till det värde som
lagras i IntuitionBase-variabeln.
Därefter överförs exekveringen till denna adress och följaktligen
till Kickstart-ROM.DisplayBeep() anropas en rutin från
DOS-biblioteket, Delay(), med parametern 5,
vilket betyder att den ska ge en fördröjning på 5/50 av en sekund.CloseLibrary().Det finns flera C-versioner till Amiga. Den "standardversion" som Commodore ursprungligen spred till programutvecklare var Lattice C V3.03. Uppdateringar till denna, Lattice C V3.1 och 4.0 (och nu V5.10 - Ö anm) finns nu tillgängliga. En annan version, som blivit populär bland programutvecklare, är Manx Aztec C. Vi utnyttjar Lattice i våra exempel, men om du har en annan kompilator kommer du att upptäcka att det hela fungerar i princip likadant.
Den ursprungliga Amiga-länkaren hette Alink
(för "Alan's länkare", inte "Amigas länkare"
- efter en programmerare på Metacomco). Denna utkom i versionerna
1.0 och 1.1, den senare ståtande med nyckelordet FASTER,
som snabbade upp det hela betydligt. En annan länkare, kallad Blink,
utvecklades av en grupp i USA som hette "The Software Distillery"
och placerades ursprungligen i public domain. Denna är snabbare än
Alink utan nyckelordet FASTER
och har fler finesser. Lattice levererar numera Blink som standard.
Fullständiga instruktioner för att upprätta en C-utvecklingsmiljö bör medfölja kompilatorn. Installeringsförfarandet kan variera något beroende på kompilatorversion, på om du har en hårddisk eller expansions-RAM och styrs delvis av personlig tycke och smak. Med Lattice C, går det i det stora hela till på följande sätt:
1. Du måste troligen skapa en "strippad"
Workbenchdiskett, som rymmer kompilatorn, länkaren och hjälpprogrammen.
Börja med att rensa bort demoprogram, klockan, kalkylatorn, notepaden
osv och spola sedan alla fonter, alla skrivardrivrutiner, utom den som
gäller din skrivare, alla tangentkartor, utom den som gäller
ditt land, alla AmigaDOS-kommandon du inte använder, och alla bibliotek
och enheter du inte behöver, t ex translator.library
och narrator.device.
Installera sedan åtminstone de två kompilatordelarna (lc1
och lc2) och länkaren Blink. Det är förmodligen
bäst att placera dem i C:-katalogen.
2. Justera filen s:startup-sequence så att den tilldelar följande logiska enhetsnamn:
LC1: | där lc1, lc2 och Blink finns. Tilldelas troligen C:
|
INCLUDE: | där systemets headerfiler ska sökas (dvs de filer som anges inom vinkelparenteser,
t ex #include <intuition/intuition.h>).
Tilldelas förmodligen ett volymnamn, såsom h_files:,
om du arbetar med disketter, eller en katalog om du arbetar med hårddisk.
Om du har massor med minne och inte bryr dig om tidsåtgången
kan du också kopiera alla headerfiler till RAM-disken vid bootningen
av datorn.
|
LIB: | där länkarens
"scanned libraries", såsom lc.lib
och amiga.lib,
ska sökas. Tilldelas troligen INCLUDE:lib
|
QUAD: | där de temporära quad-filer
som skapas under första kompileringspasset (av lc1) ska placeras.
Bör tilldelas RAM:.
|
Du har nu förutsättningar att försöka med ett enkelt exempel,
t ex skärmblinkningsprogrammet ovan. Placera dig i en
lämplig katalog och skapa en underkatalog, obj,
där du placerar objektfilerna. Du kan nu skapa källkodsfilen
beep.c med ED eller EMACS och därefter kompilera
och länka programmet med något i stil med följande:
stack 15000 LC:lc1 -iINCLUDE: -oQUAD:beep LC:lc2 -oobj/beep.o QUAD:beep LC:blink FROM LIB:c.o+obj/beep.o TO beep LIB LIB:lc.lib+LIB:amiga.lib
Först ökar vi den stackstorlek som ges
ett nytt program. Detta är viktigt, ty Lattice behöver mer än
systemets standardvärde, vilket bara är 4KB. De följande
två raderna anropar de två kompilatorpassen och anger att INCLUDE:
ska genomsökas efter alla #include <>-filer,
att tillfälliga filer ska placeras i QUAD:
samt att den slutgiltiga objektfilen ska placeras i katalogen obj.
Den fjärde raden, som bör skrivas som en enda rad, säger
åt länkaren att länka ihop Lattice:s standardstartmodul
c.o med objektmodulen obj/beep.o och
att skapa en laddmodul beep, efter att först ha tittat
i lc.lib och amiga.lib efter
biblioteksfunktioner, såsom OpenLibrary()
och DisplayBeep().
Lägg märke till att skrivarbetet kan
minskas radikalt med Lattice-programmet lc, vilket anropar såväl
de båda kompilatorpassen som (valfritt) länkaren, och genom
att utnyttja execute-filer
i AmigaDOS. Lägg också märke till att detta enkla exempel
är otypiskt i den meningen att hela programmet finns i en objektfil.
I en verklig tillämpning skulle vi förmodligen ha delat upp programmet
så att funktioner som rör menyhantering ligger i en fil, funktioner
som rör "gadgets" i en annan, ritfunktioner i en tredje
osv. Vi skulle sedan, med länkaren, sammanfoga alla dessa med en startmodul,
t ex c.o.
Att veta hur man på bästa sätt fördelar funktioner
mellan olika källfiler är en färdighet som man brukar förvärva
med tiden - nybörjare börjar vanligen med att ha allt i en enda
kolossal källfil (som det tar evinnerlig tid att kompilera), för
att sedan gå till andra ytterligheten och ha varenda funktion i en
separat fil - idealet ligger förmodligen någonstans mitt emellan
dessa ytterligheter.
Vi ska nu titta på de viktigaste byggstenarna i ett C-program: funktioner, variabler och grundläggande datatyper. Genomgången är inte fullständig - målsättningen är inte att skriva en "alternativ referenshandbok" för Amiga-C och vi gör inte anspråk på att täcka alla aspekter av språket - vi hoppas emellertid att det kan tjäna som en nyttig introduktion.
När vi diskuterar sådant som C:s "grundläggande byggstenar", stöter vi oundvikligen på problemet med C-standarden eller snarare avsaknaden av en sådan standard. C utvecklades vid Bell Laboratories i slutet av sextiotalet, parallellt med operativsystemet Unix. Den definitiva läroboken, programmeringsguiden och referensmanualen för C, är den 1978 publicerade "The C Programming Language" av Kernighan och Ritchie, som fortfarande är tillgänglig i Prentice-Hall Software Series. Denna ger en ganska rigorös definition av C:s syntax, så man hoppades att C skulle förbli ett ganska portabelt språk. (Boken finns nu även på svenska i en ny upplaga som ansluter till ANSI-standarden - Ö anm.)
Olyckligtvis blev inte så fallet, av två anledningar. För det första innehåller C självt mycket få primitiver. För nästan alla verkliga aktiviteter (t ex I/O) är det beroende av "standardfunktionsbibliotek". Olyckligtvis har Kernighan och Ritchie ganska litet att säga om dessa, med resultatet att avsevärda skillnader uppkom mellan bibliotek i olika C-implementeringar. För det andra, allteftersom C vann i popularitet utanför sin ursprungsmiljö (UNIX), kunde de som implementerade C-versioner för andra system inte emotstå frestelsen att "förbättra" den ursprungliga specifikationen, vilket resulterade i ytterligare skillnader mellan olika versioner. Detta verkar vara oundvikligt när det gäller populära programmeringssystem - de enda verkliga standarder och portabla system som finns är de som ingen använder!
Världen väntar nu på en ny "IEEE-standard" för C. Arbetet med denna har kommit så långt som till en preliminär specifikation. Under tiden bestäms standarden huvudsakligen av de viktigaste kompilatorleverantörerna, såsom Lattice och Aztec och dessa skiljer sig inte åt alltför mycket och verkar även ganska väl överensstämma med den kommande nya standarden. I denna C-introduktion ska vi titta på de faciliteter som finns tillgängliga i Lattice version 3.1, utan att bry oss så mycket om vad som härrör från Kernighan & Ritchie (K&R) och vad som är tillägg. Den bästa referensen - och en klassisk lärobok - är förmodligen K&R självt. För information om (den nu antagna - Ö anm) ANSI-standarden , se "C: A Reference Manual" (3rd edition, -91) av Harbison och Steele.
ANSI-standarden är nu antagen, så ovanstående resonemang är inaktuellt (Ö anm)!
För att få en uppfattning om C:s byggstenar är det lämpligt att utgå från ett exempel. Vi ska därför (för sista gången!) titta på vårt enkla skärmblinkningsprogram. Detta visar den typiska strukturen hos ett C-program, som kan brytas ner i följande delar:
#include-direktiv,
vilka anger att filer med standarddeklarationer ska inkluderas vid kompileringen.
Detta liknar mycket användningen av .lib
för att inkludera standarddefinitioner i assembler. Inkluderingsfilerna
kan innehålla deklarationer som följer med systemet eller som
man skrivit själv. Konventionsmässigt brukar man skilja de två
åt genom att ange filer med systemdeklarationer inom vinkelparenteser
(<filnamn>)
och de med egna deklarationer inom citationstecken ("filnamn").
Exempel:
#include <intuition/intuition.h> #include "minafiler/minamakron.h"
Lägg märke till att #include-direktiv
bryter mot reglerna för C-programsatser, eftersom de ej avslutas med
semikolon. Detta beror på att rader som inleds med #
inte betraktas som C-programsatser, utan som direktiv till en "preprocessor"
som läser igenom programmet och textmässigt infogar inkluderingsfiler
och makron innan själva kompileringen utförs. I Lattice C utförs
detta som en del av lc1.
#include-direktiven
följs av de första satserna i C-programmet, vilka är
extern-satser som talar om för kompilatorn
vad den behöver veta om funktioner i andra moduler eller länkningsbiblioteket
för att kunna kompilera det aktuella programmet. Ett exempel:
extern APTR AllocMem(ULONG, ULONG);
Detta talar om för kompilatorn att vi ska
använda en funktion, AllocMem(),
som definieras på annat håll och att denna funktion returnerar
ett värde av typen APTR,
vilket är en adresspekare för allmänt bruk - se nedan.
APTR FantastiskFunktion(); /* definieras senare */
Kompilatorn kan sedan handskas med de referenser
till FantastiskFunktion() som görs innan själva definitionen.
extern-deklarationerna
och de nödvändiga funktionsdeklarationerna följs normalt
av definitioner av de "globala" variabler som behöver accessas
från olika programdelar. Detta medför att kompilatorn reserverar
plats för variablerna i DATA- eller BSS-"hunkar", som används
för initierade resp oinitierade data, samt utmatar variabelnamnen
till objektfilen, så att andra moduler kan få tillgång till dem.Exempel:
APTR IntuitionBase; ULONG Counter = 0;
I det första fallet instruerar vi kompilatorn
att reservera plats för en pekare kallad IntuitionBase
i en BSS-"hunk". I det andra reserverar vi plats för ett
teckenlöst långord, kallat Counter,
och initierar det till noll - det kommer att hamna i DATA-"hunken".
main().
Vid behov följs den av andra funktioner. Varje funktion består
av en deklaration, som anger funktionsnamnet och informerar om vad som
skickas till funktionen och vad den returnerar, följd av en funktionsdefinition,
innesluten i krullparenteser. Exempel:
APTR GetChipMemory(ULONG amount)
{
/* här definieras funktionen */
}
Här ska vi definiera en funktion, GetChipMemory,
som allokerar minne i Amigans lägsta 512KB. Vi ska skicka den ett
långordsvärde (amount), som talar om för funktionen
hur mycket minne som vi vill ha. Funktionen ska returnera en APTR
som anger adressen till det allokerade minnet, eller noll (NULL)
om det inte finns minne tillgängligt. Det faktum att värdet amount
är ett teckenlöst långord deklareras med ULONG
amount i funktionshuvudet.
i-räknaren i beep.c:
main()
{
int i; /* räknare */
/* annat följer */
}
if och for,
tilldelningar, logiska operationer och anrop av andra funktioner.De viktigaste variabeltyperna i C är:
Globala variabler deklareras utanför funktioner. Kompilatorn reserverar utrymme för dem i BSS- eller DATA-hunkar och i senare fallet initieras de även. Globala variabler är permanenta i den meningen att när väl minne reserverats för dem så kan de inte avlägsnas. Information om globala variablers namn och plats utmatas till objektfilen så att de blir tillgängliga för andra moduler.
Externa statiska variabler är precis som globala variabler, förutom att deras namn ej utmatas till objektfilen, varför de enbart är åtkomliga i den aktuella modulen. Detta gör det möjligt att låta ett antal samverkande funktioner dela på sådant som buffertar, som man inte vill ska vara tillgängliga för andra programdelar. Exempel:
static char buffer[SIZE];
Detta uppmanar kompilatorn att reservera utrymme
för en teckenarray av storleken SIZE
i en BSS-hunk, men att inte utmata namnet buffer
till objektfilen.
Automatiska variabler deklareras inne i funktionsdefinitioner. Systemet reserverar plats för dem på stacken under exekveringen. Värdena är strikt temporära och försvinner när funktionen lämnas. Detta är användbart vid rekursion, ty när en funktion anropar sig själv skapas en ny uppsättning automatiska variabler längre ned i stacken, dessa råkar därmed inte i konflikt med de ursprungliga värdena. Automatiska variabler kan initieras i deklarationen, varvid initieringen utförs av koden vid inträdet i funktionen.
Interna statiska variabler är statiska variabler som deklareras inne i funktionsdefinitioner och är privata för den funktion i vilka de deklareras. Statiska variabler läggs inte på stacken, utan utrymme reserveras för dem i BSS- eller DATA-hunkar, så att deras värden inte går förlorade när funktionen lämnas. Detta gör det möjligt för funktioner att ha interna privata värden, t ex buffertpekare, som bevaras mellan funktionsanropen. Interna och externa statiska variabler liknar varandra när det gäller det sätt på vilket utrymme reserveras för dem, men kompilatorn tillåter enbart access av interna statiska variabler inne i den funktion de definierats, medan externa statiska variabler är åtkomliga i hela den källkodsmodul i där de definierats.
Formella variabler (parametrar) som refererar till funktionsargument, definierade antingen i funktionshuvudet (ANSI-C) eller efter funktionsdeklarationen och innan de inledande krullparenteserna (traditionell K&R-C). Dessa liknar mycket automatiska variabler i det att de refererar till stackutrymme. Skillnaden består i att stackutrymmet i detta fall reserveras av den anropande funktionen - detta diskuteras i detalj senare.
Register-variabler är en speciell
variant av automatiska variabler. När en variabel deklareras som register,
talar man om för kompilatorn att variabeln kommer att användas
mycket, varför den om möjligt bör läggas i ett maskinregister.
Lattice använder registren D2-D7 och A2-A4 för
registervariabler. A7 är stackpekaren, A6
används för biblioteksåtkomst och basrelativ adressering,
medan A5 används för att komma åt automatiska variabler,
vilket betyder att enbart registren D0,
D1, A0 och A1 är
tillgängliga för allmänt bruk. Om du skriver assemblerfunktioner
för länkning till Lattice C-program, se då till att bevara
alla register utom ev dessa fyra.
De grundläggande datatyper som Lattice C understödjer är
int | 32-bitars heltal med tecken |
long el long int | 32 bitars heltal med tecken |
short el short int | 16 bitars heltal med tecken |
char | 8 bitars kvantitet med tecken |
float | flyttal (enkel precision) |
double/long double | flyttal med dubbel precision |
Samtliga ovanstående datatyper tolkas som
teckensatta - se upp med detta, eftersom det kan ge underliga effekter,
t ex vid teckenjämförelser. Om du vill ha teckenlösa storheter
kan typen kvalificeras med unsigned
(detta gäller inte flyttalstyperna, Ö anm), som i
unsigned char buffer[100];
Problemet med dessa datatyper är att det inte finns någon garanti
för att de är likadana i andra C-versioner. Därför har Amiga definierat
egna datatyper i en fil exec/types.h,
som skiljer sig från Lattice-typerna i det att de är skrivna
med stora bokstäver. Om du inkluderar denna fil vid kompileringen
kan du använda Amiga-typerna, vilket är en bra idé om
du skulle byta C-version. Amiga-typer som ofta förekommer är:
LONG | 32 bitar med tecken |
ULONG | 32 bitar teckenlöst |
WORD | 16 bitar med tecken |
UWORD | 16 bitar teckenlöst |
BYTE | 8 bitar med tecken |
UBYTE | 8 bitar teckenlöst |
STRPTR | strängpekare |
APTR | minnespekare |
För en fullständig lista, se headerfilen exec/types.h.
I avsnittet "Kom igång med C"
tittade vi på de olika lagringsklasser som finns i C - global,
extern statisk, automatisk, intern statisk, formell och register - och
på de "aritmetiska objekt" som Lattice understödjer
direkt eller via Amiga-makrona LONG,
ULONG, WORD,
UWORD osv. I detta
avsnitt fortsätter vi med att titta på "härledda objekt",
som byggs upp med hjälp av dessa element, och som inbegriper de mycket
viktiga struktur- och pekarbegreppen. Andra härledda objekt är
strängar och arrayer - i C är dessa i själva verket specialfall
av pekare.
Vikten av strukturer och pekare i C har vi redan nämnt. Det är C:s förmåga att hantera dessa på ett någorlunda vettigt sätt som är huvudorsaken till att C är så attraktivt som ett programmeringsspråk på systemnivå.
Ett exempel på en C-struktur är en IOStdReq, som
används mycket vid Amiga-I/O, t ex vid kommunikation med konsolenheten,
vilket diskuterades i avsnittet om "enheter". IOStdReq-strukturen
definieras i headerfilen exec/io.h.
Preprocessorn kan instrueras att inkludera denna vid kompileringen med
#include <exec/io.h>
i början av programmet. Headerfilen innehåller följande strukturdefinition:
struct IOStdReq {
struct Message io_Message;
struct Device *io_Device; /* device node pointer */
struct Unit *io_Unit; /* unit (driver private)*/
UWORD io_Command; /* device command */
UBYTE io_Flags;
BYTE io_Error; /* error or warning num */
ULONG io_Actual; /* actual number of bytes transferred */
ULONG io_Length; /* requested number bytes transferred*/
APTR io_Data; /* points to data area */
ULONG io_Offset; /* offset for block structured devices */
};
Det första man bör lägga märke
till är att en strukturdefinition kan utnyttja andra strukturer. IOStdReq
börjar med en 20-bytes Message-struktur,
vilken definieras i exec/ports.h. Filen exec/io.h
innehåller själv all nödvändig logik för att inkludera exec/ports.h
om det inte redan gjorts, så du behöver inte bry dig om detta.
Message-strukturen
följs av två 4-bytes pekare till en Device-struktur
och en Unit-struktur,
vilka också definieras i andra filer som inkluderas automatiskt.
Lägg märke till avsaknaden eller närvaron av *,
som anger en verklig instans av en struktur (Message-strukturen)
eller en pekare till (dvs adresser till) strukturer som definieras
på annat ställe (Device och Unit).
De inledande strukturerna och pekarna åtföljs
av några "elementära" objekt, som definieras med makron
från exec/types.h.
Notera C-konventionen enligt vilken makron skrivs med stora bokstäver.
Användningen av makron i stället för Lattice-primitiver, t ex
unsigned short io_Command;
är till för att öka portabiliteten - unsigned short
ger inte nödvändigtvis en sexton bitars kvantitet med en annan C-kompilator.
Sedan exec/io.h
inkluderats kan man enkelt skapa en global IOStdReq-struktur
genom att skriva något i stil med följande innan den första
funktionsdefinitionen i källkoden:
struct IOStdReq myrequest;
(Detta är faktiskt mindre smart, av orsaker som vi diskuterar senare. Med den aktuella Amigan fungerar det dock riktigt, så vi använder det tills vidare för illustrativa ändamål.)
Ovanstående deklaration instruerar Lattice
att reservera utrymme för en IOStdReq-struktur
i en BSS-hunk, att utmata myrequest
som en global symbol, åtkomlig från andra moduler (för
att undvika detta, använd static struct...),
och att hålla reda på att myrequest
refererar till en struktur av detta slag.
Element i strukturen är därefter åtkomliga med punktoperatorn, t ex
myrequest.io_Command = CMD_READ;
Detta sätter kommandoordet i request-blocket
till CMD_READ,
vilket är ett annat makro (stora bokstäver) som definieras i
exec/io.h och som i själva verket har värdet 2.
Punkter evalueras från vänster till höger, varför det är möjligt att ha flera efter varandra. För att korrekt sätta upp meddelandeportnoden skulle vi t ex använda
myrequest.io_Message.mn_Node.ln_Type = NT_MESSAGE;
Detta bygger på att Message-strukturen
(definierad i exec/nodes.h)
själv innehåller en Node-struktur,
mn_Node, och att
Node-strukturen
(definierad i exec/nodes.h)
innehåller ett UBYTE-element,
ln_Type. Även
makrot NT_MESSAGE
definieras i exec/nodes.h
och har i själva verket värdet 5.
När vi satt upp en IOStdReq-struktur
och initierat nodtyp, prioritet och svarsportens adress med satser som
den ovan, då kan vi försöka använda den för att
öppna konsolenheten:
if (OpenDevice("console.device",0,&myrequest,0) !=0)
/* tar hand om misslyckat öppnande */
else
/* tar hand om lyckat öppnande */
Här anropar vi Exec-funktionen OpenDevice,
som har enhetsnamnet, ett enhetsnummer (här noll), adressen till IOStdReq-blocket
och flaggor (här noll) som argument. Detta introducerar ännu
en viktig C-operator, adressoperatorn &,
som indikerar att vi vill överföra adressen till ett objekt snarare
än objektet självt. OpenDevice
returnerar antingen noll eller en felkod, vilket vi här testar på
typiskt C-manér med relationsoperatorn !=
("icke lika med").
C:s if-sats
fungerar som motsvarigheten i många BASIC-versioner, i det att noll
tolkas som falskt och ett värde skilt från noll som sant. Vi
skulle alltså kunna ersätta ovanstående med det ännu mer C-mässiga
if (OpenDevice("console.device",0,&myrequest,0))
/* tar hand om misslyckat öppnande */
else
/* tar hand om lyckat öppnande */
Två andra faciliteter, som rör strukturer som explicit deklarerats i koden, är värda att nämnas. Den första är att en struktur kan initieras vid deklarationen, dvs vi kan sätta upp en meddelandenod:
struct Node mynode = {
NULL, /* *ln_Succ - pekare till nästa nod */
NULL, /* *ln_Pred - pekare till föregående nod */
NT_MESSAGE, /* ln_Type - nodtyp */
0, /* ln_Pri - nodprioritet */
"Min nod" /* *ln_Name - pekare till nodnamn */
};
Detta används ofta (och helt legitimt) för
att sätta upp strukturer som Intuitions NewWindow
- se flera exempel på detta, inberäknat CONSOLE.C,
på annan plats i denna bok.
Å andra sidan kan man definiera egna strukturer
precis lika enkelt som det är att använda de i headerfilerna.
Antag att du vill införa en egen struktur innehållande en IOStdReq,
samt en extra flaggbyte för dina egna skumma syften:
struct FunnyIOReq {
struct IOStdReq;
UBYTE FunnyFlags;
};
Definitionen av en struktur som denna kan kombineras med deklarationen av en (eller flera) instanser av strukturen, som i
struct FunnyIOReq {
struct IOStdReq;
UBYTE FunnyFlags;
} *myFunnyIOReq;
Detta kan även kombineras med initiering av strukturen, om du verkligen vill komma igång på allvar. Vid initiering av globala eller statiska strukturer utmatar Lattice initierade DATA- i stället för oinitierade BBS-hunkar. Det är även möjligt att initiera en automatisk variabel eller struktur, men detta hanteras annorlunda, genom att det faktiskt genereras kod som sköter initieringen.
Att införa en IOStdReq-struktur
genom att deklarera den direkt i koden och sedan accessa den med punktnotation,
betraktas, som nämnts ovan, som ett mindre lämpligt tillvägagångssätt.
Anledningen är att framtida produkter i Amiga-serien mycket väl
kan utnyttja hårdvarustyrd minnesuppdelning, så att skurkaktiga
uppdrag inte längre kan spränga sina gränser och krascha
hela datorn. Detta betyder att strukturer, t ex meddelanden, som ska
delas mellan uppdrag måste placeras i en speciell "offentlig"
minnesarea som erhålls genom att anropa AllocMem(.., MEMF_PUBLIC) - aktuella Amigor understödjer
detta som ett uppåtkompatibelt inslag.
För att vara uppåtkompatibla måste vi således allokera minne till
vår IOStdReq med ett AllocMem()-anrop
och sedan accessa minnet indirekt. Till detta behöver vi en pekare
till strukturen, som kan deklareras på följande sätt (förmodligen
som en global variabel):
struct IOStdReq *myrequestpointer;
När vi infört en pekare till en struktur kan element i strukturen
accessas med en annan operator, piloperatorn "->"
(känd under namnet SillyArrow i mjukvaruhuset Ariadne):
myrequestpointer->io_Command = CMD_READ; myrequestpointer->io_Message.mn_Node.ln_Type = NT_MESSAGE;
Även piloperatorn evaluerar från vänster till höger, vilket ger ett mycket bekvämt sätt att hantera pekare till pekare, t ex
sigmask = 1<<(myrequestpointer->io_Unit->unit_MsgPort->mp_SigBit);
Här utnyttjas myrequestpointer
för att plocka fram en pekare till en enhet, från vilken vi
plockar fram en pekare till en meddelandeport, från vilken vi plockar
fram ett signalbitnummer. Sedan vänsterskiftar vi 1 detta antal gånger
(<< är
binär vänsterskift), för att erhålla en signalbitmask,
som sedan kan utnyttjas som argument till Exec-funktionen Wait().
Lägg märke till att det i C ofta är viktigt att skilja mellan en
pekare och det som den pekar på. Antag
t ex att vi initierat IOStdReq-block
med ett OpenDevice-anrop
och vill kopiera blocket till en annan area med hjälp av en annan
pekare, myotherpointer. Vi kan försöka utföra detta med
myotherpointer = myrequestpointer;
men detta skulle enbart kopiera pekaren, något
som vi förmodligen inte eftersträvade. För att kopiera det
som en pekare pekar på, använder vi operatorn *:
*myotherpointer = *myrequestpointer;
vilket kopierar hela strukturen från en
plats till en annan. Denna förmåga att accessa en hel struktur,
är i själva verket en speciell egenskap hos Lattice C, snarare
än K&R-standarden (men överensstämmer med nya ANSI-standarden
för C - Ö anm). På ett mer konventionellt sätt används
*pekare för
att accessa enkla aritmetiska objekt med pekare - detta behandlas senare
i samband med diskussionen om arrayer.
Om vi studerar exemplet närmare och tittar
på hur MEMF_PUBLIC-minne
allokeras till en IOStdReq-struktur,
tillkommer en intressant komplikation. Att allokera minne är inget
problem, särskilt om vi utnyttjar C:s sizeof-operator,
som returnerar storleken av en struktur uttryckt i antalet bytes:
myrequestpointer = AllocMem(sizeof(struct IOStdReq), MEMF_CLEAR|MEMF_PUBLIC);
Problemet är att AllocMem
normalt deklareras för att returnera en APTR
(absolut minnespekare för allmänt bruk), t ex med
extern APTR AllocMem();
medan mypointer
måste vara en pekare till en IOStdReq-struktur:
struct IOStdReq *mypointer;
C uppskattar inte att vi försöker likställa två olika typer av pekare och ger ett varningsmeddelande, "pekarna pekar inte på samma objekt". För att undvika detta måste vi göra en explicit typomvandling.
Typomvandling är den mekanism i C som tillåter att man i en handvändning omtolkar datatyper. Detta är ytterligare en anledning till att Pascal-programmerare förmodligen inte gillar C, men för oss andra dödliga ("Är inte allt binärt när allt kommer omkring?") kommer den verkligen väl till pass. Ett enkelt exempel på typomvandling är
UBYTE bert; ULONG fred; fred = (ULONG) bert;
Typnamnet (ULONG)
säger explicit åt C att omvandla bert
till typen ULONG
innan det sätts till fred,
även om just denna typomvandling är onödig, eftersom C i
vilket fall som helst inte skulle ha något att invända. Typomtolkning
behövs oftare i samband med pekare, såsom i
APTR bert; struct funnystructure *fred; /* strukturen förutsätts vara definierad innan - Ö anm */ fred = (struct funnystructure *) bert;
Typomvandlingen utnyttjas för att konvertera
bert till en pekare
till en funnystructure-struktur
innan tilldelningen till fred.
Detta skulle annars oroa C, åtminstone så att en "pekarna
pekar inte..."-varning utfärdades, ty C är ganska kinkigt
när det gäller att få veta vad pekare förväntas peka på.
Det "riktiga" sättet att anropa AllocMem är
sålunda i vårt exempel:
myrequestpointer = (struct IOStdReq *) AllocMem(sizeof(struct IOStdReq), MEMF_CLEAR|MEMF_PUBLIC);
Om vi antar att allokeringen fungerar (mypointer
blir inte NULL),
kan vi fortsätta med att initiera meddelandeporten och sedan anropa
OpenDevice():
myrequest.io_Message.mn_Node.ln_Type = NT_MESSAGE;
myrequest.io_Message.mn_Node.ln_Pri = 0;
myrequest.io_Message.mn_ReplyPort = *myportpointer;
if (OpenDevice("console.device",0,myrequestpointer,0) != 0)
/* ta hand om misslyckat öppnande */
else
/* ta hand om lyckat öppnande */
där vi antar att *myportpointer
redan har satts upp som en pekare till en MessagePort-struktur.
I själva verket är det inte nödvändigt
att utföra allt detta innan OpenDevice-anropet,
ty det hanteras av en Exec-understödsfunktion, CreateStdIO(svarsportsadress).
Se ROM Kernel Manual, volym I för en listning av denna funktion.
Vi har introducerat flera av de viktigaste begreppen i C, några nyckeloperatorer (adressoperatorn, punktoperatorn, * och piloperatorn), samt begreppet typomvandling. En sammanfattning följer:
fred = &bert; sätt fred till bert:s adress.
| |
fred = thing.bert; | sätt fred till elementet bert i strukturen thing.
|
fred = *bert; | sätt fred till det bert pekar på.
|
fred = thing->bert; | sätt fred till det element som bert i strukturen thing pekar på.
|
fred = (struct thing *) bert; | sätt fred till bert efter att ha omvandlat detta till en thing-struktur.
|
Mer om strukturer och pekare, särskilt i samband med strängar och arrayer, följer i nästa avsnitt.
I förra avsnittet synade vi de centrala begreppen strukturer och pekare i C-program. Här fortsätter vi med att titta på arrayer och strängar, som i C bygger på de mer generella struktur- och pekarbegreppen.
Antag att vi ämnar göra något som rör diskfiler och behöver en buffert på 1000 bytes. Ett sätt att göra detta (den variant vi lutar åt att föredra) är att allokera minnet under exekveringen:
buffer = AllocMem(1000,0);
Satsen lånar minne från systemet och
sätter upp buffer
som en pekare till detsamma. Denna modell har flera fördelar, inberäknat
att det allokerade minnet med säkerhet kommer att börja på
en långordsgräns (ofta nyttigt när vi har att göra
med AmigaDOS) och att vi kan specificera det slags minne vi önskar
(MEMF_FAST, MEMF_CHIP, MEMF_PUBLIC)
och om minnet skall nollställas (med AllocMem(MEMF_CLEAR)).
Den största nackdelen är att man måste komma ihåg
att minnet allokerats och se till att det frigörs när man avslutar
- såvida inte du skriver något slags "Wallyware"
där Amigan måste återstartas för att sluta.
Därför kanske du föredrar att låta C hantera minnesallokeringen.
Ett sätt att göra detta är att anropa Lattice:s (Unix-liknande)
minnesallokeringsrutiner malloc(), calloc() etc.
Lattice håller reda på minne som allokerats på detta
sätt och frigör det vid ett Lattice-exit().
Alternativt behöver du inte använda ett funktionsanrop alls -
du kan helt enkelt deklarera en global array:
UBYTE buffer[1000];
Detta skapar en struktur bestående av 1000 UBYTE:s, med en
pekare, buffer, som pekar på strukturens första element. När det gäller
en icke initierad global variabel som denna (deklarerad utanför en
funktionskropp), blir resultatet att utrymme för arrayen reserveras
i en BSS-hunk. AmigaDOS-laddaren allokerar minnet och bestämmer buffer-variabelns
värde vid laddningen av programmet. Bufferten inlänkas i programmets
"segmentlista" och frigörs automatiskt av AmigaDOS-Exit().
Precis som enkla variabler kan globala arrayer initieras vid deklarationen. Ett klassiskt exempel (från K&R) är en array med månadernas dagar (icke skottår), deklarerad på följande sätt:
UBYTE days_in_month[] = {31,28,31,30,31,30,31,31,30,31,30,31};
Observera att i days_in_month[]-exemplet
behöver vi inte explicit tala om arrayens storlek, eftersom C klarar
av att självt räkna ut elementantalet. I allmänhet måste
arraystorlek enbart anges när C faktiskt måste reservera utrymme
för arrayen. Om vi t ex har en funktion med en array som argument,
kan argumentet deklareras så här:
cunningfunction(UBYTE somearray[])
{
/* lite kod */
}
Eftersom somearray är en pekare till början av arrayens
minnesutrymme, skulle man lika väl kunna skriva
cunningfunction(UBYTE *somearray)
{
/* kod som utnyttjar pekare */
}
Om du betraktar somearray
som ett arraynamn, som i första exemplet, eller arbetar med den direkt
som en pekare, som i andra exemplet, är i mångt och mycket en
fråga om tycke och smak. I det stora hela spelar det ingen roll för
C:s vidkommande, även om pekarvarianten i vissa fall är snabbare.
I exemplen ovan har vi tittat på det enklaste
fallet, en global array av UBYTE:s.
Statiska arrayer (interna eller externa) liknar globala arrayer.
Den enda skillnaden är att arraynamnet inte utmatas till objektfilen,
varför arrayen enbart är åtkomligt inifrån en viss
funktion (internt statisk), eller inifrån en viss källkodsmodul
(externt statisk). Externa statiska arrayer gör det möjligt för
en grupp samarbetande rutiner, t ex rutiner som sysslar med disk-I/O, att
dela på "privata buffertar", som därmed skyddas från
påverkan från andra programdelar.
Det är även möjligt att deklarera
automatiska arrayer inne i funktioner. Dessa hanteras annorlunda,
eftersom deras minnesutrymme inte reserveras i DATA- eller BSS-hunkar,
utan minnet tas från stacken vid funktionsanropet och frigörs
när funktionen lämnas. Lägg märke till att i standard-C
(K&R) är det inte möjligt att initiera en automatisk
array vid deklarationen, varför du i detta fall måste explicit
tala om arraystorleken för C (I ANSI-C går det att initiera
auto-strukturer,
så glöm anmärkningen ovan. Ö anm).
C är inte begränsat till UBYTE-arrayer,
utan man kan ha arrayer av nästan vad som helst. Detta kan vara enkla
datatyper som
ULONG somearray[SIZE]; /* array av teckenlösa 32 bitars heltal */
eller härledda datatyper, som arrayer av strukturer
struct List listarray[SIZE]; /* array av listhuvuden */
Detta skulle ge en array av liststrukturer, var och en med en länkad lista vidhängande. Detta skulle vara praktiskt i en databas eller någon liknande tillämpning, där man vill utföra hashing på en viss nyckel och sedan söka igenom en hashlista med element som har samma hashnyckel, dvs använda samma teknik som AmigaDOS utnyttjar för att finna en namngiven fil eller katalog. Med en array av listhuvuden deklarerad som ovan kan man komma åt den rätta listan med
thislistpointer = &listarray[hashvalue % SIZE];
(%, modulus-operatorn, ger resten vid heltalsdivision)
Man kan givetvis även ha arrayer av pekare, vilka kanske är mer bekanta under namnet adresstabeller. Om vi t ex vill skriva ut en lista med väntande uppdrag ordnade efter uppdragsprioritet, då skulle vi förmodligen deklarera en array så här:
struct Task *taskpointers[SIZE];
Vi skulle därefter kunna sätta upp arrayen
genom att anropa Disable()
och sedan löpa igenom Exec:s väntelista (listhuvudet ... ), sortera
listan efter uppdragsprioritet och skriva ut uppdragsnamnen i riktig ordning.
Vi kan slutligen ha arrayer av arrayer. Deklarationen
UBYTE thing[12][5];
deklarerar en array av 12 objekt, vart och ett
i sin tur en array av 5 UBYTE:s,
m a o en tvådimensionell array. Arrayer av tre eller fler dimensioner
deklareras på motsvarande sätt - ganska så behändigt, eller hur?
Antag att vi har en array av långord, deklarerad med
ULONG thing[SIZE];
Då kan vi komma åt det i:te arrayelementet med
element = thing[i];
(Lägg märke till att arrayindex alltid
startar med 0, så att index kommer att gå från 0 till
SIZE-1.) Alternativt kan vi utnyttja det faktum att thing
utgör arrayens minnesadress, dvs vi kan lika gärna skriva
element = *(thing+i);
Anledningen till att detta fungerar är att
pekararitmetiken i C är skalad efter storleken av det objekt som
pekaren pekar på. Om vi alltså försöker accessa
det fjärde elementet i arrayen, då kommer (thing+i)
i själva verket medföra att sexton adderas till thing,
ty C vet att thing
är en array av långord (storlek 4). Detta resulterar i en offset
på sexton bytes från arrayens basadress, vilket är fyra
långord - precis det vi ville ha.
Övrig pekararitmetik fungerar på samma sätt. Antag t ex att en pekare till vår array getts värde enligt
pointer = thing;
Detta är ekvivalent med
pointer = &thing[0];
dvs den pekar på arrayens första element. Nu kommer
pointer++;
att öka pekaren med storleken av det objekt den pekar på, dvs fyra bytes i detta fall, så att den pekar på nästa arrayelement. Observera att det här finns en liten skillnad mellan pekare och arrayer, i det att en pekare är en variabel, medan arraynamnet är en konstant. Om vi alltså vill flytta oss uppåt i arrayen måste vi sätta
pointer = thing; pointer++;
eftersom ett försök att utföra
thing++;
skulle ge fel.
På motsvarande sätt kommer
pointer--;
att minska pekaren med fyra bytes, så att den pekar på föregående element. Om vi har två pekare till arrayen, då skalas pekarsubtraktion på samma sätt, dvs
thing = pointer1-pointer2;
subtraherar de två pekarna och skalar sedan ned resultatet så att det blir antalet element mellan de två pekarna. Vi kan också jämföra två pekare på liknande sätt
if (pointer1 < pointer2) {
/* satser */
}
Detta är emellertid de enda tillåtna operationerna på pekare. De kan ökas eller minskas, heltal kan adderas till eller subtraheras från dem, och två pekare kan subtraheras eller jämföras. Andra operationer, såsom försök att addera två pekare eller multiplicera med en konstant är otillåtna och ger kompileringsfel. Lägg också märke till att operationer som pekarsubtraktion enbart kan skalas på ett vettigt sätt om pekarna pekar på samma objekt, t ex på element i samma array och försök att utföra pekararitmetik på två olika objekt resulterar i ett fatalt kompileringsfel.
På samma sätt som arrayer är specialfall av pekare och strukturer i C, så är strängar specialfall av arrayer - en C-sträng är helt enkelt en teckenarray avslutad med ett nolltecken. Deklarationen av en stränglitteral:
fred = "här är en sträng";
medför att "här är en sträng" följd av
ett nolltecken, utmatas som en teckenarray till data-hunken vid kompileringen.
fred blir en pekare
till denna array, dvs adressen till bokstaven "h".
Lägg märke till att
fred = 'h';
och
fred = "h";
betyder två helt olika saker i C. Den första
satsen sätter fred
till 'h':s ASCII-värde,
medan den andra utmatar ett "h",
följt av ett nolltecken till data-hunken och sätter fred
till dess adress.
Det C erbjuder i fråga om strängmanipulering
är ingenting. I Lattice C:s bibliotek lc.lib
finns standardfunktioner för att utföra strängsammanfogning,
strängavspjälkning, strängjämförelser osv. Det
faktum att man måste anropa biblioteksfunktioner för att utföra
strängoperationer är en annan svaghet hos C sett som ett högnivåspråk,
men C är ju egentligen inget riktigt högnivåspråk.
Detta sista avsnitt behandlar hur programflödet
styrs i C, dvs if...else-konstruktioner,
olika slags slingor och hur kontrollen överförs fram och tillbaka
C:s grundläggande if-konstruktion
är ganska rakt på sak:
if (uttryck) sats
eller
if (uttryck)
sats1
else
sats2
I första varianten utförs sats enbart
om uttryck inte
är noll (sant). I andra varianten utförs sats1
om uttryck är
skilt från noll, annars utförs sats2.
sats kan utgöras av en enda C-sats:
if (x == 42) printf("Livets mening upptäckt\n");
Här används likhetsoperatorn ==
(inte att förväxlas med tilldelningsoperatorn =),
som returnerar sant (1) om två saker är lika och falskt (0) annars.
Alternativt kan if
följas av flera satser inneslutna i klammerparenteser:
if (handle = Open("Saken", MODE_OLDFILE))
{
printf("Filen Saken öppnad\n");
readfile(handle);
}
else {
printf("Kunde ej öppna filen Saken\n");
exit(20);
}
Här försöker vi öppna en fil,
"Saken",
och om det lyckas läser vi den med en funktion som heter readfile()
(och som vi antar är definierad någon annanstans). AmigaDOS-funktionen
Open() returnerar
antingen ett "handtag" (handle)
som beskriver filen eller noll om öppningsförsöket misslyckas.
Vi kan således ta hand om värdet i en variabel, handle,
och på en och samma gång testa om den är skild ifrån
noll enligt ovan. Notera återigen skillnaden mellan =
och ==!
Det är ofta nödvändigt att ge en
variabel ett av två möjliga värden beroende på resultatet
av en logisk test. Om vi t ex fått information via AmigaDOS-funktionen
Examine() och
vill komma ihåg om det vi tittade på var en fil eller en katalog,
kan vi använda
if (fib->fib_DirEntryType > 0)
type = DIRECTORY;
else
type = FILE;
(Här är DIRECTORY
och FILE två
makron som vi tidigare definierat någonstans). I C finns ett kompaktare
sätt att göra sådant, i form av ett villkorsuttryck:
type = (fib->fib_DirEntryType > 0) ? DIRECTORY : FILE;
Detta beräknar uttrycket före ?
och sätter sedan type
till DIRECTORY
eller FILE beroende
på om uttrycket är sant (icke-noll) eller falskt.
Ett annat kompakt skrivsätt finns när
man ska välja mellan flera olika alternativ på grundval av olika
heltalsvärden, t ex olika möjliga meddelanden från en Intuition-IDCMP:
if (message->Class == MOUSEBUTTONS) {
/* Ta hand om musklickning */
}
if (message->Class == MENUPICK) {
/* Ta hand om menyval */
}
if (message->Class == CLOSEWINDOW) {
/* Fönster stängs - lämna */
}
etc
Detta hanteras mycket behändigare så här:
switch(message->class) {
case(MOUSEBUTTONS):
/* Ta hand om musklickning */
break;
case(MENUPICK):
/* Ta hand om menyval */
break;
case(CLOSEWINDOW):
/* Fönster stängs - lämna */
break;
default:
/* okänt meddelande - fara å färde! */
}
Lägg märke till break-satserna,
som är nödvändiga för att komma ur switch-klammerparenteserna
och till nästa sats. Utan dessa skulle varje alternativ ramla in i
nästföljande. Notera även default:,
som specificerar vad som ska göras om inget av de specificerade alternativen
inträffat - i vårt exempel skulle vi ha erhållit ett okänt
Intuition-meddelande, vilket med allra största säkerhet innebär
att vi råkat riktigt illa ut.
Alla C:s upprepningssatser är en variant
av while, med testen antingen i början eller i slutet av slingan:
while (uttryck) sats (test i början)
eller
do sats while (uttryck); (test i slutet)
Återigen kan sats antingen betyda en enda C-programsats, som avslutas med ett semikolon, eller flera satser, inneslutna i klammerparenteser:
while (ExNext(lock,fib)) {
if (fib->fib DirEntryType > 0)
printf("Katalog");
else
printf("Fil");
printf(" %s\n",fib->fib_FileName);
}
Här skriver vi ut en AmigaDOS-kataloglista med funktionen ExNext,
som ger information om fil/katalog-objekt via FileInfoBlock-parametern
(fib). När inga fler objekt finns eller vid fel returnerar funktionen noll.
I C är en for-slinga egentligen en variant av while,
med testen i början av slingan:
for (startuttryck; fortsättningsvillkor; loop-uttryck)
sats
C-motsvarigheten till FOR X=1 TO 10: PRINT "HEJ":NEXT är därför
for (x=1; x<=10; x++) printf("HEJ\n");
C-for-satsen
är emellertid mycket kraftfullare än FOR...NEXT
i BASIC. Det som ger dessa möjligheter är loop-uttrycket, som
specificerar vad som ska göras mellan en iteration och nästa.
För att t ex löpa igenom en länkad lista kan vi använda
ett loop-uttryck som plockar fram pekaren till nästa listnod från
den aktuella noden:
for (node = list->lh_Head; next = node->ln_Succ; node = next) {
/* Bearbeta här */
}
Ovan är fortsättningsvillkoret next = node->ln_Succ,
vilket betyder att iterationen fortsätter tills nästa nod är noll.
Det är möjligt att ha flera startuttryck, fortsättningsvillkor och loop-uttryck, åtskilda av kommatecken. Det är också möjligt att utelämna ett eller flera av uttrycken:
for (;;) {
/* fortsätt i all evighet */
}
Ett mycket vanligt C-trick är att ha en for-slinga med tom "kropp", t ex för att kopiera en sträng från en plats till en annan:
for (i=0;dest[i] = source[i];i++)
;
Här används fortsättningsvillkoret både för att kopiera ett tecken och
för att testa om detta är noll. Slingans kropp utför ingenting, vilket
markeras av det avslutande semikolonet. Detta bör man se upp med, ty det är lätt att
skapa en tom slinga av misstag, om man oavsiktligt placerar ett semikolon
i slutet av for-satsen.
Slingor utnyttjar inte stacken i C, varför
det för det mesta är riskfritt att hoppa ur dem. Det "snygga"
sättet att göra detta är med nyckelordet break.
Exempelvis kanske vi vill fortsätta en slinga tills dess vi får
ett CLOSEWINDOW-meddelande från Intuition:
FOREVER { /* Intuition-makro - ersätts av for (;;) */
WaitPort(OurWindow->UserPort); /* Vänta på meddelande */
message = GetMsg(OurWindow->UserPort);
/* Borde testa för mer än ett */
class = message->Class;
if (class == CLOSEWINDOW) break;
switch(class) {
/* Hantera andra meddelandetyper */
}
}
/* Bearbetningen fortsätter här efter break */
Ett annat sätt är att tillgripa (hemska tanke) goto:
if (!(buffer = AllocMem(1000,0)) goto nomemory;
/*** buffertallokering OK - fortsätter ***/
nomemory:
/* Minnet slut - städa upp o lämna */
Vår uppfattning är dock att goto är OK för
att hantera fel - i övrigt bör goto undvikas.
Överföring av kontrollen mellan C-funktioner
exemplifieras i listning 1, som visar ett program som summerar två
tal med en (fullständigt onödig) funktion sum(a,b).
Låt oss först titta på funktionen main(),
som börjar vid offseten 0010 i den disassemblerade objektmodulen.
Det som händer är följande:
first och second. Kompilatorn har placerat
dessa i "SECTION 02" (BSS-hunk), vid offsetadresserna 0 och 4.sum() och till denna
överföra de aktuella first- och second-värdena
som parametrar. Därför tar vi det aktuella second-värdet
(redan i D0-registret) och pushar det på stacken, följt
av first-värdet. Vi anropar nu sum()
med BSR 0, vilket för oss till offseten 0 i den
disassemblerade objektmodulen.sum()
är att reservera utrymme för en temporär ("automatisk")
variabel c. Detta utförs av satsen LINK A5,FFFC,
som lägger beslag på fyra bytes på stacken och placerar
en pekare till dessa i A5.sum() kan nu få tag i de formella variablerna a
och b och den automatiska variabeln c
genom att titta efter vad som pushats på stacken. Detta gör
den genom att använda A5 med offsetvärdena 0 (c),
8 (a) och 12 (b).sum() är klar, frigör den (med UNLNK A5)
det stackutrymme den tidigare lade beslag på och returnerar sedan
ett värde via D0-registret.main() återfår kontrollen, rensar den först
(med ADDQ.L #8,A7) bort de värden den pushade på
stacken och plockar sedan fram first
genom att kopiera D0-värdet
till korrekt offset i hunk 2. Därmed är historien slut.Observera att de två formella variablernas (a och
b) och den automatiska variabelns (c)
värden är strikt temporära. Dessa variabler utgörs
av ett utrymme på toppen av stacken och detta utrymme frigörs
när funktionen är klar, antingen av den anropade funktionen (c)
eller av den anropande funktionen (a och b). Detta
brukar kallas "värdeöverföring" - funktionen sum()
förses med de aktuella värdena hos variablerna first
och second via stacktoppen, men har inte tillgång till
variablerna själva, där de ligger lagrade i hunk 2, och kan därför
inte påverka deras värden.
Man måste vara lite försiktig med detta i C-program. T ex kanske
någon tror att det är möjligt att byta värden mellan first
och second med anropet swap(first, second),
där swap()-funktionen definieras på följande sätt:
int swap(a, b)
int a, b;
{
int c;
c = a; a = b; b = c;
}
Det enda som åstadkoms är emellertid
att temporära värden flyttas omkring i stacken, varefter de slängs
bort. För att komma runt detta, dvs för att förverkliga
"referensöverföring", där swap()-funktionen
kan påverka de aktuella parametrarnas värden, skickas
parametrarnas adresser, varefter de kan refereras indirekt. Anropet
blir då swap(&first, &second)
och swap()-funktionen:
void swap(a, b)
int *a, *b;
{
int c;
c = *a; *a = *b; *b = c;
}
Se listning 2 för en disassemblerad version. Vi överlåter åt dig att som övning själv undersöka vad kompilatorn gör i detta fall och övertyga dig om att det hela fungerar korrekt.
Lägg märke till att samtidigt som C
i allmänhet noga kontrollerar typen hos det värde som en funktion
returnerar, så gör C vanligen ingen stor affär av de värden
som överförs till den. Om du t ex anropar en funktion,
sum(a,b), med två flyttalsparametrar, men funktionen
definierats att ta emot heltal, då kommer sum() att
läsa värden från stacken som om de vore heltal, varför det returnerade
resultatet blir meningslöst. I Lattice C (och i ANSI-C - Ö anm)
kan man skydda sig mot detta genom att explicit ange argumentens typ:
extern sum(int,int);
men detta är inte obligatoriskt. I Lattice (och ANSI-C - Ö anm) är det också tillåtet att skicka fullständiga strukturer till funktioner och returnera dem från funktioner.
Läs Kernighan och Ritchie.
/* Trivialt exempel för Kickstart */
int first, second; /* två globala variabler */
int sum(a, b) /* Bättre (ANSI-C): int sum(int a, int b)
int a, b;
{
int c; /* auto-variabel - temporär lagring */
c = a+b;
return(c);
}
void main()
{
first = 2;
second = 4;
first = sum(first, second);
}
LATTICE OBJECT MODULE DISASSEMBLER V2.00
Amiga Object File Loader V1.00
68000 Instruction Set
EXTERNAL DEFINITIONS
_sum 0000-00 _main 0010-00 _first 0000-02 _second 0004-02
SECTION 00 "add.o" 00000034 BYTES
0000 4E55FFFC LINK A5,FFFC
0004 202D0008 MOVE.L 0008(A5),D0
0008 D0AD000C ADD.L 000C(A5),D0
000C 4E5D UNLK A5
000E 4E75 RTS
0010 7002 MOVEQ #02,D0
0012 23C0 00000000-02 MOVE.L D0,02.00000000
0014 7004 MOVEQ #04,D0
001A 23C0 00000004-02 MOVE.L D0,02.00000004
0020 2F00 MOVE.L D0,-(A7)
0022 2F39 00000000-02 MOVE.L 02.00000000,-(A7)
0028 61D6 BSR 00000000
002A 508F ADDQ.L #8,A7
002C 23C0 00000000-02 MOVE.L D0,02.00000000
0032 4E75 RTS
SECTION 01 " " 00000000 BYTES
SECTION 02 " " 00000008 BYTES
Listning 1 - sum-funktion
/* swap-exempel - avlusad version */
int first, second; /* två globala variabler */
/* funktion för att byta värde på två tal */
void swap(a, b) /* Bättre: void swap(int *a, int *b)
int *a, *b;
{
int c; /* auto-variabel - temp lagring */
c = *a;
*a = *b;
*b = c;
}
void main()
{
first = 2;
second = 4;
swap(&first, &second);
}
LATTICE OBJECT MODULE DISASSEMBLER V2.00
Amiga Object File Loader V1.00
68000 Instruction Set
EXTERNAL DEFINITIONS
_swap 0000-00 _main 001A-00 _first 0000-02 _second 0004-02
SECTION 00 "swap.o" 0000003C BYTES
0000 4E55FFFC LINK A5,FFFC
0004 202D0008 MOVEA.L 0008(A5),A0
0008 2010 MOVE.L (A0),D0
000A 206D000C MOVEA.L 000C(A5),A0
000E 226D0008 MOVEA.L 0008(A5),A1
0012 2290 MOVE.L (A0),(A1)
0014 2080 MOVE.L D0,(A0)
0016 4E5D UNLK A5
0018 4E75 RTS
001A 7002 MOVEQ #02,D0
001C 23C0 00000000-02 MOVE.L D0,02.00000000
0022 7004 MOVEQ #04,D0
0024 23C0 00000004-02 MOVE.L D0,02.00000004
002A 4879 00000004-02 PEA 02.00000004
0030 4879 00000000-02 PEA 02.00000000
0036 61C8 BSR 00000000
0038 508F ADDQ.L #8,A7
003A 4E75 RTS
SECTION 01 " " 00000000 BYTES
SECTION 02 " " 00000008 BYTES
Listning 2 - swap-funktion
Copyright © 1999 Martin Fahlgren
martin@1-1-7-46a.ghn.gbg.bostream.se[Till början av dokumentet] [Till artikelförteckningen]