2010-05-12 7 views
11

Ho usato C su alcuni progetti per un master, ma non ho mai costruito un software di produzione con esso. (.NET & Javascript sono il mio pane e burro.) Ovviamente, la necessità di memoria free() che si è malloc() è critica in C. Questo va bene, bene e bene se si può fare entrambi in una routine. Ma man mano che i programmi crescono, e le strutture si approfondiscono, tieni traccia di ciò che è stato malloc e dove e cosa è opportuno liberare diventa sempre più difficile.Domare il malloc/bestia gratis - consigli e suggerimenti

Mi sono guardato intorno all'interwebs e ho trovato solo alcuni consigli generici per questo. Quello che sospetto è che alcuni di voi codificatori di C di lunga data hanno escogitato i vostri schemi e le vostre pratiche per semplificare questo processo e mantenere il male di fronte a voi.

Quindi: come si consiglia di strutturare i programmi in C per mantenere le allocazioni dinamiche da perdite di memoria?

+4

Questo codificatore C di lungo periodo è passato a C++ e il problema è andato via. –

+1

@Neil Butterworth: O quello o (poltiglia più spesso) il problema è diventato molto peggio. – sharptooth

+1

@sharptooth: Non sono un fan del C++, ma si prende cura di un'intera classe di problemi di gestione della memoria. Lasciando quelli veramente difficili, ovviamente. :-) – JesperE

risposta

8

Progettazione a contratto. Assicurati che ogni commento di funzione sia esplicito sulla sua igiene della memoria - vale a dire, se è mallocs e di chi è la responsabilità di liberare ciò che è stato assegnato, e se prende la proprietà di tutto ciò che è passato. E ESSERE COERENTE con le tue funzioni.

Ad esempio, il file di intestazione potrebbe contenere qualcosa come:

/* 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); 

Di cuore sottoscrivo il consiglio di utilizzare Valgrind, è uno strumento veramente fantastico per rintracciare le perdite di memoria e la memoria non valida accessi. Se non stai utilizzando Linux, lo Electric Fence è uno strumento simile, anche se meno funzionale.

+0

Mi piace. Posso vederlo funzionare bene in molte situazioni. – roufamatic

+2

Si potrebbe prendere in considerazione la creazione di destroy_foobar (struct foobar ** ctx) in modo da poter estrarre NULL anche il valore del puntatore all'origine. Fornisce anche simmetria tra le chiamate: struct foobar * myFoobar; create_foobar (& myFoobar, 0); fare cose(); destroy_foobar (& myFoobar); Entrambe le chiamate sembrano simili. – jmucchiello

+0

FYI, ho usato questo approccio + destrcutor simmetrico di jmucchiello e ho usato valgrind per controllare il mio lavoro. Ho ancora un po 'di strada da fare, ma usare questo approccio con rigore ha ridotto il mio impronta di memoria alla fine di 2 MB. O come dicono i bambini, w00t! – roufamatic

3

Ho trovato Valgrind di essere di enorme aiuto nel mantenere la mia gestione della memoria sana. Ti dirà dove accedi alla memoria che non è stata allocata e dove ti stai dimenticando di deallocare la memoria (e un sacco di cose).

Esistono anche metodi più avanzati per eseguire la gestione della memoria in C, ad esempio il mio utilizzo di pool di memoria (vedere Apache APR, ad esempio).

+1

+1: Valgrind è estremamente utile – tur1ng

4

Non sarà infallibile (ma questo è probabilmente da aspettarselo con C), e potrebbe essere difficile da fare con un sacco di codice esistente, ma aiuta se documentare chiaramente il vostro codice e di indicare esattamente chi possiede allocato memoria e chi è responsabile per liberarlo (e con quale allocatore/deallocator). Inoltre, non abbiate paura di usare goto per applicare un idioma single-entry/single-exit per funzioni non banali di allocazione delle risorse.

5

Spesso i progetti di grandi dimensioni utilizzano la tecnica "pool": in questo, ogni allocazione è associata a un pool e viene automaticamente liberata quando il pool è. Questo è veramente comodo se puoi eseguire qualche elaborazione complicata con un singolo pool temporaneo, che può quindi essere liberato in un colpo solo quando hai finito. I pool secondari sono generalmente possibili; vedresti spesso uno schema del tipo:

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 */ 
    } 
} 

Rende le cose molto più semplici con la manipolazione delle stringhe. Le funzioni stringa richiedono un argomento pool in cui allocano il valore restituito, che sarà anche il valore restituito.

Gli svantaggi sono:

  • La durata assegnata di un oggetto può essere un po 'più a lungo, dal momento che si deve attendere per la piscina da cancellare o liberati.
  • Si finisce per passare un argomento extra di pool alle funzioni (da qualche parte per poter effettuare qualsiasi allocazione di cui hanno bisogno).
+0

+1, questo è il tipo di cosa che è difficile da imparare da solo! – roufamatic

2

Estrarre gli allocatori e i deallocatori per ciascun tipo. Data una definizione di tipo

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

creare una funzione allocatore

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; 
} 

una funzione di copia

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

e una funzione deallocatore

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

noti che createFoo() a sua volta chiama un copyStr() funzione responsabile dell'allocazione della memoria e della copia del contenuto di una stringa. Si noti inoltre che se copyStr() non riesce e restituisce un valore NULL, quindi newFoo non tenterà di allocare memoria e restituire un valore NULL. Allo stesso modo, destroyFoo() chiamerà una funzione per eliminare la memoria per z prima di liberare il resto della struttura. Infine, destroyFoo() imposta il valore di f su NULL.

La chiave qui è che l'allocatore e il deallocator delegano la responsabilità ad altre funzioni se gli elementi membri richiedono anche la gestione della memoria. Così, come i tipi di ottenere più complicato, è possibile riutilizzare questi ripartitori in questo modo:

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; 
} 

Ovviamente, questo esempio presuppone che i membri non hanno una vita al di fuori dei loro contenitori. Non è sempre così, e dovrai progettare la tua interfaccia di conseguenza. Tuttavia, questo dovrebbe darti un'idea di cosa deve essere fatto.

In questo modo è possibile allocare e deallocare la memoria per gli oggetti in un ordine coerente e ben definito, che rappresenta l'80% della battaglia nella gestione della memoria. L'altro 20% si sta assicurando che ogni allocazione di chiamata sia bilanciata da un deallocator, che è la parte veramente difficile.

modificare

cambiato le chiamate alle funzioni delete* in modo che io sto passando i tipi giusti.

Problemi correlati