6. Pekare och minne

 

 

·     Minnesallokering.

 

·     New - delete.

 

·     Länkad lista.

·     Indexerad pekare.

 

·     Funktionspekare.

 

·     Referensvariabler.

·     Funktionsanrop med referensvariabel.

·     Konstanta variabler.

 

 

 

 

 

 

 

 

 

 

 

 


Minnesallokering.

 

Ibland vet man inte hur mycket minne man behöver när ett program skrivs. Ta t.ex. ett program som ska hantera en lista på medlemmar i en klubb, och man vill ha alla medlemmar i en listvariabel av en egendefinierad strukturtyp. Om man då definierar variabeln så här:

 

struct medlem Medlemmar[100];

 

...så rymmer den 100 medlemmar, inte mer. Blir det fler medlemmar måste man ändra i programmet, och kompilera/länka igen. Har man dessutom strukturerat programmet med samma begränsningar kan det hända att man måste skriva om större delar av programkoden också.

 

Varför inte dra till ordentligt då? För det första måste vi tänka på att datorns minne är en begränsad resurs. Egentligen har vi inte råd att reservera större mängder utrymme som inte används. Förr eller senare behövs utrymmet till annat, som vi då tycker är viktigt. Vi kanske har reserverat för 10000 medlemmar i vår lilla lokala kattklubb (det är vi och grannen och deras bekanta samt några till och deras svåger, så vi har gott om plats för flera medlemmar) med resultat att vi måste stänga medlemsegistret varje gång vi vill köra Word eller Excel. Detta kan inte accepteras, eftersom vi köpt in dessa program för dyra pengar och klubbkassan är liten.

 

Om vi inte drar till fullt så mycket då? Vad vet vi om framtiden? Alltför ofta växer det fortare än man tänkt sig. Om ett par år kanske vår kattklubb växt i paritet med Svenska Kennelklubben (det finns ju faktiskt många fler katter än hundar i sverige). De fantasifulla 10000 medlemmarna räckte inte.

 

Lösningen på ovanstående dilemma är att programmet ber operativsystemet om utrymme efterhand som det behövs. Programmet tar då inte upp oanvänt utrymme, och när väl den lilla klubben växt sig till Svenska Kisseklubben och man ändå inte får plats med allt i minnet räcker det med att köpa mer minne, inga förändringar behöver göras i programmet.

 

Hur ber man då om minne? Till C finns funktionen malloc(). Den kan man anropa för att få minne tilldelat. Den svarar med adressen till det reserverade utrymmet, varigenom det kan nås m.h.a. pekare. Observera att man alltså inte kan använda variabelnamn i den här situationen.

 

Man måste också kontrollera pekaren! Kunde man inte få minne blir svaret = 0! Detta måste man naturligtvis kontrollera innan programmet fortsätter, annars skulle man riskera att skriva över de vektorer som står i början av datorns minne.

Ett bra sätt är t.ex:

 

if((mp = malloc(1024)) == 0)

{

   puts(”Minnet räcker inte för denna operation!\n”);

   return -1; // Eller annat lämpligt felnummer.

}

else

{

   // Använd ditt nya utrymme.

   ...

   ...

   ...

   free(mp) // Lämna alltid tillbaka utrymmet när du är klar!

}

 

Att reservera minnesutrymme kallas på engelska för ‘memory allocation’, därav funktionesnamnet malloc() och det svengelska uttrycket ‘allokera minne’.

 

Detta avsnitt är intressant för C-programmerare, men här har jag tagit med det endast för att vi ska se nyttan av ‘new’ och ‘delete’ som vi ska diskutera nedan. Därför tar vi inte heller med några övningsexempel.

 

Glöm inte att titta i help!

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

? malloc, free.


New - delete.

 

I C++ använder vi i stället ‘new’ för att be om mera minne. Allmän syntax:

 

<pekare> = new <typ>;

 

Detta betyder att vi använder operatorn ‘new’ till att reservera minnesutrymme anpassat att innehålla data av typen <typ>, samt tilldelar pekaren <*pekare> adressen till det reserverade utrymmet.

 

Vanligare är att man passar på att deklarera även pekaren i samma sats. Då behöver vi ange typen på två ställen:

 

<typ> <*pekare> = new <typ>;

 

Diverse exempel:

 

int *ip = new int;                             // Ett heltal, 2 bytes.

double *dp = new double;              // Dubbel precision.

struct person *sp = new struct person; // En hel struktur

int *ip10 = new int[10];              // Även listor!

char *cp = new char[81];              // Textsträng.

 

Observera att dessa programinstruktioner reserverar utrymme när programmet körs, inte vid kompilering. Om en programsats inte utförs kommer minne inte att reserveras (en besparing). Om en programsats utföres flera gånger kommer minne att reserveras flera gånger. Man reserverar alltså utrymme efterhand som det behövs, och aldrig i onödan.

 

När man sedan är klar med minnet ska man lämna tillbaka det m.h.a. ‘delete’:

 

delete <pekare>;

 

Om vi t.ex. ska frigöra ett par av dem vi allokerade tidigare:

 

delete ip;

delete dp;

 

En fördel med new och delete är att man bara behöver frigöra minnet om man tänker låta programmet fortsätta med någonting. Om programmet ändå avslutas när man inte längre behöver minnet, behöver man inte använda delete, minnet frigörs automatiskt när programmet avslutas.

 

? new, delete.


Länkad lista.

 

Eftersom vi inte har någon egentlig variabel deklarerad kan vi inte nå vårt data på vanligt sätt. I stället använder vi pekare. Man kan t.ex. göra en lista med pekare, men då begränsar man antalet tillåtna inmatningar igen: till  pekarlistans storlek.

 

Ett bättre sätt är att använda en länkad lista. Man  skapar en struktur som kan innehålla såväl de datafält man vill spara som pekare som håller reda på var nästa förekomst av strukturen finns. Detta kallas för en länkad lista. Om man har pekare till såväl nästa förekomst av strukturen som till föregående, kallas detta en dubbellänkad lista.

 

Betrakta nedanstående exempel:

 

#include <iostream.h>

#include <string.h>

 

void main()

{

   struct person {

                   char cNamn[31];

                   struct person *next;

                 };

 

   struct person *pCurrent = new struct person;

   struct person *pStart;

   pStart = pCurrent;

 

   cout << "Skriv in så många namn du har lust till!\n";

   cout << "Avsluta med 'slut'.\n";

   cin >> pCurrent->cNamn;

 

   while(strcmp(pCurrent->cNamn,"slut"))

   {

      pCurrent->next = new struct person;

      pCurrent = pCurrent->next;

      cin >> pCurrent->cNamn;

   }             

 

   cout << "Detta var vad du skrev:\n";

 

   while(pStart != pCurrent)

   {

      cout << pStart->cNamn << "\n";

      pStart = pStart->next;

   }

}

 

? strcmp()
Efterhand som man matar in nya uppgifter i strukturen (namn) lägger programmet in pekare till nästa förekomst av strukturen i minnet. Adressen till första förekomsten av strukturen sparas i pekaren pStart. När användaren skriver ‘slut’ avbryts inmatningen. Därefter skriver programmet ut allt som matats in.

 

Så här kan det se ut när man kör programmet:

 

                

 

Övningsuppgift:

·      Utveckla ovanstående program till att ta med namn, efternamn, adress, postnummer, ort och telefonnummer.

·      Formatera utskriften som det ska se ut i ett brevhuvud.

·      Kalla programmet telebok1.

 


En bra länkad lista brukar dock ha adresser både framåt och bakåt. Man kan också lägga till data mitt i en länkad lista genom att manipulera pekarna.

 

 

     *next         *next         *next         *next

 

     Namn          Namn          Namn          Namn

     Telefon       Telefon       Telefon       Telefon

 

     *prev         *prev         *prev         *prev

 

 

Så här lägger vi till data mellan två element i listan:

 

 

     *next         *next         *next         *next

 

     Namn          Namn          Namn          Namn

     Telefon       Telefon       Telefon       Telefon

 

     *prev         *prev         *prev         *prev

 

 


       *next

 

       Namn

       Telefon

 

       *prev        

 

 

Ovanstående exempel visar hur krångligt det kan vara att hålla reda på pekarna.

 

När vi skriver windowsprogram i C++, och använder oss av Microsoft Founda­tion Classes får vi dessutom tillgång till något som kallas ‘collections’. Det är färdiga listor i olika klasser, vilka heter ‘CObList, CObArray etc, + en klass för datat som ska höra till varje post i listan, CObject.

 

Det är alltså inte alltid  man har data och pekare i samma struktur. Vill man ha en generell lista kan den innehålla en pekare mot datat i stället.


Indexerad pekare.

 

Om man nu begränsar sig till att använda en lista med pekare mot t.ex. ett antal strukturer som skapas efter hand av programmet, så har C++ en liten förkort­ning åt oss. Man kan använda hakparanteserna för att komma åt de olika ele­menten:

 

int *pi = new int[10];    // pi pekar på en lista med 10 int.

*pi = 2;                  // Första elementet blir 2.

pi[0] = 5;                // Första elementet ändras till 5.

pi[5] = 82;               // Sjätte elementet ändras till 82.

 

Använder man denna förkortning, indexering på pekaren, så ändrar man inte pekarens värde (den adress den innehåller). Man bara använder indexet som ett offset till den adress pekaren innehåller. Det ‘gamla’ sättet att öka pekarens värde (adressen) för att peka på nästa variabel i listan, innebär att man ‘tappar bort’ adressen listans början. Man måste hantera den separat.

 

Observera att man inte behöver ange storleken när man skriver programmet. Man kan i stället använda en variabel som fyllts i av användaren eller på annat sätt, och storleken på listan sätts vid programkörningen. Det vore t.ex. tillåtet att skriva:

 

int *pi = new int[iAntal];

 

Om vi har en variabel iAntal som redan fått ett värde.

 

Övningsuppgift:

·      Förenkla föregående övningsuppgift genom att använda hakparanteser.

·      Låt programmet först fråga efter hur många uppgifter telefonboken ska innehålla.

·      Kalla programmet telebok2.


Funktionspekare.

 

Man kan anropa en funktion m.h.a. en pekare. Det är dock rätt sällan som man kan skapa en bättre kod just genom att anropa via pekare.

 

Exempel på användningsområden för funktionspekare:

 

·     Hopptabell i ett lib (programbibliotek med flera program i en fil).

 

·     Intern användning för klasser.

 

Vi kommer inte att syssla med hopptabeller, men när vi senare kommer till Visual C++ kursen kommer vi att finna nyttan med funktionspekare. Vi kommer då att deklarera s.k. objekt, vilket är ett sorts strukturer vilka utökats med inbakade funktioner.

 

Dessa funktioner kan man anropa genom att ange en pekare till objektet, och sedan kombinera den med den indirekta operatorn för att anropa en av de inbakade funktionerna.

 

Mer om detta senare.

 


Referensvariabler.

 

En ny datatyp igen? Ibland kan man tycka att de som konstruerar program­merings­verktygen skapar en massa finesser bara för att vi stackars program­mera­re ska ha ännu mer att lära oss.

 

Det här är dock en härledd typ, som kan ha ett visst praktiskt värde. När man deklarerar en referensvariabel har man egentligen inte skapat en ny variabel, bara ett nytt namn som refererar till en redan existerande variabel.

 

Och nu kommer poängen:Som ni minns kan man inte påverka anropande programs variabler från en funktion, det var därför vi började använda pekare i stället. Nu har vi en ny möjlighet: referensvariabeln arbetar med anropande programs variabel.

 

När man deklarerar en referensvariabel används &-teknet på ett nytt sätt (denna gång avser den alltså inte adressoperatorn). Låt oss deklarera en variabel och en referensvariabel till den:

 

int iMinVar;           // iMinVar är en variabel, och

int &iMinRef = iMinVar; // iMinRef blir dess pseudonym.

 

Som synes är det uteslutet att förväxla användningen av &-tecknet med adress­operatorn, syntaxen skulle i så fall bli helt fel.

 

Övningsuppgift:

·      Skriv ett programexempel refvar1 som deklarerar en variabel och en referensvariabel enligt ovanstående.

·      Låt programmet tala om adressen för dessa variabler (det ska bli samma adress för båda två).

·      Låt programmet ta emot ett tal i den ena variabeln och skriva ut värdet i den andra. Gör detta som en loop som fortsätter tills man matar in värdet 0. (Värdet ska hela tiden bli detsamma i bägge variablerna.)

·      Det ska se ut så här:

 

                     


En referensvariabel kan inte existera utan att ha något att referera till. Därför initieras den vanligen vid deklaration. Det finns ett antal undantag till detta:

 

Den deklareras som 'extern' d.v.s. den är redan initierad någon annanstans.

 

Den är medlem i en klass, vilket betyder att den initieras i klassens konstruktorfunktion, mer om det när vi lär oss om klasser.

 

Den deklareras i en funktions parameterlista, vilket innebär att den kommer att initieras när funktionen anropas.

 

Den deklareras som en funktions returtyp, vilket innebär att den initieras när funktionen ska returnera ett värde.

 

Om inget av ovanstående gäller ska man initiera referensvariabeln när man deklarerar den.

 

Man kan använda en konstant referensvariabel som 'skrivskyddad' variant av en vanlig, icke konstant variabel. I nedanstående exempel skiljer sig rTal från iTal så till vida att det inte går att ändra värdet i rTal, men väl i iTal:

 

int iTal, i;

const int &rTal = iTal;

 

iTal = 5; // Ok, iTal kan ändras.

i = rTal; // Ok, rTal kan läsas.

rTal = 7; // Fel, rTal kan inte ändras!

 

I C kan man skicka parametrar till en funktion på två olika sätt:

 

Ett värde skickas. Funktionen har en egen kopia på aktuellt data.

 

En pekare skickas. Funktionen har tillgång till anropande funktions variabel via pekaren.

 

I C++ finns ett sätt till: man kan skicka en referens i stället. Därigenom har den anropade funktionen tillgång till den anropande funktionens variabel via referensvariabeln.

 

En viktig skillnad mellan pekarvarianten och referensvarianten är den att man slipper använda pekarsyntax vid anrop. Detta kan innebära nackdelen att man inte ser på själva anropets syntax att den egna variabeln kan ändra värde under anropet. Man kan undvika att innehållet förändras om man i funktionens argumentlista anger referensen som konstant (const).

För att undvika förvirring och misstag bör man följa dessa två riktlinjer:

 

·     Om funktionen ska förändra anropande funktions variabel, använd en pekare.

·     Om funktionen inte ska förändra anropande funktions variabel, använd en referens.

 

Det sistnämnda behövs bara om man använder stora strukturer e.d. De grundläggande datatyperna, int, float etc. kan användas som vanligt, utan varken pekare eller referenser, om man inte tänker förändra värdet i dem.

 

Hela denna diskussion verkar kanske lite filosofisk, men detta.får sin nytta när man använder det i klasser. I en klass har man en naturlig motsvarighet till den globala variabeln.

 

Man använder ofta referensvariabler i klasser, vilket vi kommer att göra senare. När man använder referenser bör man tänka på följande:

 

·     En referens är en alias för en variabel.

·     En referens måste initieras, och kan inte ändras till att referera till en annan variabel senare.

 

Referenser är mycket användbara när man skickar variabler av egendefinierade typer till en funktion, och när man returnerar dylika.

 

För att man inte ska ta miste på betydelse av ett '&' kan man ha följade tumregel till hands:

 

·     Om ett '&'-tecken vilket föregår ett variabelnamn i sin tur föregås av en enkel datatyp (int, char, float etc.) betyder det att variablen är en referens.

·     Om ett '&'-tecken vilket inleder ett variabelnamn inte föregås av en elementär datatyp betyder det att variabelns adress avses.

 

Observera att om blanksteget står före eller efter '&' inte har någon betydelse. Nedanstående deklarationer är ekvivalenta:

 

int &ref = iVar; // Ok, så brukar man skriva.

int& ref = iVar; // Ok även detta.

 

Samma sak gäller för övrigt även '*' när man avser pekare. Dock är det att föredraga att man skriver enhetligt för tydlighets skull:

 

int *p = &iVar; // Kan misstolkas, var hamnar adressen?


Funktionsanrop med referensvariabel.

 

Så här var det man inte kunde göra:

 

void minfunktion(int x)

{

    x = 5;

}

 

void main()

{

    int x;

    minfunktion(x);

    cout << "x efter anrop:" << x << '\n';

}

 

Vi kommer i detta fall att ha två olika variabler med samma namn. Frågar man efter x i minfunktion så har den inte samma adress som x i main. Därför kommer x efter anrop inte att innehålla något korrekt värde. I själva verket kommer x att innehålla vad som helst som råkade stå i minnet sedan tidigare.

 

Om man däremot använder en referensvariabel kommer vi att peka på samma adress, vare sig vi använder samma namn eller ej. Så här skriver man för att ange att vi använder en referensvariabel:

 

void minfunktion(int &x)

{

    x = 5;

}

 

void main()

{

    int y;

    minfunktion(y);

    cout << "y efter anrop:" << y << '\n';

}

 

Övningsuppgift:

·      Skriv ett program refvar2 enligt följande:

·      En funktion byt() ska byta två integers med varandra. Funktionen ska använda referensvariabler.

·      Initiera två int-variabler i main(). Skriv ut deras värden med ledtexter som anger variablernas namn.

·      Anropa funktionen byt() och skriv ut på samma sätt.

·      Provkör programmet och kontrollera att variablerna byter värde.


Man kan också använda en referensvariabel som returvärde. Detta kan vid första anblick verka märkligt, men det ger oss en helt ny finess. Man kan ange en funktion till vänster om en tilldelningsvariabel.

 

Studera nedanstående exempel:

 

int mynum = 0;   // Global variabel.

 

int &num()       // Funktion som använder referens vid retur.

{

    return mynum;

}

 

void main()

{

    int i;

    i = num();   // Så här är vi vana att anropa en funktion.

    num() = 5;   // Detta är dock nytt:

                 // mynum erhåller värdet 5.

}

 

Vi kommer att använda detta mer senare, när vi syssla med klasser och dynamisk minnesreservation.


Konstanta variabler.

 

Nyckelordet 'const' förvandlar en vanlig variabel till en konstant. Kompilatorn kommer därmed att vägra acceptera kod som förändrar variabelns värde, efter att den initierats. Detta kan användas i stället för textkonstanter:

 

#define STORLEK 81’ gör att kompilatorn byter ut alla förekomster av texten 'STORLEK' mot det numeriska värdet 81.

 

const int storlek = 81;’ skapar en verklig variabel vilken dock inte kan ändra värde under programmets gång.

 

Skillnaden gör sig gällande i C när man försöker skapa en lista, t.ex. ‘char cText[STORLEK];’ tolkas av C-kompilatorn som ‘char cText[81];’ medan ‘char cText[storlek]’ genererar ett kompileringsfel, eftersom listor av variabel storlek inte tillåts i C.

 

Däremot går det sistnämnda bra i C++. Kompilatorn godkänner de konstanta variablerna som sanna konstanter, varvid listan kan dimensioneras vid kompileringen som vanligt.

 

Man kan också använda 'const' när man deklarerar pekare. Detta kan användas på två sätt. Man kan deklarera en konstant pekare, vilken pekar på samma adress under hela programmets gång, medan det data som pekaren pekar på kan ändras.

 

char *const p = cText;

*p = 'A';       // Tillåtet, data förändras.

p = cAnnanText; // Fel, pekaren kan inte ändra värde!

 

Det andra sättet är när pekaren pekar på konstant data. En sådan pekare kan peka på data som i sig inte är konstant, men pekaren är endast för läsning. Ett sorts skrivskydd alltså.

 

const char *p = cText;

p = cAnnanText; // Tillåtet, pekaren kan ändra värde.

*p = 'A';       // Fel, pekaren kan inte förändra data!

 


Det går heller inte att tilldela en icke konstant pekare värdet i en konstant pekare. Detta hindrar att man skapar en annan pekare för att påverka data som man egentligen inte ska kunna påverka. Betrakta nedanstående funktion:

 

int laesfunktion(const char *p)

{

    char *skriv; // Skapar en pekare för att kunna

                    ändra data.

    skriv = p;   // Fel! Det gick inte att låta 'skriv'

                    peka på samma data som 'p'.

}

 

Detta har en viktig roll för den s.k. inkaplingen av data i objekt, vilket vi kommer att läsa mer om senare.