Los arrays y los punteros son cosas distintas en C que se comportan igual la mayor parte del tiempo. Ese "la mayor parte del tiempo" es lo que vuelve resbaladizo el tema: el código que trata un array como un puntero suele funcionar, hasta el día en que aterriza en uno de los dos lugares donde la diferencia importa y la respuesta es silenciosamente incorrecta.
Esta página traza la línea con precisión.
El decaimiento del array: la regla detrás de todo
En casi toda expresión, el nombre de un array se convierte automáticamente en un puntero a su primer elemento. Esto se llama decaimiento. arr pasa a ser &arr[0], con tipo int *.
El nombre del array y la dirección de su primer elemento se imprimen idénticos. Eso es el decaimiento en acción, y es por lo que puedes asignar un array a un puntero sin ningún &.
Hay exactamente tres lugares donde el decaimiento no ocurre: como operando de sizeof, como operando de &, y al inicializar un array de caracteres a partir de un literal de cadena. En todos los demás, da por hecho el decaimiento.
arr[i] es literalmente *(arr + i)
El estándar de C define el operador de subíndice en términos de aritmética de punteros: a[b] significa *(a + b). La indexación no es una característica aparte: es una abreviatura.
De ahí salen dos consecuencias. Primera, un puntero se puede indexar: p[2] es *(p + 2), que es por lo que las funciones que reciben un puntero pueden seguir usando la sintaxis familiar arr[i]. Segunda, 2[arr] compila: *(2 + arr) es la misma dirección que *(arr + 2). Nadie escribe eso fuera de un examen, pero zanja la pregunta de si la indexación es "de verdad" aritmética de punteros.
Pasar un array a una función
Por el decaimiento, una función nunca recibe un array. Recibe un puntero.
Hay tres cosas que rescatar de ese ejemplo.
El tamaño en int arr[100] es documentación, nada más. El compilador reescribe el parámetro como int *arr y nunca comprueba que hayas pasado 100 de nada.
La longitud debe viajar aparte. La función tiene una dirección y ninguna idea de hasta dónde se extienden los datos. Es por esto que prácticamente cada función de C que recibe un array recibe también un conteo: memcpy(dst, src, n), fread(ptr, size, count, f), qsort(base, nmemb, size, cmp). Las cadenas son la excepción solo porque llevan su propia marca de fin, el '\0'.
Pasar un puntero es barato y los datos se comparten. No se hace ninguna copia del array, así que la función puede modificar los elementos de quien la llama, que a menudo es exactamente lo que quieres:
Marca los parámetros de solo lectura como const int *, igual que hace print_all. Documenta la intención y deja que el compilador detecte una escritura accidental.
La trampa de sizeof
Aquí está el error que atrapa a todo programador de C una vez. sizeof es uno de los dos lugares donde no ocurre el decaimiento, así que da el tamaño real del array donde el array está declarado, y el tamaño de un puntero en cualquier sitio donde el array haya decaído.
En main, sizeof data son 40 bytes y la longitud da 10. Dentro de inspect, sizeof arr es el tamaño de un puntero —8 en una máquina de 64 bits— y la "longitud" sale 2. El código se ve idéntico y está silenciosamente mal.
La regla que se deduce: calcula la longitud de un array solo en el ámbito donde el array fue declarado, y pásala desde ahí.
El & de un array no es del mismo tipo
El otro lugar donde no ocurre el decaimiento es el operador &. &arr es un puntero al array entero, no a su primer elemento. Ambos guardan la misma dirección; la diferencia es lo que significa "suma 1".
La misma dirección de partida, distinta zancada. arr tiene tipo int * y avanza de a un elemento; &arr tiene tipo int (*)[5] y avanza el array entero. Este es el mecanismo que hace que pasar una fila de un array 2D funcione, cosa que se cubre en arrays multidimensionales.
Puntero a array frente a array de punteros
Dos declaraciones que se parecen y significan cosas opuestas:
int *p[5]; // array de 5 punteros a int
int (*q)[5]; // puntero a un array de 5 ints
[] tiene mayor precedencia que *, así que int *p[5] se analiza como "p es un array, de punteros". Los paréntesis de int (*q)[5] obligan al * a asociarse primero: "q es un puntero, a un array".
int *p[5] int (*q)[5]
p[0] -> [ int ] q -> [ int | int | int | int | int ]
p[1] -> [ int ] un bloque contiguo de 5
p[2] -> [ int ]
p[3] -> [ int ]
p[4] -> [ int ]
cinco direcciones separadas
Un array de punteros es el caso común, y ya lo has usado: char *argv[] es un array de punteros a cadenas, una por argumento de la línea de comandos. También es la forma de guardar una lista de cadenas de distintas longitudes sin desperdiciar espacio.
Fíjate en (*q)[0]: desreferencia el puntero para obtener el array y luego indéxalo. Los paréntesis vuelven a ser obligatorios, por la misma razón de precedencia.
Dónde difieren de verdad arrays y punteros
Ten presente esta tabla corta y la confusión desaparece:
Array int arr[10] | Puntero int *p | |
|---|---|---|
| Qué es | 10 ints dispuestos en memoria | una variable que guarda una dirección |
sizeof | 40: el bloque entero | 8: solo el puntero |
| Asignable | no: arr = x; es un error | sí: p = x; está bien |
& da | int (*)[10] | int ** |
| De dónde sale la memoria | declarada con el array | de donde sea que lo apuntes |
El nombre de un array no es una variable que guarda una dirección: es el almacenamiento, y la dirección se calcula bajo demanda. Por eso no puedes asignarle nada.
Preguntas frecuentes
¿Los arrays y los punteros son lo mismo en C?
No, pero es fácil confundirlos porque el nombre de un array decae a un puntero a su primer elemento en casi toda expresión. El array en sí es un bloque de elementos con un tamaño conocido; un puntero es una sola variable que guarda una dirección. sizeof y & son los dos lugares donde se nota la diferencia.
¿Por qué sizeof(arr) da la respuesta equivocada dentro de una función?
Porque el parámetro no es un array. void f(int arr[]) se reescribe en silencio como void f(int *arr), así que sizeof(arr) mide un puntero (8 bytes en la mayoría de las máquinas), no el array original. Pasa la longitud como un parámetro aparte.
¿Cuál es la diferencia entre int *p[5] e int (*p)[5]?
int *p[5] es un array de 5 punteros a int. int (*p)[5] es un único puntero a un array de 5 ints. Los paréntesis asocian primero el * al nombre; sin ellos gana [], porque tiene mayor precedencia.
¿arr[i] es realmente lo mismo que *(arr + i)?
Sí: el estándar define a[b] como *(a + b). Por eso también compila y funciona el extraño i[arr]: la suma es conmutativa, así que *(i + arr) es el mismo elemento. Nunca lo escribas en código real, pero demuestra la regla.