Modo strano di allocare array bidimensionali?


110

In un progetto, qualcuno ha spinto questa linea:

double (*e)[n+1] = malloc((n+1) * sizeof(*e));

Che presumibilmente crea un array bidimensionale di (n + 1) * (n + 1) raddoppia.

Presumibilmente , dico, perché finora nessuno a cui ho chiesto potrebbe dirmi cosa fa esattamente, né da dove proviene o perché dovrebbe funzionare (che presumibilmente funziona, ma non lo sto ancora comprando).

Forse mi manca qualcosa di ovvio, ma apprezzerei se qualcuno potesse spiegarmi sopra. Perché personalmente, mi sentirei molto meglio se usassimo qualcosa che capiamo davvero.


15
Anche se funziona, spara il pusher.
Martin James,

22
@MartinJames Why? Come allochereste altrimenti un array 2D nella memoria adiacente? Un array 1D alterato? Questa è la programmazione degli anni '90, ora abbiamo i VLA.
Lundin

43
Per la cronaca, questo è l'unico modo per allocare dinamicamente un array 2D effettivo.
Quentin

15
@Kninnug No, questo dichiarerebbe un array di puntatori, non un array 2D. Se vuoi un array 2D, non c'è motivo per cui dovresti mai allocare un array di puntatori. È lento a causa della frammentazione dell'heap e dello scarso utilizzo della memoria cache e non è sicuro perché non è possibile utilizzarlo come array (con memcpy ecc.). Inoltre il codice è gonfio. Avresti bisogno di più chiamate free () e sarebbe facile causare perdite di memoria. Per quanto diffuso possa essere tale codice, è semplicemente cattivo.
Lundin

15
Questo problema sarebbe stato più chiaro da spiegare / rispondere se il campione non avesse utilizzato le stesse dimensioni valutate n+1ma invecedouble (*e)[rows] = malloc(columns * sizeof *e);
chux - Reinstate Monica

Risposte:


87

La variabile eè un puntatore a un array di n + 1elementi di tipo double.

L'utilizzo dell'operatore di dereferenziazione su eti dà il tipo di base eche è "array di n + 1elementi di tipo double".

La mallocchiamata prende semplicemente il tipo base di e(spiegato sopra) e ottiene la sua dimensione, la moltiplica per n + 1e passa quella dimensione alla mallocfunzione. Essenzialmente allocare un array di n + 1array di n + 1elementi di double.


3
@MartinJames sizeof(*e)è equivalente a sizeof(double [n + 1]). Moltiplicalo con n + 1e ne avrai abbastanza.
Un tizio programmatore il

24
@ MartinJames: cosa c'è che non va? Non è così sorprendente, garantisce che le righe allocate siano contigue e puoi indicizzarle come qualsiasi altro array 2D. Uso molto questo idioma nel mio codice.
John Bode

3
Può sembrare ovvio, ma funziona solo per array quadrati (stesse dimensioni).
Jens

18
@ Jens: Solo nel senso che se metti n+1entrambe le dimensioni, il risultato sarà quadrato. Se lo fai double (*e)[cols] = malloc(rows * sizeof(*e));, il risultato avrà qualsiasi numero di righe e colonne specificato.
user2357112 supporta Monica

9
@ user2357112 Ora che preferirei di gran lunga vedere. Anche se significa che devi aggiungere int rows = n+1e int cols = n+1. Dio ci salvi da un codice intelligente.
candied_orange

56

Questo è il modo tipico per allocare gli array 2D in modo dinamico.

  • eè un puntatore a un array di tipo double [n+1].
  • sizeof(*e)quindi fornisce il tipo del tipo puntato, che è la dimensione di un double [n+1]array.
  • Assegnate spazio a n+1tali array.
  • Si imposta il puntatore di matrice ein modo che punti al primo array in questo array di array.
  • Ciò consente di utilizzare eas e[i][j]per accedere a singoli elementi nell'array 2D.

Personalmente penso che questo stile sia molto più facile da leggere:

double (*e)[n+1] = malloc( sizeof(double[n+1][n+1]) );

12
Bella risposta tranne che non sono d'accordo con lo stile suggerito, preferendo lo ptr = malloc(sizeof *ptr * count)stile.
chux - Ripristina Monica il

Bella risposta, e mi piace il tuo stile preferito. Un leggero miglioramento potrebbe essere quello di sottolineare che è necessario farlo in questo modo perché potrebbe esserci un riempimento tra le righe che deve essere preso in considerazione. (Almeno, penso che sia il motivo per cui devi farlo in questo modo.) (Fammi sapere se sbaglio.)
davidbak

2
@davidbak È la stessa cosa. La sintassi dell'array è semplicemente codice auto-documentante: dice "alloca spazio per un array 2D" con il codice sorgente stesso.
Lundin

1
@davidbak Nota: uno svantaggio minore del commento si malloc(row*col*sizeof(double)) verifica in caso di row*col*sizeof()overflow, ma non lo sizeof()*row*colfa. (es. riga, colonna int)
chux - Ripristina Monica il

7
@davidbak: sizeof *e * (n+1)è più facile da mantenere; se decidi di cambiare il tipo di base (da doublea long double, ad esempio), devi solo cambiare la dichiarazione di e; non è necessario modificare l' sizeofespressione nella mallocchiamata (che consente di risparmiare tempo e ti protegge dal cambiarla in un punto ma non nell'altro). sizeof *eti darà sempre la taglia giusta.
John Bode

39

Questo idioma cade naturalmente fuori dall'allocazione di array 1D. Cominciamo con l'allocazione di un array 1D di un tipo arbitrario T:

T *p = malloc( sizeof *p * N );

Semplice, vero? L' espressione *p ha tipo T, quindi sizeof *pfornisce lo stesso risultato di sizeof (T), quindi stiamo allocando spazio sufficiente per un Narray -element di T. Questo è vero per qualsiasi tipoT .

Ora sostituiamo Tcon un tipo di array come R [10]. Allora la nostra allocazione diventa

R (*p)[10] = malloc( sizeof *p * N);

La semantica qui è esattamente la stessa del metodo di allocazione 1D; tutto ciò che è cambiato è il tipo di p. Invece di T *, è adesso R (*)[10]. L'espressione *pha tipo Tche è tipo R [10], quindi sizeof *pè equivalente a sizeof (T)quale è equivalente asizeof (R [10]) . Quindi stiamo allocando spazio sufficiente per un array Nper 10elemento di R.

Possiamo andare oltre se vogliamo; supponiamo che Rsia esso stesso un tipo di matrice int [5]. SostituisciloR e otteniamo

int (*p)[10][5] = malloc( sizeof *p * N);

Stessa cosa - sizeof *pè la stessa sizeof (int [10][5]), e finiamo per l'assegnazione di un pezzo di memoria contiguo abbastanza grande da contenere un Nby10 per 5schiera di int.

Quindi questo è il lato dell'allocazione; e il lato di accesso?

Ricorda che l' []operazione in pedice è definita in termini di aritmetica del puntatore: a[i]è definita come *(a + i)1 . Pertanto, l'operatore pedice dereferenzia [] implicitamente un puntatore. Se pè un puntatore a T, puoi accedere al valore a cui si punta o dereferenziando esplicitamente con l' *operatore unario :

T x = *p;

o utilizzando l' []operatore pedice:

T x = p[0]; // identical to *p

Pertanto, se ppunta al primo elemento di un array , puoi accedere a qualsiasi elemento di quell'array utilizzando un pedice sul puntatore p:

T arr[N];
T *p = arr; // expression arr "decays" from type T [N] to T *
...
T x = p[i]; // access the i'th element of arr through pointer p

Ora, eseguiamo di nuovo la nostra operazione di sostituzione e sostituiamo Tcon il tipo di matrice R [10]:

R arr[N][10];
R (*p)[10] = arr; // expression arr "decays" from type R [N][10] to R (*)[10]
...
R x = (*p)[i];

Una differenza immediatamente evidente; stiamo esplicitamente dereferenziando pprima di applicare l'operatore pedice. Non vogliamo indicarci in p, vogliamo indicarci cosa p punta (in questo caso, l' array arr[0] ). Dal momento che unario *ha la precedenza inferiore rispetto l'indice []dell'operatore, dobbiamo usare le parentesi per esplicitamente gruppo pcon *. Ma ricorda dall'alto che *pè lo stesso di p[0], quindi possiamo sostituirlo con

R x = (p[0])[i];

o semplicemente

R x = p[0][i];

Pertanto, se ppunta a un array 2D, possiamo indicizzare in quell'array in questo pmodo:

R x = p[i][j]; // access the i'th element of arr through pointer p;
               // each arr[i] is a 10-element array of R

Portando questo alla stessa conclusione di cui sopra e sostituendolo Rcon int [5]:

int arr[N][10][5];
int (*p)[10][5]; // expression arr "decays" from type int [N][5][10] to int (*)[10][5]
...
int x = p[i][j][k];

Funziona allo stesso modo se ppunta a un array normale o se punta alla memoria allocata malloc.

Questo idioma ha i seguenti vantaggi:

  1. È semplice: solo una riga di codice, al contrario del metodo di allocazione frammentario
    T **arr = malloc( sizeof *arr * N );
    if ( arr )
    {
      for ( size_t i = 0; i < N; i++ )
      {
        arr[i] = malloc( sizeof *arr[i] * M );
      }
    }
  2. Tutte le righe dell'array allocato sono * contigue *, il che non è il caso del metodo di allocazione frammentario sopra;
  3. La deallocazione dell'array è altrettanto semplice con una singola chiamata a free. Ancora una volta, non è vero con il metodo di allocazione frammentario, in cui devi deallocare ciascuno arr[i]prima di poterlo deallocare arr.

A volte il metodo di allocazione frammentario è preferibile, ad esempio quando il tuo heap è molto frammentato e non puoi allocare la tua memoria come un blocco contiguo, o vuoi allocare un array "frastagliato" in cui ogni riga può avere una lunghezza diversa. Ma in generale, questo è il modo migliore per andare.


1. Ricordare che gli array non sono puntatori, ma le espressioni di array vengono convertite in espressioni puntatore se necessario.


4
+1 Mi piace il modo in cui presenti il ​​concetto: l'assegnazione di una serie di elementi è possibile per qualsiasi tipo, anche se quegli elementi sono array stessi.
logo_writer

1
La tua spiegazione è davvero buona, ma nota che l'allocazione della memoria contigua non è un vantaggio finché non ne hai davvero bisogno. La memoria contigua è più costosa di quella non contigua. Per semplici array 2D non vi è alcuna differenza nel layout della memoria (eccetto per il numero di righe per l'allocazione e la deallocazione), quindi preferisci utilizzare la memoria non contigua.
Oleg Lokshyn
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.