2010-05-12 14 views
11

Używam C w niektórych projektach do uzyskania tytułu magistra, ale nigdy nie budowałem z nim oprogramowania produkcyjnego. (.NET & Javascript to mój chleb i masło.) Oczywiście, potrzeba free() pamięci, że jesteś malloc() jest krytyczna w C. To jest w porządku, dobrze i dobrze, jeśli możesz zrobić obie w jednej rutynie. Ale wraz z rozwojem programów i pogłębianiem się struktur, śledzenie tego, co zostało uznane za wolne, staje się coraz trudniejsze.Oswajanie bestii malloc/free - porady i wskazówki

Rozejrzałem się po interwebs i znalazłem tylko kilka ogólnych zaleceń dla tego. Podejrzewam, że niektórzy z waszych długoletnich programistów C wymyślili własne wzorce i praktyki, aby uprościć ten proces i zatrzymać zło przed sobą.

A więc: w jaki sposób zaleca się tworzenie struktur programów C tak, aby alokacje dynamiczne nie były przecieki pamięci?

+4

Ten długi czas kodera C przełączono na C++, a problem zniknął. –

+1

@Neil Butterworth: Albo to ((częściej) problem stał się znacznie gorszy. – sharptooth

+1

@sharptooth: Nie jestem fanem C++, ale zajmuje się całą klasą problemów związanych z zarządzaniem pamięcią. Oczywiście pozostawiając naprawdę trudne. :-) – JesperE

Odpowiedz

8

Projekt według umowy. Upewnij się, że każdy komentarz dotyczący funkcji jest jednoznaczny na temat higieny pamięci - to znaczy, czy jest to mallocs i czyim obowiązkiem jest uwolnić to, co zostało przydzielone, i czy bierze na siebie odpowiedzialność za wszystko, co zostało przekazane. I BĄDŹ ZGODNE Z Twoimi funkcjami.

Na przykład, plik nagłówka może zawierać coś takiego:

/* Sets up a new FooBar context with the given frobnication level. 
* The new context will be allocated and stored in *rv; 
* call destroy_foobar to clean it up. 
* Returns 0 for success, or a negative errno value if something went wrong. */ 
int create_foobar(struct foobar** rv, int frobnication_level); 

/* Tidies up and tears down a FooBar context. ctx will be zeroed and freed. */ 
void destroy_foobar(struct foobar* ctx); 

serdecznie poparcia rady użyć Valgrind, to naprawdę fantastyczne narzędzie do śledzenia wycieków pamięci i pamięć nieprawidłowy dostęp. Jeśli nie korzystasz z Linuksa, to podobne narzędzie, choć mniej funkcjonalne, to Electric Fence.

+0

Podoba mi się to. Widzę, że działa dobrze w wielu sytuacjach. – roufamatic

+2

Możesz rozważyć zrobienie destroy_foobar (struct foobar ** ctx), aby można było NULL out wartość wskaźnika również w źródle. Zapewnia również symetrię między wywołaniami: struct foobar * myFoobar; create_foobar (& myFoobar, 0); robić coś(); destroy_foobar (i myFoobar); Oba połączenia wyglądają podobnie. – jmucchiello

+0

Do tej pory użyłem tego podejścia + symetryczny destrcutor jmucchiello i użyłem valgrind do sprawdzenia mojej pracy. Ciągle jeszcze trochę do zrobienia, ale użycie tego podejścia z rygorem zmniejszyło mój ślad pamięci po zakończeniu 2 MB. Lub jak mówią dzieci, w00t! – roufamatic

3

Znalazłem Valgrind jako ogromną pomoc w utrzymaniu zdrowego zarządzania pamięcią. Powie Ci, gdzie masz dostęp do pamięci, która nie została przydzielona i gdzie zapominasz zwolnić pamięć (i całą masę rzeczy).

Istnieją również wyższe poziomy sposobów zarządzania pamięcią w C, na przykład moje wykorzystanie pul pamięci (na przykład Apache APR, na przykład).

+1

+1: Valgrind jest bardzo przydatny – tur1ng

4

Nie będzie niezawodny (ale można się tego spodziewać po C) i może być trudny do zrobienia z dużą ilością istniejącego kodu, ale pomaga, jeśli wyraźnie udokumentujesz swój kod i zawsze dokładnie określisz, kto jest właścicielem pamięć i kto jest odpowiedzialny za jej uwolnienie (i za pomocą którego alokatora/deallocator). Ponadto, nie bój się używać goto, aby wymusić idiom jednego wejścia/pojedynczego wyjścia dla nietrywialnych funkcji alokujących zasoby.

5

Duże projekty często wykorzystują technikę "pool": w tym przypadku każda alokacja jest powiązana z pulą i automatycznie zwalniana, gdy pula jest. Jest to bardzo wygodne, jeśli możesz wykonać skomplikowane przetwarzanie z pojedynczą pulą tymczasową, która może zostać zwolniona za jednym zamachem, gdy skończysz. Subpole są zazwyczaj możliwe; często widzisz wzorzec taki jak:

void process_all_items(void *items, int num_items, pool *p) 
{ 
    pool *sp = allocate_subpool(p); 
    int i; 

    for (i = 0; i < num_items; i++) 
    { 
     // perform lots of work using sp 

     clear_pool(sp); /* Clear the subpool for each iteration */ 
    } 
} 

Dzięki manipulowaniu strunami jest to o wiele łatwiejsze. Funkcje łańcuchowe pobierałyby argument pool, w którym przydzieliłyby swoją wartość zwracaną, która również będzie wartością zwracaną.

Wadami są:

  • Przyznana życia obiektu mogą być nieco dłużej, ponieważ trzeba czekać na basenie zostać wyczyszczone lub zwolniona.
  • Kończysz przekazywanie dodatkowych argumentów do puli funkcjom (gdzieś dla nich, aby uzyskać potrzebne alokacje).
+0

+1, jest to rzecz trudna do opanowania na własną rękę! – roufamatic

2

Wyróżnij alokatory i deallocatory dla każdego typu. Biorąc pod uwagę definicję typu

typedef struct foo 
{ 
    int x; 
    double y; 
    char *z; 
} Foo; 

utworzyć funkcję przydzielania

Foo *createFoo(int x, double y, char *z) 
{ 
    Foo *newFoo = NULL; 
    char *zcpy = copyStr(z); 

    if (zcpy) 
    { 
    newFoo = malloc(sizeof *newFoo); 
    if (newFoo) 
    { 
     newFoo->x = x; 
     newFoo->y = y; 
     newFoo->z = zcpy; 
    } 
    } 
    return newFoo; 
} 

funkcję kopiowania

Foo *copyFoo(Foo f) 
{ 
    Foo *newFoo = createFoo(f.x, f.y, f.z); 
    return newFoo; 
} 

i funkcję dealokator

void destroyFoo(Foo **f) 
{ 
    deleteStr(&((*f)->z)); 
    free(*f); 
    *f = NULL; 
} 

pamiętać, że createFoo() z kolei nazywa copyStr() funkcja odpowiedzialna za przydzielanie pamięci i kopiowanie zawartości ciągu. Zauważ również, że jeśli copyStr() zawiedzie i zwróci wartość NULL, to newFoo nie będzie próbował przydzielić pamięci i zwróci wartość NULL. Podobnie, destroyFoo() wywoła funkcję do usunięcia pamięci dla z przed zwolnieniem reszty struktury. Wreszcie, destroyFoo() ustawia wartość f na NULL.

Kluczem jest to, że alokator i deallocator przekazują odpowiedzialność innym funkcjom, jeśli elementy członkowskie również wymagają zarządzania pamięcią. Tak, jak twoje typy uzyskać bardziej skomplikowane, można ponownie użyć tych podzielników tak:

typedef struct bar 
{ 
    Foo *f; 
    Bletch *b; 
} Bar; 

Bar *createBar(Foo f, Bletch b) 
{ 
    Bar *newBar = NULL; 
    Foo *fcpy = copyFoo(f); 
    Bletch *bcpy = copyBar(b); 

    if (fcpy && bcpy) 
    { 
    newBar = malloc(sizeof *newBar); 
    if (newBar) 
    { 
     newBar->f = fcpy; 
     newBar->b = bcpy; 
    } 
    } 
    else 
    { 
    free(fcpy); 
    free(bcpy); 
    } 

    return newBar; 
} 

Bar *copyBar(Bar b) 
{ 
    Bar *newBar = createBar(b.f, b.b); 
    return newBar; 
} 

void destroyBar(Bar **b) 
{ 
    destroyFoo(&((*b)->f)); 
    destroyBletch(&((*b)->b)); 
    free(*b); 
    *b = NULL; 
} 

Oczywiście przykład ten zakłada, że ​​członkowie nie mają życia poza ich pojemników. Nie zawsze tak jest i musisz odpowiednio zaprojektować interfejs. Jednak powinno to dać ci poczucie, co należy zrobić.

Dzięki temu można przydzielić i zwolnić pamięć dla obiektów w spójnej, ściśle określonej kolejności, co stanowi 80% bitwy w zarządzaniu pamięcią. Pozostałe 20% upewnia się, że każde wywołanie przydziału jest zrównoważone przez dealera, który jest naprawdę trudny.

edit

Zmieniono wywołań funkcji delete* tak że ja przechodząc odpowiednie typy.

Powiązane problemy