tutorial girona español curso c arrays pointers c99

c - girona - qgis manual



¿C99 garantiza que las matrices son contiguas? (3)

Siguiendo un hilo de comentarios en otra pregunta, vine a debatir qué es y qué no está definido en el estándar C99 sobre arrays C.

Básicamente, cuando defino una matriz 2D como int a[5][5] , el C99 estándar garantiza que no será un bloque de ints contiguo, puedo convertirlo en (int *)a y estar seguro de que tendré una matriz 1D válida de 25 pulgadas.

Como entiendo que el estándar, la propiedad anterior está implícita en el tamaño de la definición y en la aritmética de punteros, pero otros parecen estar en desacuerdo y dice que convertir a (int *) la estructura anterior da un comportamiento indefinido (incluso si están de acuerdo en que todas las implementaciones existentes realmente asignan valores contiguos).

Más específicamente, si pensamos en una implementación que podría instrumentar matrices para verificar los límites de la matriz para todas las dimensiones y devolver algún tipo de error al acceder a la matriz 1D, o si no proporciona el acceso correcto a los elementos que se encuentran en la primera fila. ¿Podría tal implementación ser compilante estándar? Y en este caso, qué partes de la norma C99 son relevantes.


Deberíamos comenzar por inspeccionar lo que realmente es un [5] [5]. Los tipos involucrados son:

  • En t
  • matriz [5] de ints
  • matriz [5] de matrices

No hay una matriz [25] de entradas involucradas.

Es correcto que el tamaño de la semántica implica que la matriz como un todo es contigua. La matriz [5] de entradas debe tener 5 * sizeof (int), y aplicada recursivamente, un [5] [5] debe tener 5 * 5 * sizeof (int). No hay espacio para relleno adicional.

Además, la matriz como un todo debe estar funcionando cuando se le da a memset, memmove o memcpy con el tamaño de. También debe ser posible iterar sobre toda la matriz con un (char *). Así que una iteración válida es:

int a[5][5], i, *pi; char *pc; pc = (char *)(&a[0][0]); for (i = 0; i < 25; i++) { pi = (int *)pc; DoSomething(pi); pc += sizeof(int); }

Hacer lo mismo con un (int *) sería un comportamiento indefinido, porque, como se dijo, no hay una matriz [25] de int involucrada. Usar una unión como en la respuesta de Christoph también debería ser válido. Pero hay otro punto que complica aún más esto, el operador de igualdad:

6.5.9.6 Dos punteros se comparan igual si y solo si ambos son punteros nulos, ambos son punteros al mismo objeto (incluido un puntero a un objeto y un subobjeto al principio) o función, ambos son punteros al pasado del último elemento de el mismo objeto de matriz, o uno es un puntero a uno más allá del final de un objeto de matriz y el otro es un puntero al inicio de un objeto de matriz diferente que sigue inmediatamente al primer objeto de matriz en el espacio de direcciones. 91)

91) Dos objetos pueden estar adyacentes en la memoria porque son elementos adyacentes de una matriz más grande o miembros adyacentes de una estructura sin relleno entre ellos, o porque la implementación eligió colocarlos así, aunque no estén relacionados. Si las operaciones de puntero no válidas anteriores (como los accesos fuera de los límites de la matriz) produjeron un comportamiento indefinido, las comparaciones posteriores también producen un comportamiento indefinido.

Esto significa para esto:

int a[5][5], *i1, *i2; i1 = &a[0][0] + 5; i2 = &a[1][0];

i1 se compara como igual a i2. Pero cuando se recorre la matriz con un (int *), sigue siendo un comportamiento indefinido, ya que originalmente se deriva del primer subconjunto. No se convierte mágicamente en un puntero en el segundo subarreglo.

Incluso al hacer esto

char *c = (char *)(&a[0][0]) + 5*sizeof(int); int *i3 = (int *)c;

no ayuda Se compara igual a i1 e i2, pero no se deriva de ninguno de los subarrays; es un puntero a un solo int o una matriz [1] de int en el mejor de los casos.

No considero esto un error en la norma. Es al revés: permitir esto introduciría un caso especial que viola el sistema de tipos para matrices o las reglas para la aritmética de punteros o ambas. Puede considerarse una definición que falta, pero no un error.

Entonces, incluso si el diseño de memoria para un [5] [5] es idéntico al diseño de un [25], y el mismo bucle que usa un (char *) se puede usar para iterar sobre ambos, se permite que una implementación funcione. arriba si uno es usado como el otro. No sé por qué debería o conozco ninguna implementación que lo haga, y quizás haya un solo hecho en el Estándar que no se menciona hasta ahora que lo hace un comportamiento bien definido. Hasta entonces, lo consideraría indefinido y me mantendría en el lado seguro.


He añadido algunos comentarios más a nuestra discusión original .

sizeof semántica implica que int a[5][5] es contiguo, pero visitar los 25 enteros incrementando un puntero como int *p = *a es un comportamiento indefinido: la aritmética del puntero solo se define siempre y cuando todos los punteros invocados estén dentro (o Un elemento después del último elemento de) la misma matriz, como p &a[2][1] ej. &a[2][1] y &a[3][1] no lo hace (ver C99 sección 6.5.6).

En principio, puede solucionar este problema con casting &a , que tiene el tipo int (*)[5][5] - to int (*)[25] . Esto es legal de acuerdo con 6.3.2.3 §7, ya que no viola ningún requisito de alineación. El problema es que acceder a los enteros a través de este nuevo puntero es ilegal ya que viola las reglas de aliasing en 6.5 §7. Puede solucionar esto utilizando una union para el punning de tipo (consulte la nota 82 en TC3):

int *p = ((union { int multi[5][5]; int flat[25]; } *)&a)->flat;

Esto es, por lo que puedo decir, que cumple con los estándares C99.


Si la matriz es estática, como su matriz int a[5][5] , se garantiza que sea contigua.