This is an archived page. Back to the blog.
C++-arkivet
 

Undvik läckor och krascher: Tipset som gör minneshantering lekande lätt!

Vi börjar med lite reklam för lokala objekt:
Varför objekt på stacken (objekt som är lokala variabler) är så bra:
  1. Kompilatorn hanterar minnet åt dig. Du behöver inte göra new, men ännu bättre, du behöver inte komma ihåg att göra delete!
  2. Det går snabbare att allokera minne på stacken än att allokera minne dynamiskt.
Fakta: Stacken är lättanvänd och snabb.
Och här kommer tipset:
Lägg dina objekt på stacken så ofta du kan!
Använd new och delete endast:
  1. När objektet måste leva kvar efter funktionen returnerat.
    Låt en smart pekare eller annat objekt ansvara för att frigöra minnet.
  2. När du inte vet hur många objekt du behöver.
    Använd då en std::vector.
Tips: Hur du använder använder stacken för att skriva bättre program.



Stacken

Det finns flera olika minnestyper i C++. En av de viktigaste är stacken. Du vet att ditt objekt ligger på stacken om ditt objekt är en lokal variabel. Objekt som ligger på stacken har många fördelar, se Tabell 1.

stack
Varför objekt på stacken (objekt som är lokala variabler) är så bra:
  1. Kompilatorn hanterar minnet åt dig. Du behöver inte göra new, men ännu bättre, du behöver inte komma ihåg att göra delete!
  2. Det går snabbare att allokera minne på stacken än att allokera minne dynamiskt.
Tabell 1: Fördelarna med stacken.

Att kompilatorn hanterar minnet åt dig är en enorm fördel! Se bara Kodexempel 1, där vi använder dynamiskt minne och har minnesläckor på var och varannan rad. Raden med throw läcker minne eftersom ingen gör delete när undantaget (eng: exception) kastas. Även funktioner som du anropar (t.ex. may_throw_exception) kan kasta undantag. Vi ska därför tänka på att vi inte kan se på vår egen funktion om vi kommer kasta undantag eller inte!

Raden med return -1 läcker minne även den eftersom ingen gör delete. Och även om du har huvudet med dig och tätar alla minnesläckor så kan din kollega komma imorgon och lägga in en ny return eller kasta ett nytt undantag. Han/hon räknar ju med att du skriver kod som går att underhålla.

int get_number(int input)
{
   
int *array = new int[input]; // dynamiskt minne men
                                 // ingen ansvarig
   
if(check_error(array))
       
throw "error";           // läcker minne
    if
(some_condition(array))
        return
-1;               // läcker minne
    may_throw_exception(array);  // funktionsanrop kan
                                 // läcka minne
   
delete [] array;             // frigör minnet
   
return 7;
}
Kodexempel 1: Dynamiskt allokerat minne måste frigöras vid exceptions och return.

Vem ansvarar för minnet?

Dynamiskt allokerat minne ställer till mycket problem när man skriver program i C++. Man kan t.ex. glömma att frigöra minnet, frigöra minnet två gånger eller använda redan frigjort minne. När man ser dynamiskt minne allokerat med new bör man alltid ställa sig frågan "Vem ansvarar för minnet?" (eller "Vem äger minnet?"). Men vad betyder det att någon ansvarar för minnet?

Nyckeln till lyckad minneshantering i C++ är att dra nytta av att alla klasser har en destruktor. För varje objekt som skapas så kommer destruktorn till slut att anropas.

Din programkod blir lätt att förstå (för dig och andra) om du följer en enkel princip vid allokering av dynamiskt minne: se till att överlåta ansvaret för att frigöra minnet till ett objekt så fort som möjligt efter allokeringen (se Tabell 2). Eftersom alla objekt har begränsad livstid (förutom de dynamiskt allokerade) kommer kompilatorn till slut att anropa destruktorn. I destruktorn frigör du med delete det minne du ansvarar för.

När du allokerar dynamiskt minne, se till att överlåta ansvaret att frigöra minnet till ett objekt så fort som möjligt efter allokeringen.
Tabell 2: Bästa sättet att undvika problem med dynamisk minnesallokering.

Objekt på stacken ger tydlig ansvarsfördelning

Man kan använda stacken tillsammans med ovanstående tips i Tabell 2 för att lösa alla problem i Kodexempel 1 i ett trollslag: deklarera variabeln array som en vektor (se Kodexempel 2). Vektorn ligger nu som ett objekt på stacken. Kompilatorn skapar automatiskt minne för vektorn och anropar dess destruktor när get_number är klar (oavsett om det sker genom return, undantag eller att funktionen tar slut).
int get_number(int input)
{
    std::vector array(input); // vektorn ansvarig för
                              // dynamiskt minne
    /* ... */
}
Kodexempel 2: Array är en lokal variabel av typen vektor. Kompilatorn sköter vektorns minne och vektorn sköter det dynamiska minnet. Du bara njuter.
Att vektorn använder sig internt av dynamisk minnesallokering behöver du inte bry dig om (det är dessutom oundvikligt). Du fokuserar bara på vektorns gränssnitt och din egen funktion get_number och bryr dig inte om hur vektorn gör sitt jobb! Vektorn är alltså ansvarig för det dynamiskt allokerade minnet och du riskerar inga minnesläckor.

Vill du veta mer om hur vector fungerar så läs här (extern länk).

Dynamiskt minne är ineffektivt

Tabell 1 påstod i punkt 2 att det går snabbare att skapa ett objekt på stacken än att allokera det dynamiskt med new. Men varför är det så?

Skillnaden mellan stacken och det dynamiskt allokerade minnet är att minne allokerat med new kan man lämna tillbaka med delete. "Free store" kallas den plats där det dynamiskt allokerade minnet ligger. När du lämnar tillbaka minne med delete bildas "hål" i free store och utrymmet blir en blandning av ledigt och upptaget minne. När någon gör new måste man hitta tillräckligt med ledigt minne. Att hitta det lediga minnet tar tid hur man än gör och därför blir dynamiskt allokerat minne ineffektivt.

Stackens minne kan du aldrig "lämna tillbaka" eftersom kompilatorn är ansvarig. Allokering på stacken påminner mycket om att bygga ett torn av klossar. Man lägger till och tar bort klossar längst uppe. Man kan aldrig ta bort en kloss mitt i tornet (för då rasar bygget). Kompilatorn håller reda på var toppen av tornet finns och vet omedelbart var minne ska allokeras eller avallokeras. Därför kan man skapa minne på stacken väldigt snabbt med en enda assemblerinstruktion. Och som ni förstår har stacken fått sitt namn från datatypen "stack" där man kan lägga till och ta bort endast i ena änden.

Medlemsvariabler och dynamiskt allokerat minne

Observationerna i Tabell 1 gäller även medlemsvariabler i klasser. Tipset är att skapa medlemsvariabler som är objekt istället för pekare till dynamiskt allokerat minne. Kompilatorn kommer att hantera minnet åt dig (du behöver inte komma ihåg new/delete) och ditt program går snabbare.

Låt oss titta på ett exempel där vi blandar stackminne och dynamiskt allokerat minne (se Kodexempel 3). Objektet volvo ligger på stacken. När volvo skapas så allokeras samtidigt minne för wheel_count med en enda instruktion.

Pekaren ford ligger på stacken och pekar till ett dynamiskt allokerat objekt. När detta objekt skapas, skapas också pekaren wheel_count_ptr. Ytterligare ett objekt, av typen int, måste sedan allokeras dynamiskt. Vi kan låtsas att ett stackobjekt tar tiden en instruktion att skapa och att dynamiskt allokering är tio gånger långsammare och därmed tar tio instruktioner. Totalt tar då volvo en instruktion att skapas medan ford tar 21 instruktioner (en för pekaren på stacken och två gånger tio för dynamiskt minne). Det är stor skillnad!

class Volvo {
public:
    int wheel_count;       // wheel_count är ett objekt
};

class Ford {
public:
    Ford() : wheel_count_ptr(new int(4)) {}
    ~Ford() { delete wheel_count_ptr; }
    int *wheel_count_ptr;  // wheel_count_ptr är dynamiskt                            // allokerad med new
};

int main(void)
{
    Volvo volvo;           // volvo ligger på stacken
 
   Ford *ford = new Ford; // ford är dynamiskt allokerad
                           // med new
}

Kodexempel 3: Exempel på blandning av stackminne och dynamiskt minne.

När stacken inte räcker till

Så varför använder man inte alltid stackminne? Ett exempel fick vi i Kodexempel 2. Vektorn låg i och för sig på stacken, men internt använder vektorn new[] och delete[] för att kunna expandera vid behov. Orsaken till att vektorn behöver new[] är att vi inte vet hur mycket minne vi behöver. Ett annat problem med stackminne är att det frigörs automatiskt när funktionen exekverat klart. Livslängden är alltså begränsad. Vi får därför följande minnesregel (Tabell 3).

Lägg dina objekt på stacken så ofta du kan.
Använd new och delete endast:

  1. När objektet måste leva kvar efter funktionen returnerat.
  2. När du inte vet hur många objekt du behöver.
Tabell 3: Algoritm för att välja typ av minnesallokering.
Det finns några mer avancerade användningsområden som inte täcks av tabell 3. Man kan t.ex. använda pekare och dynamiskt allokerat minne för att minska beroenden mellan sina headerfiler (det s.k. pimpl-idiomet). Men för att inte onödigt komplicera saker så behåller vi Tabell 3 som den är.

Hjälpklasser för minneshantering

Som vi såg i Kodexempel 1 så är dynamiskt minne svårt att hantera, så Tabell 3 är egentligen inte till mycket hjälp utan att ha några objekt som är ansvariga för minnet. Vi har redan pratat om att använda std::vector istället för new[].

Motsvarigheten för new (utan hakar) är en "smart pekare". Den kallas för "pekare" för att den har samma syntax som en pekare. Den ser alltså ut som en vanlig pekare, men den kallas för "smart" för att den är smartare än en vanlig pekare. En smart pekare är nämligen en klass vars enda uppgift är att ansvara för minnet. Exempel på smarta pekare är standardbibliotekets std::auto_ptr och boost::shared_ptr i Boost.

Kodexempel 4 visar hur man använder auto_ptr för så att man inte glömmer bort att frigöra det dynamiskt allokerade minnet. När funktionen returnerat finns ett objekt kvar av typ auto_ptr. När objektet destrueras frigörs också minnet (om ingen annan tagit över ägandet).

std::auto_ptr<int>          // vi returnerar ett _objekt_
create_new_seven()
{
    int *p = new int(7);    // dynamiskt allokerat minne
   
return std::auto_ptr<int>(p); // auto_ptr-objektet                                   // ansvarar för minnet
}

Kodexempel 4: Exempel på hur auto_ptr kan se till att man inte läcker minne.

Det fina med en vektor är att den ansvarar både för allokering och avallokering. Detta är en viktigt skillnad mot den smarta pekaren: det är inte den smarta pekaren som skapar minnet, den bara ansvarar för och städar upp minnet. Så ett problem med den smarta pekaren kan vara att komma ihåg att använda den!

Tycker du inte att en smart pekare är rätt för dig så återstår bara att skapa en klass som får ansvara för minnet. I destruktorn frigör du sedan minnet du ansvarar för. Generellt kan man säga att din programkod blir lättast att förstå om samma klass ansvarar för allokering och avallokering. Men om det inte är möjligt gör man det bästa av situationen. I Tabell 4 kommer en sammanfattning av hur man tillämpar reglerna:

Lägg dina objekt på stacken så ofta du kan!
Använd new och delete endast:

  1. När objektet måste leva kvar efter funktionen returnerat.
    Låt en smart pekare eller annat objekt ansvara för att frigöra minnet.
  2. När du inte vet hur många objekt du behöver.
    Använd då en std::vector.
Tabell 4: Algoritm för att välja typ av minnesallokering och tips på klasser du kan använda.



Relaterade artiklar: