Cuando trabajas con arrays en C, es vital entender que, en la gran mayoría de las expresiones, el nombre de un array no actúa como un objeto contenedor, sino que sufre un proceso llamado decaimiento (decay), transformándose implícitamente en un puntero al primer elemento de dicho array (&arr[0]). Esta es una decisión de diseño fundamental para la eficiencia del lenguaje: en lugar de copiar cada elemento de un array de gran tamaño al pasarlo a una función, el compilador simplemente pasa la dirección de memoria de su inicio.
Este decaimiento ocurre casi siempre, excepto en tres escenarios críticos donde el compilador necesita conocer la identidad del objeto array original: cuando usas el operador sizeof (para devolver el tamaño total en bytes del array y no el del puntero), cuando usas el operador de dirección & (que devuelve un puntero al array completo, no al primer elemento), y cuando inicializas un array de caracteres con un literal de cadena.
Debes usar este comportamiento cuando necesites iterar sobre colecciones o pasar buffers a funciones de la biblioteca estándar como memcpy. Sin embargo, ten cuidado: el decaimiento conlleva una pérdida de información de tamaño. Al decaer a un puntero, el tipo de dato pasa de ser “array de N elementos” a ser simplemente “puntero a tipo”. Esto significa que una función que reciba un puntero no tiene forma de saber cuántos elementos contiene el array original; por eso, la convención en C es pasar siempre el tamaño como un parámetro adicional. Asimismo, debes recordar que, a diferencia de un puntero, un array no es una variable modificable: no puedes hacer arr = otro_puntero porque el nombre del array no es una lvalue asignable.
#include <stdio.h>
#include <stddef.h>
/*
* Una función que procesa datos debe recibir el tamaño explícitamente
* debido al decaimiento del array a puntero.
*/
void procesar_datos(int *ptr, size_t tamano) {
printf("--- Dentro de procesar_datos ---\n");
// Error común: sizeof(ptr) devuelve el tamaño del puntero, no del array
printf("sizeof(ptr) en la función: %zu bytes\n", sizeof(ptr));
for (size_t i = 0; i < tamano; ++i) {
// La sintaxis arr[i] es azúcar sintáctico para *(arr + i)
printf("Elemento [%zu]: %d\n", i, ptr[i]);
}
// La aritmética de punteros es simétrica: *(i + ptr) es lo mismo que ptr[i]
// Por tanto, i[ptr] es una expresión válida, aunque poco ortodoxa.
printf("Prueba de simetría (2[ptr]): %d\n", 2[ptr]);
}
int main(void) {
int mi_array[5] = {10, 20, 30, 40, 50};
int *p = mi_array; // mi_array decae a int*
printf("--- En main ---\n");
printf("sizeof(mi_array): %zu bytes (tamaño real del array)\n", sizeof(mi_array));
printf("Dirección de mi_array (arr): %p\n", (void*)mi_array);
printf("Dirección de mi_array (&arr): %p\n", (void*)&mi_array);
// Diferencia en aritmética de punteros:
// mi_array + 1 salta un elemento (int)
// &mi_array + 1 salta el array completo (5 ints)
printf("mi_array + 1: %p\n", (void*)(mi_array + 1));
printf("&mi_array + 1: %p\n", (void*)(&mi_array + 1));
procesar_datos(mi_array, 5);
return 0;
}
Desglose del comportamiento
En el ejemplo anterior, observa cómo sizeof(mi_array) en main devuelve el tamaño total del bloque de memoria (en este caso, 5 * sizeof(int)), mientras que dentro de procesar_datos, sizeof(ptr) devuelve únicamente el tamaño del puntero (típicamente 8 bytes en arquitecturas de 64 bits). El compilador pierde la noción de la dimensión del array en el momento en que el nombre mi_array se pasa como argumento, transformándose en un simple int *ptr.
Fíjate en la diferencia entre mi_array y &mi_array. Aunque ambos imprimen la misma dirección de memoria, sus tipos son distintos: mi_array es un int * (puntero al primer elemento) y &mi_array es un int (*)[5] (puntero al array completo). Esto se manifiesta en la aritmética de punteros: cuando sumamos 1 a mi_array, el compilador calcula la dirección del siguiente int. Pero al sumar 1 a &mi_array, el compilador salta la totalidad de la estructura, es decir, sizeof(mi_array) bytes adelante.
Finalmente, la línea 2[ptr] ilustra la equivalencia de la aritmética de punteros. Dado que ptr[i] se expande internamente como *(ptr + i), la propiedad conmutativa de la suma nos permite escribir *(i + ptr), lo que en C se traduce como i[ptr]. Es una curiosidad técnica que demuestra que la sintaxis de corchetes es solo una capa sobre la aritmética de direcciones.
El error frecuente
Un error clásico ocurre al intentar calcular el número de elementos de un array dentro de una función donde el array ha decaído a un puntero:
void error_logico(int *arr) {
// ERROR: sizeof(arr) devuelve el tamaño del puntero, NO del array.
// El bucle solo iterará 1 o 2 veces (dependiendo de la arquitectura),
// sin importar el tamaño real del array original.
for (size_t i = 0; i < sizeof(arr) / sizeof(arr[0]); ++i) {
printf("%d ", arr[i]);
}
}
Este error es extremadamente peligroso porque no genera un error de compilación. Si el tamaño del array es mayor que el tamaño del puntero, el código parecerá funcionar pero procesará datos incompletos; si es menor, podrías incurrir en un acceso fuera de límites. AddressSanitizer detectaría el desbordamiento si el error causara un acceso ilegal a memoria, pero un error de lógica por truncamiento de tamaño pasará desapercibido en pruebas superficiales.
N° 53