VK Numerisk analys och datalogi A NADA C för den som kan Java Version 1.0, 18 juli 2000 Tomas Oppelstrup och Jonas Svennebring
2
Innehåll 1 Introduktion 5 1.1 Historia.. 5 1.2 ÖversiktC. 5 1.3 Ettkortexempel. 6 2 Grundläggande struktur 8 2.1 SchematiskbildavettC-program 9 2.2 Enkladatatyper. 9 2.3 Variabler.. 9 2.4 Typomvandlingar... 10 2.5 Funktioner. 10 2.6 Satserochuttryck 11 2.7 Datastrukturer.. 12 2.7.1 Vektorer. 12 2.7.2 Strängar. 13 2.7.3 Posterochdatastrukturer. 13 2.7.4 Uppräknadetyper.. 14 2.7.5 Minnessparandekonstruktioner 15 2.8 Operatorerifallandeprioritetsordning. 15 2.9 Bitoperationer.. 16 3 Pekare 17 3.1 Vektorersompekare. 17 3.2 Funktionersomargument.. 18 4 In- och utmatning 19 4.1 Terminalfönstret. 19 4.1.1 Utmatning med printf.. 19 4.1.2 Inmatning med fgets och getchar 20 4.2 Filer. 21 4.2.1 Binärfiler 21 4.2.2 Olika sätt att öppna filer.... 21 4.2.3 Fördefinieradefiler. 21 5 Modularisering 22 5.1 Preprocessorn.. 22 5.1.1 #define. 23 5.1.2 #undef.... 23 5.1.3 #pragma. 23 5.1.4 #include. 23 5.1.5 #error.. 24 5.1.6 #if,#endif,#ifdef,#ifndef,#else,#elif 24 5.2 Standardbiblioteket.. 24 6 Minneshantering 26 6.1 Allokeringavminne. 26 6.2 Användningsområden. 27 6.3 Komplexadatastrukturer.. 28 3
7 Pekare och minneshantering ett illustrerat exempel 28 7.1 Först lite datatyper.. 28 7.2 Attskapaennystack. 29 7.3 Testaomenstackärtom.. 29 7.4 Lägga ett element till stacken... 29 7.5 Plockabortförstaelementetistackenochreturneravärdet.. 30 7.6 Tabortstacken. 30 8 Kompilering av C-program 30 8.1 Automatiseradkompilering. 31 8.2 Make 31 9 Avlusning 32 9.1 AvlusningavC-program.. 32 9.2 Tipsochvanligafel.. 33 9.3 En exempelkörning med avlusaren GDB 33 9.3.1 Copyrightanddistributionnotice.. 33 9.3.2 ASampleGDBSession. 33 10 Lathund och referenser 37 10.1Satser 37 10.2Operatorer. 37 10.3Typer 37 10.4Vanligastandardfunktioner. 38 10.4.1 math.h. 38 10.4.2 stdio.h.. 38 10.4.3 stdlib.h. 38 10.4.4 string.h. 38 10.5Referenser. 39 4
1 Introduktion Målet med häftet C för dig som kan Java är att lära ut grunderna i ANSI-C baserat på tidigare kunskaper i Java. Häftet ska ge en bred översikt med djupare beskrivning av vissa viktiga områden. Naturligtvis kan det inte fullt täcka språket varför en referenslista lagts i slutet för information om vissa specialområden. Där finns såväl tips om böcker som webbsidor och elektroniska manualer. Till häftet hör även en hemsida med ytterligare exempel och svar på vanliga frågor och problem: http://www.nada.kth.se/datorer/haften/java2c Efter att ha läst häftet bör den som behärskar Java kunna skapa relativt komplexa program i C. Eventuella fel i texten, kommentarer och frågor på innehållet mottages med tacksamhet. Jonas Svennebring, su98-sve@nada.kth.se Tomas Oppelstrup, f96-top@nada.kth.se Visst material har hämtats från C++ för den som kan Modula-3 av Viggo Kann. 1.1 Historia Den första versionen av programmeringsspråket C utvecklades och implementerades av Dennis Ritchie 1973. Systemet skrevs för en DEC PDP-11 som använde operativsystemet Unix. På den tiden utvecklades Unix vid Bell Laboratories och C skapades parallellt med operativsystemet. Flera av de operatorer som finns i C, t.ex. ++, --, += etc. var ursprungligen direkta hänvisningar till instruktioner på PDP-11. Korrekt använda kunde dessa operatorer snabba upp programmen. Idag kan intelligenta kompilatorer automatiskt göra dessa uppsnabbningar men operatorerna lever alltjämt kvar. Unixversionen av C var den som länge angav standarden. Med ett växande antal mikroprocessorsystem uppkom och spred sig C-dialekterna med stora kompatibilitetsproblem som följd. Därför bildade ANSI 1 1983 en kommitté för att skapa en standard runt språket, ANSI-C, som beskrivs i detta häfte. Det objektorienterade synsättet byggdes in av dansken Bjarne Stroustrup under 1980 och lanserades som C med klasser. 1983 bytte det namn till det idag välkända C++, som är en påbyggnad till C. En C++-kompilator kan därför även användas för att kompilera C-program. Utvecklingen har gott vidare och företaget Sun släppte i maj 1995 den första versionen av Java. Java har till stor del anammat syntaxen hos C++ och byggt vidare på den objektorienterade synen. Java har även influerats av andra objektorienterade språk och är inte bakåtkompatibelt med vare sig C eller C++. 1.2 Översikt C Till skillnad från Java och C++ är C funktionsorienterat och saknar objekt och klasser och är i det avseendet betydligt närmare språket Pascal. Arv och delegering har därför ingen motsvarighet i C. Någon polymorfi finns inte heller så funktioner måste särskiljas på namnet, inte enbart med argumenten. Det som i Java kallas för metod heter funktion i C. Mer om detta i kapitel 2.5. Historiskt har C använts mycket inom systemutveckling. Där Java skapar en mer mänskligt orienterad miljö med objekt så är C mer datororienterat. Variablerna ska ses som att de ligger direkt i datorns minne på specifika minnesadresser, vilket medför C har generösare möjligheter till typomvandling jämfört med Java. Det system med skräpsamling som används i Java, dvs. att systemet håller reda på och frigör inte längre använda objekt och variabler, finns inte i C. En viktig och ibland mycket komplicerad del är därför att vara noga med att frigöra det allokerade minne som har använts färdigt. Om inte så uppkommer minnesläckage i systemet. Mer information om hur det går till att allokera och frigöra minne återfinns i kapitel 6. Variabeldeklarationer görs i princip identiskt med Java. En viktig skillnad är att lokala variabler endast kan deklareras först i ett block, dvs. direkt efter { och före programkoden. Globala variabler bör deklareras efter definitioner i toppen av en fil. Regler för namn på variabler och funktioner är mycket likt Java. 1 American National Standards Institute 5
Skillnad görs alltså på stora och små bokstäver, och namn måste börja med en bokstav. Därefter får bokstäver, siffror och understreck användas. Understreck, _, bör inte användas som första tecken då dylika symboler främst är avsedda för systemets interna namn. ANSI C har 32 reserverade ord som inte heller får användas som namn. De finns listade i tabell 1. I kommande kapitel kommer de flesta av de reserverade orden att beskrivas. Kommentarer i C kan endast ges mellan /* */. Särskilda enraderskommentarer finns inte. auto double int struct break else long switch case enum register typedef char extern return union const float short unsigned continue for signed void default goto sizeof volatile do if static while Tabell 1: De 32 reserverade orden i ANSI-C 1.3 Ett kort exempel Nedan följer ett exempel på ett ganska enkelt C-program som visar några av de vanligaste konstruktionerna. Programmet läser in ett antal tvådimensionella koordinater från tangentbordet. Därefter sorteras de efter avståndet till origo och användaren frågas efter ett namn på en fil till vilken de sorterade koordinaterna ska skrivas ut. För att kompilera och exekvera programmet på en Unixdator kan följande kommando användas: Kompilering: gcc -o exempel1 exempel1.c -lm Exekvering: exempel1 eller./exempel1 Kommandona ovan förutsätter att det finns en fungerande C-kompilator i datorsystemet samt att programtexten i detta exempel har sparats på en fil med namn exempel1.c. -lm talar om för kompilatorn att biblioteket med matematiska funktioner ska inkluderas vid länkningen. I detta bibliotek finns funktionen sqrt som används nedan. Om det inte finns någon C-kompilator tillgänglig på Nada kan du behöva lägga till den modulen. Med kommandotmodule add gnu eller module add gcc får du tillgång till kompilatorn gcc.observera att det finns flera varianter av C på Nada och att vissa kurser kan använda en specifik variant. Har du gått med i kursen med course join så bör du redan ha tillgång till den korrekta kompilatorn. Ytterligare information om kompilatorn samt parametrar finns i kapitel 8. Detta exempel innehåller rikligt med kommentarer rörande de saker som skiljer sig mellan Java och C. Datatyperna int och double finns både i Java och C. Syntaxen för slingor och if -satserärdensammaijava och i C varför detta inte kommenterats närmare. /* Inkludera filer som innehåller definitioner av de biblioteksfunktioner som används i programmet. Användningen av #include i C-program har vissa likheter med Javas import-uttryck. */ #include <stdlib.h> #include <stdio.h> #include <math.h> /* Definiera N_POINTS till talet 4 */ 6
#define N_POINTS 4 /* Definiera en datastruktur Point som kan innehålla en 2-D koordinat */ typedef struct { double x, y; Point; /* Definiera en funktion distance() som räknar ut avståndet från origo till en punkt. Denna funktion nyttjar Pythagoras sats och funktionen sqrt() från matematikbiblioteket. sqrt() beräknar kvadratroten av ett tal. */ double distance(point p) { double r; r = sqrt(p.x*p.x + p.y*p.y); return r; /* bubblesort() tar en vektor med punkter samt vektorns längd som argument och sorterar punkterna i vektorn efter deras avstånd till origo med största avstånd först, med hjälp av bubblesort-algoritmen. Denna algoritm är långsam och passar dåligt när långa vektorer ska sorteras; den är dock enkel att implementera och passar därför bra till detta exempel. */ void bubblesort(point points[], int length) { Point swap; int i, j; for(i = 0; i<length-1; i++) for(j = i+1; j<length; j++) if( distance(points[i]) < distance(points[j]) ) { swap = points[i]; points[i] = points[j]; points[j] = swap; /* main() är den funktion där programmet börjar, precis som i ett javaprogram. main() ska alltid returnera ett heltal, int. */ int main(void) { /* filename blir en vektor med 80 tecken. Teckenvektorer används i C för att representera strängar. */ char filename[80]; /* points blir en vektor av Point med N_POINTS element. */ Point points[n_points]; /* FILE * är en adress till en datastruktur av typen FILE. En FILE används för att representera filer i C. */ FILE *file; int i; 7
/* printf() är en funktion som skriver ut text i terminalfönstret. */ printf("detta program läser in %d koordinater och sorterar dem.\n",n_points); printf("sedan skrivs koordinaterna ut på en fil.\n"); for(i = 0; i<n_points; i++) { printf("ange koordinat %d (format: x y): ", i+1); /* scanf() används för att läsa in text och tal från tangentbordet. */ scanf("%lf %lf", &points[i].x, &points[i].y); /* Sortera koordinaterna genom att anropa bubblesort-funktionen ovan. */ bubblesort(points, N_POINTS); printf("nu är punkterna sorterade.\n"); printf("vilken fil ska de skrivas ut på? "); scanf("%s", filename); /* Öppna en fil med det namn som finns i strängen filename. */ file = fopen(filename, "w"); /* Om file är NULL så gick det inte att öppna filen. */ if(file == NULL) { printf("det gick inte att öppna filen %s.\n", filename); else { for(i = 0; i<n_points; i++) /* fprintf() är som printf() med skriver till en fil. */ fprintf(file, "Koordinat %2d: %10.3lf %10.3lf\n", i+1, points[i].x, points[i].y); /* När punkterna är skrivna på filen så ska den stängas. */ fclose(file); printf("nu är koordinaterna utskrivna på filen %s.\n", filename); printf("för att se innehållet i filen kör kommandot: more %s\n", filename); /* main() ska returnera 0 (noll) om programmet inte råkade ut för något fel. Det borde kanske ha returnerat 1 (ett) om det inte gick att öppna filen, eller frågat om ett annat filnamn, men det får bli en övning till läsaren. */ return 0; 2 Grundläggande struktur Syntaxen i C har samma struktur som Java. Mycket är lika mellan de två språken, men det finns också en hel del olikheter. För programmeraren är den största skillnaden att Java är konstruerat för objektorienterad programmering medan C är helt funktionsorienterat. Ett javaprogram består ofta av ett antal klasser som beskriver olika objekt eller fenomen. Objekten kommunicerar sedan genom att anropa varandras medlemsfunktioner eller metoder. I C går det inte att kapsla in data med tillhörande metoder in en klasstruktur. 8
C-program består istället av ett antal datastrukturer och en mängd fristående funktioner skrivna för att operera på datastrukturerna. När det gäller själva koden, det vill säga definitionen av eller innehållet i en funktion så är den nästan identisk med motsvarande Javametod. 2.1 Schematisk bild av ett C-program C tillåter att typdefinitioner, variabeldeklarationer, konstanter, makron och funktioner blandas ganska friskt. Det är i dock allmänhet bra att försöka ha en röd tråd i programmet och ett intuitivt upplägg så att det lättare går att hitta i koden. Ett C-program kan bestå av flera moduler (filer) som kan kompileras var för sig och sedan länkas ihop till ett exekverbart program. Normalt så ser upplägget för en C-modul ut ungefär på följande sätt: Först kommer ett antal #include-direktiv som talar om att vissa filer innehållande olika deklarationer ska inkluderas. Sedan följer definitioner av datastrukturer och typer. Därefter kommer eventuella globala variabler och sedan deklarationer av alla funktioner i modulen. Därpå följer definitioner av funktionerna, det vill säga själva koden. Sist i huvudmodulen definieras funktionen main, vilken anropas när programmet startas. 2.2 Enkla datatyper De enkla, fördefinierade typerna är i stort sett samma som i Java. I tabell 2 listas de enkla datatyperna. Typ Beskrivning Vanlig storlek char Tecken (i textsträngar) 1 byte int Vanliga heltal 2 eller 4 byte (4 med gcc) long int Långa heltal 4 eller 8 byte short int Korta heltal, används inte så ofta 2 byte double Långa flyttal 8 byte long double Extra långa flyttal, används sällan 10, 12 eller 16 byte float Korta flyttal, används inte så ofta 4 byte Tabell 2: Datatyper Som synes finns inte någon datatyp för booleska variabler, bool i Java. Istället används int. Heltalet noll betraktas som falskt och alla övriga som sanna (även negativa tal!). Funktionerna i matematikbiblioteket använder genomgående double för att representera flyttal, och det är bara i undantagsfall som det finns anledning att använda float eller long double i ett program. 2.3 Variabler C har samma syntax som Java för variabeldeklarationer. Det som skiljer sig är var de får förekomma och hur de får initieras. I Java får variabeldeklarationer förekomma lite varstans i koden samt i klassdefinitionerna. C har striktare regler för detta. Variabler kan antingen vara globala eller lokala. De globala variablerna deklareras utanför alla funktioner och kan användas av alla funktioner i programmet. Vid deklaration av en global variabel kan väljaren static ges vilket gör att variabeln bara blir synlig inom den modul där den deklarerats. Det finns även en väljare extern som talar om att variabeln ska vara åtkomlig i hela programmet. Väljaren extern är underförstådd om ingen väljare anges. Globala variabler kan endast initieras med konstanter. Lokala variabler deklareras i funktioner och är bara åtkomliga i det block de deklarerats i. I funktioner måste alla variabeldeklarationer stå före programkoden i ett block, vilket exemplifieras i avsnittet om funktioner nedan. Lokala variabler kan initieras med godtyckliga uttryck. /* Variabeldeklarationer och initieringar */ int i, sum = 0; double pi = 3.14159265359, x, y; 9
char c = Q, bokstav, tecken; int fifteen = 0xf; /* Ett hexadecimalt tal */ Vid deklaration av heltalsvariabler kan typen föregås av signed eller unsigned. Dessa väljare betyder att heltalet ska ha respektive sakna tecken. Ett n-bitars heltal med tecken kan anta värden från 2 n 1 till 2 n 1 1 medan motsvarande intervall för teckenlösa heltal är 0 till 2 n 1. Om ingen av dessa väljare ges är signed underförstått, utom för typen char. En variabel av typen char kan vara antingen med eller utan tecken beroende på vilken kompilator som används. För att ange att en variabel ska vara konstant så används väljaren const. Den har likheter med väljaren final i Java. I C användas const oftast för att skydda argument till funktioner. Se stycket om vektorer, 2.7.1, för ett användningsexempel. 2.4 Typomvandlingar I C så sker omvandling mellan alla numeriska datatyper implicit. Det betyder att en flyttalsvariabel kan tilldelas ett heltal utan någon explicit typomvandling. I uttryck som innehåller blandade typer görs automatiskt omvandling till den noggrannaste datatyp som finns representerad. Flyttal räknas som noggrannare än heltal. Mest noggrann är typen long double och minst typen char. För att göra explicit typomvandling används samma syntax som i Java, det vill säga den typ man vill konvertera till sätts inom parentes framför uttrycket. 2.5 Funktioner int a = 1, b = 2; double c; c = a / b; /* Heltalsdivision => c = 0!! */ c = ((double) a) / b; /* c = 0.5 */ a = b + exp(c); /* Automatisk typomvandling, samma som a = (int) ( ((double) b) + exp(c) ) */ Precis som Javametoder består C-funktioner av ett funktionshuvud innehållande returtyp, funktionsnamn och parameterlista följt av ett block. Ett block är ett antal satser mellan en vänster- och en högerklammer, {. En funktions returtyp kan vara void vilket innebär att funktionen inte returnerar något värde. För att returnera ett värde från en funktion och hoppa tillbaka till anroparen används satsen return returvärde; precis som i Java. /* Definition av pseudofunktion */ returtyp funktionsnamn(typ parameter, typ parameter ) { sats sats I ett C-program måste varje funktion vara deklarerad eller definierad innan den anropas. En funktionsdeklaration består oftast bara av att funktionshuvudet (första raden i funktionsdefinitionen) skrivs på en egen rad avslutad med ett semikolon. /* Deklaration av funktionen "upphöjt till" */ double pow(double base, double exponent); /* Definition av funktionen pow() */ double pow(double base, double exponent) 10
{ return exp(log(base) * exponent); Parameternamnen behöver inte anges i deklarationen av en funktion, men det är brukligt då det underlättar för läsaren att se hur funktionen ska anropas. Om en funktion inte tar några argument bör det stå void mellan parenteserna i deklarationen. En tom parentes i en funktionsdeklaration betyder att det inte finns någon information om vilka argument funktionen tar. Det blir då tillåtet att anropa funktionen med vilka argument som helst. Alla variabler som används i en C-funktion måste deklareras först i ett block, innan några programsatser kan utföras. Detta är lätt att glömma bort eftersom variabeldeklarationer får förekomma nästan var som helst i ett javaprogram. /* En felaktig funktion */ int wrong(int a, int b) { int x, c = min(a, b); x = 4 * c + b; int d; /* FEL! Deklaration efter en sats */ d = a + b; /* En korrekt funktion */ int right(int a, int b) { int x, c = min(a, b); int d; x = 4 * c + b; d = a + b; Ibland är det önskvärt att en funktion inte ska vara nåbar utanför den modul den definierats i. Detta styrs av väljaren static i funktionens definition och deklaration. /* Deklaration och definition av lokal funktion */ static int min(int a, int b); static int min(int a, int b) { if(a < b) return a; else return b; 2.6 Satser och uttryck De allra flesta satser och uttryck är identiska med hur de ser ut i Java. Detta gäller till exempel for-loopar, while-slingor och if -satser. I lathunden längst bak i häftet finns samtliga konstruktioner listade med en kort förklaring. Uttryck formas på samma sätt i C som i Java. Det finns en konstruktion i C som helt saknas i Java, nämligen goto. Med goto kan man hoppa från ett ställe i en funktion till ett annat. Användning av goto ses av många som dålig programmering, då alltför 11
många hopp gör det svårt att följa programflödet. Ofta kan man också klara sig undan det genom att använda break, continue eller return. Ett tillfälle då det kan vara befogat att använda goto är till exempel om ett fel upptäcks långt inne i en funktion bland nästlade slingor och if -satser. Då kan det vara praktiskt att hoppa till slutet av funktionen där det finns någon felhanteringskod. Syntaxen för goto är: goto label; label är en markering som anvisar vart programmet ska hoppa. En sådan markering kan göras genom att markering: läggs först på en rad. markering är ett namn enligt C s namnkonventioner eller ett heltal. goto är en kvarleva från den gamla tidens programspråk. I äldre språk som till exempel Fortran används goto frekvent. 2.7 Datastrukturer I C finns möjlighet att definiera nya typer och datastrukturer. Detta gör det möjligt att sortera programmets data på ett mer sofistikerat och överskådligt sätt än med bara variabler av de enkla typerna. 2.7.1 Vektorer För att definiera och indexera vektorer används hakparenteser, vilket känns igen från Java. En skillnad mellan C och Java är att C-vektorer inte innehåller någon information om antalet element i vektorn. Detta måste programmet hålla reda på på annat sätt. Första elementet i en C-vektor har index noll, likt Java. /* Deklaration och initiering av en vektorvariabel innehållande tio heltal */ int i; int vector[10]; for(i = 0; i<10; i++) vector[i] = 1000; Vektorer kan också initieras direkt vid deklarationen med en kommaseparerad lista omgiven av klammerparenteser, {. Hakparentesen efter variabelnamnet kan lämnas tom och vektorn får då samma längd som listan. Om ett större antal element än listans längd specificeras mellan hakparenteserna blir överskjutande element satta till noll. int v1[] = {1, 2, 3; int v2[4] = {1, 2, 3; /* v[3] blir satt till 0 */ I definitionen till funktioner som hanterar vektorer vill man inte behöva ange hur långa vektorerna ska vara då det kan variera under programmets gång. Då kan en ospecificerad vektor användas. Detta anges med en tom hakparentes, [ ]. Om en funktion inte ska kunna ändra i vektorn anges väljaren const som beskrivits i stycket om variabler ovan. Det är den absolut vanligaste användningen av const ic. /* Definition av funktion som summerar elementen i en vektor */ double vectorsum(const double vector[], int length) { int i; double sum = 0.0; for(i = 0; i<length; i++) sum += vector[i]; return sum; 12
/* Exempel på anrop */ double sum, vector[10]; sum = vectorsum(vector, 10); För den som snabbt vill komma igång och inte behöver alla detaljer om vektorer i C räcker ovanstående beskrivning. I C så är vektorer nära förbundna med pekare och minneshantering och i kapitel 3 finns en mer utförlig beskrivning av vektorhantering. 2.7.2 Strängar I Java finns det fördefinierade klasser som gör det enkelt att arbeta med strängar, och någon närmare kunskap om vad strängar egentligen är behövs inte. I C är fallet annorlunda. Istället för att ha en speciell typ för strängar används vektorer med element av typen char. Då en vektor inte har en längd associerad till sig är konventionen att sista tecknet i en sträng alltid är 0-tecknet (en byte där alla bitar är 0). Detta tecken kan i strängar och teckenkonstanter erhållas med sekvensen n0. En textsträng anges i ett program mellan dubbla citattecken, och det avslutande 0-tecknet stoppas automatiskt in av kompilatorn. De gånger som det är nödvändigt komma ihåg det avslutande tecknet är när man skriver kod som ska arbeta med tecken inuti strängar. Ytterligare en vansklighet med stränghantering i C är när en sträng ska kopieras från ett minnesutrymme till ett annat. Då måste man vara säker på att den den nya teckenvektorn är stor nog att rymma hela strängen, inklusive det avslutande 0-tecknet. Annars kan programmet krascha. /* En funktion som summerar det numeriska värdet av alla tecken i en sträng */ int stringsum(const char string[]) { int sum = 0, i = 0; while(string[i]!= \0 ) sum += (int) string[i++]; return sum; char ab[] = "AB"; int n; n = stringsum(ab); Exemplet visar också hur strängar kan initieras vid deklarationen. I ANSI-C finns ett strängbibliotek med funktioner för de vanligaste strängoperationerna. Bland annat finns funktioner för kopiering och jämförelse av strängar samt tolkning av strängar som tal. För en listning och beskrivning av de allra mest använda, se stycket om standardbibliotek, 5.2. Kompletta listor brukar finnas bland manualsidornapå Unixdatorer.Prova man string. Utskrift och inläsning av strängar och andra data behandlas i kapitel 4 om in- och utmatning. 2.7.3 Poster och datastrukturer En post kan ses som en Javaklass som enbart innehåller data och saknar metoder. En ny posttyp kan konstrueras med det reserverade ordet struct. Notationen för att komma åt fälten i en post är samma som Javas, postvariabel.fältnamn. Nedanstående exempel skapar en post som kan användas för att representera vektorer i planet. /* Datastruktur för 2D-vektorer */ struct VECTOR_2D { double x, y; ; 13
struct VECTOR_2D v; v.x = 3.14; v.y = 2.72; Det reserverade ordet typedef kan användas för att tillverka nya typer. Syntaxen är typedef följt av en typspecifikation följt av namnet för den nya typen och semikolon. /* Fortsättning från föregående exempel Definition av post- och vektortyp */ typedef struct VECTOR_2D Vector2D; typedef int IntVector[10]; Vector2D coordinate = { 6.626e-34, 3e8 ; Vector2D point; IntVector iv; point.x = 5.6705e-8; iv[4] = 42; Liksom i fallet med vektorer kan structar initieras vid deklarationen med en kommaseparerad lista omgiven av klammerparenteser, som i exemplet ovan. Elementen initieras då i den ordning de definierats av värdena i listan. En struct behöver inte alltid ges ett namn, och en mycket vanligt förekommande konstruktion är: typedef struct { double real, imag; Complex; Ovanstående definierar en typ Complex som är en post med två fält. Naturligtvis går det att skapa vektorer av egenhändigt konstruerade typer. typedef Complex CVector[10]; typedef double Vector[3]; typedef Vector Matrix[3]; /* Användningsexempel */ double rowsum; CVector cv; Matrix m; m[1][0] = 1.4142; cv[2].real = 0.0; cv[2].imag = 1.0; rowsum = vectorsum(m[2], 3); 2.7.4 Uppräknade typer I C kan en uppräknad typ definieras med enum. typedef enum COLOUR {Red, Green, Blue Colour; /* I likhet med struct kan namnet COLOUR utlämnas */ Colour c = Red; 14
Med enum så definieras en mängd namngivna heltalskonstanter (i exemplet Red, Green och Blue). Att deklarera en variabel av typen Colour hjälper den som läser koden att förstå vad variabeln ska användas till. Den kommer dock att representeras av ett heltal och det är tillåtet göra aritmetiska operationer på den och att tilldela den andra värden än de namngivna heltalskonstanterna. Vill man specifikt ange vilka värden namnen i uppräkningen ska motsvara kan detta skrivas in i definitionen av uppräkningen enligt: typedef enum { One = 1, Three = 3, Ett = 1 Number; 2.7.5 Minnessparande konstruktioner Om man vill ha en datastruktur som ska kunna representera flera typer av objekt med olika sorters data kan detta bli onödigt minneskrävande. I C finns en speciell konstruktion union för att lösa den sortens problem. En union deklareras på samma sätt som en struct. Skillnaden är att alla fält i en union upptar samma minnesplats. /* Datastruktur för representation av objekt som antingen kan vara funktioner eller data */ typedef enum { Integral, Real Kind; typedef struct { Kind type; union { double rvalue; int ivalue; data; Object; /* Funktion som returnerar "värdet" för ett objekt */ double GetValue(Object object) { if(object.type == Integral) return (double) object.data.ivalue; else /* object.type is Real */ return object.data.rvalue; Ovanstående exemplifierar det vanligaste användningsområdet av union. Fältet i data i datatypen Object innehåller själv två fält. Dessa delar på samma minnesområde vilket innebär att om ivalue ändras så ändras också rvalue. Att deklarera data som en struct hade tagit mer minnesutrymme. Storleken av en union är storleken på det största fältet medan storleken av en struct är summan av alla fälts storlek. 2.8 Operatorer i fallande prioritetsordning Här listas alla operatorer i fallande prioritetsordning. Parenteser kan användas om man vill att uttryck ska evalueras i en annan ordning än den som ges av operatorprioriteterna. () []. -> Proceduranrop (strcpy(s,t)), indexering i vektor (v[3]), åtkomst av fält i post från postvariabel (a.skonr) eller från pekare till post (p->skonr). ++ --! + - * & sizeof () Unära operatorer: uppstegning med ett (x++ ger värdet före uppstegning, ++x ger värdet efter uppstegning), nedstegning med ett (x-- ger värdet före nedstegning, --x ger värdet efter nedstegning), logisk negation (!ok), bitvis komplement (se avsnitt 2.9), unärt plus (+17), unärt minus (-17), avpekning (*p), adressen till 15
variabel (&x), storlek av ett objekt (sizeof(*p)) eller av en typ (sizeof(int)). Explicit omtypning ((int) x). * / % Multiplikation (x * 3), division (x / 3) om båda operanderna är heltal görs heltalsdivision, annars vanlig division, modulo (x % 2). + - Addition (x + 3), subtraktion (x - 3). << >> Bitvis vänsterskiftning och högerskiftning, se avsnitt 2.9. < <= > >= ==!= Jämförelseoperatorer. Observera att lika med heter == och inte lika med heter!=. & ˆ Bitvis och, bitvis exklusivt eller, bitvis eller (se avsnitt 2.9). && Logiskt och (x >= 1 && x <= 9). Logiskt eller (x < 1 x > 9). && och evaluerar liksom i Java först den vänstra operanden och sedan den högra bara om det är nödvändigt.?: Valoperator. x < y? x : y harvärdetx om villkoret x < yär sant och värdet y om villkoret är falskt. = += -= *= /= etc. Tilldelningsoperatorer. Värdet av uttrycket är det tilldelade värdet. x = 3 sätter x till 3 och returnerar3. x += 2 lägger till 2 till x och returnerar det nya värdet. x -= 2 subtraherar 2 från x och returnerar det nya värdet. x *= 2 multiplicerar x med2och returnerar det nya värdet., Kommaoperatorn används för att beräkna flera uttryck efter varandra (x++, y++, v[y] = 0). Hela uttryckets värde blir värdet av det sista uttrycket (0 i exemplet). Denna konstruktion används nästan enbart i for-slingor när man vill göra flera tilldelningar i antingen initieringen eller uppräkningen. for(i = 0, sum = 0; i < 10; i++) sum += v[i]; 2.9 Bitoperationer Ett av kännetecknen hos C är att man kommer nära datorn. Det är till exempel viktigt att veta hur datorns minneshantering går till och det är enkelt att göra operativsystemanrop och att behandla enskilda bitar i datorns minne. Det finns tre binära bitoperationer: & bitvis och, bitvis eller, ^ bitvis exklusivt eller, och en unär bitoperation: ~ bitvis negation. Bitoperationerna behandlar tal som bitföljder (dvs. talen skrivna i binär form) och opererar på varje bit för sig enligt följande scheman: & 0 1 0 1 ^ 0 1 ~ 0 0 0 0 0 1 0 0 1 0 1 1 0 1 1 1 1 1 1 0 1 0 Uttrycket 9 3har värdet11 eftersom9 är 1001binärt, 3 är 0011binärt, 11är 1011binärt och schemat ovan ger bit för bit att 1j0! 1, 0j0! 0, 0j1! 1, 1j1! 1 (övriga bitar till vänster är nollor). Operatorerna << och >> är så kallade bitskiftningsoperatorer. Dom flyttar bitarna i den första operanden så många steg till vänster respektive höger som den andra operanden visar. Uttrycket 5 << 2 har värdet 20 eftersom 5 är 101 binärt, och om man skiftar bitarna två steg till vänster får man talet 10100 binärt, vilket är 20 decimalt. Uttrycket 25 >> 2 har värdet 6 eftersom 25 är 11001 binärt, och om man skiftar bitarna två steg till höger får man talet 110 binärt, vilket är 6 decimalt. Bitskiftning till höger, dvs. >>, är ej standardiserad för negativa tal. 16
3 Pekare Alla variabler i ett program finns lagrade i datorns minne. Till varje plats i minnet finns en adress associerad med vilken man kan komma åt minnesplatsen. Således har varje variabel i ett program en adress. Dylika adresser brukar kallas för pekare och har i de flesta större program en stor betydelse. Pekare liknar referensvariabler i Java. Den största skillnaden är syntaxen för deklaration och användning. C ställer inte heller några krav på att en pekare pekar på något vettigt. I Java är en referensvariabel alltid en referens till ett objekt eller null. En pekare kan fås att peka vart som helst i datorns minne. En pekare till en variabel kan användas för att läsa och ändra värdet på variabeln. Situationer då detta kan vara relevant är till exempel om man vill att en funktion ska ändra värdet på ett av argumenten. I C (liksom i Java) så kopieras argumenten vid ett funktionsanrop till lokala variabler innan funktionen körs. För att en funktion ska kunna ändra värdet hos ett argument så måste en pekare skickas in. Pekarens värde kopieras till en lokal variabel, men denna refererar till samma minnesplats som den ursprungliga pekaren. Således kommer en ändring av det som pekaren pekar på att kvarstå även efter funktionen har exekverat färdigt. För att deklarera en pekarvariabel skrivs en asterisk, *, framför variabelnamnet och för att få adressen till en variabel föregås den av ett och-tecken, &. Det som en pekare pekar på kan erhållas med notationen *pekare. /* pointer är en pekare till ett heltal. increase adderar ett till det som pointer pekar på. */ void increase(int *pointer) { *pointer += 1; int value = 8; /* Skicka adressen till value som argument */ increase(&value); /* Nu har value värdet 9 */ Minnesskiss under anropet av increase Bilden visar hur det kan se ut i minnet när funktionen increase körs. Adressen till variabeln value skickas som argument till increase. Variabeln pointer får då värdet 4712. Uttrycket *pointer refererar till det som finns på minnesplats 4712. Effekten av anropet till increase blir att value ökas med ett. Syntaxen för deklaration av en pekarvariabel behöver kanske en närmare förklaring, då den kan tyckas underlig. För att deklarera en pekare p till en typ T så används yttrycket T*p. Om detta tolkas som att *p (det som p pekar på) har typen T så känns syntaxen ganska naturlig. 3.1 Vektorer som pekare En vektorvariabel kan användas som om den vore en pekare till vektorns första element (index 0). Omvänt gäller också att varje pekare kan betraktas som en vektor. En vektorvariabel kan i C inte ändras, utan är en konstant pekare som alltid pekar på första elementet i vektorn. int v[10]; /* Vektor med tio heltal */ int *p, a; /* p pekare till ett heltal */ p = v; /* p pekar på v[0] */ p = &v[0]; /* Ekvivalent med ovanstående rad */ 17
p[3] = a; /* Pekare kan indexeras som vektorer */ För pekare finns de aritmetiska operationerna + och. Om ett heltal n adderas till en pekare p så gäller att p + n, &(p[n]) (parentesen i uttrycket kan utelämnas på grund av att prioritetsordningen för & är lägre än för indexering). Motsvarande gäller för subtraktion. Två pekare kan också subtraheras. Denna operation beräknar avståndet mellan pekarna, i antal vektorelement räknat. Om två pekare som pekar på element i olika vektorer subtraheras har operationen inget relevant värde. int v[10]; int *p, *q; p = &v[3]; q = &v[7]; *p == p[0] /* Alltid sant */ *(p + 1) == p[1] /* Likaså */ q - p /* Resulterar i heltalet 4 */ *p++ = 3; /* Ekvivalent med *p = 3; p = p+1; */ 3.2 Funktioner som argument För att göra kod så generell och användbar som möjligt kan det vara bra att låta funktioner ta andra funktioner som argument. Ett exempel kan vara implementation av en ekvationslösare, det vill säga en funktion som löser ekvationen f (x) = 0 för ett givet f. Detta åstadkoms i C med funktionspekare. En sådan är helt enkelt en adress till en funktion, som med hjälp av funktionspekaren kan anropas. Nedan exemplifieras detta tillvägagångssätt i ekvationslösarfallet. /* solver(a, b, f) löser ekvationen f(x) = 0. Förutsättningar: f(a) < 0 < f(b) f är en pekare till en funktion som deklarerats double funktionsnamn(double) */ double solver( double a, double b, double (* f) (double) ) { double x, fx; fx = (*f) (x); /* Funktionen som f pekar på anropas */ Allmänt deklareras en funktionspekare enligt: returtyp (* variabelnamn) (typ argument, typ argument ); Om parenteserna i (* variabelnamn) utlämnas deklareras istället en funktion som returnerar ett värde av typen returtyp *. Tilldelning av funktionspekare framgår av nedanstående exempel. double sin(double x); /* Deklaration av sin() */ double s; double (* f) (double x); /* f är en funktionspekare */ f = &sin; /* f pekar på sin() */ s = solver(3.2, 3.1, f); /* Löser sin(x) = 0 */ 18
4 In- och utmatning Java är konstruerat för att det ska vara enkelt att göra grafiska användargränssnitt och att rita bilder. Så är det inte med C och inom ANSI C så finns det ingen standard för att göra grafik. För att göra det måste man använda sig av något grafikbibliotek. Det finns många olika varianter på sådana och vilka som är tillgängliga beror mycket på vilken plattform programmet ska köras på. Grafik faller därför utanför ramen för detta kompendium. De möjligheter för in- och utmatning som finns är att skriva och läsa text från terminalfönstret samt att skriva och läsa filer. In- och utmatningsfunktionerna finns deklarerade i #include-filen stdio.h. 4.1 Terminalfönstret De funktioner som oftast används för in- och utmatning till terminalfönstret är printf och fgets. printf skriver ut text till terminalfönstret och fgets läser en rad från tangentbordet. 4.1.1 Utmatning med printf printf är deklarerad som: int printf(const char *fmt, ); De tre punkterna betyder att printf tar ett variabelt antal argument och fmt är en formatsträng som talar om hur många de resterade argumenten är och hur de ska tolkas. Vid utskrift skrivs formatsträngen ut på skärmen med undantag för vissa koder. Dessa koder reglerar hur argumenten till printf ska tolkas och på vilket sätt de ska infogas i den utskrivna texten. Alla koder börjar med ett procenttecken. I tabell 3 finns en lista med de vanligaste koderna. Varje kod (utom %%) motsvarar ett extra argument till printf, i ordning från vänster till höger. Kod Beskrivning Argumenttyp %d Heltal int %x Heltal, hexadecimalt int %o Heltal, oktalt int %c Enskilt tecken int %s Sträng char * %f Flyttal double %e Exponentform double %% Skriver ut ett procenttecken %nx Fältet fylls med blanka n heltal; x kanvarad,f,s,c från vänster till n tecken. %n.mf n som ovan; flyttalet skrivs m heltal; n kan utelämnas ut med m decimaler Tabell 3: Formatsträngar för printf /* Skriver ut ett ett heltal samt pi med tre decimaler */ double pi = 3.14159265359; int number = 1269; printf("number: %d, pi=%.3f\n", number, pi); Funktionen printf returnerar antalet utskrivna tecken, vilket sällan används. 19
4.1.2 Inmatning med fgets och getchar char *fgets(char *str, int n, FILE *infile); är en funktion som läser in en rad från filen infile. str är en sträng där inlästa tecken sparas och n är storleken på den allokerade bufferten, dvs. det maximala antal tecken som får läsas in. Syftet med n är att se till så att fgets inte läser fler tecken än som får plats i str. Om ett radframmatningstecken lästs in så skrivs det in i str. För läsning från tangentbordet anges den i C fördefinierade filen stdin. fgets returnerar str om inläsningen gick bra. Om filen tog slut innan något tecken lästs in eller något fel uppstod returneras NULL. (Förmera information om NULL, se kapitel 6 om minneshantering.) char str[80], *s; s = fgets(str, n, stdin); if(s!= NULL) printf("du skrev texten: %s\n", str); För att konvertera data i en sträng så kan int sscanf(char *str, const char *fmt, ); vara användbar. sscanf är analog med printf men läser från str och skriver till de extra argumenten med hjälp av formatsträngen fmt. fmt använder nästan samma koder som printf. En skillnad är att koden för double är %lf. int n; double x, y; char s[80]; printf("mata in ett heltal och två flyttal separerade med blanka\n>"); fgets(stdin, s, 80); sscanf(s, "%d %lf %lf", &n, &x, &y); printf("svaret blir %.3f.\n", d * (x + y)); Observera att och-tecknena framför argumenten till sscanf måstevaramed.sscanf ska ändra i variablerna och behöver därför pekare. För att läsa in till en sträng (med %s) ska inget och-tecken stå framför strängvariabeln då den redan är en pekare. När en sträng läses in anses den avslutad om sscanf stöter på ett mellanslag eller radframmatning. Det finns även en funktion int scanf(const char *fmt, ); som fungerar som sscanf men läser direkt från tangentbordet. Vid inläsning av strängar måste man se till att den sträng som läses in får plats i den vektor som den läses till. Detta kan inte kontrolleras om scanf används. Detta är en av anledningarna till att den krångligare inmatningsmetoden rekommenderas. Med scanf kan man inte heller kontrollera vad som läses in och programmet kan få konstiga indata om användaren använder fel syntax vid inmatningen. Om man är intresserad av att läsa in ett tecken i taget från tangentboret så finns en speciell funktion getchar. Den är deklarerad: int getchar(void); Det heltal som returneras är det tecken som matades in eller EOF. EOF är ett speciellt tecken som ofta inte kan representeras med datatypen char. Det betyder att det inte finns några fler tecken att läsa in, till exempel om användaren skrivit ctrl-d i terminalfönstret. 20
4.2 Filer Ibland kan det vara praktiskt att spara data på filer. Filfunktionerna finns liksom de övriga in- och utmatningsfunktionerna deklarerade i stdio.h. För att öppna en fil används FILE * fopen(const char *name, const char *mode); name är namnet på filen och mode talar om på vilket sätt filen ska öppnas (se avsnitt 4.2.2). fopen returnerar en pekare av typen FILE *. Om pekaren är skild från NULL så lyckades öppnandet och pekaren kan användas för skrivning och läsning från filen. För att skriva och läsa text kan funktionerna och int fprintf(file *file, const char *fmt, ); int fscanf(file *file, const char *fmt, ); användas. Dessa fungerar som printf och scanf med den skillnaden att de tar ett extra argument som talar om vilken fil de ska operera på. Det finns också en filvariant av getchar deklarerad int getc(file *file); När en fil inte behövs längre ska den stängas med funktionen int fclose(file *file); file talar om vilken fil som ska stängas. fclose returnerar 0 om stängningen lyckades. /* Skriva en rad på en fil med namn "hejsan.txt" */ FILE *fp; fp = fopen("hejsan.txt", "w"); if(fp!= NULL) { fprintf(fp, "och Aristoteles sade: Jorden är rund!\n"); fclose(fp) else printf("det gick inte att öppna filen hejsan.txt.\n"); 4.2.1 Binärfiler Det finns två speciella funktioner för att läsa och skriva binära data på en fil. Dessa heter fread och fwrite. Det finns också funktioner för att hoppa till olika positioner i filer, nämligen fseek och ftell. Hur dessa funktioner anropas kan läsas i manualsidorna på Unixdatorer eller i någon C-bok. 4.2.2 Olika sätt att öppna filer Filer kan öppnas på många olika sätt; som textfil, som binärfil, som läsbar, som skrivbar och så vidare. Andra argumentet till fopen talar om hur filen ska öppnas. I tabell 4 förklaras de vanligaste koderna. Observera att ingen skillnad görs mellan textfiler och binärfiler under Unix. Till exempel betyder fp = fopen("file.dat", "r+b"); att filen file.dat ska öppnas för läsning och skrivning av binära data. 4.2.3 Fördefinierade filer I stdio.h deklareras tre fördefinierade filer, stdin, stdout och stderr. stdin är programmets standardinmatningsfil (oftast tangentbordet) och stdout standardutmatningsfilen (oftast terminalfönstret). stderr är till för utmatning av felmeddelanden. Det som skrivs till denna fil brukar synas i terminalfönstret. Att göra fprintf till filen stdout är ekvivalent med motsvarande anrop av printf. 21
Kod Beskrivning w Öppna för skrivning r Öppna för läsning a Anger att skrivning alltid sker sist i filen r+ Öppna för läsning och skrivning xb Öppna som binärfil, x öppningskod 5 Modularisering Tabell 4: Koder för fopen Ett program på bara några tusen rader tenderar att börja bli svåröversiktligt och problematiskt att administrera. Liksom Java så har C stöd för modularisering. I Java delas programmet upp i de olika klasserna och kopplas sedan ihop med import. I C används så kallade headerfiler med suffixet ".h". En headerfil innehåller deklarationer av funktioner, konstanter etc. Däremot ska ingen direkt programkod ligga där. Programmets olika delar kopplas sedan samman vid länkningen. I headerfilen till en modul bör globala variabler med betydelse för flera moduler stå. Samma sak gäller för funktioner och makron (#define-satser) som ska kunna anropas av flera moduler. På det sättet behöver man endast hålla reda på definitioner och deklarationer i en fil. Informationen som ligger i headerfilen kommer att läsas in som om den stod där #include står, dvs. högst upp i programfilen. Alla variabler och funktioner i den inkluderade modulen kommer att vara direkt åtkomliga, utan punktnotation eller något liknande. Ett gott råd är därför att i möjligaste mån ge modulspecifika namn. En annan viktig sak att tänka på med variabler är att ge dem rätt räckvidd. En lokal variabel, deklarerad i en funktion, går inte att komma åt från en annan modul. Däremot är standardvalet att en global variabel eller funktion är åtkomlig från alla moduler, dvs. extern. Man bör då det finns möjlighet deklarera globala variabler och funktioner som static så att de är modulspecifika. Se även kapitel 2.3. Till skillnad från Java så kan det endast finnas en main-funktion i programmet. Alltså kan endast en modul ha en ingångspunkt. I standardbiblioteket stdlib.h finns en funktion exit som kan användas för att avsluta programmet, vanligast då ett fel inträffat. Se även kapitel 5.2. Programmet avslutas även då return nås i funktionen main. Ett mer konkret exempel på hur ett program kan delas upp i moduler finns under www.nada.kth.se/datorer/haften/java2c eller på Nadas datorer i οwww/datorer/haften/java2c 5.1 Preprocessorn Till ANSI-standarden hör en grupp så kallade preprocessordirektiv. Dessa är inte en direkt del av språket utan är instruktioner till kompilatorn och kan användas för att underlätta programmeringen, framför allt för modularisering. Preprocessning är som namnet anger något som sker före den direkta tolkningen av programkoden. Alla preprocessordirektiv ska inledas med # först på raden och endast ett direktiv per rad. Preprocessorn i ANSI-C har följande 12 instruktioner: #if #endif #elif #else #define #undef #ifdef #ifndef #include #line #error #pragma Tabell 5: De 12 preprocessordirektiven Till preprocessorn hör även fem stycken fördefinierade makronamn. LINE, ett heltal som anger aktuell rad i programmet, FILE, en sträng som innehåller det aktuella filnamnet, DATE, en sträng med aktuellt datum på formatet månad/dag/år, TIME, en sträng innehållandes aktuell tid på 22
formatet timme:minut:sekund, samt STDC som är ett heltal satt till 1 om kompilatorn stödjer standarden ANSI-C. Det finns en viss osäkerhet i detta och man bör inte lita på värdet helt. int main(void) { printf("kompilerad den "); printf( DATE ); printf("programmet har nu nått rad %d", LINE ); return 0; 5.1.1 #define Define används för att skapa makronamn för strängar, värden eller uttryck, mycket likt const. Skillnaden är att define inte genererar någon programkod utan endast ersätter makronamnet innan koden kompileras. #define ONE 1 #define TWO (ONE+ONE) #define THREE ONE+TWO /* Det här kan skapa problem */ int fyra = 2*THREE; /* eftersom 2*1+2 = 4 */ #define MAX_SIZE 1000 buffer = malloc (MAX_SIZE); #define ERROR "Something has gone wrong!" #define LONG_ERROR "Something has gone \ very wrong!" printf(error); /* Ett makro för absolutbelopp */ #define ABS(a) (a)>0? (a) : -(a) 5.1.2 #undef Undef, undefine, används för att ta bort ett makronamn definierat med define. #undef ERROR 5.1.3 #pragma Pragma-direktiv kan ställas till kompilatorn för att den exempelvis ska generera avlusningskod eller specificera formatet på den exekverbara koden. ANSI-standarden säger inte vilka pragma direktiv som ska finnas eller hur de ska se ut; du bör därför läsa i kompilators manual om du är intresserad av det. 5.1.4 #include Include stoppar in innehållet i en annan fil på denna plats i filen. Det används nästan enbart när man vill inkludera en moduls gränssnitt. En närmare beskrivning på detta finns i kapitel 5. Inkluderingen av filer kan ske på två sätt: #include <stdio.h> #include "exempel.h" 23
Det första sättet söker i fördefinierade systemkataloger och det andra söker först filen i den aktuella katalogen och därefter i de fördefinierade katalogerna. Det finns inte någon strikt standard på hur detta ska ske och variationer mellan kompilatorer kan förekomma. 5.1.5 #error Error avbryter kompileringen så fort det påträffas samt skriver ut ett felmeddelande. Detta används framförallt i avlusning av komplexa moduluppbyggnader. 5.1.6 #if, #endif, #ifdef, #ifndef, #else, #elif För att man ska kunna bygga upp flera versioner av en modul i samma fil, till exempel då flera plattformer ska stödjas av ett program, men varje plattform ska ha vissa specifika programdelar inlagda, så finns dessa preprocessordirektiv. Dessa är kanske mest använda då en headerfil i sig ska inkludera andra headerfiler, men man vill undvika risk för upprepad eller rekursiv inkludering. #ifndef MY_HEADER_FILE #define MY_HEADER_FILE #include <my_header_file.h> #endif Nedan ges ett lite mer avancerat exempel med de flesta preprocessor-instruktionerna involverade. #define SYSTEM 3 #if SYSTEM > 2 #define UNIX #endif #ifdef UNIX #define GUI #include <unix.h> #elif defined VMS #include <vms.h> #else #error Plattform ej specificerad! #endif int main(void) { #ifdef GUI gui_startup (); #endif #ifndef GUI console_startup (); #endif return 0; 5.2 Standardbiblioteket Till ANSI-C följer 15 fördefinierade headerfiler med standardfunktioner. De flesta av dessa är oftast inte direkt intressanta för programmeraren men kan spela en bakomliggande roll. Till de viktigaste ges en utförligare förklaring. 24
assert.h Används för felsökning och diagnostik. ctype.h Funktioner för teckenbehandling, datatypen char. errno.h Felkoder från vissa biblioteksfunktioner. float.h Gränserna för plattformens datatyp float. limits.h Övriga gränser för plattformens datatyper. locale.h Funktioner för språklokalisering. math.h De vanligaste matematiska funktionerna. setjmp.h Hopp ut ur funktioner. signal.h Signaler. stdarg.h Hantering av funktioner med variabelt antal argument. stddef.h Standarddefinitioner. stdio.h In- och utmatning på skärm, tangentbord och fil. stdlib.h En serie användbara funktioner såsom minneshantering. string.h Strängfunktioner. time.h Tid- och datumfunktioner. ctype.h innehåller funktioner för teckenbehandling, såsom att detektera och konvertera mellan stora och små bokstäver, kontrollera om bokstaven är ett mellanslag, en siffra osv. math.h har de flesta av de elementära matematiska funktionerna. Tänk på att dessa funktioner ofta behöver en speciell länkning, på Unix krävs t.ex. att flaggan -lm ges vid kompilering. Ett utval av de vanligaste funktionerna följer nedan: sqrt(x); p x fabs(x); jxj cos(x); cos x sin(x); sin x acos(x); arccos x asin(x); arcsin x tan(x); tan x tanh(x); tanh x pow(x,y); x y exp(x); e x log(x); ln x log10(x); lg x Alla funktionerna ovan arbetar med datatypen double. Det finns specifika funktioner för datatypen long double. I regel har de samma namn men avslutas med l. Exempelvis sinl(x); för sinus. stdio.h med en serie funktioner för in- och utmatning av data, t ex från tangentbordet, skärmen och filer. Se kapitel 4. stdlib.h innehåller en mängd viktiga funktioner. Här återfinns bland annat funktioner för minneshantering, diskuteras närmare i kapitel 6. Övrigt viktiga funktioner här är: int abs(int); /* Absolutbelopp för heltal */ int atoi(const char *); /* Konverterar siffersträng till heltal */ double atof(const char *); /* Konverterar siffersträng till flyttal */ void exit(int rc); /* Avslutar programmet med rc som returkod */ int rand(void); /* Genererar ett pseudo-slumptal */ En samling varianter av dessa funktioner finns också, för noggrannare styrning samt för andra variabeltyper. Exempelvis kräver long, long double och float specifika funktioner. string.h har funktioner för stränghantering. 25