Introduction
Jusqu'ici, chaque variable stocke une seule valeur. Mais que faire si tu veux stocker 10 notes, 100 mesures, ou les lettres d'un texte ? Déclarer int note1, note2, note3, ... serait ingérable.
Un tableau c'est une suite de variables du même type, stockées côte à côte en mémoire, accessibles par un numéro (l'index). Au lieu de 10 variables séparées, tu déclares un seul tableau de 10 cases.
Déclarer un tableau
| 1 | int tab[5]; |
Ça crée 5 int côte à côte en mémoire. Chaque case est accessible par son index.
En mémoire
tab : ┌──────┬──────┬──────┬──────┬──────┐
│ ? │ ? │ ? │ ? │ ? │
└──────┴──────┴──────┴──────┴──────┘
index: [0] [1] [2] [3] [4]Les 5 cases sont consécutives — pas de trou entre elles. Chaque case fait sizeof(int) = 4 octets. Le tableau entier occupe 5 × 4 = 20 octets.
La taille doit être constante
La taille du tableau doit être connue au moment de la compilation — pas une variable :
| 1 | int tab[5]; // OK — 5 est une constante |
| 2 | int tab[size]; // INTERDIT à 42 (VLA) |
Pour des tailles dynamiques, il faudra malloc (cours sur malloc et free).
Sans initialisation
| 1 | int tab[5]; |
| 2 | // Le contenu de tab[0], tab[1], etc. est du GARBAGE |
Comme pour les variables simples, un tableau non initialisé contient des valeurs aléatoires.
Accéder aux éléments
Lire et écrire
| 1 | int tab[5]; |
| 2 | |
| 3 | tab[0] = 10; // Écrire dans la première case |
| 4 | tab[1] = 20; |
| 5 | tab[4] = 50; // La dernière case (index 4, pas 5) |
| 6 | |
| 7 | int val; |
| 8 | |
| 9 | val = tab[0]; // Lire la première case → val = 10 |
Parcourir avec une boucle
C'est le pattern le plus courant 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 | } |
Pour lire :
| 1 | i = 0; |
| 2 | while (i < 5) |
| 3 | { |
| 4 | // faire quelque chose avec tab[i] |
| 5 | i++; |
| 6 | } |
Tableaux et pointeurs — La relation
Le nom du tableau est une adresse
En C, le nom d'un tableau est automatiquement converti en un pointeur vers son premier élément :
| 1 | int tab[5]; |
| 2 | int *p; |
| 3 | |
| 4 | p = tab; // p pointe vers tab[0] |
tab[i] est un raccourci
L'expression tab[i] est en réalité *(tab + i) — "prends l'adresse de tab, avance de i éléments, et lis la valeur". Le compilateur fait ce calcul automatiquement.
C'est pour ça que les index commencent à 0 : tab[0] = *(tab + 0) = la valeur à l'adresse du début du tableau, sans avancer.
Passer un tableau à une fonction
Quand tu passes un tableau à une fonction, tu passes un pointeur. La fonction peut modifier le tableau original :
| 1 | void ft_remplir(int *tab, int taille, int valeur) |
| 2 | { |
| 3 | int i; |
| 4 | |
| 5 | i = 0; |
| 6 | while (i < taille) |
| 7 | { |
| 8 | tab[i] = valeur; |
| 9 | i++; |
| 10 | } |
| 11 | } |
| 12 | |
| 13 | int main(void) |
| 14 | { |
| 15 | int notes[5]; |
| 16 | |
| 17 | ft_remplir(notes, 5, 0); |
| 18 | // notes = {0, 0, 0, 0, 0} |
| 19 | return (0); |
| 20 | } |
Deux points importants :
Les opérations courantes sur les tableaux
Trouver le maximum
Le principe : prendre le premier élément comme maximum provisoire, puis comparer chaque élément suivant. Si un élément est plus grand, il devient le nouveau maximum.
Inverser un tableau
Le principe : échanger les éléments symétriques — le premier avec le dernier, le deuxième avec l'avant-dernier, etc. On s'arrête au milieu.
Avant : [10, 20, 30, 40, 50]
swap(10, 50) → [50, 20, 30, 40, 10]
swap(20, 40) → [50, 40, 30, 20, 10]
milieu atteint → fin
Après : [50, 40, 30, 20, 10]Trier un tableau
Le tri le plus simple c'est le tri par sélection : pour chaque position, chercher le plus petit élément dans le reste du tableau et l'échanger avec la position actuelle. Deux boucles imbriquées.
[50, 30, 10, 40, 20] Position 0 : min dans tout le tableau = 10 → swap avec 50 → [10, 30, 50, 40, 20] Position 1 : min dans le reste = 20 → swap avec 30 → [10, 20, 50, 40, 30] Position 2 : min dans le reste = 30 → swap avec 50 → [10, 20, 30, 40, 50] Position 3 : min dans le reste = 40 → pas de swap → [10, 20, 30, 40, 50]
Les limites des tableaux
Pas de vérification des bornes
Le C ne vérifie jamais que ton index est dans les limites :
| 1 | int tab[5]; |
| 2 | |
| 3 | tab[5] = 99; // HORS LIMITES — le 6ème élément n'existe pas |
| 4 | tab[100] = 99; // HORS LIMITES — très loin |
| 5 | tab[-1] = 99; // HORS LIMITES — avant le début |
Le programme ne crashe pas forcément — c'est pire. Il peut :
- Écraser d'autres variables silencieusement
- Crasher plus tard à un endroit inattendu
- Sembler fonctionner (le bug le plus dangereux)
C'est un comportement indéfini (le programme peut faire n'importe quoi). C'est ta responsabilité de rester dans les bornes.
Pas de copie par affectation
| 1 | int a[5]; |
| 2 | int b[5]; |
| 3 | |
| 4 | b = a; // ERREUR de compilation |
Pour copier un tableau, il faut copier élément par élément avec une boucle.
Pas de retour de tableau local
| 1 | int *mauvais(void) |
| 2 | { |
| 3 | int tab[5]; |
| 4 | |
| 5 | return (tab); // DANGER — tab est détruit quand la fonction se termine |
| 6 | } |
Le tableau local vit dans la mémoire de la fonction. Quand elle se termine, cette mémoire est libérée. Le pointeur retourné pointe vers du vide. Pour retourner un tableau, il faudra malloc (cours sur malloc et free).
Questionnaire
Vérifie tes acquis — sélectionne la bonne réponse pour chaque question
int tab[10] ?tab (le nom du tableau) quand il est utilisé dans une expression ?tab[10] dans un tableau int tab[5] ?int *tab et int tab[] dans les paramètres d'une fonction — quelle différence ?int b[5]; b = a; ne compile pas ?int tab[5] occupe combien d'octets en mémoire ?