¿Forma extraña de asignar una matriz bidimensional?

110

En un proyecto, alguien empujó esta línea:

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

Lo que supuestamente crea una matriz bidimensional de (n + 1) * (n + 1) dobles.

Supuestamente , digo, porque hasta ahora, nadie a quien le pregunté podría decirme qué hace esto, exactamente, ni de dónde se originó o por qué debería funcionar (lo que supuestamente sí funciona, pero aún no lo estoy comprando).

Quizás me estoy perdiendo algo obvio, pero agradecería que alguien me lo explicara arriba. Porque personalmente, me sentiría mucho mejor si usáramos algo que realmente comprendamos.

Usuario1291
fuente
15
Incluso si funciona, dispare el empujador.
Martin James
22
@MartinJames ¿Por qué? ¿Cómo asignaría una matriz 2D en la memoria adyacente de otra manera? ¿Una matriz 1D destrozada? Esa es la programación de la década de 1990, ahora tenemos VLA.
Lundin
43
Para el registro, esa es la única forma de asignar dinámicamente una matriz 2D real.
Quentin
15
@Kninnug No, eso declararía una matriz de punteros, no una matriz 2D. Si desea una matriz 2D, no hay ninguna razón por la que quiera asignar una matriz de punteros. Es lento debido a la fragmentación del montón y la mala utilización de la memoria caché, y no es seguro porque no puede usarlo como una matriz (con memcpy, etc.) Además, el código está inflado. Necesitaría múltiples llamadas free () y sería fácil provocar pérdidas de memoria. Por muy extendido que pueda ser ese código, es simplemente malo.
Lundin
15
Este problema habría sido más claro de explicar / responder si la muestra no hubiera usado las mismas dimensiones valoradas, n+1pero en cambiodouble (*e)[rows] = malloc(columns * sizeof *e);
chux - Restablezca a Monica

Respuestas:

87

La variable ees un puntero a una matriz de n + 1elementos de tipo double.

El uso del operador de desreferencia en ele da el tipo base del ecual es "matriz de n + 1elementos de tipo double".

La mallocllamada simplemente toma el tipo base de e(explicado anteriormente) y obtiene su tamaño, lo multiplica por n + 1y pasa ese tamaño a la mallocfunción. Esencialmente, asigna una matriz de n + 1matrices de n + 1elementos de double.

Algún tipo programador
fuente
3
@MartinJames sizeof(*e)es equivalente a sizeof(double [n + 1]). Multiplique eso con n + 1y obtendrá suficiente.
Algún tipo programador
24
@MartinJames: ¿Qué tiene de malo? No es tan llamativo, garantiza que las filas asignadas sean contiguas y puede indexarlo como cualquier otra matriz 2D. Utilizo mucho este modismo en mi propio código.
John Bode
3
Puede parecer obvio, pero esto solo funciona para matrices cuadradas (mismas dimensiones).
Jens
18
@Jens: Solo en el sentido de que si pones n+1ambas dimensiones, el resultado será cuadrado. Si lo hace double (*e)[cols] = malloc(rows * sizeof(*e));, el resultado tendrá el número de filas y columnas que haya especificado.
user2357112 apoya a Monica
9
@ user2357112 Ahora que preferiría verlo. Incluso si eso significa que debe agregar int rows = n+1y int cols = n+1. Dios nos salve del código inteligente.
candied_orange
56

Esta es la forma típica en que debe asignar matrices 2D de forma dinámica.

  • ees un puntero de matriz a una matriz de tipo double [n+1].
  • sizeof(*e)por lo tanto, da el tipo del tipo apuntado, que es el tamaño de una double [n+1]matriz.
  • Asigna espacio para n+1tales matrices.
  • Establece el puntero de matriz epara que apunte a la primera matriz de esta matriz de matrices.
  • Esto le permite usar eas e[i][j]para acceder a elementos individuales en la matriz 2D.

Personalmente creo que este estilo es mucho más fácil de leer:

double (*e)[n+1] = malloc( sizeof(double[n+1][n+1]) );
Lundin
fuente
12
Buena respuesta, excepto que no estoy de acuerdo con su estilo sugerido, prefiriendo el ptr = malloc(sizeof *ptr * count)estilo.
chux - Reincorporar a Monica
Buena respuesta, y me gusta tu estilo preferido. Una ligera mejora podría ser señalar que debe hacerlo de esta manera porque puede haber un relleno entre las filas que debe tenerse en cuenta. (Por lo menos, creo que esa es la razón por la que hay que hacerlo de esta manera.) (Quiero saber si estoy equivocado.)
davidbak
2
@davidbak Eso es lo mismo. La sintaxis de la matriz es simplemente un código autodocumentado: dice "asignar espacio para una matriz 2D" con el código fuente en sí.
Lundin
1
@davidbak Nota: una pequeña desventaja del comentario malloc(row*col*sizeof(double)) ocurre cuando se row*col*sizeof()desborda, pero no sizeof()*row*coles así. (por ejemplo, fila, col son int)
chux - Reincorporar a Monica
7
@davidbak: sizeof *e * (n+1)es más fácil de mantener; si alguna vez decide cambiar el tipo base (de doublea long double, por ejemplo), entonces solo necesita cambiar la declaración de e; no es necesario modificar la sizeofexpresión en la mallocllamada (lo que ahorra tiempo y lo protege de cambiarla en un lugar pero no en el otro). sizeof *esiempre te dará el tamaño correcto.
John Bode
39

Este idioma cae naturalmente de la asignación de matrices 1D. Comencemos con la asignación de una matriz 1D de algún tipo arbitrario T:

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

Simple, ¿verdad? La expresión *p tiene tipo T, por lo que sizeof *pda el mismo resultado que sizeof (T), por lo que estamos asignando suficiente espacio para una Nmatriz de elementos de T. Esto es cierto para cualquier tipoT .

Ahora, sustituyamos Tpor un tipo de matriz como R [10]. Entonces nuestra asignación se convierte en

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

La semántica aquí es exactamente la misma que la del método de asignación 1D; todo lo que ha cambiado es el tipo de p. En lugar de T *, es ahora R (*)[10]. La expresión *ptiene tipo Tque es tipo R [10], por lo que sizeof *pes equivalente a lo sizeof (T)que es equivalente a sizeof (R [10]). Así que estamos asignando suficiente espacio para una matriz Npor 10elemento de R.

Podemos llevar esto aún más lejos si queremos; supongamos que Res en sí mismo un tipo de matriz int [5]. Sustituye eso Ry obtenemos

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

Lo mismo pasa - sizeof *pes la misma que sizeof (int [10][5]), y que terminan asignando un trozo contiguo de memoria lo suficientemente grande como para contener una Npor 10por 5variedad de int.

Así que ese es el lado de la asignación; ¿qué pasa con el lado de acceso?

Recuerde que la []operación de subíndice se define en términos de aritmética de punteros: a[i]se define como *(a + i)1 . Por tanto, el operador de subíndice desreferencia [] implícitamente un puntero. Si pes un puntero a T, puede acceder al valor apuntado ya sea desreferenciando explícitamente con el *operador unario :

T x = *p;

o usando el []operador de subíndice:

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

Por lo tanto, si papunta al primer elemento de una matriz , puede acceder a cualquier elemento de esa matriz utilizando un subíndice en el puntero 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

Ahora, hagamos nuestra operación de sustitución nuevamente y reemplacemos Tcon el tipo de matriz 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 diferencia inmediatamente aparente; estamos desreferenciando explícitamente pantes de aplicar el operador de subíndice. No queremos subíndices en p, queremos subíndices en lo que p apunta (en este caso, la matriz arr[0] ). Dado que unario *tiene menor precedencia que el []operador de subíndice , tenemos que usar paréntesis para agrupar explícitamente pcon *. Pero recuerde que desde arriba *pes lo mismo que p[0], por lo que podemos sustituirlo por

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

o solo

R x = p[0][i];

Por lo tanto, si papunta a una matriz 2D, podemos indexar en esa matriz de la siguiente pmanera:

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

Llevando esto a la misma conclusión anterior y sustituyéndolo Rpor 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];

Esto funciona igual si papunta a una matriz regular o si apunta a la memoria asignada malloc.

Este modismo tiene los siguientes beneficios:

  1. Es simple: solo una línea de código, a diferencia del método de asignación por partes
    T **arr = malloc( sizeof *arr * N );
    if ( arr )
    {
      for ( size_t i = 0; i < N; i++ )
      {
        arr[i] = malloc( sizeof *arr[i] * M );
      }
    }
  2. Todas las filas de la matriz asignada son * contiguas *, lo que no es el caso con el método de asignación por partes anterior;
  3. Desasignar la matriz es igual de fácil con una sola llamada a free. Nuevamente, no es cierto con el método de asignación por partes, donde debe desasignar cada uno arr[i]antes de poder desasignar arr.

A veces, es preferible el método de asignación por partes, como cuando su montón está muy fragmentado y no puede asignar su memoria como un fragmento contiguo, o desea asignar una matriz "irregular" donde cada fila puede tener una longitud diferente. Pero en general, esta es la mejor manera de hacerlo.


1. Recuerde que las matrices no son punteros; en cambio, las expresiones de matriz se convierten en expresiones de puntero según sea necesario.

John Bode
fuente
4
+1 Me gusta la forma en que presenta el concepto: asignar una serie de elementos es posible para cualquier tipo, incluso si esos elementos son arreglos en sí mismos.
logo_writer
1
Su explicación es realmente buena, pero tenga en cuenta que la asignación de memoria contigua no es un beneficio hasta que realmente la necesite. La memoria contigua es más cara que la no contigua. Para matrices 2D simples, no hay diferencia en el diseño de la memoria para usted (excepto por el número de líneas para asignación y desasignación), así que prefiera usar memoria no contigua.
Oleg Lokshyn