Un array es un bloque de tamaño fijo de elementos del mismo tipo, dispuestos uno tras otro en memoria. Esa disposición contigua lo explica todo: es la razón de que indexar sea instantáneo, de que arrays y punteros estén tan relacionados, y de que C pueda darte la dirección del elemento cero y dejar que encuentres el resto con aritmética.
Declarar un array
Tipo de los elementos, nombre y un tamaño entre corchetes:
int scores[5]; // 5 ints
double prices[100]; // 100 doubles
char initials[3]; // 3 chars
El tamaño es el número de elementos, y debe conocerse donde se declara el array: normalmente un literal o una constante definida con #define. La memoria se reserva de inmediato; no se asigna nada después.
Un array local recién declarado contiene basura, no ceros:
La segunda fila puede imprimir ceros, puede imprimir números enormes, y puede cambiar entre ejecuciones o entre una compilación de depuración y una de release. Leer esos valores es comportamiento indefinido; inicializa antes de leer.
Inicializar
Varias formas, todas útiles:
El truco del {0} es el que hay que recordar: todos los elementos que no listes se inicializan a cero, así que un único cero pone a cero el array completo. Los inicializadores designados ([4] = 99) te dejan fijar posiciones concretas y dejar el resto en cero, lo que resulta cómodo para tablas de consulta que están casi vacías.
Algo que no puedes hacer es asignar un array después:
int a[3] = {1, 2, 3};
int b[3];
b = a; // error: un array no es asignable
memcpy(b, a, sizeof a); // así es como se copia uno
Los índices empiezan en cero
El primer elemento es arr[0] y el último es arr[n-1]. No existe arr[n].
Ejecuta ese último bucle y mira las direcciones: cada una está exactamente sizeof(int) bytes más allá de la anterior. Los elementos están realmente dispuestos uno a continuación de otro:
indice 0 1 2 3 4
+--------+--------+--------+--------+--------+
valor | 88 | 95 | 75 | 60 | 100 |
+--------+--------+--------+--------+--------+
offset +0 +4 +8 +12 +16 bytes
^
&scores[0], que es tambien a lo que `scores` se degrada
Indexar desde cero no es una elección arbitraria: hace que el índice sea un desplazamiento desde el inicio, que es exactamente lo que necesita la aritmética de direcciones. scores[i] se define como *(scores + i); ver punteros y arrays.
Obtener la longitud
C no guarda la longitud de un array en ningún sitio en tiempo de ejecución, pero el compilador la conoce, y sizeof te da acceso a ella:
sizeof(arr) / sizeof(arr[0]) es el modismo. Dividir entre arr[0] en lugar de entre un sizeof(double) escrito a mano hace que la línea siga funcionando si cambias el tipo de los elementos.
La trampa: esto solo funciona donde se declaró el array. Pasa el array a una función y el parámetro es un puntero, así que sizeof mide el puntero: normalmente 8 bytes, dando una "longitud" de 1 o 2. Por eso toda función de C que recibe un array recibe también un conteo:
Recorrer un array
La forma estándar es un bucle for desde 0 mientras i < n:
Fíjate en i < n, no en i <= n. Con n elementos los índices válidos van de 0 a n - 1, así que <= da una pasada de más y lee más allá del final. Inicializar hottest y coldest con temps[0] en lugar de con 0 es el otro hábito que vale la pena copiar: arrancar un máximo en cero se rompe en silencio con datos que son todos negativos.
Salirse de los límites es comportamiento indefinido
Esta es la parte de C que sorprende a quien viene de otros lenguajes. No hay comprobación de límites. Ninguna en tiempo de compilación, ninguna en tiempo de ejecución. arr[10] en un array de cinco elementos no es un error: es un cálculo de dirección que el compilador realiza sin decir nada.
int arr[5] = {1, 2, 3, 4, 5};
arr[7] = 99; // escribe 8 bytes mas alla del final del array
int x = arr[-1]; // lee antes del inicio
Lo que pasa después es comportamiento indefinido, y sus síntomas son de una variedad poco útil:
- parece funcionar, porque la memoria que tocó no se estaba usando;
- otra variable cambia sin razón visible;
- el programa falla con un error de segmentación, posiblemente mucho después;
- el comportamiento difiere entre
-O0y-O2.
"Parece funcionar" es el peligroso, porque significa que el error llega a producción. Tres defensas:
- Recorre con
i < n. La mayoría de los desbordamientos son errores de desplazamiento por uno en la condición de un bucle. - Valida los índices que vienen de la entrada.
- Compila con un sanitizer mientras desarrollas.
gcc -fsanitize=address -g prog.cconvierte la mayoría de los desbordamientos en un informe inmediato que nombra el archivo, la línea y el array implicado.
Arrays de otros tipos
La misma sintaxis funciona para cualquier tipo de elemento, incluidas las estructuras:
La línea char word[6] = "hello"; merece una pausa: una cadena de C no es más que un array de char cuyo último byte significativo es '\0'. Ese terminador es la razón de que el array necesite seis huecos para cinco letras.
Arrays de longitud variable, en breve
C99 permite un array cuyo tamaño es un valor de tiempo de ejecución:
int n = get_count();
int buffer[n]; // un array de longitud variable
Dos advertencias. El tamaño queda fijo una vez creado el array, así que un VLA no es una lista que crezca. Y la memoria viene de la pila, así que una n grande o controlada por un atacante puede desbordarla y tumbar el programa, que es la razón de que muchos proyectos, incluido el kernel de Linux, prohíban los VLAs por completo. Además, para las implementaciones de C11 su soporte es opcional.
Cuando el tamaño realmente no se conoce hasta tiempo de ejecución, reserva memoria en su lugar:
Fíjate en que arr[i] se lee de forma idéntica tanto si arr es un array como si es un puntero a memoria reservada: a la sintaxis de indexación le da igual. Todos los detalles en memoria dinámica.
Preguntas frecuentes
¿Cómo se declara un array en C?
Indica el tipo de los elementos, un nombre y un tamaño entre corchetes: int scores[5]; reserva espacio para cinco enteros. El tamaño debe ser una constante conocida donde se declara el array (salvo en los arrays de longitud variable), y los elementos quedan sin inicializar a menos que les des valores.
¿Cómo se obtiene la longitud de un array en C?
sizeof(arr) / sizeof(arr[0]) - el tamaño total en bytes dividido entre el tamaño de un elemento. Solo funciona en el ámbito donde se declaró el array: una vez que lo pasas a una función ya se degradó a un puntero y sizeof mide el puntero.
¿Qué pasa si te sales del final de un array en C?
Nada te detiene. C no comprueba los límites, así que arr[10] en un array de 5 elementos lee o escribe la memoria que haya ahí. Eso es comportamiento indefinido: puede imprimir basura, corromper otra variable o fallar - y puede parecer que funciona hasta que deja de hacerlo.
¿Cómo se inicializan a cero todos los elementos de un array en C?
int arr[100] = {0}; - todos los elementos que no listes se inicializan a cero, así que un solo cero pone a cero el array entero. int arr[100] = {}; también funciona en C23. Sin ningún inicializador, el contenido de un array local es basura indeterminada.