Chapitre 4 : La STL (Standard Template Library) — Flux, Conteneurs et Algorithmes
Exploiter la puissance de la bibliothèque standard C++ : maîtriser les flux d'entrées/sorties
(std::cout, std::cerr), choisir et manipuler les conteneurs adaptés
(std::vector, std::set, std::map) et appliquer les algorithmes
génériques indispensables (std::sort, std::reverse, std::count, std::count_if) avec des exemples simples.
Objectifs pédagogiques de la séance
- Comprendre l'architecture et les trois piliers de la STL : Conteneurs, Itérateurs et Algorithmes.
- Différencier le flux de sortie standardisé
std::coutdu flux d'erreur immédiatstd::cerr(tamponné vs non-tamponné). - Manipuler les conteneurs séquentiels dynamiques avec
std::vectoret comprendre l'accès sécuriséat(). - Organiser et filtrer des données uniques sans doublon grâce à l'ensemble ordonné
std::set. - Associer des données avec le dictionnaire clé-valeur
std::mapet éviter le piège d'insertion accidentelle de l'opérateur[]. - Appliquer les algorithmes de
<algorithm>(tri, inversion, dénombrement) et s'initier aux prédicats avec les fonctions lambdas.
Introduction et Philosophie de la STL
1.1. Qu'est-ce que la STL ?
La STL (Standard Template Library) est le cœur battant de la bibliothèque standard C++. Conçue à l'origine par Alexander Stepanov, elle met en pratique à grande échelle les concepts de généricité (templates) étudiés au Chapitre 3.
Son objectif est fondamental : fournir aux développeurs un catalogue complet de structures de données et d'algorithmes ultra-optimisés, testés, sécurisés et interopérables, sans avoir à réinventer la roue à chaque projet.
En C++, l'utilisation des composants de la STL ne génère aucun ralentissement par rapport à du code C écrit manuellement au plus bas niveau : « What you don't use, you don't pay for. And what you use, you couldn't hand code any better. » (Bjarne Stroustrup).
1.2. Les Trois Piliers Fondamentaux de la STL
La STL découple complètement le stockage des données des algorithmes qui les manipulent grâce à une passerelle universelle : les itérateurs.
- Stockent les éléments en mémoire
- Séquentiels :
vector,list,deque - Associatifs :
set,map
- Traitements indépendants du conteneur
sort,reversecount,count_if,find
Un algorithme comme std::sort ou std::count ne sait rien de la structure interne d'un vector : il travaille exclusivement sur des itérateurs délimitant une plage [debut, fin).
Les Flux Standards d'Entrée / Sortie : std::cout & std::cerr
2.1. La notion de Flux (Stream) en C++
En C++, les opérations d'entrée/sortie sont modélisées par des flux (streams) définis dans l'en-tête <iostream>.
Un flux est une séquence ordonnée d'octets circulant entre le programme et un périphérique physique (écran, clavier, fichier ou socket réseau).
- L'opérateur
<<(insertion) permet d'envoyer des données vers un flux de sortie. - L'opérateur
>>(extraction) permet de lire des données depuis un flux d'entrée (std::cin).
2.2. std::cout vs std::cerr : Une distinction cruciale
Bien que ces deux flux écrivent par défaut sur la console d'affichage, ils répondent à des rôles et des mécaniques internes totalement différents :
🖥️ std::cout (Standard Output Stream)
- Destiné aux résultats normaux et attendus du programme.
- Bufferisé (tamponné) : les caractères sont accumulés dans un tampon mémoire en RAM et ne sont écrits sur le terminal que lorsque le tampon est plein ou lorsqu'un vidage (flush) est requis.
- Très performant car il regroupe les appels système.
🚨 std::cerr (Standard Error Stream)
- Destiné aux messages d'erreur, alertes critiques et diagnostics.
- Non bufferisé (unbuffered) : chaque caractère est envoyé instantanément à l'écran sans attendre.
- Garantit que le message d'erreur apparaîtra à l'écran même si le programme plante immédiatement après (ex: Segfault ou exception non rattrapée).
L'instruction std::cout << std::endl; effectue deux actions : elle insère un saut de ligne ET force le vidage physique du tampon (flush).
Dans une boucle traitant 100 000 lignes, l'emploi répété de std::endl ralentit considérablement l'exécution. Préférez systématiquement '\n' pour les boucles rapides.
2.3. Exemple pratique commenté
Voici un exemple simple illustrant l'usage conjoint de std::cout et std::cerr :
/** * Chapitre 4 - Exemple 1 : Les Flux Standards en C++ (std::cout, std::cerr) * Différence entre flux standard bufferisé et flux d'erreur immédiat */ #include <iostream> #include <string> int main() { std::cout << "=== Les Flux d'Entrée / Sortie en C++ ===\n\n"; // 1. std::cout : Flux de sortie standard (standard output stream) // Il est bufferisé : les caractères sont stockés en mémoire tampon avant d'être émis std::cout << "[INFO] Bienvenue dans le programme d'initiation à la STL.\n"; std::cout << "[INFO] std::cout est utilisé pour les résultats nominaux et les messages réguliers.\n"; int valeur = 42; double temperature = 21.5; std::string module = "Licence 3 Pro Dev Web"; // Chaînage naturel avec l'opérateur d'insertion << std::cout << "Module : " << module << " | Variable = " << valeur << " | Température = " << temperature << " °C\n\n"; // 2. std::cerr : Flux d'erreur standard (standard error stream) // Il n'est PAS bufferisé (unbuffered) : chaque caractère est envoyé immédiatement au terminal. // Idéal pour les messages d'alerte, diagnostics et exceptions. std::cerr << "[ERREUR std::cerr] Exemple d'alerte critique : fichier de configuration introuvable !\n"; std::cerr << "[ATTENTION std::cerr] Ce message s'affiche immédiatement sans attendre le vidage du tampon.\n\n"; // 3. std::endl vs '\n' // std::endl ajoute un saut de ligne ET force le vidage physique du tampon (flush). // '\n' est beaucoup plus rapide lors de boucles répétitives. std::cout << "Ligne avec vidage explicite du tampon." << std::endl; return 0; }
Les Conteneurs STL Fondamentaux : vector, set et map
La STL propose une vaste famille de conteneurs. Pour un développeur moderne, trois d'entre eux couvrent l'immense majorité des besoins :
std::vector, std::set et std::map.
3.1. std::vector : Le tableau dynamique universel (En-tête <vector>)
std::vector<T> est le conteneur à utiliser par défaut en C++.
Il s'agit d'un tableau contigu en mémoire dont la taille s'ajuste automatiquement au fur et à mesure des insertions.
| Opération | Syntaxe C++ | Complexité | Description |
|---|---|---|---|
| Ajout en queue | v.push_back(valeur) |
$O(1)$ amorti | Ajoute l'élément à la fin et réalloue si nécessaire. |
| Accès rapide | v[i] |
$O(1)$ | Accès direct sans contrôle des bornes. |
| Accès sécurisé | v.at(i) |
$O(1)$ | Vérifie l'indice et lève std::out_of_range si invalide. |
| Taille actuelle | v.size() |
$O(1)$ | Renvoie le nombre d'éléments présents. |
| Retrait en queue | v.pop_back() |
$O(1)$ | Supprime le dernier élément. |
/** * Chapitre 4 - Exemple 2 : Le Conteneur Séquentiel std::vector * Tableau dynamique redimensionnable de la STL */ #include <iostream> #include <vector> #include <string> #include <stdexcept> int main() { std::cout << "=== Le Conteneur Dynamique std::vector ===\n\n"; // 1. Déclaration et initialisation std::vector<int> nombres = {10, 20, 30, 40}; std::cout << "Taille initiale : " << nombres.size() << " éléments.\n"; // 2. Ajout dynamique en fin de vecteur avec push_back() std::cout << "Ajout de 50 et 60...\n"; nombres.push_back(50); nombres.push_back(60); std::cout << "Nouvelle taille : " << nombres.size() << " | Capacité allouée : " << nombres.capacity() << "\n"; // 3. Parcours moderne par référence constante (Range-based for) std::cout << "Contenu du vecteur : [ "; for (const auto& x : nombres) { std::cout << x << " "; } std::cout << "]\n\n"; // 4. Accès indicé : operator[] vs méthode sécurisée at() std::cout << "Premier élément (front) : " << nombres.front() << "\n"; std::cout << "Dernier élément (back) : " << nombres.back() << "\n"; std::cout << "Élément à l'indice 2 : " << nombres[2] << "\n"; try { std::cout << "Tentative d'accès sécurisé à l'indice 10 avec at()...\n"; int valeurInvalide = nombres.at(10); // Lève une exception std::out_of_range std::cout << "Valeur : " << valeurInvalide << "\n"; } catch (const std::out_of_range& e) { std::cerr << ">> [Exception interceptée] " << e.what() << "\n"; } // 5. Retrait du dernier élément avec pop_back() std::cout << "\nRetrait du dernier élément avec pop_back()...\n"; nombres.pop_back(); std::cout << "Contenu final : [ "; for (const auto& x : nombres) { std::cout << x << " "; } std::cout << "] (taille = " << nombres.size() << ")\n"; return 0; }
3.2. std::set : L'ensemble ordonné sans doublon (En-tête <set>)
Un std::set<T> stocke des éléments uniques selon un ordre strict (par défaut croissant).
Sous le capot, il est implémenté sous forme d'un arbre binaire de recherche équilibré (Arbre Rouge-Noir).
- Unicité garantie : si vous essayez d'insérer un élément déjà existant, l'insertion est tout simplement ignorée.
- Éléments toujours triés : le parcours d'un
setvisite systématiquement les valeurs de la plus petite à la plus grande. - Recherche ultra-rapide : la recherche, l'insertion et la suppression s'exécutent en complexité logarithmique $O(\log N)$.
/** * Chapitre 4 - Exemple 3 : Le Conteneur Associatif std::set * Ensemble ordonné d'éléments uniques (Arbre Rouge-Noir sous-jacent) */ #include <iostream> #include <set> #include <string> int main() { std::cout << "=== L'Ensemble Ordonné std::set ===\n\n"; // 1. Déclaration : un ensemble de chaînes de caractères // Les éléments sont automatiquement triés et TOUT DOUBLON EST REJETÉ ! std::set<std::string> prenoms; // 2. Insertion d'éléments avec insert() prenoms.insert("Charlie"); prenoms.insert("Alice"); prenoms.insert("Bob"); prenoms.insert("David"); // Tentative d'insertion d'un doublon auto resultat = prenoms.insert("Alice"); if (!resultat.second) { std::cout << "[INFO] \"Alice\" existe déjà dans le set : doublon ignoré !\n"; } // 3. Parcours : les éléments apparaissent toujours par ordre alphabétique / croissant std::cout << "\nContenu du set (trié automatiquement) : { "; for (const auto& nom : prenoms) { std::cout << "\"" << nom << "\" "; } std::cout << "} (taille = " << prenoms.size() << ")\n\n"; // 4. Recherche efficace en O(log N) // En C++20, la méthode .contains() vérifie la présence en une ligne très lisible : std::string recherche = "Bob"; if (prenoms.contains(recherche)) { std::cout << ">> \"" << recherche << "\" est bien présent dans l'ensemble.\n"; } else { std::cout << ">> \"" << recherche << "\" est absent.\n"; } // 5. Suppression d'un élément std::cout << "\nSuppression de \"David\"...\n"; prenoms.erase("David"); std::cout << "Contenu après suppression : { "; for (const auto& nom : prenoms) { std::cout << "\"" << nom << "\" "; } std::cout << "}\n"; return 0; }
3.3. std::map : Le dictionnaire associatif clé-valeur (En-tête <map>)
Un std::map<Cle, Valeur> est un conteneur associatif qui stocke des paires composées d'une clé unique et d'une valeur associée.
Les éléments sont automatiquement triés selon la clé.
Lorsque vous écrivez valeur = map[cle];, si la clé n'existe pas encore dans la map,
l'opérateur crochet l'insère automatiquement avec la valeur par défaut du type (0 pour un int, "" pour un string) !
Pour vérifier si une clé existe sans modifier la map, utilisez toujours la méthode C++20 :
if (map.contains(cle)) { ... }
/** * Chapitre 4 - Exemple 4 : Le Tableau Associatif std::map * Dictionnaire Clé-Valeur ordonné (Arbre Binaire Équilibré) */ #include <iostream> #include <map> #include <string> int main() { std::cout << "=== Le Dictionnaire Associatif std::map ===\n\n"; // 1. Déclaration : Clé = string (nom du fruit), Valeur = double (prix au kilo) std::map<std::string, double> catalogue; // 2. Insertion avec l'opérateur crochet [] catalogue["Pomme"] = 2.40; catalogue["Banane"] = 1.90; catalogue["Orange"] = 2.80; catalogue["Fraise"] = 5.50; // 3. Parcours moderne avec décomposition structurée (Structured Binding C++17/C++20) std::cout << "Catalogue des prix (trié automatiquement par clé) :\n"; for (const auto& [fruit, prix] : catalogue) { std::cout << " • " << fruit << " : " << prix << " € / kg\n"; } // 4. Recherche sécurisée en C++20 sans créer de fausse entrée std::cout << "\nRecherche d'articles :\n"; std::string recherche = "Cerise"; if (catalogue.contains(recherche)) { std::cout << ">> " << recherche << " coûte " << catalogue[recherche] << " €\n"; } else { std::cout << ">> " << recherche << " n'est pas au catalogue.\n"; } // 5. ⚠️ Piège classique de l'operator[] : // Si la clé n'existe pas, l'opérateur [] L'INSÈRE avec sa valeur par défaut (ici 0.0) ! std::cout << "\nAppel de catalogue[\"Kiwi\"] sans assignation préalable :\n"; double prixKiwi = catalogue["Kiwi"]; // Insère silencieusement "Kiwi" avec la valeur 0.0 ! std::cout << "Prix retourné pour Kiwi : " << prixKiwi << " €\n"; std::cout << "\nVérification : Le catalogue contient maintenant Kiwi !\n"; for (const auto& [fruit, prix] : catalogue) { std::cout << " • " << fruit << " : " << prix << " € / kg\n"; } return 0; }
3.4. Tableau Récapitulatif des Conteneurs
| Conteneur | Structure interne | Ordre des éléments | Doublons permis ? | Cas d'usage typique |
|---|---|---|---|---|
std::vector<T> |
Tableau contigu | Ordre d'insertion | Oui | Stockage linéaire général, accès par indice $O(1)$. |
std::set<T> |
Arbre Rouge-Noir | Trié automatiquement | Non (uniques) | Filtrage de doublons, test d'appartenance rapide $O(\log N)$. |
std::map<K, V> |
Arbre Rouge-Noir | Trié par les clés $K$ | Clés uniques | Annuaires, dictionnaires, tables de correspondances. |
Les Algorithmes Standards de la STL (<algorithm>)
L'en-tête <algorithm> fournit plus d'une centaine de fonctions génériques capables d'opérer sur n'importe quelle séquence délimitée par une paire d'itérateurs : begin() et end().
[ v.begin() → Éléments valides → v.end() )
v.end() pointe juste après le dernier élément valide (sentinelle d'arrêt).
4.1. Quatre algorithmes incontournables
std::sort(debut, fin): Trie les éléments par ordre croissant en $O(N \log N)$ (utilise un algorithme hybride très rapide appelé IntroSort).std::reverse(debut, fin): Inverse en place l'ordre des éléments du conteneur en $O(N)$.std::count(debut, fin, valeur): Compte le nombre d'éléments strictement égaux à une valeur cible donnée.std::count_if(debut, fin, predicat): Compte les éléments satisfaisant une condition logique (prédicat) exprimée sous forme de fonction Lambda.
Une lambda est une fonction anonyme locale définie directement sur place au moment où on en a besoin.
Syntaxe minimale : [](int n) { return n >= 10; }
Ici, la lambda prend un entier n et renvoie true s'il est supérieur ou égal à 10.
4.2. Exemple pratique complet
Voici un programme complet appliquant ces 4 algorithmes sur un tableau de notes d'étudiants :
/** * Chapitre 4 - Exemple 5 : Les Algorithmes de la STL (<algorithm>) * Mise en œuvre de sort, reverse, count et count_if avec expressions lambda */ #include <iostream> #include <vector> #include <algorithm> #include <string> // Fonction utilitaire d'affichage void afficher(const std::vector<int>& v, const std::string& message) { std::cout << message << " : [ "; for (int x : v) { std::cout << x << " "; } std::cout << "]\n"; } int main() { std::cout << "=== Les Algorithmes Fondamentaux de la STL ===\n\n"; std::vector<int> notes = {12, 5, 18, 12, 9, 14, 12, 20, 7}; afficher(notes, "Notes initiales"); // 1. std::sort : Tri croissant en O(N log N) (IntroSort) std::cout << "\n--- 1. std::sort (Tri dans l'ordre croissant) ---\n"; std::sort(notes.begin(), notes.end()); afficher(notes, "Après std::sort"); // 2. std::reverse : Inversion en place des éléments std::cout << "\n--- 2. std::reverse (Inversion du vecteur) ---\n"; std::reverse(notes.begin(), notes.end()); afficher(notes, "Après std::reverse (ordre décroissant)"); // 3. std::count : Comptage des occurrences exactes d'une valeur std::cout << "\n--- 3. std::count (Comptage de valeurs exactes) ---\n"; int cible = 12; auto nbDouze = std::count(notes.begin(), notes.end(), cible); std::cout << "Nombre de fois où la note " << cible << " apparaît : " << nbDouze << "\n"; int absente = 15; std::cout << "Nombre de fois où la note " << absente << " apparaît : " << std::count(notes.begin(), notes.end(), absente) << "\n"; // 4. std::count_if : Comptage selon un prédicat (Fonction Lambda) std::cout << "\n--- 4. std::count_if (Comptage conditionnel avec Lambda) ---\n"; // Prédicat 1 : Notes supérieures ou égales à 10 (la moyenne) auto nbMoyenne = std::count_if(notes.begin(), notes.end(), [](int n) { return n >= 10; }); std::cout << "Nombre d'étudiants ayant la moyenne (>= 10) : " << nbMoyenne << "\n"; // Prédicat 2 : Nombres pairs auto nbPairs = std::count_if(notes.begin(), notes.end(), [](int n) { return n % 2 == 0; }); std::cout << "Nombre de notes paires : " << nbPairs << "\n"; return 0; }
Synthèse & Auto-évaluation
5.1. Fiche Mémo Récapitulative
| Composant | En-tête | Règle essentielle à retenir |
|---|---|---|
std::cout |
<iostream> |
Sortie normale bufferisée (privilégier '\n' pour la vitesse). |
std::cerr |
<iostream> |
Sortie d'erreur immédiate non-tamponnée pour diagnostics urgents. |
std::vector |
<vector> |
Conteneur séquentiel dynamique par défaut, accès indexé direct $O(1)$. |
std::set |
<set> |
Ensemble ordonné rejetant automatiquement les doublons ($O(\log N)$). |
std::map |
<map> |
Dictionnaire clé-valeur. Préférer .contains() à [] pour tester l'existence. |
std::sort / reverse |
<algorithm> |
Opèrent sur des itérateurs v.begin(), v.end() sans connaître le conteneur. |
count_if |
<algorithm> |
Comptage conditionnel piloté par un prédicat ou une lambda. |
5.2. QCM de Réflexion (Niveau L3)
std::cerr apparaît-il immédiatement sur le terminal alors qu'un message sur std::cout peut tarder à s'afficher ?
std::cout accumule les caractères dans un tampon en RAM pour optimiser les performances. Au contraire, std::cerr est non-tamponné pour s'assurer que les messages d'erreur sont affichés même en cas de crash imminent du processus.
42 dans un std::set<int>, quelle sera la taille du conteneur ?
std::set est un ensemble d'éléments uniques. Tout doublon inséré est silencieusement ignoré sans lever d'exception.
double p = catalogue["Inconnu"]; sur un std::map<std::string, double> si la clé n'existe pas ?
operator[] d'une map a un effet de bord modificateur : il insère automatiquement une valeur par défaut si la clé est introuvable. Pour une simple consultation sans risque d'insertion parasite, on utilise if (map.contains(cle)) ou map.at(cle).
📝 Mini-Exercice de mise en pratique
Soit un texte représenté sous forme de liste de mots : std::vector<std::string> mots = {"le", "chat", "mange", "le", "souris", "le", "chat"};
- 1. Utilisez un
std::set<std::string>pour éliminer instantanément tous les doublons et afficher les mots distincts par ordre alphabétique. - 2. Utilisez un
std::map<std::string, int>pour calculer la fréquence d'apparition (occurrences) de chaque mot. - 3. Utilisez
std::count_ifpour dénombrer combien de mots ont une longueur strictement supérieure à 3 lettres.
Navigation dans le cours
Vous pouvez revenir aux chapitres précédents ou naviguer vers les feuilles de travaux dirigés :