A13

Arrays

Punteros y Arrays

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

C
1int 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:

C
1int tab[5]; // OK -- 5 es una constante
2int tab[size]; // PROHIBIDO en 42 (VLA)

Para tamaños dinámicos, necesitarás malloc (curso sobre malloc y free).

Sin inicialización

C
1int 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

C
1int tab[5];
2
3tab[0] = 10; // Escribir en la primera casilla
4tab[1] = 20;
5tab[4] = 50; // La última casilla (índice 4, no 5)
6
7int val;
8
9val = tab[0]; // Leer la primera casilla -> val = 10

Recorrer con un bucle

Es el patrón más común en C:

C
1int tab[5];
2int i;
3
4i = 0;
5while (i < 5)
6{
7 tab[i] = i * 10; // tab = {0, 10, 20, 30, 40}
8 i++;
9}

Para leer:

C
1i = 0;
2while (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:

C
1int tab[5];
2int *p;
3
4p = 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:

C
1void 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
13int 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:

C
1int tab[5];
2
3tab[5] = 99; // FUERA DE LÍMITES -- el 6to elemento no existe
4tab[100] = 99; // FUERA DE LÍMITES -- muy lejos
5tab[-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

C
1int a[5];
2int b[5];
3
4b = a; // ERROR de compilación

Para copiar un array, hay que copiar elemento por elemento con un bucle.

No devolver un array local

C
1int *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

1¿Cuál es el índice del último elemento de un array int tab[10]?
2¿Qué vale tab (el nombre del array) cuando se usa en una expresión?
3¿Por qué hay que pasar siempre el tamaño del array como parámetro?
4¿Qué pasa si accedes a tab[10] en un array int tab[5]?
5int *tab e int tab[] en los parámetros de una función -- ¿qué diferencia hay?
6Para invertir un array, ¿por qué pararse en el medio?
7¿Por qué int b[5]; b = a; no compila?
8Un array int tab[5] ocupa ¿cuántos bytes en memoria?
9¿Se puede usar una variable como tamaño de array en 42?
10¿Por qué no devolver un array local desde una función?