Esiste una best practice per l'allocazione / deallocazione di più matrici dinamiche in C?

4

Mi chiedevo quale sarebbe stato il miglior approcio per allocare / deallocare più matrici dinamiche unidimensionali in C. Questo all'inizio sembra facile, tuttavia, per me è diventato problematico Considre il seguente programma di esempio, che illustra il mio problema:

#include <stdio.h>
#include <stdlib.h>

#define N 10

int my_func(size_t n);


int main(void)
{   
    int ret = my_func(N);
    printf("%d\n", ret); 
    return 0;
}


int my_func(size_t n)
{

    double *x = malloc(n * sizeof (double));
    if (x==NULL) return 0;

    double *y = malloc(n * sizeof (double));
    if (y==NULL) { free(x); return 0; }

    double *z = malloc(n * sizeof (double));
    if (z==NULL) { free(x); free(y); return 0;}

    /* some computations */

    free(x); free(y); free(z);
    return 1;
}

Assegnazione e controllo di x , è semplice. Tuttavia, se l'allocazione di y fallisce, è necessario liberare x . Se c'è un ulteriore array, come z nell'esempio, e la sua allocazione fallisce, ci si deve preoccupare di x e y . E come sempre, tutta la memoria deve essere liberata alla fine della funzione. In generale, il controllo di un'allocazione fallita da qualche parte nel programma richiede di occuparsi di tutti i blocchi di memoria allocati in precedenza.

Ho pensato che sarebbe stato "più semplice" automatizzare la deallocazione di tutti i blocchi di memoria allocati in precedenza, così ho trovato la seguente implementazione che utilizza un "registro di memoria" per memorizzare i puntatori ai blocchi di memoria allocati.

#include <stdio.h>
#include <stdlib.h>

#define N 10
#define N_BLOCKS 3

double *mem_reg[] = { NULL, NULL, NULL };

void clean_up(void);
int my_func(size_t n);


int main(void)
{   
    int ret = my_func(N);
    printf("%d\n", ret); 
    return 0;
}


int my_func(size_t n)
{
    double *x = malloc(n * sizeof (double));
    if (x!=NULL) mem_reg[0] = x; else goto fail;

    double *y = malloc(n * sizeof (double));
    if (x!=NULL) mem_reg[1] = y; else goto fail;

    double *z = malloc(n * sizeof (double));
    if (x!=NULL) mem_reg[2] = z; else goto fail;

    /* some computations */

    clean_up();
    return 1;

fail:
    clean_up();
    return 0;
}

void clean_up(void)
{
    for (size_t i=0; i<N_BLOCKS; i++)
    {
        if (mem_reg[i] != NULL)
            free(mem_reg[i]);
    }
}

L'advanatge del secondo esempio è che esiste un punto di uscita singolo in my_func definito e l'etichetta fail . Quindi, anche se dovessi verificare alcuni errori nei calcoli (non mostrati negli esempi), potrei semplicemente goto fail per pulire facilmente tutte le allocazioni dinamiche. Lo svantaggio è che il codice del secondo esempio non è più semplice del codice del primo esempio.

Potresti aver riconosciuto che non ho molta esperienza in C, quindi questa domanda. C'è un vantaggio in uno di entrambi gli esempi, che non ho visto? Allora quale è da preferire. O c'è anche un modo migliore?

    
posta MaxPowers 27.11.2018 - 10:50
fonte

7 risposte

8

Risposta seria: usa C ++ per beneficiare di RAII. Puoi mescolare liberamente C e C ++ all'interno della tua base di codice e mantenere la compatibilità C dichiarando le tue funzioni come extern "C" . Ma C ++ potrebbe non essere praticabile per vari motivi.

In tal caso, torna a questo consiglio quasi obsoleto: una funzione dovrebbe avere solo un punto di ingresso e un solo punto di uscita. Ad esempio, esattamente un return . Qualsiasi risorsa viene allocata in modo lineare e distribuita in ordine inverso prima di questo ritorno. Ciò significa che, dove il codice esistente viene restituito direttamente, dovrebbe impostare una variabile sul valore restituito e andare alla sezione di pulizia.

Goto è dannoso? Non qui, dove è necessario implementare un comportamento che non è disponibile utilizzando le normali utilità della lingua. (Ma notare che il RAII del C ++ è esattamente una tale utility che rende inutile questo uso del goto.)

int my_func(size_t n)
{
  double *x = NULL;
  double *y = NULL;
  double *z = NULL;
  int ok = 1;

  x = malloc(n * sizeof(*x));
  if (x == NULL) { ok = 0; goto cleanup; }

  y = malloc(n * sizeof(*y));
  if (y == NULL) { ok = 0; goto cleanup; }

  z = malloc(n * sizeof(*z));
  if (z == NULL) { ok = 0; goto cleanup; }

  /* some computations */

cleanup:
  free(z); free(y); free(x);
  return ok;
}
    
risposta data 27.11.2018 - 12:27
fonte
3

Chiamare gratis (NULL) è innocuo in qualsiasi implementazione corretta di C.

Quindi la soluzione più semplice è quella di malloc tutti e tre gli array. Verifica che tutti e tre non siano nulli prima di eseguire il calcolo. Quindi libera tutti e tre i puntatori alla fine.

    
risposta data 27.11.2018 - 11:18
fonte
2

Il tuo metodo sta facendo troppe cose. Tu:

allocare memoria
eseguire operazioni
gestire gli errori
deallocare la memoria
restituisce un risultato

Immagina il tuo codice se ogni metodo ha fatto tutto questo. Non è gestibile, quindi lo dividi. L'allocazione della memoria non è banale da avvolgere, quindi lasciatela a posto. Ma le operazioni e la gestione degli errori devono andare. Scrivi un metodo separato che accetta il tre double* s ed esegue operazioni su di essi, restituendo un risultato significativo (quindi il risultato letterale del calcolo o un codice di successo / errore).

// keep your method
int my_func(size_t n) {
    // allocation will always be ugly in C
    double *x = malloc(n * sizeof (double));
    double *y = malloc(n * sizeof (double));
    double *z = malloc(n * sizeof (double));

    // operations should have their own methods
    int foo = my_computations(x, y, z);

    // with allocation comes cleaning duty
    free(x);
    free(y);
    free(z);

    // methods should return a result
    return foo;
}

// this method now assumes correct allocation
int my_computations(double* x, double* y, double* z) {
    if (x==NULL || x==NULL || x==NULL) { return NOT_OK; }

    /* some computations */

    return OK;
}

Questo è un piccolo cambiamento, ma mantiene le tue esigenze separate dal calcolo. Ti permette di espandere liberamente il metodo di calcolo, usarlo da altri metodi, scrivere test per esso, ecc. Tutto senza dover affrontare la parte allocazione e deallocazione. Risolve anche il tuo problema e ti protegge da esso in futuro.

    
risposta data 10.12.2018 - 16:12
fonte
1

Il secondo approccio ti porta quasi nel posto giusto. Dai un'occhiata al tuo codice e noterai uno schema ripetuto che equivale a "allocare il blocco successivo e liberare eventuali blocchi assegnati in precedenza in caso di errore". È un algoritmo maturo per trasformarsi in una funzione generica. Non sei il primo ad imbattersi in questo problema, e mi capita di averne uno nella mia cassetta degli attrezzi che lo copre (e batte l'inevitabile prolisse expanation):

// Allocate multiple blocks of memory, making sure no allocated
// block remains unfreed in the event of a failure.  Returns 0
// on success, nonzero otherwise.  Nothing placed in 'results'
// should be considered valid on failure.

int multi_malloc(
  size_t blocks,        // Number of blocks
  size_t *block_sizes,  // Size of each block
  void **results        // Where to put the allocated pointers
  )
{
  assert(blocks > 0);
  assert(block_sizes != NULL);
  assert(results != NULL);

  size_t block;

  for ( block = 0 ; block < blocks; ++block ) {
    void *allocated = malloc(block_sizes[block]);
    if ( allocated == NULL ) {
      break;
    }
    results[block] = allocated;
  }

  // Declare success if all blocks were allocated
  if ( block == blocks ) {
    return 0;
  }

  // Otherwise, free allocated memory and declare failure.
  while ( block > 0 ) {
    --block;
    assert(results[block] != NULL);
    free(results[block]);
  }
  assert(block == 0);

  return 1;
}

Mettere questa attività in una funzione significa non dover scrivere di nuovo lo stesso codice una volta che lo hai corretto. Ancora più importante, questo approccio funziona per un numero arbitrariamente grande di blocchi con un numero arbitrario di tipi diversi senza essere ingombrante.

Chiamarlo richiede un po 'di più in termini di ginnastica, ma è un compromesso decente per poter avere l'intero insieme di allocazioni riuscite o fallite come gruppo.

#define ELEMENTS 100
#define LENGTH_OF(x) ( (sizeof(x)) / (sizeof(*(x))) )

int main(int argc, char **argv)
{
  // Size of each block with one differing type just for grins.
  size_t blocks[] = {
    (ELEMENTS * sizeof(double)),
    (ELEMENTS * sizeof(double)),
    (ELEMENTS * sizeof(int))
  };

  void *memory[LENGTH_OF(blocks)];

  if ( multi_malloc( LENGTH_OF(blocks), blocks, memory ) ) {
    puts("Allocation failed.");
    exit(1);
  }

  double *x = (double *)(memory[0]);
  double *y = (double *)(memory[1]);
  int    *z = (int    *)(memory[2]);

  // ...Computations...

  multi_free(LENGTH_OF(blocks), memory);
}

Lascerò scrivere multi_free() come esercizio per te.

    
risposta data 27.11.2018 - 13:20
fonte
1

Se prima puoi allocare tutto ciò di cui hai bisogno, fare i calcoli e poi rilasciare quelle risorse, valuta se la suddivisione in due funzioni potrebbe aiutarti:

int my_func(size_t n)
{
    double *x, *y, *z;
    int r = 0;
    if ((x = malloc(n * sizeof *x))) {
        if ((y = malloc(n * sizeof *y))) {
            if ((z = malloc(n * sizeof *z))) {
                r = my_func_impl(x, y, z, n);
                free(z);
            }
            free(y);
        }
        free(x);
    }
    return r;
}

Puoi semplificare le cose accettando l'eventuale lieve penalità per free(NULL) :

int my_func(size_t n)
{
    double *x = 0, *y = 0, *z = 0;
    int r = 0;
    if ((x = malloc(n * sizeof *x))
    && (y = malloc(n * sizeof *y))
    && (z = malloc(n * sizeof *z)))
        r = my_func_impl(x, y, z, n);
    free(z);
    free(y);
    free(x);
    return r;
}

O preferibilmente, unisci queste allocazioni:

int my_func(size_t n)
{
    double *x = malloc(3 * n * sizeof *x);
    if (x) {
        double *y = x + n;
        double *z = y + n;
        int r = my_func_impl(x, y, z, n);
        free(x);
    }
    return r;
}

Naturalmente, se ci sono diversi tipi di risorse, la coalescenza è impossibile. L'uso giudizioso di goto non è un peccato in C.

Potresti chiederti perché detesto sizeof(TYPE) . Il problema è che il tipo di assegnatario e il tipo per cui è dimensionato potrebbe cambiare in modo indipendente. L'utilizzo di sizeof *pointer invece li accoppia strongmente.

    
risposta data 27.11.2018 - 13:37
fonte
1

Per essere un po 'strani e diversi dalle risposte esistenti, quando i tipi sono omogenei quasi certamente lo farei:

double* xyz = malloc(n*3 * sizeof *xyz);
if (xyz)
{
    double* x = xyz;
    double* y = x + n;
    double* z = y + n;
    ...
    free(xyz);
    return success;
}
return error;

Potrei non preoccuparmi se i tipi non sono omogenei e hanno requisiti di allineamento diversi, ma quasi certamente farei quanto sopra se sono tutti gli array dinamici di double e non solo semplificano il codice un po 'in questo modo ma anche ridurre il numero di allocazioni e deallocazioni di heap superflui.

Oggi uso C in dosi molto piccole per piccole parti del sistema molto core che spesso gestiscono solo bit e byte (es: implementazioni di allocatori di memoria di livello più basso che non beneficiano affatto del tipo sicurezza come fornita in, diciamo, C ++, e dove la gestione delle eccezioni potrebbe complessificarsi invece di semplificare l'implementazione) e tendono ad essere critici in termini di prestazioni, quindi in genere voglio mettere un po 'in anticipo lo sforzo per mantenere le allocazioni / deallocazioni dell'heap un minimo tipicamente insieme a preservare la località spaziale e così via nei rari casi in cui raggiungo C.

Se sono mescolati come:

int* x = malloc(...);
double* y = malloc(...);
char* z = malloc(...);

Quindi tendo a fare una cosa del genere:

if (x && y && z)
{
    ...
}
free(x);
free(y);
free(z);

That's not necessarily less efficient practically speaking if you can allocate those in advance since it's not necessarily doing any more work in the non-exceptional execution paths when all three allocations succeed.

... o usa goto etichetta di pulizia in fondo (che sono d'accordo con molte altre risposte è una pratica ragionevole in C per ridurre la probabilità di errore umano).

I thought it would be "easier" to automate the deallocation of all previously allocated memory blocks, so I came up with the following implementation that uses a "memory registry" to store pointers to the allocated memory blocks.

Se sei tentato di generalizzare la gestione delle risorse in questo modo, farei assolutamente eco al suggerimento di usare C ++.

    
risposta data 10.12.2018 - 15:11
fonte
0

Ho avuto un problema simile quando stavo scrivendo un programma che doveva allocare un sacco di stringhe, fare un po 'di elaborazione e quindi rilasciare le stringhe.

Ho finito per scrivere un semplice allocatore di memoria in pool. Chiederei all'allocatore una stringa di dimensioni particolari. L'allocatore alloca un blocco grande se necessario, quindi mi fornisce un puntatore alla posizione appropriata nel blocco, utilizzando quel blocco per le allocazioni finché non è stato riempito. I blocchi erano incatenati insieme e alla fine ho avuto una sola chiamata per deallocare tutti i blocchi.

Un vantaggio di parte era che segnavo l'inizio dello spazio libero ogni volta che assegnavo, e controllavo che non avessi avuto un sovraccarico del buffer sulla prossima allocazione. In questo caso particolare ho avuto un'idea abbastanza precisa di quali stringhe di dimensioni avrei dovuto gestire, quindi è stato abbastanza semplice scegliere una dimensione di blocco che mi permettesse di allocare un gruppo di stringhe.

È un po 'più di lavoro fare una soluzione generale, ma poi è nella tua casella degli strumenti per ogni volta che ne hai bisogno.

    
risposta data 10.12.2018 - 16:14
fonte

Leggi altre domande sui tag