Niveau : Licence 3 Module : Programmation Orientée Objet Volume : 1h30 (CM/TD) Coloration : GeSHi

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::cout du flux d'erreur immédiat std::cerr (tamponné vs non-tamponné).
  • Manipuler les conteneurs séquentiels dynamiques avec std::vector et 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::map et é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.
1

Introduction et Philosophie de la STL

⏱️ 15 minutes

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.

💡 Le principe du « Zero-Overhead » (Coût nul de l'abstraction)

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.

Architecture Tripartite de la STL
📦 1. Les Conteneurs
  • Stockent les éléments en mémoire
  • Séquentiels : vector, list, deque
  • Associatifs : set, map
◀ Itérateurs ▶
⚡ 2. Les Algorithmes
  • Traitements indépendants du conteneur
  • sort, reverse
  • count, 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).

2

Les Flux Standards d'Entrée / Sortie : std::cout & std::cerr

⏱️ 20 minutes

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).

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).
⚠️ Performance : std::endl vs '\n'

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 :

CPPstl_flux.cpp
  1. /**
  2.  * Chapitre 4 - Exemple 1 : Les Flux Standards en C++ (std::cout, std::cerr)
  3.  * Différence entre flux standard bufferisé et flux d'erreur immédiat
  4.  */
  5.  
  6. #include <iostream>
  7. #include <string>
  8.  
  9. int main() {
  10. std::cout << "=== Les Flux d'Entrée / Sortie en C++ ===\n\n";
  11.  
  12. // 1. std::cout : Flux de sortie standard (standard output stream)
  13. // Il est bufferisé : les caractères sont stockés en mémoire tampon avant d'être émis
  14. std::cout << "[INFO] Bienvenue dans le programme d'initiation à la STL.\n";
  15. std::cout << "[INFO] std::cout est utilisé pour les résultats nominaux et les messages réguliers.\n";
  16.  
  17. int valeur = 42;
  18. double temperature = 21.5;
  19. std::string module = "Licence 3 Pro Dev Web";
  20.  
  21. // Chaînage naturel avec l'opérateur d'insertion <<
  22. std::cout << "Module : " << module
  23. << " | Variable = " << valeur
  24. << " | Température = " << temperature << " °C\n\n";
  25.  
  26. // 2. std::cerr : Flux d'erreur standard (standard error stream)
  27. // Il n'est PAS bufferisé (unbuffered) : chaque caractère est envoyé immédiatement au terminal.
  28. // Idéal pour les messages d'alerte, diagnostics et exceptions.
  29. std::cerr << "[ERREUR std::cerr] Exemple d'alerte critique : fichier de configuration introuvable !\n";
  30. std::cerr << "[ATTENTION std::cerr] Ce message s'affiche immédiatement sans attendre le vidage du tampon.\n\n";
  31.  
  32. // 3. std::endl vs '\n'
  33. // std::endl ajoute un saut de ligne ET force le vidage physique du tampon (flush).
  34. // '\n' est beaucoup plus rapide lors de boucles répétitives.
  35. std::cout << "Ligne avec vidage explicite du tampon." << std::endl;
  36.  
  37. return 0;
  38. }
3

Les Conteneurs STL Fondamentaux : vector, set et map

⏱️ 30 minutes

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.
CPPstl_vector.cpp
  1. /**
  2.  * Chapitre 4 - Exemple 2 : Le Conteneur Séquentiel std::vector
  3.  * Tableau dynamique redimensionnable de la STL
  4.  */
  5.  
  6. #include <iostream>
  7. #include <vector>
  8. #include <string>
  9. #include <stdexcept>
  10.  
  11. int main() {
  12. std::cout << "=== Le Conteneur Dynamique std::vector ===\n\n";
  13.  
  14. // 1. Déclaration et initialisation
  15. std::vector<int> nombres = {10, 20, 30, 40};
  16.  
  17. std::cout << "Taille initiale : " << nombres.size() << " éléments.\n";
  18.  
  19. // 2. Ajout dynamique en fin de vecteur avec push_back()
  20. std::cout << "Ajout de 50 et 60...\n";
  21. nombres.push_back(50);
  22. nombres.push_back(60);
  23.  
  24. std::cout << "Nouvelle taille : " << nombres.size()
  25. << " | Capacité allouée : " << nombres.capacity() << "\n";
  26.  
  27. // 3. Parcours moderne par référence constante (Range-based for)
  28. std::cout << "Contenu du vecteur : [ ";
  29. for (const auto& x : nombres) {
  30. std::cout << x << " ";
  31. }
  32. std::cout << "]\n\n";
  33.  
  34. // 4. Accès indicé : operator[] vs méthode sécurisée at()
  35. std::cout << "Premier élément (front) : " << nombres.front() << "\n";
  36. std::cout << "Dernier élément (back) : " << nombres.back() << "\n";
  37. std::cout << "Élément à l'indice 2 : " << nombres[2] << "\n";
  38.  
  39. try {
  40. std::cout << "Tentative d'accès sécurisé à l'indice 10 avec at()...\n";
  41. int valeurInvalide = nombres.at(10); // Lève une exception std::out_of_range
  42. std::cout << "Valeur : " << valeurInvalide << "\n";
  43. } catch (const std::out_of_range& e) {
  44. std::cerr << ">> [Exception interceptée] " << e.what() << "\n";
  45. }
  46.  
  47. // 5. Retrait du dernier élément avec pop_back()
  48. std::cout << "\nRetrait du dernier élément avec pop_back()...\n";
  49. nombres.pop_back();
  50.  
  51. std::cout << "Contenu final : [ ";
  52. for (const auto& x : nombres) {
  53. std::cout << x << " ";
  54. }
  55. std::cout << "] (taille = " << nombres.size() << ")\n";
  56.  
  57. return 0;
  58. }

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).

CPPstl_set.cpp
  1. /**
  2.  * Chapitre 4 - Exemple 3 : Le Conteneur Associatif std::set
  3.  * Ensemble ordonné d'éléments uniques (Arbre Rouge-Noir sous-jacent)
  4.  */
  5.  
  6. #include <iostream>
  7. #include <set>
  8. #include <string>
  9.  
  10. int main() {
  11. std::cout << "=== L'Ensemble Ordonné std::set ===\n\n";
  12.  
  13. // 1. Déclaration : un ensemble de chaînes de caractères
  14. // Les éléments sont automatiquement triés et TOUT DOUBLON EST REJETÉ !
  15. std::set<std::string> prenoms;
  16.  
  17. // 2. Insertion d'éléments avec insert()
  18. prenoms.insert("Charlie");
  19. prenoms.insert("Alice");
  20. prenoms.insert("Bob");
  21. prenoms.insert("David");
  22.  
  23. // Tentative d'insertion d'un doublon
  24. auto resultat = prenoms.insert("Alice");
  25. if (!resultat.second) {
  26. std::cout << "[INFO] \"Alice\" existe déjà dans le set : doublon ignoré !\n";
  27. }
  28.  
  29. // 3. Parcours : les éléments apparaissent toujours par ordre alphabétique / croissant
  30. std::cout << "\nContenu du set (trié automatiquement) : { ";
  31. for (const auto& nom : prenoms) {
  32. std::cout << "\"" << nom << "\" ";
  33. }
  34. std::cout << "} (taille = " << prenoms.size() << ")\n\n";
  35.  
  36. // 4. Recherche efficace en O(log N)
  37. // En C++20, la méthode .contains() vérifie la présence en une ligne très lisible :
  38. std::string recherche = "Bob";
  39. if (prenoms.contains(recherche)) {
  40. std::cout << ">> \"" << recherche << "\" est bien présent dans l'ensemble.\n";
  41. } else {
  42. std::cout << ">> \"" << recherche << "\" est absent.\n";
  43. }
  44.  
  45. // 5. Suppression d'un élément
  46. std::cout << "\nSuppression de \"David\"...\n";
  47. prenoms.erase("David");
  48.  
  49. std::cout << "Contenu après suppression : { ";
  50. for (const auto& nom : prenoms) {
  51. std::cout << "\"" << nom << "\" ";
  52. }
  53. std::cout << "}\n";
  54.  
  55. return 0;
  56. }

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é.

🚨 Piège classique : L'opérateur crochet [] de std::map

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)) { ... }

CPPstl_map.cpp
  1. /**
  2.  * Chapitre 4 - Exemple 4 : Le Tableau Associatif std::map
  3.  * Dictionnaire Clé-Valeur ordonné (Arbre Binaire Équilibré)
  4.  */
  5.  
  6. #include <iostream>
  7. #include <map>
  8. #include <string>
  9.  
  10. int main() {
  11. std::cout << "=== Le Dictionnaire Associatif std::map ===\n\n";
  12.  
  13. // 1. Déclaration : Clé = string (nom du fruit), Valeur = double (prix au kilo)
  14. std::map<std::string, double> catalogue;
  15.  
  16. // 2. Insertion avec l'opérateur crochet []
  17. catalogue["Pomme"] = 2.40;
  18. catalogue["Banane"] = 1.90;
  19. catalogue["Orange"] = 2.80;
  20. catalogue["Fraise"] = 5.50;
  21.  
  22. // 3. Parcours moderne avec décomposition structurée (Structured Binding C++17/C++20)
  23. std::cout << "Catalogue des prix (trié automatiquement par clé) :\n";
  24. for (const auto& [fruit, prix] : catalogue) {
  25. std::cout << " • " << fruit << " : " << prix << " € / kg\n";
  26. }
  27.  
  28. // 4. Recherche sécurisée en C++20 sans créer de fausse entrée
  29. std::cout << "\nRecherche d'articles :\n";
  30. std::string recherche = "Cerise";
  31.  
  32. if (catalogue.contains(recherche)) {
  33. std::cout << ">> " << recherche << " coûte " << catalogue[recherche] << " €\n";
  34. } else {
  35. std::cout << ">> " << recherche << " n'est pas au catalogue.\n";
  36. }
  37.  
  38. // 5. ⚠️ Piège classique de l'operator[] :
  39. // Si la clé n'existe pas, l'opérateur [] L'INSÈRE avec sa valeur par défaut (ici 0.0) !
  40. std::cout << "\nAppel de catalogue[\"Kiwi\"] sans assignation préalable :\n";
  41. double prixKiwi = catalogue["Kiwi"]; // Insère silencieusement "Kiwi" avec la valeur 0.0 !
  42. std::cout << "Prix retourné pour Kiwi : " << prixKiwi << " €\n";
  43.  
  44. std::cout << "\nVérification : Le catalogue contient maintenant Kiwi !\n";
  45. for (const auto& [fruit, prix] : catalogue) {
  46. std::cout << " • " << fruit << " : " << prix << " € / kg\n";
  47. }
  48.  
  49. return 0;
  50. }

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.
4

Les Algorithmes Standards de la STL (<algorithm>)

⏱️ 25 minutes

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().

La Plage semi-ouverte [debut, fin)

[ 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

💡 Qu'est-ce qu'une fonction Lambda en C++ ?

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 :

CPPstl_algorithmes.cpp
  1. /**
  2.  * Chapitre 4 - Exemple 5 : Les Algorithmes de la STL (<algorithm>)
  3.  * Mise en œuvre de sort, reverse, count et count_if avec expressions lambda
  4.  */
  5.  
  6. #include <iostream>
  7. #include <vector>
  8. #include <algorithm>
  9. #include <string>
  10.  
  11. // Fonction utilitaire d'affichage
  12. void afficher(const std::vector<int>& v, const std::string& message) {
  13. std::cout << message << " : [ ";
  14. for (int x : v) {
  15. std::cout << x << " ";
  16. }
  17. std::cout << "]\n";
  18. }
  19.  
  20. int main() {
  21. std::cout << "=== Les Algorithmes Fondamentaux de la STL ===\n\n";
  22.  
  23. std::vector<int> notes = {12, 5, 18, 12, 9, 14, 12, 20, 7};
  24. afficher(notes, "Notes initiales");
  25.  
  26. // 1. std::sort : Tri croissant en O(N log N) (IntroSort)
  27. std::cout << "\n--- 1. std::sort (Tri dans l'ordre croissant) ---\n";
  28. std::sort(notes.begin(), notes.end());
  29. afficher(notes, "Après std::sort");
  30.  
  31. // 2. std::reverse : Inversion en place des éléments
  32. std::cout << "\n--- 2. std::reverse (Inversion du vecteur) ---\n";
  33. std::reverse(notes.begin(), notes.end());
  34. afficher(notes, "Après std::reverse (ordre décroissant)");
  35.  
  36. // 3. std::count : Comptage des occurrences exactes d'une valeur
  37. std::cout << "\n--- 3. std::count (Comptage de valeurs exactes) ---\n";
  38. int cible = 12;
  39. auto nbDouze = std::count(notes.begin(), notes.end(), cible);
  40. std::cout << "Nombre de fois où la note " << cible << " apparaît : " << nbDouze << "\n";
  41.  
  42. int absente = 15;
  43. std::cout << "Nombre de fois où la note " << absente << " apparaît : "
  44. << std::count(notes.begin(), notes.end(), absente) << "\n";
  45.  
  46. // 4. std::count_if : Comptage selon un prédicat (Fonction Lambda)
  47. std::cout << "\n--- 4. std::count_if (Comptage conditionnel avec Lambda) ---\n";
  48.  
  49. // Prédicat 1 : Notes supérieures ou égales à 10 (la moyenne)
  50. auto nbMoyenne = std::count_if(notes.begin(), notes.end(), [](int n) {
  51. return n >= 10;
  52. });
  53. std::cout << "Nombre d'étudiants ayant la moyenne (>= 10) : " << nbMoyenne << "\n";
  54.  
  55. // Prédicat 2 : Nombres pairs
  56. auto nbPairs = std::count_if(notes.begin(), notes.end(), [](int n) {
  57. return n % 2 == 0;
  58. });
  59. std::cout << "Nombre de notes paires : " << nbPairs << "\n";
  60.  
  61. return 0;
  62. }
5

Synthèse & Auto-évaluation

⏱️ 10 minutes

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)

❓ Question 1 : Pourquoi un message envoyé sur std::cerr apparaît-il immédiatement sur le terminal alors qu'un message sur std::cout peut tarder à s'afficher ?
A. Parce que std::cerr écrit avec des polices de caractères plus légères.
B. Parce que std::cerr n'est pas bufferisé (unbuffered), ce qui force l'émission directe sans attendre que le tampon mémoire soit plein.
C. Parce que std::cout utilise un thread séparé qui met les messages en pause.
Réponse B : Le flux standard 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.
❓ Question 2 : Si vous insérez trois fois la valeur 42 dans un std::set<int>, quelle sera la taille du conteneur ?
A. 3, car chaque appel à insert() alloue un nouveau nœud.
B. 1, car std::set garantit l'unicité stricte de chaque élément et ignore les doublons.
C. Une exception std::duplicate_key est levée à la deuxième insertion.
Réponse B : Par définition mathématique et informatique, un std::set est un ensemble d'éléments uniques. Tout doublon inséré est silencieusement ignoré sans lever d'exception.
❓ Question 3 : Quel est le comportement de l'instruction double p = catalogue["Inconnu"]; sur un std::map<std::string, double> si la clé n'existe pas ?
A. Le programme lève immédiatement une exception std::out_of_range.
B. La compilation échoue car la clé doit être déclarée au préalable.
C. La clé "Inconnu" est automatiquement insérée dans la map avec la valeur par défaut 0.0, puis cette valeur est renvoyée.
Réponse C : C'est l'un des pièges les plus célèbres du C++ ! L'opérateur 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_if pour 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 :

← Revoir Chapitre 3 (Généricité) Feuille de TD 3 (Templates) ➔ Passer au Chapitre 5 : POO en Python ➔ Accueil du Cours ➔