¿Cómo determina este fragmento de código el tamaño de la matriz sin usar sizeof()?
arrays language-lawyer (3)
Pasando por algunas preguntas de la entrevista con C, encontré una pregunta que dice "¿Cómo encontrar el tamaño de una matriz en C sin usar el operador sizeof?", Con la siguiente solución. Funciona, pero no puedo entender por qué.
#include <stdio.h>
int main() {
int a[] = {100, 200, 300, 400, 500};
int size = 0;
size = *(&a + 1) - a;
printf("%d/n", size);
return 0;
}
Como era de esperar, devuelve 5.
edición: la gente señaló this respuesta, pero la sintaxis difiere un poco, es decir, el método de indexación
size = (&arr)[1] - arr;
así que creo que ambas preguntas son válidas y tienen un enfoque ligeramente diferente al problema. ¡Gracias a todos por la inmensa ayuda y la completa explicación!
Cuando agrega 1 a un puntero, el resultado es la ubicación del siguiente objeto en una secuencia de objetos del tipo apuntado (es decir, una matriz).
Si
p
apunta a un objeto
int
, entonces
p + 1
apuntará al siguiente
int
en una secuencia.
Si
p
apunta a una matriz de 5 elementos de
int
(en este caso, la expresión
&a
),
p + 1
apuntará a la siguiente
matriz de 5 elementos de
int
en una secuencia.
Restar dos punteros (siempre que ambos apunten al mismo objeto de matriz, o uno apunte uno más allá del último elemento de la matriz) produce el número de objetos (elementos de matriz) entre esos dos punteros.
La expresión
&a
produce la dirección de
a
, y tiene el tipo
int (*)[5]
(puntero a la matriz de 5 elementos de
int
).
La expresión
&a + 1
produce la dirección de la siguiente matriz de 5 elementos de
int
sigue
a
, y también tiene el tipo
int (*)[5]
.
La expresión
*(&a + 1)
anula el resultado de
&a + 1
, de modo que produce la dirección del primer
int
sigue al último elemento de
a
, y tiene el tipo
int [5]
, que en este contexto "decae" a un expresión de tipo
int *
.
De manera similar, la expresión
a
"decae" en un puntero al primer elemento de la matriz y tiene el tipo
int *
.
Una imagen puede ayudar:
int [5] int (*)[5] int int *
+---+ +---+
| | <- &a | | <- a
| - | +---+
| | | | <- a + 1
| - | +---+
| | | |
| - | +---+
| | | |
| - | +---+
| | | |
+---+ +---+
| | <- &a + 1 | | <- *(&a + 1)
| - | +---+
| | | |
| - | +---+
| | | |
| - | +---+
| | | |
| - | +---+
| | | |
+---+ +---+
Estas son dos vistas del mismo almacenamiento: a la izquierda, lo vemos como una secuencia de arreglos de 5 elementos de
int
, mientras que a la derecha, lo vemos como una secuencia de
int
.
También muestro las diversas expresiones y sus tipos.
Tenga en cuenta que la expresión
*(&a + 1)
produce
un comportamiento indefinido
:
...
Si el resultado apunta a uno más allá del último elemento del objeto de la matriz, no se utilizará como el operando de un operador unario * que se evalúa.
Borrador en línea C 2011 , 6.5.6 / 9
Esta línea es de la mayor importancia:
size = *(&a + 1) - a;
Como puede ver, primero toma la dirección de
a
y le agrega una.
Luego, hace referencia a ese puntero y resta el valor original de a.
La aritmética de punteros en C hace que esto devuelva el número de elementos en la matriz, o
5
.
Agregar uno y
&a
es un puntero a la siguiente matriz de 5
int
s después de
a
.
Después de eso, este código anula el puntero resultante y resta
a
(un tipo de matriz que se ha desintegrado en un puntero) de eso, dando el número de elementos en la matriz.
Detalles sobre cómo funciona la aritmética de punteros:
Supongamos que tiene un puntero
xyz
que apunta a un tipo
int
y contiene el valor
(int *)160
.
Cuando restas cualquier número de
xyz
, C especifica que la cantidad real que se resta de
xyz
es ese número multiplicado por el tamaño del tipo al que apunta.
Por ejemplo, si restó
5
de
xyz
, el valor de
xyz
resultante sería
xyz - (sizeof(*xyz) * 5)
si la aritmética de punteros no se aplicara.
Como
a
es una matriz de
5
tipos
int
, el valor resultante será 5. Sin embargo, esto no funcionará con un puntero, solo con una matriz.
Si lo intentas con un puntero, el resultado siempre será
1
.
Aquí hay un pequeño ejemplo que muestra las direcciones y cómo esto no está definido. El lado izquierdo muestra las direcciones:
a + 0 | [a[0]] | &a points to this
a + 1 | [a[1]]
a + 2 | [a[2]]
a + 3 | [a[3]]
a + 4 | [a[4]] | end of array
a + 5 | [a[5]] | &a+1 points to this; accessing past array when dereferenced
Esto significa que el código está restando
a
a
&a[5]
(o
a+5
), dando
5
.
Tenga en cuenta que este es un comportamiento indefinido, y no debe utilizarse bajo ninguna circunstancia. No espere que el comportamiento de esto sea consistente en todas las plataformas, y no lo use en programas de producción.
Hmm, sospecho que esto es algo que no hubiera funcionado en los primeros días de C. Aunque es inteligente.
Tomando los pasos uno a la vez:
-
&a
obtiene un puntero a un objeto de tipo int [5] -
+1
obtiene el siguiente objeto de este tipo asumiendo que hay una matriz de esos -
*
convierte efectivamente esa dirección en puntero de tipo a int -
-a
resta los dos punteros int, devolviendo el recuento de instancias int entre ellos.
No estoy seguro de que sea completamente legal (en este sentido, me refiero a la legalización del lenguaje, no funcionará en la práctica), dado el tipo de operaciones de tipo que se están realizando
Por ejemplo, solo está "permitido" para restar dos punteros cuando apuntan a elementos en la misma matriz.
*(&a+1)
se sintetizó al acceder a otra matriz, aunque se trate de una matriz principal, por lo que no es realmente un puntero a la misma matriz que
a
.
Además, aunque se le permite sintetizar un puntero más allá del último elemento de una matriz, y puede tratar cualquier objeto como una matriz de 1 elemento, la operación de desreferenciación (
*
) no está "permitida" en este puntero sintetizado, aunque ¡No tiene ningún comportamiento en este caso!
Sospecho que en los primeros días de C (sintaxis de K&R, ¿alguien?), Una matriz se descomponía en un puntero mucho más rápidamente, por lo que el
*(&a+1)
podría devolver la dirección del siguiente puntero de tipo int **.
Las definiciones más rigurosas de C ++ moderno definitivamente permiten que el puntero al tipo de matriz exista y conozcan el tamaño de la matriz, y probablemente los estándares de C hayan seguido su ejemplo.
Todo el código de función C solo toma los punteros como argumentos, por lo que la diferencia técnica visible es mínima.
Pero solo estoy adivinando aquí.
Este tipo de pregunta de legalidad detallada generalmente se aplica a un intérprete de C, o una herramienta de tipo de pelusa, en lugar del código compilado. Un intérprete podría implementar una matriz 2D como una matriz de punteros a matrices, porque hay una característica de tiempo de ejecución menos que implementar, en cuyo caso, la desreferenciación del +1 sería fatal, e incluso si funcionara daría una respuesta incorrecta.
Otra posible debilidad puede ser que el compilador de C pueda alinear la matriz externa.
Imagínese si se tratara de una matriz de 5 caracteres (
char arr[5]
), cuando el programa realiza
&a+1
está invocando el comportamiento de "matriz de matriz".
El compilador podría decidir que una matriz de matriz de 5 caracteres (
char arr[][5]
) se genere realmente como una matriz de matriz de 8 caracteres (
char arr[][8]
), de modo que la matriz externa se alinee bien.
El código que estamos discutiendo ahora reportaría el tamaño del arreglo como 8, no 5. No estoy diciendo que un compilador en particular definitivamente haría esto, pero podría hacerlo.