C++-arkivet
|
Och här kommer tipset:
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.
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.
Objekt på stacken ger tydlig ansvarsfördelningMan 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).
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 ineffektivtTabell 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 minneObservationerna 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!
När stacken inte räcker tillSå 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).
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 minneshanteringSom 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).
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:
Relaterade artiklar:
|