Programmeringsspråket C

Pekare, forts.

Pekare och arrayer

Pekare och arrayer har många likheter, eftersom arrayer i grund och botten är pekare. En array har vi redan gått igenom kan lagra flera värden med en initialisation. En array har vi även nämnt lagras i minnet som en enda lång följd. Det sista är det som intresserar oss för tillfället, eftersom det betyder att om man har minnesadressen till första värdet, så kan man lätt komma åt de andra värdena. De ligger ju efter varandra!

Vi har tidigare använt arrays på formen enarray[n], där n har varit numret på plats i arrayen. Det du inte visste var att enarray i det här fallet är en minnesadress, en pekare. Inom hakparenteser anger vi offset; hur långt ifrån den pekaren vi vill hämta värde. Det kanske blev lite klurigt att förstå, så jag drar en parallell. Tänk dig att du ska vägleda en vän till ditt hus. Vännen vet hur man hittar till början av din gata, men inte vilket hus som är ditt. Då kan du säga, "gå till början av gatan, och sen är det sjuttonde huset du ser." Nummer sjutton är i detta fall en offset.

Samma sak gäller arrayer. Först säger vi åt datorn, "gå till den här minnesadressen", och inom hakparenteser anger vi, "jag vill åt värdet som är n platser bort". Om du tänker efter är detta rätt logiskt: vi minns att första platsen var plats 0, detta beror på att vi vill åt värdet som är på minnesadressen. Nästa värde är en plats bort från minnesadressen, och så vidare.

Kan man använda pekare och arrayer på samma sätt? I princip, de två följande raderna utför exakt samma sak.

a = enarray[4];
a = *(enarray + 4);

Först använder vi en array som vi är van, med hakparenteser. Den andra raden är intressant. Först kommer vi åt minnesadressen som enarray pekar till (kom ihåg att arrayer är pekare), sedan adderar vi denna adress med fyra, och får på så sätt åt en adress fyra platser "fram" i minnet. Sedan utför vi en dereferencing för att komma åt värdet som finns på denna plats. Själva fenomenet att ta en pekare och lägga på/ta bort siffror för att komma åt en annan adress kallas pekararitmetik.

Som du minns vill scanf() ha en minnesadress när man ska läsa in saker till variabler. Som du även borde minnas behövde man inte göra en referencing (ampersand innan variabelnamnet) när man skulle läsa in till en sträng med gets(). Detta har naturligtvis sin förklaring i att en sträng är en array, som i sin tur är en pekare. Alltså har vi redan en minnesadress. Ta en titt på strängutskrivningsfunktionen i del 9, här är den omskriven, och är denna gång utan hakparenteser.

void skriv_ut_strang(char *str) {
    int i;
 
    i = 0;
    while (*(str + i) != '\0') {
        putchar(*(str + i));
        i++;
    }
    putchar('\n');
}

Den anropas på precis samma sätt som förra. Och nu kommer något klurigt. Som du märker i funktionen så är allt vi gör att komma åt nästa position i minnet hela tiden, från starten som pekas till av str. I detta fall kan vi faktiskt skippa i-variabeln helt som offset, och bara öka pekaren hela tiden, så den själv pekar på en ny adress direkt. Det ser ut som följer.

void skriv_ut_strang(char *str) {
    while (*str != '\0')
        putchar(*str++);
    putchar('\n');
}

Vad händer här? Så länge värdet som str pekar på inte är null-tecknet (alltså slutet på strängen), så skriver den ut värdet str pekar på, och ökar str med 1. Det innebär att för varje varv kommer str att peka på nästa plats i minnet.

Just denna operation, att peka om strängar, innebär att när du är klar kan du i princip inte längre komma åt första bokstaven i strängen längre. Den finns kvar på samma plats i minnet, men såvida du inte sparade var, har du inte en aning om var. slaeshjag avskyr därför att göra så. I övrigt är det också större risk att man tänker fel när man pekar om pekare, och på så sätt ökar även risken för segfaults. Generellt avråder vi från denna form av behandling om du inte verkligen vet vad du gör.

malloc()

Du har en pekare, och du vill kunna lagra grejer i minnet. Tidigare har du initierat en variabel för detta, och sedan pekat pekaren på variabeln. Hur ska man gå tillväga om man inte vill initiera en variabel först? Går det att bara peka pekaren på vilken minnesadress som helst och börja skriva? Självklart inte, det måste vara den fräckaste typen av segfault. Det minnet tillhör eventuellt andra program, där får man inte börja rota.

Det finns en funktion som har till uppgift att leta upp ett stycke oanvänt minne, så stort som man vill ha, och sedan returnera adressen till det, så man kan skriva till det och läsa från det. Denna funktion finns i standardbiblioteket stdlib.h, som inkluderas med raden #include <stdlib.h>. Funktionen heller malloc() och tar som argument hur många bytes man vill ha. Hur många bytes vill man ha? Som vi nämner i tabellen är en char 1 byte stor. Därför blir den bra som exempel.

char *enpekare;
enpekare = malloc(1);
 
*enpekare = 65;

Först initierar vi en pekare som vi är vana, denna är för att peka på char-värden. Sedan ändrar vi dess värde, med andra ord adressen den pekar till, till en adress vi får från malloc(). Vad gör då malloc()? Den har den lilla, men ändå så stora, uppgiften att ta argumentet, leta upp ett utrymme i minnet där det finns ledigt så många bytes på rad som man anger i argumentet, och sedan returnera adressen till första byten i detta område. Notera att vi inte vet något om det området vi får. Det har förmodligen tidigare tillhört något annat program som har lagrat sitt skräp där, och malloc() gör ingen ansats att rensa bort det gamla skräpet, så det finns fortfarande kvar i form av konstiga värden som inte säger oss nånting.

free()

Nu har vi allokerat minne för oss själva, minne som inga andra program får rota i, precis som vi inte får rota i deras. När vårat program stängs kommer detta minne frias upp för andra program igen, men inte förrän vårat program stängs. Om man allokerar större mängder minne temporärt, kan det vara bra att manuellt frigöra det när man är klar, innan programmet stängs, så att andra program får ta del av minnet på datorn. Detta görs med funktionen free(). Den tar också bara ett argument: en pekare till ett minnessegment som är allokerat med malloc(). Följande exempel allokerar lite minne och frigör det direkt efteråt.

char *enpekare;
enpekare = malloc(1);
 
free(enpekare)

Att frigöra minne är mycket viktigt, framförallt om: 1. ens program är tänkt att köras under en längre tid (så det tar tid innan det frigörs automatiskt), 2. om man allokerar stora mängder minne. När ett program allokerar minne men inte frigör det, kallas det för att en memory leak har uppstått. Följande program bör du helst inte köra okontrollerat.

#include <stdio.h>
#include <stdlib.h>
 
int main() {
    void *pekare;
 
    while (1)
        pekare = malloc(1024);
 
    return 0;
}

Detta program initialiserar en pekare, går in i en oändlig loop, och pekar gång på gång om pekaren till ett nyallokerat minnessegment på 1kB. Detta program kommer i slutändan ha allokerat näst intill allt ledigt minne i datorn. Detta borde räcka som bevis på att det är viktigt att frigöra minne man allokerar.

Ny typ! void-typen! Typen void används i pekarsammanhang när man endast hanterar minnesadresser, och aldrig värdena bakom. malloc() returnerar till exempel en void-pekare, eftersom malloc() aldrig bryr sig om vad som finns på adressen den returnerar. I detta program behöver vi aldrig heller lagra någon information någonstans, därför går void-pekare utmärkt. Mer om pekartyper snart.

sizeof()

När vi allokerar minne för en char-pekare gör vi det en byte stort. Detta borde betyda att när vi allokerar minne för en int-pekare gör vi det 4 byte stort? Ja, i princip, på vissa datorer. Som vi nämnde i första varningen i del 11 så är variabler olika stora i olika system. I de flesta moderna system gäller de siffror som vi angett i tabellen, dock långt ifrån alla följer detta. Om du vill veta hur stor en int är i just det systemet du kompilerar för, används sizeof()-funktionen (det är egentligen ingen funktion, men för enkelhetens skull säger vi så). Den tar en typ som argument och returnerar hur stor den typen är, i antal bytes. För att allokera minne för en int-pekare, gör man med fördel på följande vis.

int *pekare;
pekare = malloc(sizeof(int));

Hur många bytes allokerar vi här? Precis, så många som sizeof(int) returnerar, med andra ord så många som en int är. Samma sak gäller för alla andra typer när vi allokerar minne. Använd sizeof(), för Guds skull, så blir det lättare att kompilera programmet på andra system än ditt eget sen.

Pekartyper

Vad sjutton gör det för skillnad vilken typ pekaren har, den pekar ju ändå bara på en minnesadress i taget? Korrekt observation, pekarens typ spelar bara roll när man läser eller skriver information, eller ändrar pekarens värde.

Tänk dig för exemplets skull att du har ett litet ram-minne på 32 bytes. Det har följande värden innan ditt program startar. (Notera att det är fullt med skräpvärden precis som på riktigt.)

.. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. (allokerade)
00 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f 10 11 12 13 14 15 16 17 18 19 1a 1b 1c 1d 1e 1f (adress)
a7 f1 d9 2a 82 c8 d8 fe 43 4d 98 55 8c e2 b3 47 17 11 98 54 2f 11 2d 05 58 f5 6b d6 88 07 99 92 (värde)

Nu kör du följande kod på datorn med det ram-minnet.

intpekare = malloc(4);
charpekare = malloc(4);

Vi förutsätter att intpekare fick adressen 0x02, och charpekare fick adressen 0x07. Nu har följande hänt med minnet (skillnader fetmarkerade):

.. .. ja ja ja ja .. .. ja ja ja ja .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. (allokerade)
00 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f 10 11 12 13 14 15 16 17 18 19 1a 1b 1c 1d 1e 1f (adress)
a7 f1 d9 2a 82 c8 d8 fe 43 4d 98 55 8c e2 b3 47 17 11 98 54 2f 11 2d 05 58 f5 6b d6 88 07 99 92 (värde)

Båda har alltså allokerat 4 byte. Nu kör vi följande kod.

*intpekare = 42;    //42 är 0x2a hexadecimalt
*charpekare = 42;

Det kommer ha följande effekt på minnet:

.. .. ja ja ja ja .. .. ja ja ja ja .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. (allokerade)
00 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f 10 11 12 13 14 15 16 17 18 19 1a 1b 1c 1d 1e 1f (adress)
a7 f1 2a 00 00 00 d8 fe a2 4d 98 55 8c e2 b3 47 17 11 98 54 2f 11 2d 05 58 f5 6b d6 88 07 99 92 (värde)

Här ser man hur int-pekaren ändrade på fyra värden, eftersom den är fyra bytes stor. char-pekaren ändrade bara på ett värde, eftersom den är en byte stor. Nu vill vi ändra på det efterföljande char-värdet också.

*(charpekare + 1) = 42;

.. .. ja ja ja ja .. .. ja ja ja ja .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. (allokerade)
00 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f 10 11 12 13 14 15 16 17 18 19 1a 1b 1c 1d 1e 1f (adress)
a7 f1 2a 00 00 00 d8 fe a2 a2 98 55 8c e2 b3 47 17 11 98 54 2f 11 2d 05 58 f5 6b d6 88 07 99 92 (värde)

Känner du igen den där formuleringen? Just precis, det hade gått att uttrycka på hakparentesform också.

charpekare[2] = 42;

.. .. ja ja ja ja .. .. ja ja ja ja .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. (allokerade)
00 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f 10 11 12 13 14 15 16 17 18 19 1a 1b 1c 1d 1e 1f (adress)
a7 f1 2a 00 00 00 d8 fe a2 a2 a2 55 8c e2 b3 47 17 11 98 54 2f 11 2d 05 58 f5 6b d6 88 07 99 92 (värde)

Det går även att peka om pekaren.

charpekare += 3;    //ökar pekarens värde, med andra ord ändrar adressen den pekar på
*charpekare = 42;

.. .. ja ja ja ja .. .. ja ja ja ja .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. (allokerade)
00 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f 10 11 12 13 14 15 16 17 18 19 1a 1b 1c 1d 1e 1f (adress)
a7 f1 2a 00 00 00 d8 fe a2 a2 a2 a2 8c e2 b3 47 17 11 98 54 2f 11 2d 05 58 f5 6b d6 88 07 99 92 (värde)

Tänk på att nu pekar charpekare på 0x0b, och inte längre på 0x07. Om vi inte håller koll på var allokerade segmentet börjar, går det förlorat för alltid.

Nu har vi lite snabbt gått igenom skillnader i pekartyperna när det gäller läsning och skrivning, samt utfört pekararitmetik på en char-pekare. int-pekare beter sig inte likadant. Tänk dig följande allokering.

free(intpekare);    //frigöra gamla pekaren så det utrymmet inte fortsätter vara allokerat
 
intpekare = malloc(sizeof(int) * 2); //allokera ett utrymme som är stort som _två_ int, peka intpekare till första byten

.. .. .. .. .. .. .. .. ja ja ja ja .. .. .. .. .. .. .. .. .. ja ja ja ja ja ja ja ja .. .. .. (allokerade)
00 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f 10 11 12 13 14 15 16 17 18 19 1a 1b 1c 1d 1e 1f (adress)
a7 f1 2a 00 00 00 d8 fe a2 a2 a2 a2 8c e2 b3 47 17 11 98 54 2f 11 2d 05 58 f5 6b d6 88 07 99 92 (värde)

Nu pekar intpekare på 0x15, den och sju bytes framåt är allokerade. Vi provar att skriva till dit pekaren pekar.

*intpekare = 42;

.. .. .. .. .. .. .. .. ja ja ja ja .. .. .. .. .. .. .. .. .. ja ja ja ja ja ja ja ja .. .. .. (allokerade)
00 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f 10 11 12 13 14 15 16 17 18 19 1a 1b 1c 1d 1e 1f (adress)
a7 f1 2a 00 00 00 d8 fe a2 a2 a2 a2 8c e2 b3 47 17 11 98 54 2f 2a 00 00 00 f5 6b d6 88 07 99 92 (värde)

Som man kunde misstänka skrev den endast till de första fyra byten. För att skriva till nästa fyra kan man skriva på följande sätt.

*(intpekare + 1) = 42;

.. .. .. .. .. .. .. .. ja ja ja ja .. .. .. .. .. .. .. .. .. ja ja ja ja ja ja ja ja .. .. .. (allokerade)
00 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f 10 11 12 13 14 15 16 17 18 19 1a 1b 1c 1d 1e 1f (adress)
a7 f1 2a 00 00 00 d8 fe a2 a2 a2 a2 8c e2 b3 47 17 11 98 54 2f 2a 00 00 00 2a 00 00 00 07 99 92 (värde)

Det intressanta här är att vi ökar intpekare med 1, och det gör att den hoppar 4 bytes, eftersom en int är 4 bytes stor. Skulle man öka en intpekare med 2 skulle den hoppa 8 bytes. På så sätt gör pekartypen skillnad, och om du tänker efter är det ganska logiskt trots allt, eftersom om pekare inte skulle fungera på detta sätt skulle det bli konstigt med arrayer också. Det är på grund av detta som void-pekaren inte lämpar sig när man ska ha med värden att göra. En void-pekare hoppar bara en byte i taget, och läser/skriver bara en byte i taget. Däremot är den inte av någon bestämd typ, så därför är den perfekt att använda när man ska skicka runt minnesadresser men inte bryr sig om vad som finns bakom.

Pekare, avslutning

Detta var nog allt vi hade att säga om pekare. Pekare är en stor del av C, som du märker bara på att det fick två delar av denna tutorial alldeles för sig själv. Det är också en svår, och potentiellt förvirrande del av C, så vänta dig inte att förstå allt direkt. Men övning ger färdighet, och någon dag kommer du plötsligt känna, "Aaah! Jag fattar! Allt hänger ihop!" ungefär. Eller, några dagar. Du kommer känna så för varje liten detalj kring pekare, jag kände så kring en detalj senast för några timmar sen. ;)

← Pekare

Copyleft kqr & slaeshjag 2009, 2012 some rights reserved