Internet Info, s.r.o. Lupa Měšec Podnikatel Root Zdroják DigiZone Slunečnice Vitalia TopDrive KupDnes Navrcholu NovýTarif Dobrý web Weblogy Woko Jagg Computer.cz SK: MojeLinky

Hlavní navigace

Máte dost paměti?

Přestože poučka o tom, že každá alokovaná paměť se musí uvolnit, je zmíněna ve všech učebnicích programování, patrně není programátora, kterému by neuvoněná paměť alespoň jednou nezpůsobila bolení hlavy. Naštěstí nám jazyk C++ dává do rukou jednoduché, ale účinné prostředky k boji s neuvolněnou pamětí, která se v cizojazyčné literatuře označuje jako memory leaks.

Tweetni to Twitter Jaggni to! Jagg Del.icio.us Delicious

Klíčem k úspěchu je okolnost, že C++ umožňuje předefinovat globální operátor new (a pochopitelně též delete). Je třeba si totiž uvědomit, že vytváření instancí pomocí operátoru new vykonává činnosti dvě – alokuje potřebnou paměť a poté volá příslušný konstruktor. Část, která se stará o alokaci paměti, můžeme snadno přetížit; o zavolání konstruktoru se postará překladač, ovšem pouze tehdy, pokud alokace paměti proběhne úspěšně.

Stačí tedy maličkost: při každé alokaci paměti si ukazatel na alokovanou pamět uložíme do seznamu alokované paměti a při každém uvolnění paměti ukazatel ze seznamu odebereme. Při ukončení programu pak stačí zkontrolovat seznam, zda neobsahuje odkazy na neuvolněnou paměť.

Existuje mnoho volně dostupných knihoven, které tuto práci obstarají (namátkou: Valgrind, memprof, mpatrol, eletric fence) a rovněž řada komerčních produktů, přesto ale existují i důvody, abychom se alespoň na princip podívali sami. Zmíněné knihovny totiž často přinášejí nežádoucí závislost na dalších knihovnách nebo je kontrola až příliš „dokonalá“, tj. knihovna umí mnohem více, než ve skutečnosti potřebujeme. V následujících řádcích proto popíšu činnost nejjednodušší kontroly správného uvolňování paměti, která hlídá nejčastější alokace pomocí new/malloc. Rožšíření o kontrolu alokací realloc/calloc/strdup nebo i třeba exotičtější _expand přenechávám laskavému čtenáři.

Hodit se budou struktura popisující alokovanou paměť

struct MemoryPosition{
          //adresa pameti
          void *ptr;
          //popis alokovane pameti (velikost, misto alokace atd.)
          char position[256];
};

a třída, která obstará práci se seznamem alokované paměti.

class MemoryPositionList{
private:
      bool MallocatedMemoryChecked;
      std::list<MemoryPosition*> mlist;
public:
      MemoryPositionList(bool=false);
      ~MemoryPositionList();
      void Add(MemoryPosition*);
      void Remove(void*);
private:
      void Dump();
};

Jako seznam je zde použit seznam z STL, což nemusí být nejšťastnější řešení, s ohledem na proklamovanou míru jednoduchosti (rozuměj: lenost autora) je však alespoň zpočátku akceptovatelné. Vzhledem k tomu, že i v C++ bývá občas výhodné nezapomínat na klasickou alokaci paměti pomocí malloc, vytvoříme dvě statické instance třídy MemoryPositionList. Paměť alokovanou použitím operátoru new budeme zapisovat do jedné instance, paměť alokovanou pomocí malloc budeme zapisovat do druhé. Výpis neuvolněné paměti (metoda Dump) můžeme volat v destruktoru třídy MemoryPositionList. Díky tomu, že destruktor statických objektů volá překladač na konci programu sám, budeme mít zajištěno vypsání neuvolněné paměti na konci programu.

Implementace metod třídy MemoryPositionList je snadná: Jediný parametr konstruktoru určuje, zda sledujeme pamět alokovovanou pomocí new, nebo pomocí malloc:

MemoryPositionList::MemoryPositionList(bool mem){
      MallocatedMemoryChecked=mem;
}

Přidání nově alokované paměti zařadí příslušný popis pozice paměti na konec seznamu (push_back je metoda std::list):

void MemoryPositionList::Add(MemoryPosition *item){
      mlist.push_back(item);
}

Metoda Remove odstraní ze seznamu odkaz na alokovanou paměť zadanou její adresou (ihned je patrné, že držení informací o alokované paměti v seznamu je nevýhodné, protože nalezení požadované položky při odstraňování může být zdlouhavé, a to dokonce tak, že zpomalení bude i pro debugovací účely neúnosné):

void MemoryPositionList::Remove(void *ptr){
    if(mlist.size()==0) return;
    std::list<MemoryPosition*>::iterator i=mlist.begin();
    while (i!=mlist.end()){
        if((*i)->ptr==ptr){
             mlist.remove(*i);
             return;
        }
        i++;
    }
}

Metoda Dump se postará o vypsání neuvolněné paměti, pokud taková existuje, na standardní chybový výstup:

void MemoryPositionList::Dump(){
    std::list<MemoryPosition*>::iterator i=mlist.begin();
    if(mlist.size()>0){
         if(MallocatedMemoryChecked)
              fprintf(stderr,"Memory leaks caused by malloc detected:\n");
         else
              fprintf(stderr, "Memory leaks caused by new detected:\n");
         while (i!=mlist.end()){
              fprintf(stderr,"%X %s\n",(*i)->ptr, (*i)->position);
              i++;
        }
    }
}

A konečně destructor, který výpis neuvolněné paměti při ukončování programu zavolá.

MemoryPositionList::~MemoryPositionList(){
    Dump();
    std::list<MemoryPosition*>::iterator i=mlist.begin();
    while(i!=mlist.end()){
         if(MallocatedMemoryChecked) free(*i);
         else delete(*i);
         i++;
     }
     mlist.clear();
}

V dalším předpokládejme, že máme dvě statické instance třídy MemoryPositionList:

static MemoryPositionList MallocMemory(true);
static MemoryPositionList NewMemory(false);

Stačí tedy již jen předefinovat operátor new. K tomu potřebujeme vědět, že kromě standardního new(size_t) mohou existovat i přetížené operátory s větším počtem parametrů. V našem případě tedy např.

void* operator new (size_t size, char const * file, int line){
    void *ptr=malloc(size);
    MemoryPosition *m=(MemoryPosition*) malloc(sizeof(MemoryPosition));
    m->ptr=ptr;
    sprintf(m->position,"%s:%d (%d bytes)", file, line, size);
    NewMemory.Add(m);
    return ptr;
}

Zbývá doplnit, že operátor new vrací ukazatel na void, paměť alokovanou pomocí malloc tak není třeba nijak přetypovávat. Při volání operátoru new se první parametr size_t vynechává, neboť velikost objektu doplní překladač za nás. K tomu, abychom věděli, kde paměť alokujeme, pak využijeme standardní makra __FILE__ a __LINE__, za která překladač dosadí jméno zdrojového souboru a číslo řádky. Výše uvedený operátor new pak můžeme zavolat třeba tako:

int *i= new(__FILE__, __LINE__) int;

Nejnovější norma jazyka C, označovaná jako C99, pak navíc zavádí identifikátor __func__, které obsahuje název funkce. Překladač gcc jde ještě dále, když zavádí identifikátory __FUNCTION__ a __PRETTY_FUNCTI­ON__. První je shodný s  __func__, druhý se v C++ rozvíjí v „košatější“ název funkce. Pro úplnost je třeba dodat, že zmíněné identifikátory jsou implicitně kompilátorem deklarovány tak, jako by ve funkci bylo deklarováno

static const char __func__[] = "jmeno_funkce";

a z toho pramení i rozdílné použití v porovnání s makry __FILE__LINE__. Používáme-li např. překladač gcc, můžeme definovat nadstandardní new třeba takto:

void* operator new (size_t size, char const * file, int line, char const *func){
     void *ptr=malloc(size);
     MemoryPosition *m=(MemoryPosition*) malloc(sizeof(MemoryPosition));
     m->ptr=ptr;
     sprintf(m->position,"%s:%d (%d bytes), %s", file, line, size, func);
     NewMemory.Add(m);
     return ptr;
}

Odpovídající operátor delete musí kromě uvolnění paměti ještě odebrat paměť ze seznamu alokované paměti:

void operator delete (void * ptr){
    NewMemory.Remove(ptr);
    free (ptr);
}

Úplně stejně, jako jsme definovali vlastní operátor new, je třeba definovat i operátor new[] a odpovídající delete[]. Pro sledování alokace pomocí malloc pak ještě přidejme debugovací verze dmalloc a dfree takto:

void *dmalloc(const char *file, int line, size_t size){
    void *ptr=malloc(size);
    MemoryPosition *m=(MemoryPosition*)malloc(sizeof(MemoryPosition));
    m->ptr=ptr;
    sprintf(m->position,"%s:%d (%d bytes)", file, line, size);
    MallocMemory.Add(m);
    return ptr;
}

void dfree(const char *file, int line, void *ptr){
    MallocMemory.Remove(ptr);
    free(ptr);
}

Nyní jsme prakticky hotovi, máme k dispozici soubory: memory_check.cpp, memory_check.h a use_memory_chec­k.h a zbývá je použít v praxi. Krátký testovací prográmek

#include <cstdlib>
#include <iostream>

#include "use_memory_check.h"

class testClass{
public:
    testClass();
private:
    int m;
};

testClass::testClass(){m=0;}

int main(){
    testClass *m;
    m=new testClass;
    return 0;
}

nám vypíše kupříkladu:

Memory leaks caused by new detected:
440030 c:\martin\root\memory\main.cpp:16 (4 bytes)

K úplné spokojenosti ale stále chybí několik detailů. Předně náš nový operátor new neošetřuje případ, kdy nedojde k alokaci požadované paměti, třeba z důvodu, že jí není dostatek. Standardní operátor new by v takové situaci měl vyhodit výjimku bad_alloc. Některé překladače (např. Visual C++) nás proto varují:

davame_internetu_obsah
       

warning C4291: ‚void *__cdecl operator new(unsigned int,const char *,int)‘: no matching operator delete found; memory will not be freed if initialization throws an exception

Rovněž držení informací o alokované paměti v seznamu (natož v std::list) není příliš efektivní a spíše by se hodilo použití hašovacích tabulek. A konečně, známou chybu, kdy pole je akolováno pomocí new[], ale „smazáno“ použitím delete (což je pochopitelně memory leak) takto jednoduše napsaná kontrola neodhalí. A to jsme se ještě nezmínili o těžkostech ve vícevláknových aplikacích… Opravdu nechcete použít Valgrind?

Školení: Linux – Firemní server

Na třídenním školení se naučíte nainstalovat a spravovat kompletní linuxový server do Vaší firmy se všemi základními službami, které potřebujete pro provoz Vaší sítě, firemních emailů a webových stránek.

Podrobnější informace a přihláška

Ohodnoťte jako ve škole:
Průměrná známka 3,14

Přehled názorů

zasnu
Libor 31. 3. 2005 00:27
Nový
fiha
coccyx 31. 3. 2005 06:45
Nový
linuxi jadro a nedostatek pameti
Tomas Navara 31. 3. 2005 07:32
Nový
├ 
Re: linuxi jadro a nedostatek pameti
Jindrich Makovicka 31. 3. 2005 09:23
Nový
└ 
Re: linuxi jadro a nedostatek pameti
Karel Zak 31. 3. 2005 09:45
Nový
 
└ 
Re: linuxi jadro a nedostatek pameti
Zdenek 31. 3. 2005 10:01
Nový
 
 
└ 
Re: linuxi jadro a nedostatek pameti
Ivan Brezina 31. 3. 2005 11:24
Nový
Pouzitelnost, rychlost atd.
kvr 31. 3. 2005 10:48
Nový
├ 
Re: Pouzitelnost, rychlost atd.
Mormegil 31. 3. 2005 12:26
Nový
│
└ 
Re: Pouzitelnost, rychlost atd.
phoenix 31. 3. 2005 15:54
Nový
│
 
├ 
Re: Pouzitelnost, rychlost atd.
Ped 31. 3. 2005 16:50
Nový
│
 
└ 
Re: Pouzitelnost, rychlost atd.
Mormegil 31. 3. 2005 17:23
Nový
└ 
Re: Pouzitelnost, rychlost atd.
gep 3. 4. 2005 11:06
Nový
Článek neřeší základní problém vzniku
Oldřich Jedlička 31. 3. 2005 12:21
Nový
├ 
Re: Článek neřeší základní problém vzniku
Oldřich Jedlička 31. 3. 2005 12:30
Nový
│
└ 
Re: Článek neřeší základní problém vzniku
majka 31. 3. 2005 13:24
Nový
└ 
Re: Článek neřeší základní problém vzniku
Kamil Podlešák 31. 3. 2005 16:42
Nový
režie
mike 31. 3. 2005 21:12
Nový
└ 
Re: režie
Oldřich Jedlička 1. 4. 2005 08:43
Nový
Poolovana alokace
Michal Kára 1. 4. 2005 08:14
Nový
dovetek
martin kukacka 1. 4. 2005 11:22
Nový
└ 
Re: dovetek
Oldřich Jedlička 5. 4. 2005 10:29
Nový
       

Tento text je již více než dva měsíce starý. Chcete-li na něj reagovat v diskusi, pravděpodobně vám již nikdo neodpoví. Pro řešení aktuálních problémů doporučujeme využít naše diskusní fórum.

Zasílat nově přidané příspěvky e-mailem