0% ont trouvé ce document utile (0 vote)
3 vues28 pages

Explication Complete Code

Le projet de gestion de bibliothèque universitaire en C utilise un arbre binaire de recherche pour stocker les livres et une liste chaînée pour gérer les emprunts. Le code est structuré en trois fichiers principaux : arbre.h pour les déclarations, arbre.c pour les implémentations, et main.c pour l'interface utilisateur. Les fonctionnalités incluent la création, l'insertion, la recherche de livres, ainsi que la gestion des emprunts et la persistance des données via des fichiers binaires.

Transféré par

asaphcoulibaly4
Copyright
© All Rights Reserved
Nous prenons très au sérieux les droits relatifs au contenu. Si vous pensez qu’il s’agit de votre contenu, signalez une atteinte au droit d’auteur ici.
Formats disponibles
Téléchargez aux formats DOCX, PDF, TXT ou lisez en ligne sur Scribd
0% ont trouvé ce document utile (0 vote)
3 vues28 pages

Explication Complete Code

Le projet de gestion de bibliothèque universitaire en C utilise un arbre binaire de recherche pour stocker les livres et une liste chaînée pour gérer les emprunts. Le code est structuré en trois fichiers principaux : arbre.h pour les déclarations, arbre.c pour les implémentations, et main.c pour l'interface utilisateur. Les fonctionnalités incluent la création, l'insertion, la recherche de livres, ainsi que la gestion des emprunts et la persistance des données via des fichiers binaires.

Transféré par

asaphcoulibaly4
Copyright
© All Rights Reserved
Nous prenons très au sérieux les droits relatifs au contenu. Si vous pensez qu’il s’agit de votre contenu, signalez une atteinte au droit d’auteur ici.
Formats disponibles
Téléchargez aux formats DOCX, PDF, TXT ou lisez en ligne sur Scribd

EXPLICATION DÉTAILLÉE DU CODE

Projet : Gestion de Bibliothèque Universitaire en C

1. VUE D'ENSEMBLE DU PROJET

Ce projet implémente un système complet de gestion de bibliothèque en langage C. Il utilise deux


structures de données fondamentales :

• Un Arbre Binaire de Recherche (ABR) pour stocker les livres, permettant des recherches
rapides en O(log n)
• Une Liste Chaînée pour gérer les emprunts de manière dynamique

Le code est organisé en 3 fichiers modulaires : arbre.h (déclarations), arbre.c (implémentations), et


main.c (interface utilisateur).

2. FICHIER arbre.h — DÉCLARATIONS

2.1 Les Directives de Préprocesseur

#ifndef ARBRE_H
#define ARBRE_H
...
#endif

Ces lignes forment un GARDE D'INCLUSION (include guard). Elles empêchent le fichier d'être inclus
plusieurs fois dans le même programme, ce qui causerait des erreurs de redéfinition. Fonctionnement :
si ARBRE_H n'est pas défini, on le définit et on inclut tout le contenu jusqu'à #endif. Si le fichier est
inclus une deuxième fois, ARBRE_H est déjà défini, donc tout le contenu est ignoré.

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

Inclusion des bibliothèques standard nécessaires :


• stdio.h : pour printf, scanf, FILE*, fopen, fclose, etc.
• stdlib.h : pour malloc, free, qsort, NULL
• string.h : pour strncpy, strcmp, strlen
• time.h : pour time_t, struct tm, time(), localtime(), mktime(), difftime()
2.2 Les Constantes (Macros)

#define MAX_TITRE 100


#define MAX_NOM 50
#define MAX_LIVRES 500
#define FICHIER_LIVRES "[Link]"
#define FICHIER_EMPRUNTS "[Link]"

Les #define créent des CONSTANTES SYMBOLIQUES qui seront remplacées par le préprocesseur
avant la compilation :
• MAX_TITRE (100) : taille maximale du titre d'un livre (99 caractères + \0)
• MAX_NOM (50) : taille maximale des noms/prénoms
• MAX_LIVRES (500) : capacité du tableau temporaire utilisé pour le tri alphabétique
• FICHIER_LIVRES / FICHIER_EMPRUNTS : noms des fichiers binaires de sauvegarde

💡 Avantage des constantes : si on veut changer MAX_TITRE à 200, on modifie une seule ligne
au lieu de chercher tous les 100 dans le code.

2.3 Structure Livre — Nœud de l'ABR

typedef struct Livre {


int num_livre;
char titre[MAX_TITRE];
char nom_auteur[MAX_NOM];
char prenom_auteur[MAX_NOM];
int annee_edition;
int nb_exemplaires;
struct Livre *gauche;
struct Livre *droite;
} Livre;

Cette structure définit un NŒUD de l'Arbre Binaire de Recherche. Chaque livre contient :

DONNÉES DU LIVRE :
• num_livre (int) : identifiant unique, CLÉ DE TRI de l'ABR
• titre[100] (char) : tableau de 100 caractères pour stocker le titre
• nom_auteur[50] et prenom_auteur[50] : nom complet de l'auteur
• annee_edition (int) : année de publication
• nb_exemplaires (int) : stock disponible (décrémenté lors d'un emprunt)

POINTEURS DE L'ARBRE :
• gauche (struct Livre*) : pointeur vers le fils gauche (livres avec num_livre < clé)
• droite (struct Livre*) : pointeur vers le fils droit (livres avec num_livre > clé)
🔑 Propriété de l'ABR : Pour tout nœud N, tous les nœuds du sous-arbre gauche ont num_livre <
N.num_livre, et tous les nœuds du sous-arbre droit ont num_livre > N.num_livre. Cela permet
une recherche en O(log n) en moyenne.

2.4 Structure Emprunt — Nœud de la Liste Chaînée

typedef struct Emprunt {


int num_livre;
int num_etudiant;
char nom_etudiant[MAX_NOM];
char prenom_etudiant[MAX_NOM];
time_t date_emprunt;
time_t date_retour;
struct Emprunt *suivant;
} Emprunt;

Cette structure définit un MAILLON de la liste chaînée des emprunts. Chaque emprunt contient :

DONNÉES DE L'EMPRUNT :
• num_livre (int) : référence au livre emprunté (lien logique avec l'ABR)
• num_etudiant (int) : identifiant de l'étudiant
• nom_etudiant[50] et prenom_etudiant[50] : identité de l'emprunteur
• date_emprunt (time_t) : timestamp UNIX (secondes depuis 01/01/1970) de la date d'emprunt
• date_retour (time_t) : timestamp UNIX de la date de retour prévue

POINTEUR DE LA LISTE :
• suivant (struct Emprunt*) : pointeur vers l'emprunt suivant dans la liste (NULL si dernier)

📅 time_t est un entier (généralement long) qui représente le nombre de secondes écoulées
depuis le 1er janvier 1970 à 00:00:00 UTC (epoch UNIX). Exemple : 1708358400 = 19 février
2024 16:00:00 UTC.

2.5 Prototypes des Fonctions — Partie 1 : ABR

Les prototypes déclarent les fonctions sans les implémenter. Cela permet au compilateur de vérifier les
types lors des appels, même si la définition est dans arbre.c.

Livre *creer_livre(int num, const char *titre,


const char *nom, const char *prenom,
int annee, int nb_ex);

FONCTION : creer_livre()
RÔLE : Alloue dynamiquement (malloc) un nouveau nœud Livre et initialise ses champs.
PARAMÈTRES : 6 informations du livre (num, titre, nom, prenom, annee, nb_ex).
RETOUR : Pointeur vers le nouveau nœud créé, ou NULL si malloc échoue.
Note : const char* signifie que la fonction ne modifiera pas les chaînes passées en paramètre.

Livre *inserer_livre(Livre *racine, Livre *nouveau);

FONCTION : inserer_livre()
RÔLE : Insère récursivement un nœud dans l'ABR en respectant la propriété num_livre.
PARAMÈTRES : racine (pointeur vers la racine actuelle), nouveau (pointeur vers le nœud à insérer).
RETOUR : La nouvelle racine de l'arbre (peut changer si l'arbre était vide).
ALGORITHME : Si racine NULL → retourner nouveau. Sinon, si [Link] < [Link] → insérer
à gauche, sinon insérer à droite. Refuse les doublons.

void afficher_tous(Livre *racine);


void afficher_livre(const Livre *l);
void afficher_alpha(Livre *racine);

FONCTIONS D'AFFICHAGE :
• afficher_tous() : Parcours INFIXE (gauche → racine → droite) de l'ABR, affiche les livres dans
l'ordre croissant des numéros.
• afficher_livre() : Affiche tous les champs d'un seul livre donné.
• afficher_alpha() : Collecte tous les livres dans un tableau, le trie par titre avec qsort(), puis
affiche.

Livre *rechercher_par_num(Livre *racine, int num);


Livre *rechercher_par_titre(Livre *racine, const char *titre);

FONCTIONS DE RECHERCHE :
• rechercher_par_num() : Recherche BST classique en O(log n) moyen. Compare num avec
racine.num_livre, descend à gauche si < ou à droite si >.
• rechercher_par_titre() : Parcours complet O(n) car le titre n'est pas la clé de tri. Compare les
titres en ignorant la casse.

void verifier_disponibilite(Livre *racine, int num);


void liberer_arbre(Livre *racine);

AUTRES FONCTIONS ABR :


• verifier_disponibilite() : Recherche le livre par numéro, puis vérifie si nb_exemplaires > 0.
• liberer_arbre() : Parcours POST-ORDRE (gauche → droite → racine) pour libérer
récursivement tous les nœuds avec free().

2.6 Prototypes des Fonctions — Partie 2 : Emprunts

time_t saisir_date_retour(void);
FONCTION : saisir_date_retour()
RÔLE : Demande à l'utilisateur une date (JJ MM AAAA) et la convertit en time_t avec mktime().
RETOUR : Timestamp UNIX de la date saisie, ou time(NULL) si saisie invalide.

int emprunter_livre(Livre *racine, Emprunt **liste,


int num_l, int num_e,
const char *nom, const char *prenom,
time_t date_retour);

FONCTION : emprunter_livre()
RÔLE : Enregistre un nouvel emprunt.
ÉTAPES :
1. Vérifie que le livre existe dans l'ABR et a au moins 1 exemplaire disponible.
2. Vérifie que l'étudiant n'a pas déjà emprunté ce livre (parcours de la liste).
3. Crée un nouveau nœud Emprunt avec malloc.
4. L'insère EN TÊTE de la liste chaînée (insertion en O(1)).
5. Décrémente nb_exemplaires du livre dans l'ABR.
PARAMÈTRES : racine (ABR), liste (pointeur vers le pointeur de tête), num_l (numéro du livre), num_e
(numéro étudiant), nom/prenom, date_retour.
RETOUR : 1 si succès, 0 si échec.
Note : Emprunt** (double pointeur) permet de modifier le pointeur de tête de la liste.

void afficher_emprunts(const Emprunt *liste, Livre *racine);


int retourner_livre(Livre *racine, Emprunt **liste,
int num_l, int num_e);

FONCTIONS DE GESTION :
• afficher_emprunts() : Parcourt la liste chaînée, affiche chaque emprunt avec dates formatées
(localtime + strftime).
• retourner_livre() : Cherche l'emprunt (num_l, num_e) dans la liste, le supprime, incrémente
nb_exemplaires dans l'ABR.

2.7 Prototypes des Fonctions — Partie 3 : Requêtes

void livres_etudiant(const Emprunt *liste, Livre *racine, int num_e);


void livres_en_retard(const Emprunt *liste, Livre *racine, time_t date_limite);
void liberer_liste(Emprunt *liste);

REQUÊTES AVANCÉES :
• livres_etudiant() : Parcourt la liste et affiche tous les emprunts dont num_etudiant correspond.
• livres_en_retard() : Parcourt la liste, compare date_retour avec date_limite via difftime(). Si
date_retour < date_limite, le livre est en retard.
• liberer_liste() : Parcourt la liste et libère chaque nœud avec free().
2.8 Prototypes des Fonctions — Partie 4 : Fichiers

void sauvegarder_livres(Livre *racine, const char *fichier);


Livre *charger_livres(const char *fichier);
void sauvegarder_emprunts(Emprunt *liste, const char *fichier);
Emprunt *charger_emprunts(const char *fichier);

PERSISTANCE DES DONNÉES :


Ces fonctions utilisent fwrite/fread pour sauvegarder/charger les données dans des fichiers binaires.

STRATÉGIE POUR L'ABR :


- Sauvegarde : Parcours PRÉFIXE (racine → gauche → droite), écriture d'un flag (1=nœud, 0=NULL)
avant chaque nœud.
- Chargement : Lecture séquentielle, reconstruction en réinsérant chaque livre dans un nouvel ABR.

STRATÉGIE POUR LA LISTE :


- Sauvegarde : Parcours linéaire, écriture séquentielle de chaque nœud.
- Chargement : Lecture séquentielle, création de la liste en insérant en queue.

2.9 Macro TITRE_SECTION

#define TITRE_SECTION(t) \
printf("\n >> %s\n", t); \
printf("%s\n", "-------------------------------------------")

Cette MACRO affiche un en-tête de section formaté. Le backslash \ en fin de ligne permet de continuer
la définition sur plusieurs lignes.
Exemple d'utilisation : TITRE_SECTION("AJOUTER UN LIVRE"); sera remplacé par :
printf("\n >> %s\n", "AJOUTER UN LIVRE");
printf("%s\n", "-------------------------------------------");
3. FICHIER arbre.c — IMPLÉMENTATIONS DÉTAILLÉES

Ce fichier contient le code de toutes les fonctions déclarées dans arbre.h. Nous allons expliquer
chaque fonction ligne par ligne.

3.1 Fonction creer_livre()

Livre *creer_livre(int num, const char *titre,


const char *nom, const char *prenom,
int annee, int nb_ex)
{
Livre *nouveau = (Livre *)malloc(sizeof(Livre));

LIGNE 1 : malloc(sizeof(Livre)) alloue dynamiquement de la mémoire sur le TAS (heap) pour stocker
une structure Livre. sizeof(Livre) retourne le nombre d'octets nécessaires (environ 230 octets : 8
champs + 2 pointeurs). malloc retourne void*, on le caste en (Livre*).

if (nouveau == NULL) {
fprintf(stderr, "Erreur : allocation memoire echouee.\n");
return NULL;
}

VÉRIFICATION CRITIQUE : Si malloc échoue (mémoire insuffisante), il retourne NULL. On affiche un


message d'erreur sur stderr (flux d'erreur standard) et on retourne NULL. L'appelant doit vérifier ce
retour.

nouveau->num_livre = num;
nouveau->annee_edition = annee;
nouveau->nb_exemplaires = nb_ex;
nouveau->gauche = NULL;
nouveau->droite = NULL;

INITIALISATION DES CHAMPS ENTIERS : L'opérateur -> accède aux champs d'une structure via un
pointeur (équivalent à (*nouveau).num_livre). On initialise les 3 entiers avec les valeurs passées en
paramètre. IMPORTANT : Les pointeurs gauche et droite sont mis à NULL car ce nœud n'a pas encore
d'enfants.

strncpy(nouveau->titre, titre, MAX_TITRE - 1);


strncpy(nouveau->nom_auteur, nom, MAX_NOM - 1);
strncpy(nouveau->prenom_auteur, prenom, MAX_NOM - 1);

COPIE SÉCURISÉE DES CHAÎNES : strncpy(dest, src, n) copie au maximum n caractères de src vers
dest. On utilise MAX_TITRE-1 (99) pour laisser de la place au caractère nul terminal \0. Contrairement
à strcpy, strncpy évite le débordement de buffer si la chaîne source est trop longue.
nouveau->titre[MAX_TITRE - 1] = '\0';
nouveau->nom_auteur[MAX_NOM - 1] = '\0';
nouveau->prenom_auteur[MAX_NOM - 1] = '\0';

GARANTIE DU \0 TERMINAL : Si la chaîne source fait exactement MAX_TITRE caractères, strncpy ne


copie pas le \0. On le force manuellement à la dernière position pour garantir que la chaîne est bien
terminée. Sans cela, printf pourrait lire au-delà du buffer (comportement indéfini).

return nouveau;
}

RETOUR : On retourne le pointeur vers le nœud nouvellement créé et initialisé. L'appelant est
responsable de libérer cette mémoire avec free() quand elle n'est plus nécessaire.

3.2 Fonction inserer_livre() — Algorithme Récursif

Livre *inserer_livre(Livre *racine, Livre *nouveau)


{
if (racine == NULL)
return nouveau;

CAS DE BASE : Si l'arbre est vide (racine NULL), le nouveau nœud devient la racine. C'est la condition
d'arrêt de la récursion.

if (nouveau->num_livre < racine->num_livre)


racine->gauche = inserer_livre(racine->gauche, nouveau);

INSERTION À GAUCHE : Si le numéro du nouveau livre est INFÉRIEUR au numéro de la racine


actuelle, on doit l'insérer dans le sous-arbre gauche. On appelle récursivement inserer_livre sur racine-
>gauche. Le résultat (qui peut être le nouveau nœud si gauche était NULL, ou le même pointeur
gauche sinon) est réaffecté à racine->gauche.

else if (nouveau->num_livre > racine->num_livre)


racine->droite = inserer_livre(racine->droite, nouveau);

INSERTION À DROITE : Si le numéro est SUPÉRIEUR, on insère dans le sous-arbre droit. Même
logique récursive.

else {
printf(" Numero %d deja present.\n", nouveau->num_livre);
free(nouveau);
}
return racine;
}
GESTION DES DOUBLONS : Si nouveau->num_livre == racine->num_livre, le livre existe déjà. On
affiche un message, on libère la mémoire du nouveau nœud avec free() (sinon fuite mémoire), et on ne
l'insère pas. Dans tous les cas, on retourne racine (l'arbre n'a pas changé, ou a été modifié
récursivement).

🔄 RÉCURSIVITÉ : inserer_livre s'appelle elle-même. Chaque appel descend d'un niveau dans
l'arbre. La pile d'appels se construit en profondeur jusqu'au cas de base (NULL), puis se
déroule en remontant, réaffectant les pointeurs modifiés.
3.3 Fonction afficher_tous() — Parcours Infixe

void afficher_tous(Livre *racine)


{
if (racine == NULL) return;

CAS DE BASE : Si le nœud est NULL (arbre vide ou feuille atteinte), on arrête la récursion.

afficher_tous(racine->gauche);

ÉTAPE 1 : On affiche d'abord TOUT le sous-arbre GAUCHE (appel récursif). Cela garantit que les
livres avec numéros inférieurs sont affichés en premier.

printf(" [%04d] %-40s | %-20s | %d | %d ex.\n",


racine->num_livre, racine->titre,
racine->nom_auteur,
racine->annee_edition,
racine->nb_exemplaires);

ÉTAPE 2 : On affiche la RACINE actuelle. Format :


- %04d : entier sur 4 chiffres, complété par des zéros (0078, 0312)
- %-40s : chaîne alignée à gauche sur 40 caractères
- %-20s : chaîne alignée à gauche sur 20 caractères
- %d : entier standard

afficher_tous(racine->droite);
}

ÉTAPE 3 : On affiche TOUT le sous-arbre DROIT (numéros supérieurs). Résultat : les livres sont
affichés dans l'ORDRE CROISSANT des numéros grâce au parcours INFIXE (gauche → racine →
droite).

🌳 PARCOURS INFIXE : Pour un ABR, ce parcours visite les nœuds dans l'ordre trié. Exemple :
arbre {5, 3, 7, 1, 4} → affiche 1, 3, 4, 5, 7.

3.4 Fonction afficher_alpha() — Tri avec qsort

Cette fonction affiche les livres par ordre alphabétique de titre. Comme l'ABR est trié par numéro (pas
par titre), on doit :
1. Collecter tous les livres dans un tableau
2. Trier ce tableau avec qsort()
3. Afficher le tableau trié

static void collecte_infixe(Livre *racine, Livre *tab[], int *n)


{
if (racine == NULL || *n >= MAX_LIVRES) return;
collecte_infixe(racine->gauche, tab, n);
tab[(*n)++] = racine;
collecte_infixe(racine->droite, tab, n);
}

FONCTION AUXILIAIRE collecte_infixe() :


- tab[] : tableau de POINTEURS vers Livre
- n : pointeur vers un compteur (nombre de livres collectés)
- Parcours infixe : à chaque visite d'un nœud, on stocke son adresse dans tab[*n] puis on incrémente
*n
- static : fonction interne au fichier arbre.c, non visible depuis main.c

static int cmp_titre(const void *a, const void *b)


{
const Livre *la = *(const Livre **)a;
const Livre *lb = *(const Livre **)b;
int i = 0;
while (la->titre[i] && lb->titre[i]) {
char ca = (la->titre[i] >= 'A' && la->titre[i] <= 'Z') ? la->titre[i] +
32 : la->titre[i];
char cb = (lb->titre[i] >= 'A' && lb->titre[i] <= 'Z') ? lb->titre[i] +
32 : lb->titre[i];
if (ca != cb) return ca - cb;
i++;
}
return la->titre[i] - lb->titre[i];
}

FONCTION COMPARATEUR pour qsort :


- qsort() exige une fonction de signature int (*)(const void*, const void*)
- a et b sont des pointeurs vers des éléments du tableau (ici, pointeurs vers Livre*)
- On caste en (Livre**) puis on déréférence pour obtenir les Livre*
- Comparaison caractère par caractère en IGNORANT LA CASSE : on convertit A-Z en a-z (+32 en
ASCII)
- Retour : <0 si a<b, 0 si égaux, >0 si a>b

void afficher_alpha(Livre *racine)


{
Livre *tab[MAX_LIVRES];
int n = 0;
collecte_infixe(racine, tab, &n);

ÉTAPE 1 : Déclaration d'un tableau statique de 500 pointeurs. Collecte de tous les livres via parcours
infixe.
if (n == 0) {
printf(" Aucun livre dans la bibliotheque.\n");
return;
}
qsort(tab, n, sizeof(Livre *), cmp_titre);

ÉTAPE 2 : qsort(tableau, nombre_elements, taille_element, fonction_comparaison) trie le tableau en


place selon le comparateur. Complexité : O(n log n).

printf(" %-4s %-40s %-20s %-6s %s\n", "N°", "Titre", "Auteur", "Annee",
"Exemplaires");
printf(" %s\n",
"--------------------------------------------------------------------------------
-");
for (int i = 0; i < n; i++) {
printf(" %-4d %-40s %s %s\n",
tab[i]->num_livre, tab[i]->titre,
tab[i]->prenom_auteur, tab[i]->nom_auteur);
}
printf(" %d livre(s) affiche(s).\n", n);
}

ÉTAPE 3 : Affichage du tableau trié avec une boucle for. Les livres apparaissent dans l'ordre
alphabétique des titres.
3.5 Fonctions de Recherche

3.5.1 rechercher_par_num() — Recherche BST

Livre *rechercher_par_num(Livre *racine, int num)


{
if (racine == NULL || racine->num_livre == num)
return racine;

CAS DE BASE : Si l'arbre est vide (NULL) ou si on a trouvé le livre (num_livre == num), on retourne le
pointeur (NULL ou le livre trouvé).

if (num < racine->num_livre)


return rechercher_par_num(racine->gauche, num);
return rechercher_par_num(racine->droite, num);
}

RECHERCHE RÉCURSIVE : Si num < racine, le livre est forcément à gauche (propriété ABR). Sinon, il
est à droite. On descend récursivement jusqu'à trouver ou atteindre NULL. Complexité : O(log n) en
moyenne, O(n) si l'arbre est déséquilibré (devient une liste).

3.5.2 rechercher_par_titre() — Parcours Complet

Livre *rechercher_par_titre(Livre *racine, const char *titre)


{
if (racine == NULL) return NULL;
int egal = 1, i = 0;
while (racine->titre[i] || titre[i]) {
char ca = racine->titre[i];
char cb = titre[i];
if (ca >= 'A' && ca <= 'Z') ca += 32;
if (cb >= 'A' && cb <= 'Z') cb += 32;
if (ca != cb) { egal = 0; break; }
i++;
}
if (egal) return racine;

COMPARAISON INSENSIBLE À LA CASSE : On compare caractère par caractère en convertissant A-


Z en a-z. Si tous les caractères correspondent, egal reste à 1.

Livre *res = rechercher_par_titre(racine->gauche, titre);


if (res != NULL) return res;
return rechercher_par_titre(racine->droite, titre);
}
PARCOURS COMPLET : Comme le titre n'est pas la clé de tri, on doit explorer TOUT l'arbre. On
cherche d'abord à gauche, si trouvé on retourne, sinon on cherche à droite. Complexité : O(n) dans
tous les cas.

3.6 Fonction emprunter_livre() — Gestion des Emprunts

Cette fonction est complexe car elle interagit avec les deux structures de données (ABR et liste
chaînée).

int emprunter_livre(Livre *racine, Emprunt **liste,


int num_l, int num_e,
const char *nom, const char *prenom,
time_t date_retour)
{
Livre *l = rechercher_par_num(racine, num_l);
if (l == NULL) {
printf(" Livre n°%d introuvable.\n", num_l);
return 0;
}
if (l->nb_exemplaires <= 0) {
printf(" Aucun exemplaire disponible pour \"%s\".\n", l->titre);
return 0;
}

VÉRIFICATION 1 : Le livre existe-t-il ? On recherche dans l'ABR. Si NULL, échec.


VÉRIFICATION 2 : Y a-t-il au moins un exemplaire disponible ? Si nb_exemplaires <= 0, échec.

Emprunt *curr = *liste;


while (curr) {
if (curr->num_livre == num_l && curr->num_etudiant == num_e) {
printf(" Cet etudiant a deja emprunte ce livre.\n");
return 0;
}
curr = curr->suivant;
}

VÉRIFICATION 3 : L'étudiant a-t-il déjà emprunté ce livre ? On parcourt la liste chaînée. Si on trouve
un emprunt avec le même (num_livre, num_etudiant), échec. Cela évite qu'un étudiant emprunte 2 fois
le même livre.

Emprunt *e = (Emprunt *)malloc(sizeof(Emprunt));


if (e == NULL) {
fprintf(stderr, "Erreur : allocation memoire echouee.\n");
return 0;
}
e->num_livre = num_l;
e->num_etudiant = num_e;
e->date_emprunt = time(NULL);
e->date_retour = date_retour;
CRÉATION DU NŒUD EMPRUNT : On alloue dynamiquement la mémoire. date_emprunt est généré
avec time(NULL) qui retourne le timestamp actuel (nombre de secondes depuis epoch). date_retour est
passé en paramètre (saisi par l'utilisateur).

strncpy(e->nom_etudiant, nom, MAX_NOM - 1);


strncpy(e->prenom_etudiant, prenom, MAX_NOM - 1);
e->nom_etudiant[MAX_NOM - 1] = '\0';
e->prenom_etudiant[MAX_NOM - 1] = '\0';

COPIE SÉCURISÉE : Même logique que pour creer_livre. On force le \0 terminal.

e->suivant = *liste;
*liste = e;

INSERTION EN TÊTE DE LISTE : C'est l'insertion la plus rapide O(1). Le nouveau nœud pointe vers
l'ancienne tête (*liste), puis on met à jour la tête pour qu'elle pointe vers le nouveau nœud. Note : *liste
est un double pointeur, donc *liste modifie réellement la variable du main.

l->nb_exemplaires--;
printf(" Emprunt enregistre : \"%s\" -> %s %s\n", l->titre, prenom, nom);
return 1;
}

MISE À JOUR DU STOCK : On décrémente le nombre d'exemplaires dans l'ABR. Le livre est toujours
dans l'arbre, mais son stock diminue. Message de confirmation et retour 1 (succès).
3.7 Fonction afficher_emprunts() — Bug de localtime CORRIGÉ

Cette fonction affiche tous les emprunts avec leurs dates formatées. Elle contenait un BUG SUBTIL lié
à localtime().

void afficher_emprunts(const Emprunt *liste, Livre *racine)


{
if (liste == NULL) {
printf(" Aucun emprunt en cours.\n");
return;
}
int i = 1;
const Emprunt *curr = liste;
while (curr) {
printf("\n -- Emprunt #%d --\n", i++);
printf(" Etudiant : %s %s (n°%d)\n",
curr->prenom_etudiant, curr->nom_etudiant, curr->num_etudiant);

PARCOURS DE LA LISTE : On itère avec curr = curr->suivant jusqu'à atteindre NULL.

Livre *l = rechercher_par_num(racine, curr->num_livre);


if (l)
printf(" Livre : \"%s\" (n°%d)\n", l->titre, curr->num_livre);
else
printf(" Livre : n°%d\n", curr->num_livre);

RECHERCHE DU TITRE : Avec num_livre de l'emprunt, on recherche le livre dans l'ABR pour obtenir
son titre. Si introuvable (livre supprimé ?), on affiche juste le numéro.

char buf_e[30], buf_r[30];


struct tm te_copy, tr_copy;
struct tm *te = localtime(&curr->date_emprunt);
te_copy = *te;
struct tm *tr = localtime(&curr->date_retour);
tr_copy = *tr;
strftime(buf_e, sizeof(buf_e), "%d/%m/%Y", &te_copy);
strftime(buf_r, sizeof(buf_r), "%d/%m/%Y", &tr_copy);

FORMATAGE DES DATES — VERSION CORRIGÉE :


PROBLÈME : localtime() retourne un POINTEUR VERS UNE STRUCTURE STATIQUE INTERNE.
Chaque appel ÉCRASE cette structure. Donc si on fait :
te = localtime(&date1);
tr = localtime(&date2);
te et tr pointent vers LA MÊME MÉMOIRE contenant la dernière date convertie (date2). Résultat : les
deux dates affichent la même valeur !

SOLUTION : On COPIE la structure tm AVANT le deuxième appel :


te = localtime(&date_emprunt); → te pointe vers buffer statique
te_copy = *te; → on copie le contenu dans une variable locale
tr = localtime(&date_retour); → te est maintenant invalide, mais te_copy est sauvegardé
tr_copy = *tr; → on copie aussi tr

strftime(buffer, taille, format, struct_tm*) convertit struct tm en chaîne formatée. %d/%m/%Y donne le
format JJ/MM/AAAA.

printf(" Emprunte : %s\n", buf_e);


printf(" Retour : %s\n", buf_r);
if (difftime(time(NULL), curr->date_retour) > 0)
printf(" EN RETARD\n");
curr = curr->suivant;
}
}

DÉTECTION DE RETARD : difftime(t2, t1) retourne le nombre de secondes entre t2 et t1. Si


time(NULL) (maintenant) > date_retour, le livre est en retard.

3.8 Fonctions de Sauvegarde/Chargement

Ces fonctions permettent de persister les données dans des fichiers binaires.

3.8.1 Sauvegarde de l'ABR (Parcours Préfixe)

typedef struct {
int num_livre;
char titre[MAX_TITRE];
char nom_auteur[MAX_NOM];
char prenom_auteur[MAX_NOM];
int annee_edition;
int nb_exemplaires;
} LivreFlat;

STRUCTURE AUXILIAIRE LivreFlat : Version de Livre SANS les pointeurs gauche/droite. On ne peut
pas sauvegarder les pointeurs directement car ils sont des adresses mémoire valides uniquement dans
l'exécution actuelle. LivreFlat contient seulement les données.

static void sauvegarder_abr_rec(Livre *racine, FILE *f)


{
int flag;
if (racine == NULL) {
flag = 0;
fwrite(&flag, sizeof(int), 1, f);
return;
}
flag = 1;
fwrite(&flag, sizeof(int), 1, f);

FLAG INDICATEUR : Avant chaque nœud, on écrit un entier (1 = nœud existant, 0 = NULL). Cela
permet de reconstruire la structure exacte de l'arbre au chargement. fwrite(adresse, taille_element,
nombre_elements, fichier) écrit les octets bruts en mémoire.

LivreFlat flat;
flat.num_livre = racine->num_livre;
flat.annee_edition = racine->annee_edition;
flat.nb_exemplaires = racine->nb_exemplaires;
strncpy([Link], racine->titre, MAX_TITRE - 1);
strncpy(flat.nom_auteur, racine->nom_auteur, MAX_NOM - 1);
strncpy(flat.prenom_auteur, racine->prenom_auteur, MAX_NOM - 1);
[Link][MAX_TITRE - 1] = '\0';
flat.nom_auteur[MAX_NOM - 1] = '\0';
flat.prenom_auteur[MAX_NOM - 1] = '\0';
fwrite(&flat, sizeof(LivreFlat), 1, f);

COPIE DANS LivreFlat : On recopie tous les champs de racine dans flat (sans les pointeurs). Puis on
écrit flat en un seul bloc avec fwrite. sizeof(LivreFlat) = environ 210 octets.

sauvegarder_abr_rec(racine->gauche, f);
sauvegarder_abr_rec(racine->droite, f);
}

PARCOURS PRÉFIXE RÉCURSIF : racine → gauche → droite. Cette ordre est CRUCIAL pour pouvoir
reconstruire l'arbre à l'identique au chargement.

3.8.2 Chargement de l'ABR

Livre *charger_livres(const char *fichier)


{
FILE *f = fopen(fichier, "rb");
if (f == NULL) {
printf(" Aucun fichier '%s' trouve.\n", fichier);
return NULL;
}
Livre *racine = NULL;
LivreFlat flat;
int flag;
while (fread(&flag, sizeof(int), 1, f) == 1) {
if (flag == 0) continue;
if (fread(&flat, sizeof(LivreFlat), 1, f) != 1) break;
Livre *nouveau = creer_livre(flat.num_livre, [Link],
flat.nom_auteur, flat.prenom_auteur,
flat.annee_edition, flat.nb_exemplaires);
if (nouveau)
racine = inserer_livre(racine, nouveau);
}
fclose(f);
printf(" Livres charges depuis '%s'.\n", fichier);
return racine;
}

STRATÉGIE DE CHARGEMENT : On lit séquentiellement les flags et les nœuds. Pour chaque nœud
lu, on le recrée avec creer_livre() puis on le réinsère dans un NOUVEL ABR avec inserer_livre(). Cette
méthode est simple mais ne garantit PAS que l'arbre reconstruit ait exactement la même forme (car
inserer_livre suit l'ordre des numéros, pas l'ordre préfixe). Cependant, l'arbre reste fonctionnellement
équivalent (mêmes livres, même propriété BST).

Note : fread retourne le nombre d'éléments lus (1 si succès, 0 si fin de fichier).


4. FICHIER main.c — INTERFACE UTILISATEUR

Ce fichier contient le programme principal avec le menu interactif, la boucle événementielle, et toutes
les fonctions handler qui réagissent aux choix de l'utilisateur.

4.1 Variables Globales

#include "arbre.h"
static Livre *racine = NULL;
static Emprunt *liste = NULL;

VARIABLES GLOBALES STATIQUES :


- racine : pointeur vers la racine de l'ABR des livres (NULL si arbre vide)
- liste : pointeur vers la tête de la liste chaînée des emprunts (NULL si liste vide)
- static : limite leur portée au fichier main.c (pas accessibles depuis arbre.c)

Ces variables sont globales car elles doivent être accessibles par toutes les fonctions handler
(handle_ajouter_livre, handle_emprunter, etc.). C'est un choix de conception simple pour ce projet.

4.2 Fonctions Utilitaires de Saisie

static void vider_buffer(void)


{
int c;
while ((c = getchar()) != '\n' && c != EOF);
}

FONCTION vider_buffer() :
Après un scanf, il reste souvent des caractères dans le buffer d'entrée (notamment le \n). Cette
fonction lit et ignore tous les caractères jusqu'au \n ou EOF. C'est essentiel pour éviter que le prochain
scanf ou getchar ne lise des restes.

static int lire_entier(const char *invite)


{
int val;
printf("%s", invite);
fflush(stdout);
while (scanf("%d", &val) != 1) {
vider_buffer();
printf(" Entier invalide, reessayez : ");
fflush(stdout);
}
vider_buffer();
return val;
}

FONCTION lire_entier() :
- Affiche l'invite (ex: 'Numéro du livre : ')
- fflush(stdout) force l'affichage immédiat (utile si stdout est bufferisé)
- scanf('%d', &val) lit un entier. Retourne 1 si succès, 0 si échec (ex: si l'utilisateur tape 'abc')
- BOUCLE while : tant que scanf échoue, on vide le buffer et on redemande
- Une fois l'entier lu, on vide le buffer pour éliminer le \n

static void lire_chaine(const char *invite, char *buf, int taille)


{
int c, len;
printf("%s", invite);
fflush(stdout);
len = 0;
while (len < taille - 1) {
c = getchar();
if (c == EOF || c == '\n') break;
buf[len++] = (char)c;
}
buf[len] = '\0';
}

FONCTION lire_chaine() — VERSION CORRIGÉE :


PROBLÈME INITIAL : fgets() lit le \n laissé par scanf précédent, retournant une chaîne vide
immédiatement.
SOLUTION : Lire caractère par caractère avec getchar() dans une boucle. On s'arrête au \n ou EOF.
Cette méthode est IMMUNISÉE contre les restes dans le buffer car elle consomme explicitement tous
les caractères jusqu'au \n.

4.3 Fonction afficher_menu()

static void afficher_menu(void)


{
printf("\n");
printf(" +==========================================+\n");
printf(" | BIBLIOTHEQUE UNIVERSITAIRE - MENU |\n");
printf(" +==========================================+\n");
printf(" | -- PARTIE 1 : Gestion des Livres -- |\n");
printf(" | 1. Ajouter un livre |\n");
// ... (lignes 2-13)
printf(" | 0. Quitter |\n");
printf(" +==========================================+\n");
printf(" Votre choix : ");
fflush(stdout);
}
Affiche le menu principal avec les 13 options + quitter. Utilise des caractères ASCII (+ - |) pour dessiner
les bordures (compatibles tous terminaux). fflush à la fin pour afficher immédiatement sans attendre
un \n.

4.4 Fonctions Handler — Exemple : handle_ajouter_livre()

static void handle_ajouter_livre(void)


{
TITRE_SECTION("AJOUTER UN LIVRE");
char titre[MAX_TITRE], nom[MAX_NOM], prenom[MAX_NOM];
int num = lire_entier(" Numero du livre : ");
if (rechercher_par_num(racine, num)) {
printf(" Numero %d deja existant.\n", num);
return;
}
lire_chaine(" Titre : ", titre, MAX_TITRE);
lire_chaine(" Nom de l'auteur : ", nom, MAX_NOM);
lire_chaine(" Prenom de l'auteur : ", prenom, MAX_NOM);
int annee = lire_entier(" Annee d'edition : ");
int nb_ex = lire_entier(" Nombre d'exemplaires : ");
if (nb_ex < 0) {
printf(" Nombre d'exemplaires invalide.\n");
return;
}
Livre *nouveau = creer_livre(num, titre, nom, prenom, annee, nb_ex);
if (nouveau) {
racine = inserer_livre(racine, nouveau);
printf(" Livre \"%s\" ajoute avec succes.\n", titre);
}
}

DÉROULEMENT :
1. Affiche un titre de section avec la macro TITRE_SECTION
2. Déclare 3 buffers locaux pour titre, nom, prenom
3. Lit le numéro avec lire_entier()
4. Vérifie qu'il n'existe pas déjà (recherche dans l'ABR)
5. Lit les 5 autres champs (titre, nom, prenom avec lire_chaine, annee et nb_ex avec lire_entier)
6. Valide que nb_exemplaires >= 0
7. Crée le nœud avec creer_livre()
8. L'insère dans l'ABR avec inserer_livre() en mettant à jour racine
9. Affiche un message de confirmation

Note : racine = inserer_livre(racine, nouveau) est ESSENTIEL car si l'arbre était vide (racine NULL),
inserer_livre retourne le nouveau nœud qui devient la nouvelle racine.
4.5 Fonction main() — Boucle Principale

int main(void)
{
int choix;
printf("\n");
printf(" +==================================================+\n");
printf(" | SYSTEME DE GESTION DE BIBLIOTHEQUE - v1.0 |\n");
printf(" | Universite Aube Nouvelle | L2IT 2025-2026 |\n");
printf(" +==================================================+\n");

INITIALISATION : Affiche une bannière de bienvenue.

racine = charger_livres(FICHIER_LIVRES);
liste = charger_emprunts(FICHIER_EMPRUNTS);
if (racine == NULL)
charger_donnees_demo();

CHARGEMENT AUTOMATIQUE : Au démarrage, le programme tente de charger les données depuis


les fichiers. Si les fichiers n'existent pas (première exécution), racine reste NULL et on charge des
données de démonstration pour permettre de tester l'application immédiatement.

do {
afficher_menu();
while (scanf("%d", &choix) != 1) {
vider_buffer();
printf(" Choix invalide, reessayez : ");
fflush(stdout);
}
vider_buffer();

BOUCLE DO-WHILE : Garantit au moins une itération. À chaque tour :


1. Affiche le menu
2. Lit le choix avec scanf (avec validation en boucle)
3. Vide le buffer

switch (choix) {
case 1: handle_ajouter_livre(); break;
case 2: handle_afficher_tous(); break;
// ... cases 3-13
case 0:
handle_sauvegarder();
printf("\n Au revoir !\n\n");
break;
default:
printf(" Choix invalide (0-13).\n");
}
} while (choix != 0);
SWITCH-CASE : Selon le choix, appelle la fonction handler appropriée. Case 0 (quitter) : sauvegarde
automatiquement avant de sortir de la boucle. Default : message d'erreur si choix hors plage.

liberer_arbre(racine);
liberer_liste(liste);
return 0;
}

NETTOYAGE FINAL : Après la sortie de la boucle, on libère TOUTE la mémoire allouée


dynamiquement. Cela évite les fuites mémoire. return 0 indique au système d'exploitation que le
programme s'est terminé avec succès.
5. CONCEPTS AVANCÉS ET PIÈGES À ÉVITER

5.1 Gestion de la Mémoire Dynamique

RÈGLE D'OR : Pour chaque malloc(), il doit y avoir un free() correspondant.

ALLOCATION : malloc(taille) alloue taille octets sur le TAS et retourne un pointeur. Le TAS est une
zone mémoire persistante (contrairement à la pile qui libère automatiquement les variables locales).

LIBÉRATION : free(pointeur) libère la mémoire pointée. Après free(), le pointeur devient invalide
(dangling pointer). Il faut le mettre à NULL : free(p); p = NULL;

FUITES MÉMOIRE : Si on perd le seul pointeur vers une zone allouée sans la libérer, cette mémoire
devient irrécupérable (leak). Exemple :
Livre *l = creer_livre(...);
l = creer_livre(...); // FUITE : le premier livre est perdu

5.2 Pointeurs et Double Pointeurs

POINTEUR SIMPLE (Livre*) : Variable qui contient l'adresse d'un Livre en mémoire.

DOUBLE POINTEUR (Emprunt**) : Pointeur vers un pointeur. Utilisé quand on veut modifier le pointeur
lui-même (pas juste ce qu'il pointe). Exemple : inserer_livre() modifie les pointeurs gauche/droite dans
l'arbre. emprunter_livre() modifie le pointeur de tête de la liste.

void modifier(int **pp) {


*pp = malloc(sizeof(int)); // modifie le pointeur externe
**pp = 42; // modifie la valeur pointée
}

5.3 Récursivité : Comment Ça Marche ?

Une fonction récursive s'appelle elle-même. Chaque appel crée un nouveau CADRE DE PILE (frame)
avec ses propres variables locales et paramètres.

EXEMPLE : inserer_livre(racine, nouveau)


1. Appel initial avec racine = nœud 50
2. [Link] = 30 < 50 → appel récursif avec racine = nœud 50->gauche
3. [Link] = 30 > 20 → appel récursif avec racine = nœud 20->droite
4. racine = NULL → retourne nouveau (cas de base)
5. Retour au cadre 3 : 20->droite = nouveau
6. Retour au cadre 2 : 50->gauche reste inchangé
7. Retour au cadre 1 : retourne 50

PILE D'APPELS : Les cadres s'empilent en descendant, puis se dépilent en remontant. Chaque return
remonte d'un niveau.

⚠️DANGER : Récursion infinie si pas de cas de base, ou si la condition d'arrêt n'est jamais
atteinte → Stack Overflow (débordement de pile).

5.4 Complexité Algorithmique

NOTATION BIG-O : Mesure la croissance du temps d'exécution en fonction de la taille n des données.

O(1) — CONSTANT : Temps fixe, indépendant de n. Ex: insertion en tête de liste.


O(log n) — LOGARITHMIQUE : Divise le problème par 2 à chaque étape. Ex: recherche dans ABR
équilibré.
O(n) — LINÉAIRE : Parcourt toutes les données une fois. Ex: parcours de liste, recherche par titre.
O(n log n) — LINÉARITHMIQUE : Tri efficace. Ex: qsort().
O(n²) — QUADRATIQUE : Boucles imbriquées. Ex: tri à bulles.

POUR L'ABR : En moyenne O(log n) pour insertion/recherche. MAIS si l'arbre est déséquilibré (ex:
insertion dans l'ordre croissant → liste chaînée), dégénère en O(n). Solution : ABR auto-équilibrés
(AVL, Rouge-Noir).

5.5 Bug localtime() Expliqué en Détail

localtime() convertit un time_t (timestamp) en struct tm (date décomposée).

PROBLÈME : localtime() utilise un BUFFER STATIQUE INTERNE unique. Chaque appel écrase ce
buffer.

struct tm *t1 = localtime(&date1); // t1 pointe vers buffer[A]


struct tm *t2 = localtime(&date2); // t2 pointe vers buffer[A] (écrasé !)
// t1 et t2 pointent vers la MÊME adresse → t1 est invalidé

SOLUTION 1 : Copier la structure immédiatement :


struct tm t1_copy = *localtime(&date1);
SOLUTION 2 (notre code) : Appeler, copier, appeler, copier :
struct tm *te = localtime(&date1);
struct tm te_copy = *te; // copie locale
struct tm *tr = localtime(&date2);
struct tm tr_copy = *tr; // copie locale

ALTERNATIVE : localtime_r() (réentrante) qui prend un buffer fourni par l'utilisateur. Disponible sur
POSIX (Linux/Mac), pas sur Windows standard.
6. POINTS CLÉS À RETENIR

• L'ABR est efficace O(log n) SEULEMENT s'il est équilibré. Sinon, il dégénère en O(n).
• Les listes chaînées permettent des insertions/suppressions en O(1) mais recherche en O(n).
• Toujours vérifier le retour de malloc() contre NULL.
• strncpy() protège contre les débordements mais ne garantit PAS le \0 terminal si la source est
trop longue.
• localtime() retourne un pointeur statique qui est écrasé à chaque appel → copier
immédiatement.
• Le parcours infixe d'un ABR donne les éléments dans l'ordre trié.
• Un double pointeur (T**) est nécessaire pour modifier un pointeur passé en paramètre.
• time_t est un entier (timestamp UNIX), struct tm est une structure décomposée (jour, mois,
année, etc.).
• fwrite/fread écrivent/lisent des octets bruts. Les pointeurs ne peuvent PAS être sauvegardés
directement.
• La récursivité nécessite un cas de base clair pour éviter le stack overflow.

FIN DU DOCUMENT D'EXPLICATION


Ce code représente 869 lignes de C implémentant un système complet de gestion de bibliothèque avec ABR,
liste chaînée, et persistance fichier.

Vous aimerez peut-être aussi