Logo HE-ARC Logo HES-SO

1242.2 Langage C++ - 2024-2025

Chapitre 9

Bibliothèque standard

1. Vue d'ensemble

2. Flux et fichiers

3. Flux et std::string

4. Conteneurs séquentiels

5. Itérateurs

6. Conteneurs associatifs

7. Smart pointers

9.1 Vue d'ensemble

Ensemble de classes et de fonctions qui fournissent des fonctionnalités courantes

Divisée en plusieurs parties, chacune ayant un but spécifique

Définie dans le namespace std

Incluse par défaut dans la plupart des compilateurs C++ modernes

Plus de 50 fichiers d'en-tête : iostream, vector, list, string, ...

En particulier

- La bibliothèque standard C

- Gestion des exceptions

- Gestion des flux

- Gestion des chaînes de caractères

- La bibliothèque standard template library (STL)

- Support pour le calcul numérique

- Des codes éprouvés (algorithmes, ...)

9.2 Flux et fichiers

Un flux est une séquence d'octets transférés entre la mémoire et les périphériques

C'est le programme qui donne une signification aux octets d'un flux

Les flux fournissent une méthode unifiée pour traiter les E/S quels que soient les périphériques et les types de données à échanger

Classes implémentant des flux :

- iostream : flux standard d'E/S (clavier / écran)

- fstream : flux d'E/S dans les fichiers

- sstream : flux d'E/S dans la mémoire (string)

- ...


Toutes ces classes redéfinissent les opérateurs << et >>

9.2 Les fichiers

2 types de fichiers : texte (par défaut) ou binaire

2 modes d'accès : séquentiel ou direct (random access)

#include <fstream> ⇒ 3 classes :

- ifstream : entrée (lecture)

- ofstream : sortie (écriture)

- fstream : entrée/sortie (lecture/écriture)

L'ouverture peut se faire par l'appel du constructeur ou de la méthode open() ayant la même signature, seules les valeurs par défaut changent :

ifstream::ifstream(const char* f, openmode mode = ios::in)

ofstream::ofstream(const char* f, openmode mode = ios::out)

fstream::fstream(const char* f, openmode mode = ios::in | ios::out)

Fermeture avec la méthode close() ou automatiquement par le destructeur

9.2 Les paramètres d'ouverture

Le paramètre f est le nom du fichier à ouvrir

Le paramètre mode est un bitfield qui définit le mode d'ouverture du fichier

On le combine avec l'opérateur bit à bit |

Exemple : ouverture d'un fichier texte en écriture par ajout (à la fin)

fstream fs("log.txt", ios::out | ios::app)


Modes

- ios::binary : mode binaire (mode texte par défaut)

- ios::in : lecture (défaut pour ifstream)

- ios::out : écriture (défaut pour ofstream)

- ios::app : ajout à la fin (append)

- ios::trunc : écrase le contenu précédent (truncate)

- ios::ate : position à la fin dès l'ouverture

9.2 Les paramètres d'ouverture

2 manières d'ouvrir un fichier


1. Par le constructeur de la classe

ifstream myFile("data.bin", ios::in | ios::binary)

2. Par la méthode open()

ifstream myFile

myFile.open("data.bin", ios::in | ios::binary)

Pour des fichiers en écriture et lecture, utiliser fstream

fstream myFile("data.bin", ios::in | ios::out | ios::binary)


⚠️ Attention : mode "binaire", vraiment ? voir (<< vs .write())

9.2 Exemple



            #include <iostream>
            #include <fstream>
            
            int main()
            {
              std::ofstream fOutput("myFile.txt");
              fOutput << "PI = " << 3.14159 << '.' << std::endl;
              fOutput.close();
              
              std::ifstream fInput("myFile.txt");
              std::string s; double d; char c;
              fInput >> s >> d >> c;
              fInput.close();
              
              std::cout << s << d  << c << std::endl;
            
              return 0;
            }
          

9.2 Exemple

⚠️ Attention

L'utilisation des flux pour accéder aux fichiers soulève les mêmes problématiques que pour la console

En particulier, les caractères blancs sont considérés comme des séparateurs

On peut utiliser la fonction getline() pour lire une ligne entière

9.2 Traitements d'erreurs

Traitement d'erreurs identique pour tous les flux (fichier, écran/clavier, mémoire)

Basé sur 4 bits (flags ios_base::iostate) : goodbit, badbit, failbit et eofbit

On peut tester et manipuler l'état du flux avec les méthodes suivantes :

- good() : aucune erreur

- bad() : impossible d'extraire des données du flux

- fail() : échec de l'opération

- eof() : fin de fichier

- clear() : réinitialisation des bits d'état

9.2 Traitements d'erreurs

📚 iostate (cppreference.com)

9.2 Traitements d'erreurs (exceptions)



            #include <iostream> // std::cerr 
            #include <fstream>  // std::ifstream 
            
            int main () {
              std::ifstream file;
              file.exceptions(std::ifstream::failbit | std::ifstream::badbit);
              try {
                file.open("test.txt");
                while(!file.eof()) file.get();
                file.close(); 
              }
              catch (const std::ifstream::failure &e) {
                std::cerr << "Exception opening/reading/closing file" << std::endl;
              } 
              
              return 0;
            }
          

9.2 E/S Formatées ou non-formatées

ios::binary ⇒ s'assure que les données sont lues ou écrites sans traduire à la volée des caractères spéciaux (ex. les caractères de fin de ligne \r\n)

<< ⇒ Écriture formatée (pour les fichiers textuels)

>> ⇒ Lecture formatée (pour les fichiers textuels)

Méthode write() ⇒ Écriture non formatée (pour les fichiers binaires)

9.2 Méthodes de lecture de std::istream

Lecture formatée

istream &operator>>(...)

cin >> delta >> integer;

Lecture non formatée

.get : lecture d'un seul caractère

.getline : lecture d'une suite de caractères

.ignore : lecture de caractères «à fond perdu»

.peek : donne le prochain caractère sans l'extraire

.seekg : positionnement sur un flux

.read : lecture de n caractères

9.2 Méthodes de lecture de std::ostream

Écriture formatée

ostream &operator<<(...)

cout << delta << integer;

Écriture non formatée

.put : écriture d'un seul caractère

.write : écriture du contenu du tampon

.flush : vide le tampon de sortie

.tellp : positionnement courant sur le flux

.seekp : positionnement sur un flux

9.3 Le flux std::stringstream

Les flux peuvent être utilisés pour lire et écrire des chaînes de caractères

#include <sstream> ⇒ 3 classes :

- istringstream : entrée (lecture)

- ostringstream : sortie (écriture)

- stringstream : entrée/sortie (lecture/écriture)

9.3 La classe std::string

La bibliothèque standard implémente une classe std::string

#include <string>

Spécialisation d'un modèle : typedef basic_string<char> string;


Avantages

- Plus simple et plus sûre que les tableaux de caractères

- Gestion automatique de la mémoire

- Définition de méthodes et d'opérateurs

9.3 La classe 9.3 La classe std::string

Opérateurs

Affectation : =, Concaténation : + et +=, Comparaisons : == < > <= >=

Méthodes

- size() : renvoie le nombre de caractères

- length() : renvoie le nombre de caractères

- capacity() : taille possible sans réallocation

- empty() : vrai si la chaîne est vide, faux sinon

- find() : position de la chaîne s dans la chaîne courante à partir depos

- substr() : sous-chaîne de longueur length à partir destart

- at() : accès à un caractère par son index

- c_str() : renvoie un pointeur vers le tableau de caractères

9.3 Conversions entre std::string et char *



              #include <iostream>
              #include <string>
              #include <cstdio>
              
              int main()
              {
                auto p = "Hello"; // const char *
                auto s = std::string(p);
                auto s2 = std::string("World!");
                std::cout << s << " " << s2 << std::endl;
                printf("%s %s\n", s, s2); // ⚠️ warning C4477: 'printf'...
                
                auto p2 = s2.c_str(); // const char *
                std::cout << p << " " << p2 << std::endl;
                printf("%s %s\n", p, p2);

                return 0;
              }
            

9.3 Conversions avec les flux de chaîne



              #include <iostream>
              #include <string>
              #include <sstream>
              
              int main()
              {
                  std::ostringstream toto;
                  toto << 10;
                  auto texte = toto.str();
                  
                  std::istringstream titi("10");
                  int nombre;
                  titi >> nombre;
              
                  return 0;
              }
            

Les flux (vue d'ensemble)

📚 Stream-based I/O (cppreference.com)

9.4 Standard Template Library (STL)

Bibliothèque de classes et d'algorithmes génériques (A. Stepanov)

Implémentée en C++

Intégrée à la procédure de standardisation de C++

Basée sur les modèles de classes

Contient

- Conteneurs séquentiels

- Conteneurs associatifs

- Itérateurs

- Algorithmes

- Objets fonctions (foncteurs)

- Adaptateurs

- Allocateurs

9.4 Les conteneurs

Les conteneurs sont des classes qui permettent de stocker des objets

Ils sont définis dans le namespace std

Gérer les objets contenus : ajout, suppression (parfois tri, recherche, ...)

Accéder aux objets contenus grâce aux itérateurs

Les itérateurs permettent de parcourir une collection d'objets sans avoir à se préoccuper de l'implémentation

⇒ Avoir une interface de manipulation commune pour les algorithmes génériques de la STL (tri, recherche, remplacement, ...)

9.4 Les conteneurs

2 types de conteneurs


Conteneurs séquentiels

Conteneurs dont les éléments sont ordonnés. Parcourir le conteneur suivant cet ordre et insérer ou supprimer un élément en un endroit explicitement choisi.

vector , list , deque , ...


Conteneurs associatifs

Conteneurs dont les éléments sont identifiés par une clé et ordonnés suivant celle-ci. Pour insérer un élément, il n'est en théorie pas utile de préciser un emplacement.

map , ...

9.4 Les conteneurs séquentiels

Tableau dynamique contigu : vector

Tableau statique contigu : array

File à double entrée : deque

Liste doublement chainée : list

Liste simplement chainée : forward_list

File : queue , priority_queue

Pile : stack

9.4 Méthodes courantes

clear() : vide le conteneur

size() : retourne le nombre d'éléments

empty() : teste si le conteneur est vide

push_back(...) : ajoute un élément à la fin

push_front(...) : ajoute un élément au début

front() : renvoie le 1er élément

pop_front() : supprime le premier élément

insert() : insère un élément

erase() : supprime un élément

9.4 Exemple



            #include <iostream>
            #include <vector>
            #include <string>

            int main()
            {
              std::vector<std::string> vect;
              vect.push_back("Introduction");
              vect.push_back("une phrase");
              vect.push_back("Conclusion");
              
              for (const auto &elem : vect)
                std::cout << elem << std::endl;
                
              for (int i = 0; i < vect.size(); ++i)
                std::cout << vect[i] << std::endl;
                
              return 0;
            }
          

9.4 Exemple



            #include <list>
            
            int main()
            {
              std::list<int> lst;
              lst.push_back(1); // 1
              lst.push_front(0); // 0 1
              lst.insert(lst.begin(), 2); // 2 0 1
              lst.insert(lst.end(), 3); // 2 0 1 3
              lst.pop_front(); // 0 1 3
              
              return 0;
            }
          

9.4 Exemple



            #include <iostream>
            #include <array>
            
            int main()
            {
              std::array<int, 3> a1{ {10,11,12} };
              // std::array<int, 3> a1 = { 10,11,12 };
            
              for (int i = 0; i < a1.size(); ++i)
              {
                std::cout << a1[i] << std::endl; // ⚠️ no range check
                std::cout << a1.at(i) << std::endl; // ✅ range check (throws exception)
              }
            
              return 0;
            }
          

9.4 Destruction

Faut-il effacer ce qui est stocké dans un vecteur ?

⇒ Tout dépend de la nature de ce qui est stocké dans le std::vector


Objets

Les objets sont détruits lors du retrait (pop), ou lorsque le vecteur est détruit


Pointeurs

Les pointeurs sont détruits lorsque le vecteur est détruit mais pas la mémoire allouée !

⇒ Il faut delete chaque pointeur stocké, sinon il y aura une fuite de mémoire.

9.4 Imbrication de conteneurs

Il est possible d'imbriquer les conteneurs de la STL

On peut ainsi, par exemple, créer un vecteur de listes de chaînes de caractères


Exemple

std::vector<std::list<std::string>>


La définition d'un type intermédiaire peut augmenter la lisibilité :

typedef std::list<std::string> list_string;

typedef std::vector<list_string> vector_list_string;

9.4 Imbrication de conteneurs

Des std::vector imbriqués simulent des tableaux à plusieurs dimensions



            #include <iostream>
            #include <vector>

            typedef vector<double> Line;
            typedef vector<Line> Matrix;
            
            Matrix m(5); // 5 rows of size 0 initially
            for(int i=0; i<5; i++)
            {
              m[i].resize(5); // rows are now of size 5
              for(int j=0; j<5; j++)
              {
                if (i==j) m[i][j] = 1;
                else m[i][j] = 0;
              }
            }
          

9.5 Les itérateurs

Les itérateurs sont une généralisation des pointeurs

Définis dans le namespace std

Utilisés par les algorithmes de la STL

Utiliser les conteneurs de façon uniforme

Spécifier une position à l'intérieur d'un conteneur. Ils peuvent être incrémentés, et déréférencés (avec *); deux itérateurs peuvent être comparés.

Tous les conteneurs disposent des méthodes suivantes

begin() : renvoie un itérateur pointant sur le 1er élément

end() : renvoie un itérateur pointant APRES le dernier élément

⚠️ On ne peut donc pas déréférencer l'itérateur renvoyé par end()

9.5 Les itérateurs

Déclarations

std::vector<double>::iterator it;


Un itérateur peut s'initialiser grâce aux méthodes begin(), end(), ou find() :

1) std::vector<double>::iterator it(vect.begin());

2) auto it = vect.begin();

9.5 Exemple



            #include <iostream>
            #include <list>
            
            int main()
            {
              std::list<int> lst;
            
              std::list<int>::iterator it;
              for (it = lst.begin(); it != lst.end(); ++it)
                std::cout << *it << std::endl; // *it = each element
              
              // C++11
              auto it = lst.begin();
              for (; it != lst.end(); ++it)
                std::cout << *it << std::endl;

              // C++11 range-based for loop
              for (auto &elem : lst)
                std::cout << elem << std::endl; // elem = each element
            }
          

9.6 Conteneurs associatifs

Les conteneurs associatifs sont des conteneurs qui stockent des éléments sous forme de paires clé/valeur

Les clés permettent d'accéder aux valeurs

Les conteneurs associatifs sont ordonnés selon la clé


std::map, std::multimap, std::set, std::multiset, ...


Ces conteneurs utilisent std::pair<T1, T2> pour stocker les paires clé/valeur

9.6 Tables associatives

Généralisation des tableaux : les indices peuvent être non entiers

- Tableau que l'on pourrait indicer par des chaînes de caractères et écrire par exemple moyenne["Informatique"]

- On parle d'association "clé - valeur"

Les tables associatives sont définies dans <map>

Elles nécessitent 2 types pour leur déclaration : le type des "clés" (les index) et le type des éléments indexés



            #include <string>
            #include <map>
            
            std::map<std::string, double> moyenneParBranche;  // Unique key
            std::multimap<std::string, double> notesParBranche; // Duplicated keys
            

9.6 Exemple



            #include <string>
            #include <map>
            #include <iostream>
            
            int main()
            {
              std::map<std::string, double> moyenne;
              moyenne["Informatique"] = 5.5;
              moyenne["Physique"] = 4.5;
              
              // if->first to access the key, if->second to access the value
              for (auto it = moyenne.begin(); it != moyenne.end(); ++it)
                std::cout << "En " << it->first << ", j'ai " << it->second <<
                           " de moyenne." << std::endl ;
              
              std::cout << "Ma moyenne en Informatique est de ";
              std::cout << moyenne.find("Informatique")->second << std::endl;

              return 0;
            }
          

9.6 Exemple



            // First element of map moyenne
            auto it = moyenne.begin();

            // Found element in map (or end() if not found)
            auto it2 = moyenne.find("Informatique");

            // End of map (after last element)
            // ⚠️ it2 == it3 if not found
            auto it3 = moyenne.end();

          

9.6 Recherche

⚠️ Attention : le simple fait d'accéder à une clé via operator[] insère cette clé (avec la valeur par défaut pour le type T)

operator[] n'est donc pas adapté pour vérifier si une clé est présente dans une map


✅ Pour chercher une clef : utiliser la méthode find()

9.6 std::pair<T1, T2>

Dans le code auto it = moyenne.begin();, it est un itérateur sur une map

Il est possible de déréférencer l'itérateur pour accéder à la paire clé/valeur avec (*it)

(*it) est de type std::pair<const std::string, double>

Une paire est une structure contenant 2 éléments pouvant être de types différents

Certains algorithmes de la STL retournent des paires. La méthode find(), par exemple, retourne la position de l'élément trouvé et un booléen indiquant s'il a été trouvé ou non

9.6 Exemple



            // Not needed in practice,
            // because it is implicit when using e.g. <vector>
            #include <pair>
            #include <iostream>
            #include <string>
            
            int main()
            {
              auto p =  std::make_pair(8, "coeur");
              std::cout << p.first << ' ' << p.second << std::endl;
              
              return 0;
            }
          

9.6 Exemple



            #include <iostream>
            #include <map>
            
            int main()
            {
              std::multimap notes;
            
              // ⚠️ Indices do not work with multimap!
              // moyenne["Informatique"] = 5.5;
              
              notes.insert(std::pair	("Informatique", 5.5));
              notes.insert(std::pair	("Informatique", 4.0)); 
            
              for (auto it = notes.begin(); it != notes.end(); ++it)
                std::cout << "En " << it->first << ", j'ai " << it->second <<
                     " de moyenne." << std::endl;

              return 0;              
            }
          

9.6 Les conteneurs

📚 Containers library (cppreference.com)

9.7 Les smart pointers

Les smart pointers sont des classes qui gèrent la mémoire de manière automatique

Utiliser #include <memory>

Définis dans le namespace std

Gérer la mémoire de manière plus sûre et plus efficace que les pointeurs bruts


std::unique_ptr : pointeur unique. Lorsque le unique_ptr devient hors de portée, l'objet pointé est également détruit

std::shared_ptr : pointeur partagé. Plusieurs shared_ptr peuvent pointer vers le même objet. Lorsque le dernier shared_ptr devient hors de portée, l'objet pointé est également détruit

std::weak_ptr : pointeur faible. Il permet de référencer un objet sans en prendre possession. Il est utilisé pour éviter les références circulaires entre shared_ptr

Exemple avec les unique_ptr



              #include <iostream>
              #include <memory>
              
              int main()
              {
                // Raw pointers
                {
                  auto ptr = new int(5); // dynamic allocation
                  std::cout << *ptr; // 5
                  delete ptr; // ⚠️ free memory to avoid memory leak
                  ptr = nullptr; // avoid dangling pointer  
                }

                // Smart pointers
                {
                  auto s_ptr = std::unique_ptr(new int(5)); // dynamic allocation
                  std::cout << *s_ptr; // 5
                  // ✅ No need to delete, memory is automatically freed when ptr goes out of scope
                }

                return 0;
              }
            

Exemple avec les unique_ptr



              #include <iostream>
              #include <memory>
              
              int main()
              {
                // Smart pointers
                {
                  auto s_ptr = std::unique_ptr(new int(5)); // dynamic allocation
                  std::cout << *s_ptr; // 5

                  // ⛔ error: use of deleted function...
                  auto s_ptr1 = std::unique_ptr(s_ptr);

                  // ⛔ error: use of deleted function...
                  auto s_ptr2 = s_ptr; // ⛔ error: use of deleted function...
                }

                return 0;
              }
            

Exemple avec les shared_ptr



              #include <iostream>
              #include <memory>
              
              int main()
              {
                // Raw pointers
                {
                  auto ptr = new int(5); // dynamic allocation
                  std::cout << *ptr; // 5
                  delete ptr; // ⚠️ free memory to avoid memory leak
                  ptr = nullptr; // avoid dangling pointer  
                }

                // Smart pointers
                {
                  auto s_ptr = std::shared_ptr(new int(5)); // dynamic allocation
                  std::cout << *s_ptr; // 5
                  // ✅ Memory is automatically freed when ptr goes out of scope
                }

                return 0;
              }
            

Particularité des shared_ptr

On ne peut pas affecter directement une adresse à un shared_ptr. Il faut utiliser la méthode reset() pour lui assigner un nouvel objet alloué dynamiquement

Plusieurs shared_ptr peuvent pointer vers le même objet. Ils partagent la propriété de l'objet pointé

En particulier, les shared_ptr peuvent être copiés entre eux


⚠️ Attention : il faut faire attention à ne pas créer de références circulaires entre shared_ptr

⇒ Utiliser std::weak_ptr pour éviter les références circulaires


⚠️ Attention : un shared_ptr ne peut pas pointer sur un tableau d'objets !

⇒ Il faut utiliser std::unique_ptr pour cela

Exemple avec les shared_ptr



              #include <iostream>
              #include <memory>

              int main()
              {
                std::shared_ptr sp1;
                {
                  auto circle = new Circle(2);
                  auto sp2 = std::shared_ptr(circle);
                  // 📌 circle has now 1 reference (sp2)

                  // ✅ OK
                  sp1 = sp2;
                  // 📌 circle has now 2 references (sp2 and sp1)
                  ...
                }
                // 📌 circle has 1 reference (sp2) 
                ...
                return 0;
              }
              // 📌 circle has 0 reference and is deleted