Del 3 - Kom igång med C

Avsnitt 1 - En introduktion till C

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.

C:s fördelar

Ä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:

  1. Sett som en "super-assembler" har C en hel del förtjänster. Om du har en förståelse för vad din C-kompilator verkligen gör, då är det möjligt att skriva C-kod med förvånansvärt liten overhead, både vad gäller kodstorlek och exekveringshastighet. För de rutiner där extra snabbhet verkligen är ett krav är det också relativt enkelt att utnyttja assemblerkod tillsammans med C.
  2. I mycket större utsträckning än assembler inbjuder C till att man programmerar "strukturerat", med oberoende funktioner som kommunicerar med varandra. Det är ganska enkelt att bygga egna "bibliotek" med användbara funktioner och dra nytta av bibliotek från andra källor, t ex de "standardbibliotek" som finns tillgängliga från Lattice och Amiga, vilket dramatiskt kan reducera utvecklingstiden. I motsats till vad vissa tror, är det möjligt att skriva ganska läsbara C-program, vilket även underlättar underhåll och avlusning.
  3. I mångt och mycket är C Amigans modersmål, eftersom en så stor del av systemmjukvaran är skriven i C. Med C är det särskilt enkelt att till fullo utnyttja "ROM-kärnans" faciliteter. Dessa håller i allmänhet hög standard och det vore dumt att inte utnyttja dem.
  4. C betraktas som ett portabelt (flyttbart) språk, även om det i praktiken är mer problematiskt än det borde vara. Om portabilitet är viktigt för dig kan C åtminstone underlätta flyttning av dina program mellan olika datorer, särskilt om du är noga med att hålla dig till "standardfunktioner".
  5. Den viktigaste fördelen med C är tidsfaktorn. Assemblerprogrammerare som arbetat med stora projekt och bytt till C har rapporterat att de efter några månaders erfarenhet kan vara uppemot tio gånger så produktiva när det gäller att producera fungerande, avlusad kod. Det må så vara att det resulterande programmet exekverar lite långsammare än om det skrivits i assembler, men är detta verkligen värt extrakostnaderna i utvecklingstid?

Till C från BASIC eller assembler

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:

  1. Först måste du skriva in "källkoden" med en editor. Det finns en enkel fullskärmseditor på Workbench-disketten och denna kan anropas med att skriva ED från CLI. (CLI står får Command Language Interface - en ikon för denna visas i system-lådan om CLI-rutan står på ON i Preferences. Vi förutsätter viss CLI-kännedom - om du inte bekant med denna finns ett antal böcker som förklarar det hela, inklusive den officiella AmigaDOS-manualen.) Det finns andra editorer tillgängliga från många olika källor - en favorit som många Amiga-programmerare använder är MicroEMACS (på Amiga Extras-disketten fr o m version 1.2 - Ö anm). Ett C-program består av ett antal funktioner - mer om dessa senare - som i sin tur anropar andra funktioner i samma modul, i någon annan modul eller i ett systembibliotek. Systembibliotekets funktioner utför standardoperationer, som att skriva ut en sträng, så man kan inte utföra särskilt mycket utan dem.
  2. Sedan måste du spara källkoden och kanske lämna editorn - om du använder en editor som EMACS och har tillräckligt med ledigt minne kanske du föredrar att låta editorn fortsätta att köra i bakgrunden för senare användning. Nu måste du köra C-kompilatorn. Lattice-kompilatorn exekverar i två pass. I det första skapas en temporär s k "q-fil" (för "quad"), som du förmodligen vill ha på RAM-disk, och i det andra skapas objektkoden (o-fil).
  3. Kompilatorn kan ge olika felmeddelanden, varvid den vägrar att producera någon o-fil och du måste då återgå till steg 1 och redigera på nytt. Den kan även ge "varningsmeddelanden", om den t ex tror att du försöker använda en variabel utan att den initierats korrekt. Du avgör själv om du ska ta fasta på varningsmeddelandena (och återgå till steg 1) eller om du med gott samvete kan strunta i dem.
  4. Kompilatorns o-fil är fortfarande inte i exekverbar form, ty den innehåller referenser till funktioner och variabler som definieras i andra moduler eller i standardbibliotek. I själva verket har o-filen ett komplicerat format och innehåller flera olika slags poster. En del av dessa innehåller en slags "partiell" 68000-kod utan adressinformation, andra innehåller information om funktioner och variabler som definieras i andra moduler, åter andra innehåller "relokeringsinformation" om hur koden ska justeras beroende på var i minnet den kommer att hamna.
    För att lösa referenserna till andra moduler osv måste man köra ett annat program, kallat länkare. Normalt används länkaren för att sammanfoga någon standard-"start-modul" med de moduler som utgör ditt program och med standardfunktioner, valda ur ett eller flera "bibliotek". Om länkaren lyckas, dvs om den finner alla funktioner och variabler som behövs, då produceras en laddbar AmigaDOS-fil (laddfil). I annat fall ger den felmeddelanden och du måste återgå till steg 1.
  5. Det är värt att notera att laddfilen fortfarande inte innehåller absolut 68000-kod. I stället innehåller den ett antal poster, s k "hunkar", som var och en motsvarar ett stycke kod eller data. Hunkarna innehåller fält, s k "block", med kod och relokeringsinformation, som gör det möjligt att ladda koden var som helst i minnet där det råkar finnas plats för den, samt (valfritt) "symbolisk" information som medger att variabelnamn m m från det ursprungliga programmet överförs till en "symbolisk avlusare" (debugger) som Wack eller Metascope.
    Själva "spridningsladdningen" åstadkoms genom att man skriver programnamnet från CLI. Det kan även ordnas så att det sker vid klickning på en Workbench-ikon. AmigaDOS bestämmer då var i minnet de olika "hunkarna" ska laddas och relokerar efterhand. Exekveringen startas sedan från början av den första "hunken", som bör vara startmodulen. Du kan nu avgöra om din kod fungerar eller inte - om inte så får du återgå till steg 1.

C:s särdrag

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.

Ett enkelt exempel

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.

Access av bibliotek

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:

  1. #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.
  2. #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).
  3. Därefter deklareras en "global variabel", 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.
  4. Vår funktion 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.
  5. Det första vi gör är att försöka öppna biblioteket 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.
  6. Om 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").
  7. Vi använder en slinga för att blinka skärmen sex gånger. 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).
  8. Inne i slingan anropas Intuition-funktionen 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.
  9. Efter 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.
  10. När vi är klara måste Intuition-biblioteket "stängas" för att informera systemet om att uppdraget inte längre behöver Intuition. Detta görs med ett anrop av Exec-funktionen CloseLibrary().

C på Amiga

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.

Avsnitt 2 - Grundkomponenterna i C

Introduktion

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)!

C:s grundbeståndsdelar

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:

  1. Programmet börjar med flera #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.

  2. #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.

  3. Sedan kan vi behöva informera kompilatorn om funktioner som definieras senare i den aktuella modulen. Med tanke på att C-kompilatorer för det mesta arbetar i flera pass är de ganska dåliga på framåtreferenser av funktioner och kräver att få veta vilket slags objekt en funktion returnerar innan den refereras för första gången. En lösning på detta är att bygga upp modulen från "botten och uppåt", så att funktioner alltid definieras innan de refereras. Ett annat sätt är att deklarera funktionerna med ett avslutande semikolon för att tala om för kompilatorn att de verkliga definitionerna följer senare i modulen:
      APTR FantastiskFunktion(); /* definieras senare */

    Kompilatorn kan sedan handskas med de referenser till FantastiskFunktion() som görs innan själva definitionen.

  4. 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.
  5. 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".

  6. Efter globala data kommer själva programmet. Ett C-program består av ett antal funktioner, vilka är generella byggstenar, som spelar samma roll som funktioner, procedurer och subrutiner i andra språk. Den första funktionen i ett program ska heta 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:
  7.   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.

  8. Inne i funktionsdefinitionen brukar vanligen först följa deklarationer av lokala ("automatiska") variabler. Minne för dessa reserveras på stacken vid inträde i funktionen och frigörs när funktionen avslutas. Dessa variabler är därför strikt lokala och temporära. Ett exempel är i-räknaren i beep.c:
  9.   main()
      {
        int i; /* räknare */
        /* annat följer */
      }
    
  10. Efter de lokala variablerna följer de C-programsatser som utför någonting och består av styrsatser som if och for, tilldelningar, logiska operationer och anrop av andra funktioner.

Variabeltyper

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.

Datatyper

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.

Avsnitt 3 - Strukturer och pekare

Introduktion

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.

Strukturer och 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.

Direkt användning av strukturer

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.

Indirekt användning av strukturer

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.

Typomvandling

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.

Sammanfattning

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.

Deklaration av en array

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};

Arrayer i funktioner

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.

Andra arraytyper

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).

Arrayer av andra objekt

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?

Access av arrayer - pekararitmetik

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.

Allt som är värt att veta om strängar i C

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.

Avsnitt 5 - Slutkläm i C

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

if..else

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 ==!

Andra logiska tester

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.

Slingor

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.

Att komma ur slingor

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.

Flödeskontroll med funktioner

Ö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:

  1. Först av allt sätter vi upp två globala variabler, first och second. Kompilatorn har placerat dessa i "SECTION 02" (BSS-hunk), vid offsetadresserna 0 och 4.
  2. Nu vill vi anropa funktionen 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.
  3. Det första som görs i 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.
  4. 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).
  5. När 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.
  6. När 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.

En sista punkt

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.

Vad göra nu

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]