Introducción
Hasta ahora, cada variable almacena un solo valor. Pero ¿qué haces si quieres almacenar 10 notas, 100 medidas, o las letras de un texto? Declarar int nota1, nota2, nota3, ... sería inmanejable.
Un array es una serie de variables del mismo tipo, almacenadas una al lado de otra en memoria, accesibles por un número (el índice). En lugar de 10 variables separadas, declaras un solo array de 10 casillas.
Declarar un array
| 1 | int tab[5]; |
Esto crea 5 int uno al lado del otro en memoria. Cada casilla es accesible por su índice.
En memoria
tab : +------+------+------+------+------+
| ? | ? | ? | ? | ? |
+------+------+------+------+------+
índice: [0] [1] [2] [3] [4]Las 5 casillas son consecutivas -- sin huecos entre ellas. Cada casilla ocupa sizeof(int) = 4 bytes. El array entero ocupa 5 x 4 = 20 bytes.
El tamaño debe ser constante
El tamaño del array debe ser conocido en el momento de la compilación -- no una variable:
| 1 | int tab[5]; // OK -- 5 es una constante |
| 2 | int tab[size]; // PROHIBIDO en 42 (VLA) |
Para tamaños dinámicos, necesitarás malloc (curso sobre malloc y free).
Sin inicialización
| 1 | int tab[5]; |
| 2 | // El contenido de tab[0], tab[1], etc. es BASURA |
Como con las variables simples, un array no inicializado contiene valores aleatorios.
Acceder a los elementos
Leer y escribir
| 1 | int tab[5]; |
| 2 | |
| 3 | tab[0] = 10; // Escribir en la primera casilla |
| 4 | tab[1] = 20; |
| 5 | tab[4] = 50; // La última casilla (índice 4, no 5) |
| 6 | |
| 7 | int val; |
| 8 | |
| 9 | val = tab[0]; // Leer la primera casilla -> val = 10 |
Recorrer con un bucle
Es el patrón más común en C:
| 1 | int tab[5]; |
| 2 | int i; |
| 3 | |
| 4 | i = 0; |
| 5 | while (i < 5) |
| 6 | { |
| 7 | tab[i] = i * 10; // tab = {0, 10, 20, 30, 40} |
| 8 | i++; |
| 9 | } |
Para leer:
| 1 | i = 0; |
| 2 | while (i < 5) |
| 3 | { |
| 4 | // hacer algo con tab[i] |
| 5 | i++; |
| 6 | } |
Arrays y punteros -- La relación
El nombre del array es una dirección
En C, el nombre de un array se convierte automáticamente en un puntero hacia su primer elemento:
| 1 | int tab[5]; |
| 2 | int *p; |
| 3 | |
| 4 | p = tab; // p apunta hacia tab[0] |
tab[i] es un atajo
La expresión tab[i] es en realidad *(tab + i) -- "toma la dirección de tab, avanza i elementos, y lee el valor". El compilador hace este cálculo automáticamente.
Por eso los índices empiezan en 0: tab[0] = *(tab + 0) = el valor en la dirección del inicio del array, sin avanzar.
Pasar un array a una función
Cuando pasas un array a una función, pasas un puntero. La función puede modificar el array original:
| 1 | void ft_rellenar(int *tab, int tamanio, int valor) |
| 2 | { |
| 3 | int i; |
| 4 | |
| 5 | i = 0; |
| 6 | while (i < tamanio) |
| 7 | { |
| 8 | tab[i] = valor; |
| 9 | i++; |
| 10 | } |
| 11 | } |
| 12 | |
| 13 | int main(void) |
| 14 | { |
| 15 | int notas[5]; |
| 16 | |
| 17 | ft_rellenar(notas, 5, 0); |
| 18 | // notas = {0, 0, 0, 0, 0} |
| 19 | return (0); |
| 20 | } |
Dos puntos importantes:
Las operaciones comunes sobre arrays
Encontrar el máximo
El principio: tomar el primer elemento como máximo provisional, luego comparar cada elemento siguiente. Si un elemento es mayor, se convierte en el nuevo máximo.
Invertir un array
El principio: intercambiar los elementos simétricos -- el primero con el último, el segundo con el penúltimo, etc. Se para en el medio.
Antes : [10, 20, 30, 40, 50]
swap(10, 50) -> [50, 20, 30, 40, 10]
swap(20, 40) -> [50, 40, 30, 20, 10]
medio alcanzado -> fin
Después: [50, 40, 30, 20, 10]Ordenar un array
El ordenamiento más simple es el ordenamiento por selección: para cada posición, buscar el elemento más pequeño en el resto del array e intercambiarlo con la posición actual. Dos bucles anidados.
[50, 30, 10, 40, 20]
Posición 0 : min en todo el array = 10 -> swap con 50 -> [10, 30, 50, 40, 20]
Posición 1 : min en el resto = 20 -> swap con 30 -> [10, 20, 50, 40, 30]
Posición 2 : min en el resto = 30 -> swap con 50 -> [10, 20, 30, 40, 50]
Posición 3 : min en el resto = 40 -> no hay swap -> [10, 20, 30, 40, 50]Los límites de los arrays
Sin verificación de límites
C nunca verifica que tu índice esté dentro de los límites:
| 1 | int tab[5]; |
| 2 | |
| 3 | tab[5] = 99; // FUERA DE LÍMITES -- el 6to elemento no existe |
| 4 | tab[100] = 99; // FUERA DE LÍMITES -- muy lejos |
| 5 | tab[-1] = 99; // FUERA DE LÍMITES -- antes del inicio |
El programa no crashea necesariamente -- es peor. Puede:
- Sobreescribir otras variables silenciosamente
- Crashear más tarde en un lugar inesperado
- Parecer funcionar (el bug más peligroso)
Es un comportamiento indefinido (el programa puede hacer cualquier cosa). Es tu responsabilidad quedarte dentro de los límites.
Sin copia por asignación
| 1 | int a[5]; |
| 2 | int b[5]; |
| 3 | |
| 4 | b = a; // ERROR de compilación |
Para copiar un array, hay que copiar elemento por elemento con un bucle.
No devolver un array local
| 1 | int *malo(void) |
| 2 | { |
| 3 | int tab[5]; |
| 4 | |
| 5 | return (tab); // PELIGRO -- tab se destruye cuando la función termina |
| 6 | } |
El array local vive en la memoria de la función. Cuando termina, esa memoria se libera. El puntero devuelto apunta al vacío. Para devolver un array, necesitarás malloc (curso sobre malloc y free).
Cuestionario
Pon a prueba tus conocimientos — selecciona la respuesta correcta para cada pregunta
int tab[10]?tab (el nombre del array) cuando se usa en una expresión?tab[10] en un array int tab[5]?int *tab e int tab[] en los parámetros de una función -- ¿qué diferencia hay?int b[5]; b = a; no compila?int tab[5] ocupa ¿cuántos bytes en memoria?