¿Cómo se formatean las matrices multidimensionales en la memoria?

9 minutos de lectura

avatar de usuario
Chris Cooper

En C, sé que puedo asignar dinámicamente una matriz bidimensional en el montón usando el siguiente código:

int** someNumbers = malloc(arrayRows*sizeof(int*));

for (i = 0; i < arrayRows; i++) {
    someNumbers[i] = malloc(arrayColumns*sizeof(int));
}

Claramente, esto en realidad crea una matriz unidimensional de punteros a un grupo de matrices unidimensionales separadas de enteros, y “El Sistema” puede descubrir a qué me refiero cuando pido:

someNumbers[4][2];

Pero cuando declaro estáticamente una matriz 2D, como en la siguiente línea…:

int someNumbers[ARRAY_ROWS][ARRAY_COLUMNS];

… ¿se crea una estructura similar en la pila, o es completamente diferente? (es decir, ¿es una matriz 1D de punteros? Si no, ¿qué es y cómo se descubren las referencias a él?)

Además, cuando dije, “El Sistema”, ¿qué es realmente responsable de averiguarlo? ¿El núcleo? ¿O el compilador de C lo soluciona mientras compila?

  • @toohonestforthissite De hecho. Para ampliar eso: Bucle y llamando malloc() no da como resultado una matriz N-dimensional.. Da como resultado matrices de punteros. [to arrays of pointers[…]]para separar completamente unidimensional arreglos Ver Asignación correcta de matrices multidimensionales para ver cómo asignar CIERTO matriz N-dimensional.

    –Andrew Henle

    11 de enero de 2020 a las 14:16

avatar de usuario
carl norum

Una matriz bidimensional estática se parece a una matriz de matrices: simplemente se presenta de forma contigua en la memoria. Las matrices no son lo mismo que los punteros, pero debido a que a menudo se pueden usar de manera bastante intercambiable, a veces puede resultar confuso. Sin embargo, el compilador realiza un seguimiento adecuado, lo que hace que todo se alinee muy bien. Debe tener cuidado con las matrices 2D estáticas como menciona, ya que si intenta pasar una a una función que toma un int ** parámetro, van a pasar cosas malas. He aquí un ejemplo rápido:

int array1[3][2] = {{0, 1}, {2, 3}, {4, 5}};

En la memoria se ve así:

0 1 2 3 4 5

exactamente lo mismo que:

int array2[6] = { 0, 1, 2, 3, 4, 5 };

Pero si intentas pasar array1 a esta función:

void function1(int **a);

recibirá una advertencia (y la aplicación no podrá acceder a la matriz correctamente):

warning: passing argument 1 of ‘function1’ from incompatible pointer type

Porque una matriz 2D no es lo mismo que int **. La descomposición automática de una matriz en un puntero solo tiene “un nivel de profundidad”, por así decirlo. Necesitas declarar la función como:

void function2(int a[][2]);

o

void function2(int a[3][2]);

Para hacer todo feliz.

Este mismo concepto se extiende a norte-matrices dimensionales. Sin embargo, aprovechar este tipo de negocios divertidos en su aplicación generalmente solo hace que sea más difícil de entender. Así que ten cuidado ahí fuera.

  • Gracias por la explicación. Así que “función vacía2 (int a[][2]);” aceptará 2D declarados estática y dinámicamente? Y supongo que sigue siendo una buena práctica/esencial pasar la longitud de la matriz también si la primera dimensión se deja como []?

    – Chris Cooper

    2 abr 2010 a las 17:32

  • @Chris, no lo creo: le resultará difícil hacer que C agite una matriz asignada globalmente o apilada en un montón de punteros.

    –Carl Norum

    03/04/2010 a las 16:00

  • @JasonK. – no. Las matrices no son punteros. Los arreglos “decaen” en punteros en algunos contextos, pero son absolutamente no lo mismo.

    –Carl Norum

    10 de octubre de 2016 a las 1:25

  • Para ser claros: Sí, Chris, “todavía es una buena práctica pasar la longitud de la matriz” como un parámetro separado; de lo contrario, use std::array o std::vector (que es C++, no C antiguo). Creo que estamos de acuerdo con @CarlNorum tanto conceptualmente para nuevos usuarios como en la práctica, para citar a Anders Kaseorg en Quora: “El primer paso para aprender C es comprender que los punteros y las matrices son lo mismo. El segundo paso es comprender que los punteros y las matrices son diferentes”.

    – Jason K.

    15/10/2016 a las 21:23

  • @JasonK. “El primer paso para aprender C es comprender que los punteros y las matrices son lo mismo”. – ¡Esta cita es muy incorrecta y engañosa! De hecho, es el paso más importante para entender que son no lo mismo, pero que las matrices son convertido a un puntero a la primer elemento para la mayoría de los operadores! sizeof(int[100]) != sizeof(int *) (a menos que encuentre una plataforma con 100 * sizeof(int) bytes/intpero eso es otra cosa.

    – demasiado honesto para este sitio

    18/11/2016 a las 18:40

avatar de usuario
coste y flete

La respuesta se basa en la idea de que C realmente no tener Arreglos 2D: tiene arreglos de arreglos. Cuando declaras esto:

int someNumbers[4][2];

estas pidiendo someNumbers ser una matriz de 4 elementos, donde cada elemento de esa matriz es de tipo int [2] (que es a su vez una matriz de 2 ints).

La otra parte del rompecabezas es que las matrices siempre se disponen de forma contigua en la memoria. Si pides:

sometype_t array[4];

entonces eso siempre se verá así:

| sometype_t | sometype_t | sometype_t | sometype_t |

(4 sometype_t objetos colocados uno al lado del otro, sin espacios entre ellos). Así que en tu someNumbers array-of-arrays, se verá así:

| int [2]    | int [2]    | int [2]    | int [2]    |

Y cada int [2] El elemento es en sí mismo una matriz, que se ve así:

| int        | int        |

Entonces, en general, obtienes esto:

| int | int  | int | int  | int | int  | int | int  |

  • mirar el diseño final me hace pensar que en un[][] se puede acceder como int *… ¿verdad?

    – Narcisse Doudieu Siewe

    22 mayo 2015 a las 15:54

  • @user3238855: los tipos no son compatibles, pero si obtiene un puntero al primero int en la matriz de matrices (por ejemplo, evaluando a[0] o &a[0][0]) entonces sí, puede compensar eso para acceder secuencialmente a cada int).

    – café

    22 mayo 2015 a las 22:03

avatar de usuario
sqr163

Supongamos que tenemos a1 y a2 definido e inicializado como a continuación (c99):

int a1[2][2] = {{142,143}, {144,145}};
int **a2 = (int* []){ (int []){242,243}, (int []){244,245} };

a1 es una matriz 2D homogénea con un diseño continuo simple en memoria y expresión (int*)a1 se evalúa como un puntero a su primer elemento:

a1 --> 142 143 144 145

a2 se inicializa a partir de una matriz 2D heterogénea y es un puntero a un valor de tipo int*es decir, expresión de desreferencia *a2 se evalúa en un valor de tipo int*el diseño de la memoria no tiene que ser continuo:

a2 --> p1 p2
       ...
p1 --> 242 243
       ...
p2 --> 244 245

A pesar de la disposición de la memoria y la semántica de acceso totalmente diferentes, la gramática del lenguaje C para las expresiones de acceso a matrices se ve exactamente igual para matrices 2D homogéneas y heterogéneas:

  • expresión a1[1][0] obtendrá valor 144 fuera de a1 formación
  • expresión a2[1][0] obtendrá valor 244 fuera de a2 formación

El compilador sabe que la expresión de acceso para a1 opera en tipo int[2][2]cuando la expresión de acceso para a2 opera en tipo int**. El código ensamblador generado seguirá la semántica de acceso homogéneo o heterogéneo.

El código generalmente falla en tiempo de ejecución cuando una matriz de tipo int[N][M] se convierte en tipo y luego se accede como tipo int**por ejemplo:

((int**)a1)[1][0]   //crash on dereference of a value of type 'int'

avatar de usuario
Kangai

unsigned char MultiArray[5][2]={{0,1},{2,3},{4,5},{6,7},{8,9}};

en memoria es igual a:

unsigned char SingleArray[10]={0,1,2,3,4,5,6,7,8,9};

En respuesta a su también: Ambos, aunque el compilador está haciendo la mayor parte del trabajo pesado.

En el caso de arreglos asignados estáticamente, “El Sistema” será el compilador. Reservará la memoria como lo haría con cualquier variable de pila.

En el caso de la matriz malloc, “El Sistema” será el implementador de malloc (generalmente el kernel). Todo lo que el compilador asignará es el puntero base.

El compilador siempre manejará el tipo como se declara, excepto en el ejemplo que dio Carl, donde puede descubrir el uso intercambiable. Es por eso que si pasas en un [][] a una función debe asumir que es un plano asignado estáticamente, donde ** se supone que es un puntero a puntero.

  • @Jon L. No diría que el kernel implementa malloc, sino la libc sobre las primitivas del kernel (como brk)

    –Manuel Selva

    2 de noviembre de 2016 a las 15:36


  • @ManuelSelva: Dónde y cómo malloc se implementa no está especificado por el estándar y se deja a la implementación, resp. medioambiente. Para entornos independientes, es opcional, como todas las partes de la biblioteca estándar que requieren funciones de enlace (eso es lo que realmente dan como resultado los requisitos, no literalmente lo que establece el estándar). Para algunos entornos alojados modernos, de hecho se basa en las funciones del kernel, ya sea las cosas completas o (por ejemplo, Linux) como escribiste usando tanto stdlib como kernel-primitives. Para sistemas de un solo proceso de memoria no virtual, solo puede ser stdlib.

    – demasiado honesto para este sitio

    18/11/2016 a las 18:46


avatar de usuario
AlfaGoku

Para acceder a una matriz 2D en particular, considere el mapa de memoria para una declaración de matriz como se muestra en el código a continuación:

    0  1
a[0]0  1
a[1]2  3

Para acceder a cada elemento, basta con pasar la matriz que le interesa como parámetro a la función. Luego use el desplazamiento para la columna para acceder a cada elemento individualmente.

int a[2][2] ={{0,1},{2,3}};

void f1(int *ptr);

void f1(int *ptr)
{
    int a=0;
    int b=0;
    a=ptr[0];
    b=ptr[1];
    printf("%d\n",a);
    printf("%d\n",b);
}

int main()
{
   f1(a[0]);
   f1(a[1]);
    return 0;
}

  • @Jon L. No diría que el kernel implementa malloc, sino la libc sobre las primitivas del kernel (como brk)

    –Manuel Selva

    2 de noviembre de 2016 a las 15:36


  • @ManuelSelva: Dónde y cómo malloc se implementa no está especificado por el estándar y se deja a la implementación, resp. medioambiente. Para entornos independientes, es opcional, como todas las partes de la biblioteca estándar que requieren funciones de enlace (eso es lo que realmente dan como resultado los requisitos, no literalmente lo que establece el estándar). Para algunos entornos alojados modernos, de hecho se basa en las funciones del kernel, ya sea las cosas completas o (por ejemplo, Linux) como escribiste usando tanto stdlib como kernel-primitives. Para sistemas de un solo proceso de memoria no virtual, solo puede ser stdlib.

    – demasiado honesto para este sitio

    18/11/2016 a las 18:46


¿Ha sido útil esta solución?

Esta web utiliza cookies propias y de terceros para su correcto funcionamiento y para fines analíticos y para mostrarte publicidad relacionada con sus preferencias en base a un perfil elaborado a partir de tus hábitos de navegación. Al hacer clic en el botón Aceptar, acepta el uso de estas tecnologías y el procesamiento de tus datos para estos propósitos. Configurar y más información
Privacidad