Göm menyn

TDP004 Objektorienterad programmering

Förberedelsematerial Minneshantering och pekarstrukturer


Här följer en kort sammanfattning av pekarbegreppet: En variabel är en "låda" i minnet som har ett namn och innehåller ett värde. Värdet kräver ett visst utrymme och tolkas på ett visst sätt. Datatypen anger utrymmet och tolkningen.
// Kod:
int foo{5};

// Motsvarande figur:
      +-----+
foo   |  5  |
      +-----+
       (int)
Vi har skapat en "heltalsvariabel" som lagrar heltal. En pekare är en "låda" i minnet som har ett namn och innehåller ett värde. Värdet kräver ett visst utrymme och tolkas på ett visst sätt. För en pekare är värdet ett tal som tolkas som en adress i minnet. Datatypen är "adress" (*) som leder till (pekar på) en viss datatyp.
// Kod:
int* bar{nullptr}; // nullptr motsvaras av adressen 0

// Motsvarande figur:
      +-----+
bar   |  0  |
      +-----+
        (*)
Varför deklarerar "bar" med datatypen (int*) och inte bara (*)? Därför att vi planerar peka ut heltalsvärden (int)! Vi har skapat en "pekarvariabel" som lagrar adresser (pekare). "nullptr" motsvarande adressen "0" anger att pekaren inte leder någonstans ännu. Den pekar alltså inte ut någon annan variabel än. Pekarvariabler är inte magiska utan fungerar precis som vanliga variabler. Skriver vi "bar" i programmet kan vi komma åt värdet (adressen).
cout << bar << endl; // skriver ut adressen 0
Vi kan under programkörning "låna" minne stort nog för en viss datatyp från operativsystemet. Detta görs med operatorn "new" och ger som resultat adressen till minnet vi fick lov att använda.

// Kod:
bar = new int(8);

// Motsvarande figur:
//      +-----+          +-----+
//bar   |  o------------>|  8  |
//      +-----+          +-----+
//        (*)             (int)

Nu kan vi se varför "bar" deklarerades som (int*)! Pilen i "bar" pekar ju ut heltalet åtta! D.v.s. (int*). Vi kan även se att den nya variabeln inte har något namn. Vi kan bara komma åt den genom att hålla reda på dess adress i en annan variabel. Skriver vi ut "bar" igen får vi dess värde (adressen till vår nya anonyma variabel)
cout << bar << endl; // skriver ut adressen till vår anonyma variabel
Hur kommer vi åt värdet av vår nya anonyma variabel? Jo vi använder operatorn * för att "gå till" en adress!
cout << *bar << endl; // "gå till" adressen i bar och skriv ut värdet (8)
En pekarvariabel kan även sättas att peka på en vanlig variabel (med namn). Adressen av en variabel kan hämtas med operatorn &
// Kod:
bar = &foo;

// Motsvarande figur:
      +-----+
foo   |  5  |<--+
      +-----+   |
       (int)    |
                |
      +-----+   |      +-----+
bar   |  o------+      |  8  |
      +-----+          +-----+
        (*)             (int)
Nu pekar bar på heltalsvariabeln "foo" med värdet 5. Läser vi av variabeln "foo" får vi värdet (5) som vanligt. Läser vi av variabeln "bar" får vi värdet (adressen till "foo") som vanligt. Det som är intressant nu är att exakt samma kod som tidigare kommer ge oss värdet av en annan variabel:
cout << *bar << endl; // "gå till" adressen i bar och skriv ut värdet (5)
Hur gör vi nu för att komma åt värdet på vår anonyma variabel? Det går inte! Vi har tappat bort adressen. Detta är ett allvarligt programmeringsfel som kallas "minnesläcka". Vi har "lånat" minne av operativsystemet, och sedan tappat bort det vi lånat. Gör vi detta upprepat kommer operativsystemet till slut få slut på minne att låna ut. För att undvika att operativsystemet får slut på minne kräver korrekt användning av "new" att allt som lånas också återlämnas. Återlämning sker med "delete" följt av den adress som skall återlämnas. Det är rätt att återlämna varje lån exakt en gång. Allt annat är fel. Korrekt terminologi är som följer:
new
allokerar (lånar) minne för en variabel
delete
avallokerar (återlämnar) minne för en variabel allokerad med new
*
avrefererar en pekarvariabel (går till adressen som pekas ut)
&
hämtar adressen av en befintlig variabel
En pekarvariabel som pekar ut heltal kan inte på ett korrekt sätt peka ut en annan datatyp.

// Kod:
double dum{1.0}
bar = reinterpret_cast<int*>(dum);

// Motsvarande figur (som vi ser det):
//      +-----+          +-----+-----+
//bar   |  o------------>| +1.0000e1 |
//      +-----+          +-----+-----+
//        (*)               (double)

// Motsvarande figur (som kompilatorn ser det):
//      +-----+          +-----+
//bar   |  o------------>| +1.0|
//      +-----+          +-----+
//        (*)             (int)

Så här blir det för vi i deklarationen av "bar" har sagt att "bar" pekar ut heltal! Försöker vi skriva ut värdet blir det konstigt. Grafiskt kan vi se det som att plusset och punkten gör att heltalet tolkas helt fel (det blir något helt annat än 1).
cout << *bar << endl; // "gå till" adressen i bar och skriv ut värdet (??)
Vi kan även kombinera pekare med det vi tidigare kan:

// Kod:
struct Point
{
  int x
  int y;
};

Point* pt = new Point{3, 5};

// Motsvarande figur:
//     +-----+          +-----------+
//pt   |  o------------>|   +-----+ |
//     +-----+          | x |  3  | |
//       (*)            |   +-----+ |
//                      |    (int)  |
//                      |   +-----+ |
//                      | y |  5  | |
//                      |   +-----+ |
//                      |    (int)  |
//                      +-----------+
//                         (Point)

cout << (*pt).y << endl; // "gå till" adressen i pt och i den
                         // variabeln, skriv ut värdet av y (5)

Eftersom det är obekvämt och "fult" att skriva "(*pt)." finns ett förenklat skrivsätt:
cout << pt->y << endl; // "gå till" adressen i pt och i den
                       // variabeln, skriv ut värdet av y (5)
Detta kan vi kalla "piloperatorn" och är den som brukar användas.
Nu kommer en fortsatt utblick om C-arrayer (som vi för det mesta inte använder i C++, I C++ använder vi istället std::vector eller std::array.) En pekarvariabel pekar ut en annan variabel. Men egentligen kan vi se den utpekade variabeln som den första i en sekvens, pekarvariabeln blir ju likadan om vi håller reda på sekvensens längs separat.

// Kod:
int size{6};
int* array = new int[size];


// Motsvarande figur:
      +-----+
size  |  6  |
      +-----+
       (int)              0     1     2     3     4     5
      +-----+          +-----+-----+-----+-----+-----+-----+
array |  o------------>|     |     |     |     |     |     |
      +-----+          +-----+-----+-----+-----+-----+-----+
        (*)             (int)
Hur kan vi nu komma åt delarna?

// Kod:
for (int i = 0; i < size; ++i)
{
  *(array + i) = i*i;
}

// Resultat:
                          0     1     2     3     4     5
      +-----+          +-----+-----+-----+-----+-----+-----+
array |  o------------>|  0  |  1  |  4  |  9  | 16  | 25  |
      +-----+          +-----+-----+-----+-----+-----+-----+
        (*)             (int)
Men även skrivsättet "*(array + i)" är obekvämt och "fult" så vi brukar istället skriva samma kod med "indexoperatorn":

// Kod:
for (int i = 0; i < size; ++i)
{
  array[i] = i*i; // samma som *(array + i) = i*i;
}

Eftersom en sekvens minne allokerat med "new []" måste avallokeras med "delete []" (precis som minne allokerat med "new" måste avallokeras med "delete") så går det även att skapa sekvenser med automatiskt minne om vi på förhand vet storleken:

// Kod:
int field[6];

// Motsvarande figur (som det beter sig):
                          0     1     2     3     4     5
      +-----+          +-----+-----+-----+-----+-----+-----+
field |  o------------>|     |     |     |     |     |     |
      +-----+          +-----+-----+-----+-----+-----+-----+
        (*)             (int)

// Motsvarande figur (som det faktiskt lagras i minnet):
                          0     1     2     3     4     5
                       +-----+-----+-----+-----+-----+-----+
                field  |     |     |     |     |     |     |
                       +-----+-----+-----+-----+-----+-----+
                        (int)

//Dock finns små skillnader i beteende mellan variablerna "field" och "array":

cout << sizeof(array) << endl; // en pekare är alltid samma storlek (sizeof(int*))
cout << sizeof(field) << endl; // antalet bytes som används (6 * sizeof(int))

array = field;     // går bra, pekarvariabeln "array" får adressen till
                   // första elementet i field
array = &field;    // går bra, pekarvariabeln "array" får adressen till
                   // första elementet i field
array = &field[0]; // går bra, pekarvariabeln "array" får adressen till
                   // första elementet i field
field = array;     // fel, field är inte en pekarvariabel

//Observera att felkontroll *inte* sker!!!

// Kod:
int a{0};
int b[4]{0,0,0,0};
int c{0};
b[4] = 5;  // utanför "b" !! allvarligt fel, kompilatorn varnar inte
b[-1] = 8; // utanför "b" !! allvarligt fel, kompilatorn varnar inte

// Motsvarande figur (så som variablerna är packade i minnet):
        a    b[0]  b[1]  b[2]  b[3]   c
     +-----+-----+-----+-----+-----+-----+
     |  8  |  0  |  0  |  0  |  0  |  5  |
     +-----+-----+-----+-----+-----+-----+
      (int) (int) (int) (int) (int) (int)
Felanvändning ("index out of bound" gör alltså att andra variabler i programmet skrivs över! (I bästa fall blir det "segmentation fault" under körning, men troligen går det bra och a och c byter oväntat värde enligt ovan.)

Sidansvarig: Christoffer Holm, Simon Ahrenstedt
Senast uppdaterad: 2023-10-26