Programmeringsspråket C

Typer

Typer

Detta kapitel kommer att ha många underrubriker, och vara nästan strikt teoretiskt till en början, eftersom du redan kan koden till allt vi går igenom; det vi gör här är att förklara ytterligare hur det fungerar. Detta är för att nu har vi gått igenom det mest grundläggande i C, och det börjar bli dags att gå in lite på djupet kring hur det faktiskt fungerar i datorn. Fenomenet typer är en typisk grej som existerar just för att datorer fungerar som de gör.

Heltal

Än så länge har vi bara jobbat med en sorts lagring: heltal. Denna har reflekterat sig i typerna int och char, för heltal och tecken, respektive. Anledningen att vi började med int var att den är väldigt grundläggande, och i princip den enklaste typen i C. Sen har vi gått igenom char också, som även den är ett heltal, fast till för att lagra bokstäver. Som vi visade i tabellen finns det många fler typer. De flesta av dessa initieras genom att lägga på extra alternativ till "huvudtypen". Exempelvis om man vill använda en unsigned short int initialiseras den som följer.

unsigned short int heltal;    //ger variabel heltal som är en int, fast unsigned och short

short, long och long long

Teoretiska skillnaden mellan short, vanlig, long och long long int är att de rymmer olika mycket. Det jag kommer förklara här är endast delvis nödvändigt att kunna för att veta hur variabler beter sig, så försök läsa det, men om du inte förstår är det ingen panik. För den här underrubriken, glöm alla "unsigned"-alternativ, de förklaras under nästa rubrik.

Datorn lagrar tal i binär form, alltså som en lång serie "ettor och nollor". Den minsta beståndsdelen i datorminnet är en bit, den kan anta ett läge som är antingen av eller på, i folkmun "etta eller nolla". En byte är nästa beståndsdel, en byte består av åtta bitar på följd. Med en byte, åtta bitar, kan man skilja på 256 olika lägen. Från 00000000 till 11111111. Som du kan utläsa av tabellen används endast en byte för att lagra en variabel av char-typ. Detta innebär att en char kan lagra ett tal från -128 till 127. Detta räcker gott och väl för alla våra tecken, som ju har siffermotsvarigheter. (För övrigt använder vi i denna tutorial bara 0 till 127, eftersom bokstäverna som finns i -128 till -1 är olika på olika datorer. Ja, det är anledningen att vi inte gör några utskrifter med åäö.)

För att gå vidare med den andra typen vi redan är bekant med, ta en titt på int. Den lagras alltså i fyra byte. Det ger väldigt många möjliga tal, och de som kan lagras i en vanlig int är -2 147 483 648 till 2 147 483 647. Det är inte alltid man behöver så många tal, därför kom man på short int. I beskrivningen till den i tabellen står det att den "rymmer färre tal än vanlig int", vilket är sant. En short int rymmer tal från -32 768 till 32 767. Nästa variant av int är då alltså long int, som "rymmer lika mycket som vanlig int, lustigt nog" enligt tabellen. En long int var tänkt att rymma fler tal än en vanlig int, men av någon anledning är det inte så på de flesta moderna system, se varningen.

Varning: Hur mycket varje sorts variabel rymmer beror helt på din kompilator. I vissa implementationer är en short int en byte, int två bytes, och long int fyra bytes. Tills vidare förutsätter vi att ditt system följer vårat.

Det finns även en fjärde extremtyp, long long int. Denna lagras i åtta bytes, och kan därför rymma totalt 18 446 744 073 709 551 616 tal (hälften är negativa). Det är sällan man behöver så höga tal, och i de fall man gör det behöver man oftast ännu högre också, så long long blir meningslös igen.

Signedness

Hittills har detta varit en genomgång av alla heltalstyper, men som du ser på tabellen finns det varianter på alla dessa, nämligen unsigned. Det engelska ordet "sign" betyder tecken, och det är även vad man säger i matematiken om ett tal är positivt eller negativt. "Signedness" betyder därför i detta fall "huruvida talet har ett tecken eller inte". Om talet har ett tecken kan det vara negativt. unsigned, som är varianten vi inte har utforskat än, betyder "har inget tecken". Detta borde betyda att de typerna vi har utforskat har ett tecken, eller hur? Och därmed kan vara negativa? Ja, att de kan vara negativa visste vi redan sen förra underrubriken, och eftersom de kan vara negativa har de alltså ett tecken.

Så vad är då fördelen med unsigned, att inte kunna ha negativa tal? Jo, det innebär att man kan ha många fler positiva tal. En byte har som bekant möjlighet till 256 olika tal. Eftersom våra variabler tidigare har varit signed, så har hälften av dessa 256 tal gått åt till negativa tal. Betänk då hur det är med en int, som rymmer 4 294 967 296 olika tal. Lite synd att hälften av dessa går till negativa tal om vi planerar att bara använda positiva tal.

Heltal, forts.

Nu har vi snabbspolat teorin bakom alla heltalstyper och deras varianter, och du bör ha grundläggande koll på dessa. Du måste självklart inte förstå exakt hur allt fungerar och hur man vet hur många tal en typ rymmer, men det är viktigt att förstå skillnaderna. short rymmer färre tal, long borde rymma fler tal men gör inte det, unsigned gör att variabeln rymmer dubbelt så många positiva tal, på bekonstnad av att man inte kan använda negativa alls. Nu går vi in på nästa sorts tal.

Decimaltal

Äntligen decimaltal! Slippa göra heltal av allt överallt! Nåja, det fanns en anledning till att vi har väntat med decimaltal. Decimaltal tillsammans med heltal kan förvirra en hel del. kqr använder personligen nästan aldrig decimaltalsvariabler när han programmerar. (För den intresserade, han lagrar hellre värdena som heltal och gör dem temporärt till decimaltal när de behövs, se typecasting längre fram i detta kapitel.)

Det finns som synes i tabellen endast tre typer av decimaltal: float, double och long double. float lagrar decimaltal med en viss precision (antal decimaler). double har dubbelt så hög precision, och long double har en tredjedel bättre precision än double. Tyvärr har vi inga exakta siffror på precisionen, eftersom vi inte vet hur de lagras mer än att det är "på något sätt i exponentform". Dock spelar det ingen roll, det finns en hel del att lära om decimaltal ändå. Studera följande exempel.

double rotenurtva;
rotenurtva = 1.41421356;
 
printf("Roten ur tva ar ungefar %f\n", rotenurtva);

Vad är nytt här då? Initialiseringen av ett decimaltal är ny, vi använder oss av typen double. Sedan får variabeln ett värde. Här vill jag att du observerar att i C används decimalpunkt, inget kommatecken.

Varning: Alltid när decimaltal anges i anknytning till C används en punkt för att skilja heltalsdelen från decimaldelen. I matematiken i Sverige används ett kommatecken, denna ovana får alltså inte halka över till C-programmerandet.

Sedan används ett nytt format till printf(). %f skriver alltså ut ett decimaltal med defaultprecision. Om du kör denna kod kommer du märka att den skriver ut talet 1.414214. Eftersom vi inte har angett vilken precision den ska skriva ut med kommer den välja defaultvärdet, 6 decimaler. Notera även hur den avrundar talet som skrivs ut.

Defaultprecisionen är alltså 6 decimaler. Vad händer om vi bara behöver två decimaler? Demonstreras i följande exempel.

printf("Roten ur tva ar ungefar: %.2f\n", rotenurtva);

Precisionen man vill skriva ut ett decimaltal med anges mellan procenttecknet och f, på formen .n, punkten betyder att man vill ange antal decimaltal som ska visas, och n är antalet.

Inläsning av decimaltal

Inläsning av decimaltal sker med scanf(), som du kanske förutsatt vid det här laget. Dock inte med %f, som man skulle kunna tro. När det gäller scanf() så läser %f bara in till variabler av typen float. Vill du läsa in till en double måste du använda %lf (som intressant nog står för long float). Enligt följande exempel.

double pi;
 
scanf("%lf", &pi);    //pi är förresten ungefär 3.141592653589793238462643383
printf("Pi ar ungefar: %.10f\n", pi);

Typfel

I förbifarten nämnde vi i fjärde varningsrutan i del 6 vad som kallas heltalsdivision. Detta är alltså en division mellan två heltal, som inte alltid ger önskade resultat. När vi nämnde det tog vi upp exemplet 19 genom 10. Alla vet att det blir 1.9, som avrundat till närmsta heltal blir 2. Detta med vanlig division. 19 genom 10, heltalsdivision, ger oss svaret 1, rest 9. Eftersom vi är ute efter svaret och inte resten innebär det alltså att 19 / 10 = 1. Detta kan ge fel svar på många ställen, och du har förmodligen redan stött på det.

Hur undviker man då denna heltalsdivision? Jo, genom att inte dividera med två heltal. Om man vill ha 19 genom 10, så måste man omformulera det till 19 genom 10.0. Eller 19.0 genom 10. När C ska genomföra en division kollar den vad alla involverade tal har för typ. Fristående tal utan decimalpunkt antar den är heltal, och har talet en decimalpunkt förutsätter den att det är ett decimaltal. Om båda talen i divisionen är heltal utför den en heltalsdivision. Om ett eller flera av talen är decimaltal gör den en vanlig division, och svaret blir ett decimaltal. Om du sedan lagrar detta tal i en heltalsvariabel går du miste om alla decimaler, gör därför inte detta.

Och det leder in oss på nästa variant av typfel, när man försöker lagra ett tal i en variabel som inte klarar det. Följande exempel innehåller två typfel på rad.

double a;
int b;
char c;
 
a = 396.9763;
b = a;
c = b;

Först ställer vi in en decimaltalsvariabel till 396.9763, detta är helt okej. Direkt påföljande det ger vi en heltalsvariabel värdet av vårat decimaltal. Eftersom heltalsvariabeln inte kan rymma decimalerna kommer de trunkeras (helt enkelt huggas bort), utan någon avrundning. Variabeln b har nu värdet 396. Efter det försöker vi ge en char-variabel värdet av våran int-variabel. Som du minns från tidigare i denna del så kan en vanlig char bara rymma tal upp till 127, och därför kommer även denna få fel värde (nämligen -116).

Typecasting

Har du en kompilator som ger dig en massa varningar när du kompilerar förra koden? Bra. Det gör inte min av någon anledning. För att slippa dessa varningar, och göra detta på det sättet som är tänkt, måste du genomföra typecasting. Det innebär att en typ kvickt konverteras till en annan när programmet körs. Förra programmets tilldelningar skulle se ut så här, med korrekt casting:

a = 396.9763;
b = (int) a;
c = (char) b;

En typ inom parentes innebär att datorn ska casta (konvertera) talet till höger till den typen som står inom parentes. Detta går att utnyttja för en simpel avrundningsfunktion. Studera funktionen nedan först, som är en förenkling av våran egentliga avrundningsfunktion.

double avrunda(double decimaltal) {
    int trunkerad;
    double avrundad;
 
    trunkerad = (int) (decimaltal * 10 + 0.5);
    avrundad = trunkerad / 10.0;
 
    return avrundad;
}

Denna funktion tar ett decimaltal av typ double som argument, och returnerar ett decimaltal av typ double. Det returnerade talet är argumentet avrundat till en decimal. Hur fungerar då detta virrvarr av matematik? Det är enklast att avläsa genom att följa vad som händer. Vi antar att decimaltal har värdet 396.8763. Det första som händer är att vi får ut decimaltal * 10, det är 3968.763. Sen lägger vi på 0.5 för att avrunda korrekt, då har vi 3969.263. Sedan castas detta till en int och det värdet (3969) lagras i trunkerad. Efter det får avrundad värdet av trunkerad / 10.0 (notera att vi undviker heltalsdivision), alltså 396.9. avrundad returneras, för det är talet avrundat till en decimal. Nu när du förstår förenklingen kommer här en mycket kompaktare version av samma funktion.

double avrunda(double decimaltal) {
    return (double) (int) (decimaltal * 10 + 0.5) / 10;
}

Här typecastas det vilt. Denna funktion förlitar sig tungt på prioritetsreglerna som finns i C. Först räknar vi ut parentesen, och då får vi fram värdet av decimaltal * 10 + 0.5, som vi vet sen tidigare vad det innebär (till exempel 3969.263). Sedan castar vi detta till en int (och får då till exempel 3969). Direkt på detta castar vi denna int till en double igen! (Och den har nu värdet 3969.0.) Eftersom det ena talet är en double utför datorn en vanlig division, och svaret blir även det en double. Alltså returneras en double. Komplicerat? Ja. Här är händelseförloppet steg för steg, vi förutsätter att decimaltal har värdet 396.8763.

return (double) (int) (decimaltal * 10 + 0.5) / 10;
return (double) (int) (396.8763 * 10 + 0.5) / 10;    //variabeln ersatt med ett värde
return (double) (int) (3968.763 + 0.5) / 10;
return (double) (int) 3969.263 / 10;    //parenteserna försvunna, de behövs inte kring ett ensamt tal
return (double) 3969 / 10;    //casting har mycket hög prioritet, och utförs innan divisionen!
return 3969.0 / 10;    //castingen först, återigen
return 396.9;    //nu är divisionen avklarad, nu kan talet returneras

Läser du igenom den steg-för-steg-analysen bör du se hur det går till.

Typer, forts.

Nu har vi alltså gått igenom alla typer i C, med undantaget void, för den uppmärksamme. Detta är för att void-typen inte har någon funktion i variabelsammanhang. Den har användning endast i fallen funktioner och pekare.

Detta kapitel har varit mycket läsning, men i det här fallet är det en stor fördel att förstå bakgrunden till varför variabler beter sig som de gör, så läsningen är värd mödan, så att säga. Det kanske däremot inte är så jättemycket att öva på gällande typer. Se till att du kan lite löst vilka de är och hur de används så blir det bra. Typecasting är dock viktigt att hålla koll på. Experimentera gärna för att ta reda på exakt hur hög prioritet casting har, jag vet inte själv. Ett sånt experiment skulle också vara bra övning i casting.

← Switch-sats

Pekare →

Copyleft kqr & slaeshjag 2009, 2012 some rights reserved