Skip to content

TP : Maîtriser la Mémoire et les Structures en C

Durée estimée : 3h00

Contexte : Séance non évaluée. L'objectif est de comprendre les mécanismes de la mémoire et des pointeurs étape par étape, sans stress.

Pré-requis : Bases du C (boucles, fonctions, variables).


Partie 1 : L'Allocation Dynamique Simple (malloc)

📘 Rappel de cours

Jusqu'à présent, vous déclariez des tableaux de taille fixe (ex: int tab[100]). C'est la mémoire statique (allouée sur la pile/stack). Problème : Si on ne connaît pas la taille à l'avance (elle dépend d'une saisie utilisateur), on est coincé ou on gaspille de la mémoire.

La solution : L'allocation dynamique (dans le tas/heap). On demande manuellement de la mémoire au système.

  1. Allocation : On utilise malloc (Memory Allocation).
    • Syntaxe : pointeur = (Type*) malloc( nombre_elements * sizeof(Type) );
  2. Vérification : Toujours vérifier si le pointeur renvoyé n'est pas NULL (erreur d'allocation, par exemple si la mémoire est pleine).
  3. Libération : Impératif ! Il faut rendre la mémoire avec free(pointeur) à la fin, c'est à dire lorsqu'on utilise plus la mémoire.

🛠️ Exercice 1 : Mon premier tableau dynamique

  1. Demandez à l'utilisateur de saisir un nombre entier positif n.
  2. Déclarez un pointeur int *tab.
  3. Utilisez malloc pour allouer de la place pour n entiers.
  4. Remplissez ce tableau avec les valeurs 1, 2, ..., n.
  5. Affichez le tableau.
  6. Libérez la mémoire.
// Exemple de syntaxe pour vous aider :
int *tab = (int*) malloc(n * sizeof(int));
if (tab == NULL) {
    exit(1); // Erreur
}
// ... utilisation ...
free(tab);

Partie 2 : Structures - Syntaxe, Copie et Passage par Adresse

📘 Rappel de cours

1. Définition et typedef

Une structure permet de regrouper des variables. En C "pur", on doit répéter le mot struct partout (struct Personne p1;). Pour simplifier, on utilise typedef souvent combiné à une structure anonyme (sans nom interne) :

typedef struct {
    char nom[20];
    int age;
    int gros_tableau[1000]; // Une structure peut être lourde en octets !
} Personne;

Personne p1; // Plus besoin de "struct", c'est plus propre.

2. Comportement "Primitif" (Copie)

C'est un point crucial : Une structure se comporte comme un entier (int). Si vous faites p2 = p1;, le C effectue une copie intégrale de tous les octets de p1 vers p2. Les deux variables deviennent indépendantes.

3. Passage par Valeur vs Adresse (La Pile vs Le Pointeur)

Pourquoi utiliser des pointeurs (Personne *p) ?

  1. Pour modifier : Si on passe par valeur (copie), la fonction modifie sa copie locale, pas l'original.
  2. Pour la performance (La Pile/Stack) :
    • Par Valeur : La structure entière est copiée sur la "pile" (mémoire temporaire). Si la structure fait 4000 octets, on perd du temps et de la mémoire à copier ces 4000 octets pour rien.
    • Par Adresse : On envoie juste l'adresse de la structure (8 octets). C'est instantané, quelle que soit la taille de la structure.

Règle d'or :

  • Accès via variable : p1.age (le point)
  • Accès via pointeur : ptr->age (la flèche)

🛠️ Exercice 2 : La "lourdeur" de la copie

Copiez et tester ce code. Il contient une structure Joueur volontairement lourde pour illustrer le problème de la copie sur la pile.

#include <stdio.h>
#include <string.h>

// Utilisation de typedef avec une structure anonyme
typedef struct {
    char nom[50];
    // Un gros tableau pour simuler une structure lourde en mémoire (4Ko)
    int donnees_lourdes[1000]; 
    int score;
} Joueur;

// --------------------------------------------------------
// Fonction 1 : Passage par VALEUR (Copie)
// --------------------------------------------------------
// Ici, 'j' est une COPIE locale sur la PILE. 
// Toute la structure (4054 octets) a été dupliquée.
void modifierScoreCopie(Joueur j) {
    j.score = 100; // On modifie la copie
    printf("[Fonction Copie] Score mis a 100.\n");
}

// --------------------------------------------------------
// Fonction 2 : Passage par ADRESSE (Pointeur)
// --------------------------------------------------------
// Ici, on reçoit juste une adresse (8 octets). Pas de duplication.
// On accède à l'original via la flèche '->'
void modifierScorePointeur(Joueur *ptr) {
    // TODO : Mettre le score à 500 en utilisant le pointeur et la flèche ->

    printf("[Fonction Pointeur] Score mis a 500.\n");
}

int main() {
    Joueur j1;
    strcpy(j1.nom, "Mario");
    j1.score = 0;

    printf("Score initial de %s : %d\n", j1.nom, j1.score);

    // 1. Essai avec passage par valeur
    modifierScoreCopie(j1); 
    // Rappel : j1 a été copié. L'original n'a pas bougé.
    printf("Apres modifierScoreCopie : %d (Devrait etre 0)\n", j1.score);

    printf("--------------------------------\n");

    // 2. Essai avec passage par adresse
    // TODO : Appeler modifierScorePointeur. 
    // Indice : La fonction attend une adresse, utilisez '&' pour envoyer l'adresse de j1

    printf("Apres modifierScorePointeur : %d (Devrait etre 500)\n", j1.score);

    return 0;
}

Partie 3 : Tableaux Dynamiques à 2 Dimensions

📘 Rappel de cours

En C, un tableau 2D dynamique n'est pas un bloc unique. C'est un tableau de pointeurs où chaque pointeur dirige vers un tableau d'entiers (les lignes).

Le type de la variable principale est int** (pointeur de pointeur).

L'allocation se fait en escalier :

  1. Allouer le tableau vertical de pointeurs (malloc de taille nblignes * sizeof(int*)).
  2. Boucle for : Pour chaque case, allouer un tableau horizontal (malloc de taille nbColonnes * sizeof(int)).

🛠️ Exercice 3 : La table de multiplication

Écrivez un programme complet qui :

  1. Demande nbLignes et nbColonnes.
  2. Alloue la matrice dynamique.
  3. Remplit la matrice : matrice[i][j] = (i+1) * (j+1).
  4. Affiche la matrice sous forme de grille alignée.
  5. Libération (Ordre inverse) : Faites une boucle pour free chaque ligne d'abord, PUIS free le tableau principal.

Partie 4 : Tableau Dynamique de Structures

📘 Rappel de cours

On peut combiner malloc et structures. C'est très utilisé pour gérer des bases de données en RAM (inventaires, listes d'étudiants...). Une fois alloué, cela s'utilise comme un tableau classique.

  • Allocation : Article *stock = (Article*) malloc(n * sizeof(Article));
  • Accès : stock[i].prix (On utilise le point . car stock[i] est la case elle-même, pas un pointeur).

🛠️ Exercice 4 : Gestion de stock

Définissez une structure Article contenant : char designation[50] et float prix.

  1. Demandez le nombre d'articles n.
  2. Allouez dynamiquement le tableau de structures.
  3. Saisissez les données (pour simplifier, utilisez scanf("%s", ...) sans espaces).
  4. Affichez uniquement les articles dont le prix est supérieur à 100€.
  5. Libérez la mémoire.

Partie 5 : Initiation aux Listes Chaînées

📘 Rappel de cours

Un tableau oblige à avoir toute la mémoire contiguë (côte à côte). Une liste chaînée est flexible : chaque élément (Maillon) contient une valeur et l'adresse du suivant. Ils sont éparpillés en mémoire et reliés par des fils (pointeurs).

Le dernier élément pointe vers NULL pour dire "C'est la fin".

🛠️ Exercice 5 : Parcours et Nettoyage

Le code de gestion de la liste est complexe à écrire au début. Nous vous fournissons le "moteur". Vous devez écrire le parcours.

Copiez-collez ce code et complétez les fonctions vides :

#include <stdio.h>
#include <stdlib.h>

// La structure du Maillon
typedef struct Maillon {
    int nombre;
    struct Maillon *suivant; // Le lien vers le prochain
} Maillon;

// Fonction utilitaire pour ajouter au début (Fournie)
Maillon* ajouterTete(Maillon* liste, int valeur) {
    Maillon* nouvelElement = (Maillon*) malloc(sizeof(Maillon));
    nouvelElement->nombre = valeur;
    nouvelElement->suivant = liste;
    return nouvelElement;
}

// --- A VOUS DE JOUER CI-DESSOUS ---

/**
 * Doit parcourir la liste et afficher : 10 -> 20 -> 5 -> NULL
 */
void afficherListe(Maillon* liste) {
    Maillon* courant = liste;
    // TODO : Écrire la boucle while
    // 1. Tant que courant n'est pas NULL
    // 2. Afficher courant->nombre
    // 3. Avancer : courant prend la valeur de courant->suivant

    printf("NULL\n");
}

/**
 * Doit libérer toute la mémoire
 */
void toutEffacer(Maillon* liste) {
    Maillon* courant = liste;
    while (courant != NULL) {
        Maillon* aSupprimer = courant;
        // TODO : Sauvegarder l'adresse du suivant dans une variable temporaire AVANT de supprimer
        // TODO : free(aSupprimer)
        // TODO : Passer au suivant (avec la variable temporaire)
    }
}

int main() {
    Maillon* maListe = NULL; // Liste vide

    // Remplissage : 5, puis 8 devant, puis 12 devant.
    // La liste sera : 12 -> 8 -> 5 -> NULL
    maListe = ajouterTete(maListe, 5);
    maListe = ajouterTete(maListe, 8);
    maListe = ajouterTete(maListe, 12);

    printf("Ma liste chainee :\n");
    afficherListe(maListe);

    toutEffacer(maListe);
    printf("Memoire nettoyee.\n");

    return 0;
}

Sujet rédigé à l'aide de Gemini