Big Tuto : Apprenez le C++

Chapitre 22 : Les conteneurs séquentiels (1)

 

Tutoriel présenté par : Robert Gillard (Gondulzac)
Date de publication : 3 juillet 2017
Date de révision : -

 

Retrouvez les projets complets de ce chapitre :

  

 

 

   Préliminaires

 

    Les conteneurs séquentiels font partie de la STL ou ''Standard Template Library'' qui est une bibliothèque étant à la base de la programmation générique en C++. Cette bibliothèque utilise des templates (modèles), permettant de créer des classes ou des fonctions pouvant générer des types différents et réaliser diverses opérations sur ceux-ci. Nous verrons plus tard comment définir et utiliser des fonctions et class templates. wink

   La STL nous permet en outre d'utiliser de nombreux types de conteneurs différents. Ces conteneurs (ou containers en anglais), comme leur nom l'indique, sont des objets pouvant emmagasiner des collections d'objets d'un type spécifique. Nous avons déjà vu auparavant plusieurs exemples sur les classes ''vector'' et ''string'' qui sont également des conteneurs appartenant à la STL. Les différents types d'accès à ces conteneurs sont facilités par les itérateurs qui, nous en avons parlé dès le chapitre 5 (§ 1.3 – Introduction aux itérateurs), peuvent être considérés comme des pointeurs d'un type spécial. Nous allons bientôt reparler des itérateurs et voir leur rôle d'accès aux conteneurs de manière plus approfondie que dans les différents exemples montrés jusqu'à présent. angel

   Nous commencerons donc notre découverte de la STL, dans cette première partie, par la présentation des différents types de conteneurs pouvant être utilisés et, notamment dans ce chapitre ainsi que dans les deux suivants, des conteneurs séquentiels. wink

 

 

   1 – Les différents types de conteneurs de la STL

 

   Nous parlons ici des trois grands types de conteneurs se la STL, ceux-ci n'étant généralement différenciés que par leur forme d'initialisation et des différents types d'accès qui s'y s'appliquent.

1. Les Conteneurs séquentiels

2. Les Conteneurs associatifs

3. Les Algorithmes génériques

   Pour l'instant, nous commencerons par la découverte des conteneurs séquentiels. smiley

 

 

      1.1 – Types de conteneurs séquentiels  

 

vector Tableau à dimension flexible supportant un accès aléatoire rapide.
L'insertion ou la suppression d'éléments ailleurs qu'en fin de tableau peut être lente.  
string Conteneur spécial, similaire à un vector, et contenant des caractères.
Accès aléatoire rapide. Insertion ou suppression d'élément en fin de tableau rapide.
deque Conteneur pouvant être étendu ou contracté des deux côtés (double-ended queue).
Accès aléatoire rapide. Insertion ou suppression d'élément en début ou fin de tableau rapide.
array Tableau à dimension fixe. Accès aléatoire rapide.
On ne peut ajouter ni supprimer un élément dans un tableau de type array. 
list Liste doublement liée. Supporte un accès séquentiel bidirectionnel.
Insertion et suppression rapide à n'importe quel endroit de la liste.
forward_list Liste simplement liée. Ne supporte qu'un accès séquentiel dans une direction.
Insertion et suppression rapide à n'importe quel endroit de la liste. 

 

   Voilà. Parmi ces types de conteneurs séquentiels, les types forward_list et array ont été introduits par la norme C++ 11. Il est aussi conseillé que les programmes actuels écrits en C++ utilisent les bibliothèques de conteneurs plutôt que les anciennes structures issues du C comme les tableaux.

   Nous avons vu cependant que les tableaux étaient encore utiles, rappelez-vous notamment le chapitre 9 (§ 2.4 – Utiliser un tableau pour initialiser un vector). Et ce n'est pas le seul exemple. wink

   Oui, c'est bien beau cela, mais parmi tous ces types de conteneurs, lesquels utiliser et pourquoi un tel plutôt qu'un autre ? angry

   Et bien, une première réponse à cette question est d'utiliser un vector à moins qu'il n'y ait une bonne raison d'utiliser un autre conteneur. cheeky

 

   Quelques règles à suivre pour le choix d'un conteneur séquentiel :

- A moins d'avoir une raison d'utiliser un autre conteneur, utiliser un ''vector''.

- Si un programme possède de nombreux petits éléments à emmagasiner, ne pas employer une ''list'' ou une ''forward_list''.

- Si un programme nécessite des accès aléatoires fréquents aux éléments d'un conteneur, utilisez un ''vector'' ou un conteneur ''deque''.

- Si un programme a besoin d'insérer ou de supprimer des éléments au milieu d'un conteneur, utilisez une ''list'' ou une ''forward_list''.

- Si un programme nécessite d'insérer ou de supprimer des éléments au début (front) ou à la fin (back) mais pas au milieu d'un conteneur, utilisez un conteneur ''deque''.

 


   2 – Généralités sur tous les types de conteneurs

 

   En général, chaque conteneur sera défini dans un fichier header portant le même nom que le conteneur lui-même. Rappelez-vous les inclusions des fichiers : 

#include <string>
#include <vector>
 

   afin de pouvoir utiliser ces bibliothèques. Par exemple, ''deque'' devra se trouver dans le header <deque>, ''list'' dans le header <list> et ainsi de suite... wink

   Les conteneurs étant des classes templates, ainsi que pour les vectors, nous devons ajouter une information additionnelle pour générer un conteneur d'un certain type. Pour la plupart, mais pas pour tous les conteneurs, cette information sera le type des éléments du conteneur. Ainsi :

    vector<string> générera un vector d'éléments de type ''string''

    list<Achat_item> générera une liste d'éléments de type Achat_item (que nous aurions pu rencontrer dans notre petit challenge du chapitre 20 cheeky).

   D'autre part, presque n'importe quel type peut être considéré comme le type d'un élément d'un conteneur séquentiel. En particulier, nous pouvons définir un conteneur dont le type des éléments est lui-même un autre conteneur. Par exemple :

    vector<vector<string>> items représentera un vector ''items'' dont les éléments sont des vectors de strings.

   Les compilateurs de dernière génération autorisent l'écriture d'un tel vector sans espace entre les brackets ''>>''. Autrefois, une telle déclaration devait s'écrire :

    vector<vector<string> > items

 

 

   3 – Itérateurs

 

   Tous les itérateurs des types de conteneurs standards permettent l'accès aux éléments de ces conteneurs. De même, les itérateurs de la bibliothèque des conteneurs définissent tous un itérateur d'incrémentation pour parcourir tous les éléments d'un élément vers le suivant.

   Il existe cependant une exception en ce qui concerne les itérateurs du conteneur ''forward_list''. cheeky

   En effet, ce conteneur ne supporte pas l'opérateur de décrémentation (- -) et nous verrons que certains itérateurs ne s'appliquent qu'aux conteneurs ''string'', ''vector'', ''deque'' et ''array''.

   Le domaine d'itération d'un conteneur est défini par une paire d'itérateurs, le premier étant positionné au début du conteneur et le second à la fin, ou plutôt à la dernière plus une position d'un élément du conteneur (one past the end). Ces deux itérateurs, dénommés begin et end, définissent donc le domaine d'itération d'un conteneur qui est appelé un intervalle inclusif à gauche et exclusif à droite, soit [ begin, end [ en notation mathématique (voir chapitre 8, § 1.4 – Utilisation de conventions de la Standard Library).

   Les itérateurs begin et end doivent se référer au même conteneur, l'itérateur end pouvant être égal à l'itérateur begin mais ne peut en aucun cas se référer à un élément précédent begin.

   Il s'ensuit que :

- Si begin égale end, le conteneur est vide.

- Si begin n'est pas égal à end, cela veut dire qu'il y a au moins un élément dans le conteneur et que begin se réfère au premier élément du conteneur.

- On peut incrémenter begin tant que begin n'est pas égal à end soit begin != end.

   Nous allons présenter un simple exemple en modifiant quelque peu le projet 057Fonctions20 du chapitre 8angel

 

   Exemple 1 :

      Projet 132ContSequentiels_1

 

//Projet 132ContSequentiels_1
//Lecture d'une liste d'entiers passée à une fonction
//Utilisation des membres begin() et end() de la Standard Library
#include <iostream>
#include <list>
#include <conio.h>
 
using namespace std;
 
// Déclaration de la fonction Lire_liste()
void Lire_liste(list<int> &nombres);
 
 
int main()
 
{
//Initialisation de la liste d'entiers
list<int> nombres = { 3, -2, -9, 6, 12, 21, -17, -11, 7, 1 };
 
cout << endl;
cout << "Une fonction qui lit les valeurs entieres d'un conteneur 'list'" << endl <<
endl;
 
Lire_liste(nombres);
 
_getch();
return 0;
}
 
 
void Lire_liste(list<int> &nombres)
{
//Le domaine d'itération de la liste est défini par les membres begin et end.
// Identique à list<int>::iterator debut = begin(nombres);
 
auto debut = begin(nombres);
 
// Identique à list<int>::iterator fin = end(nombres);
auto fin = end(nombres);
 
while(debut != fin)
{
cout << *debut++ << endl;
}
} 

 

   On commence par déclarer une fonction Lire_liste à qui l'on passe une référence sur un conteneur ''list''.

   Dans la fonction main(), une ''list'' nombres est créée à l'aide d'une liste d'initialisation et on appelle ensuite la fonction Lire_liste() à qui l'on passe l'adresse du conteneur.

   L'implémentation de la fonction Lire_list() se fait en lui passant une référence au conteneur ''list''.

   Dans cette fonction, on initialise les variables début et fin avec les membres begin et end à l'aide du spécificateur ''auto'' de la STL.

   Et le résultat dans la console est identique à celui du projet 057Fonctions20 présenté dans le chapitre 8. wink

 

   Remarquez que ce programme marche également avec les conteneurs vector, deque, list et forward_list, en incluant les bons headers bien entendu.
   Nous verrons un peu plus loin qu' il faut passer deux arguments au conteneur ''array'', le second étant le nombre d'éléments dans le tableau.

 


    4 – Les différentes versions des membres begin et end d'un conteneur

 

   Les membres begin et end retournent des itérateurs qui se réfèrent au premier et au dernier plus un élément, d'un conteneur. Ces membres présentent différentes versions selon certains cas, nous les présentons ci-dessous. wink

 

Accès aux itérateurs
nom.begin(), nom.end() 
Retourne un itérateur sur le premier et dernier élément de nom.
nom.cbegin(), nom.cend()
Retourne un const_iterator.
Membres additionnels de conteneurs réversibles (non valide pour forward_list)
nom.rbegin(), nom.rend()
Retourne un itérateur du dernier vers le premier moins un élément de nom (reverse_iterator).
nom.crbegin(), nom.crend()
Retourne un const_reverse_iterator.

 

   En utilisant le spécificateur ''auto'' de la Standard Library, les itérateurs retournés par les différentes versions des membres begin() et end() seraient pour une liste quelconque d'un conteneur :

   Si ''armes'' est par exemple le nom donné à une liste d'initialisation d'un conteneur ''list'' tel que :

list<string> armes = { ''Arc de la fureur de Myin'', ''Terrible hache de plaie'', ''Dague de Nélos'' };

   nous aurions alors :

 

   auto iter1 = armes.begin()       <=>     list<string>::iterator                 iter1 = armes.begin()
   auto iter2 = armes.rbegin()      <=>     list<string>::reverse_iterator         iter2 = armes.rbegin()
   auto iter3 = armes.cbegin()      <=>     list<string>::const_iterator           iter3 = armes.cbegin()
   auto iter4 = armes.crbegin()     <=>     list<string>::const_reverse_iterator   iter4 = armes.crbegin()

 

   Avant que le standard C++ 11 ait introduit l'utilisation de ''auto'' avec les fonctions begin() et end(), le programmeur n'avait pas d'autre choix que de déclarer de façon explicite le type d'iterateur qu'il voulait utiliser. Par exemple :

list<string>::iterator iter = armes.begin();

 


   Exercice 1 :

   Ecrivez un programme avec une fonction qui affiche la liste inversée des entiers du projet 132ContSequentiels_1 ci-dessus. Utilisez le conteneur ''deque''. angel

 


   5 – Fonctions templates implémentées dans le header <iterator>

 

   Le header <iterator> définit quatre fonctions templates qui agissent d'une façon spéciale sur un itérateur.

- Fonction advance()
Incrémente l'itérateur spécifié par le premier argument d'un nombre d'éléments spécifié par le second argument.
Ex : advance(iter, n);

- Fonction distance()
Retourne le nombre d'éléments dans un domaine spécifié par deux itérateurs passés en arguments.
Ex : distance((begin(data), end(data));

- Fonction next()
Retourne l'itérateur qui résulte de l'incrémentation de l'itérateur spécifié comme premier argument par le nombre d'éléments spécifié par le second argument.
Ex : next(iter, n);

- Fonction prev()
Retourne l'itérateur qui résulte de la décrémentattion de l'itérateur spécifié comme premier argument par le nombre d'éléments spécifé par le second argument.
Ex : prev(iter, n);

   Bon, pas de panique, nous allons voir une implémentation de ces quatre fonctions dans un exemple. indecision

 

   Exemple 2 :

      Projet 133ContSequentiels_2 

 

//Projet 133ContSequentiels_2
//Utilisation des fonctions du header <iterator>
#include <iostream>
#include <iterator>
#include <conio.h>
 
using namespace std;
using uint = unsigned int;
 
 
int main()
 
{
//Initialisation d'un tableau d'unsigned
uint nombres[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
 
cout << endl;
cout << "Utilisation des fonctions templates du header <iterator>" << endl << endl;
 
//Lecture de la liste
cout << "Lecture du tableau :" << endl;
for (auto iter = begin(nombres); iter != end(nombres); ++iter)
cout << *iter << " ";
cout << endl << endl;
 
// 1 - Fonction advance()
//L'itétateur est placé sur le premier élément du tableau
auto iter = begin(nombres);
auto valeur = 5;
cout << "1 - Fonction advance()" << endl;
cout << "L'iterateur fait un saut de 5 elements" << endl;
advance(iter, valeur);
cout << "L'iterateur est passe de l'element 1 a l'element " << *iter << endl << endl;
 
// 2 - Fonction next()
//On laisse l'itérateur à sa position actuelle
auto valeur2 = 3;
auto suivant = next(iter, valeur2);
cout << "2 - Fonction next()" << endl;
cout << "L'iterateur fait un saut de 3 elements a partir de " << endl;
cout << "sa position actuelle" << endl;
cout << "La position actuelle est " << *iter << " et la suivante est " << *suivant <<
endl << endl;
 
// 3 - Fonction prev()
//On décrémente l'itérateur de sa position actuelle vers une nouvelle position
auto valeur3 = 7;
auto precedent = prev(suivant, valeur3);
cout << "3 - Fonction prev()" << endl;
cout << "On decremente l'iterateur de 7 elements a partir de" << endl;
cout << "sa position actuelle" << endl;
cout << "La position actuelle est " << *suivant << endl;
cout << "La nouvelle position est " << *precedent << endl << endl;
 
// 4 - Fonction distance()
//Détermine le nombre d'éléments du tableau
cout << "3 - Fonction distance()" << endl;
cout << "Determine le nb d'elements du tableau" << endl;
 
cout << "La quantite d'elements du tableau 'nombres' est ";
cout << distance(begin(nombres), end(nombres)) << endl;
 
_getch();
return 0;
}  

 

   Allez, il n'y a quand-même pas de quoi pleurer sur cet exemple. laugh

   On initialise un tableau de 10 valeurs unsigned et on lit ces valeurs à l'aide d'un itérateur. wink

   Dans la fonction advance(), on place un itérateur au début du tableau et on lui fait faire un saut de 5 éléments. Sa position actuelle est donc de 1 + 5 = 6.

   Dans la fonction next(), nous partons de la position actuelle (6) et on détermine une position ''suivant'' située à 3 positions de cette position actuelle. La nouvelle position se situe alors à 6 + 3 = 9.

   Dans la fonction prev(), on décrémente l'itérateur d'une valeur de 7 positions à partir de la position actuelle (qui est l'iterateur déréférencé *suivant de la fonction next() ). La nouvelle position est donc 9 – 7 = 2.

   La fonction distance(), quant-à-elle, nous donne simplement la longueur du tableau. Cette fonction est bien entendu identique à l'expression end(nombres) – begin(nombres) que nous connaissons depuis quelque temps, maintenant. wink

   Vous voyez que ces fonctions simplifient les codes par rapport à d'autres accès aléatoires à des éléments de conteneurs qui devraient utiliser des boucles pour arriver aux mêmes résultats. angel

   Et tout ceci nous donne comme résultat dans la console :

 

 

 

 

   6 – Définir et initialiser un conteneur

 

   Tous les types de conteneurs définissent un constructeur par défaut. A l'exception du conteneur ''array'', le constructeur par défaut crée un conteneur vide du type spécifié. wink

 

 

      6.1 – Initialiser un conteneur comme copie d'un autre conteneur

 

   Nous avons deux moyens de créer un nouveau conteneur comme copie d'un autre. Nous pouvons directement copier un conteneur entier dans un autre (excepté ''array'') ou bien nous pouvons copier une partie d'un conteneur définie par une paire d'itérateurs. Pour toute copie autorisée, il est bien entendu que les types des deux conteneurs doivent être semblables.

   Si nous reprenons notre petite liste d'armes que nous venons d'initialiser dans le § 4.0, nous pouvons bien entendu écrire :

list<string> armes = { ''Arc de la fureur de Myin'', ''Terrible hache de plaie'', ''Dague de Nelos'' };
list<string>armes2(armes);

   Ok, les types sont semblables, on peut donc copier ''armes'' dans ''armes2''. wink

   Par contre nous ne pourrions pas écrire : deque<string>armes2(armes), les types des conteneurs étant différents. sad

   Nous pourrions aussi copier un conteneur dans un autre par la conversion d'éléments const char en strings. Par exemple :

vector<const char*> nombres = { ''un'', ''deux'', ''trois'' };

   pourrait être copié dans une forward_list en écrivant :

forward_list<string> mots (nombres.begin(), nombres.end() ) ;

 

 

      6.2 – Listes d'initialisation

 

   Nous l'avons vu, le standard C++ 11 permet de créer des conteneurs à l'aide de listes d'initialisation. C'est pourquoi nous pouvons écrire :

list<string> armes = { ''Arc de la fureur de Myin'', ''Terrible hache de plaie'', ''Dague de Nélos'' };

vector<const char*> nombres = { ''un'', ''deux'', ''trois'' };

   Cette façon de procéder permet de nommer de façon explicite chaque élément faisant partie du conteneur. Pour les types autres que ''array'', une liste d'initialisation peut implicitement spécifier
la taille d'un conteneur. wink

 

 

      6.3 – Constructeurs de conteneurs initialisés à partir de leur taille

 

   En complément aux constructeurs que les conteneurs séquentiels ont en commun avec les conteneurs associatifs, on peut également initialiser les conteneurs séquentiels (excepté ''array'') à partir d'une taille et d'un élément optionnel. Si nous ne définissons pas un élément pour initialiser un conteneur, la librairie créera une valeur initialisée à notre place.

 

   Quelques exemples :

vector<int> monVector(20, -5) créera 20 éléments, chacun initialisé à -5.

list<string> maChaine(15, ''Legends of Meruvia'') créera 15 chaînes, chacune nommée ''Legends of meruvia'' (oui, ça fait 15 histoires à raconter...)

forward_list<int> maListe(20) créera 20 éléments, chacun initialisé à 0.

deque<string> maDeque(15) créera 15 éléments, chacun étant une chaîne vide.

   Nous pouvons ajouter que les constructeurs prenant une taille en argument ne sont pas supportés par les conteneurs associatifs. wink

 

 

      6.4 – Le conteneur ''array''

 

   De même que la taille d'un tableau de type prédéfini fait partie de son type, la taille d'un conteneur de la librairie ''array'' est une part entière de son type. Cela veut dire que si nous définissons un tableau, en plus de spécifier le type des éléments, nous devons également spécifier la taille du tableau (une fois la taille d'un conteneur ''array'' spécifiée, celle-ci ne peut plus être modifiée ! surprise).

 

   Quelques exemples :

- array<int, 20> créera un tableau de 20 éléments de type int.

- array<string, 15> créera un tableau de 15 éléments de type chaîne.

- array<int, 20> Tab1 créera un tableau de 20 entiers initialisés par défaut (soit 0).

- array<int, 5> Tab2 = { 6, 7, 8, 9, 10 } : une liste d'initialisation est supportée par array.

- array<int, 5> Tab3 = { 25 } créera un tableau dont le premier élément (tab3[0]) est 25 et les quatre suivants initialisés à 0 par défaut.

 

   Soit une liste d'initialisation d'un tableau : int mesEntiers[5] = { 6, 7, 8, 9, 10 };

   Il est bien entendu impossible de faire :

int nombres[5] = mesEntiers; (on ne peut pas copier un tableau de type prédéfini dans un autre ! indecision)

   Et soit une liste d'initialisation d'un conteneur de type array :

array<int, 5> mesEntiers = { 6, 7, 8, 9, 10 };

   Les types des éléments étant les mêmes ainsi que leur taille, on peut faire :

array<int, 5> nombres = mesEntiers;

   De même que pour les tableaux de type prédéfini, la bibliothèque array permet l'assignation des données. La condition est que les opérandes de gauche (left-hand) et de droite (right-hand) doivent être de type identique. wink

 

   Quelques exemples :

array<int, 5> mesEntiers_1 = { 6, 7, 8, 9, 10 };

array<int, 5> mesEntiers_2 = {0}; crée 5 éléments de type int et de valeur 0.

mesEntiers_1 = mesEntiers_2 remplacera tous les éléments de mesEntiers_1 par les éléments de mesEntiers_2.

   Si la taille des deux conteneurs diffère, il n'est pas possible de copier un tableau vers l'autre :

mesEntiers_2 = {11};  - impossible de réassigner mesEntiers_2 car la taille est 5 et non 1.

 

Remarque : Il faut bien se rendre compte que la STL apporte de grandes opportunités au langage C++. Cette puissance du langage est constamment revue et améliorée au fur et à mesure de la sortie des nouvelles normes ( C++ 11, C++ 14 et, en cours de mise à jour au moment où ce tutoriel est écrit, de la norme C++ 17).

 

   Et pour démontrer cette puissance du langage, nous allons présenter, parmi d'autres que nous verrons plus tard dans les algorithmes génériques, un exemple qui calcule la somme de valeurs de type double (par exemple) dans un tableau et qui utilise une fonction ''accumulate()'' faisant partie du header <numeric> devant être ajouté en début de programme. wink

 

   Exemple 3 :

      Projet 134ContSequentiels_3 

 

//Projet 134ContSequentiels_3
//Calcul de la somme des valeurs des éléments de type double
//d'un tableau de type array.
//Utilisation de la fonction accumulate() du header <numeric>
#include <iostream>
#include <array>
#include <numeric>
#include <conio.h>
 
using namespace std;
 
int main()
{
//Initialisation d'une liste de doubles
array<double, 10> valeurs = { 3.5, -2.1, -9.9, 6.7, 12.4, 21.6, -17.1, -11.3, 7.9, 1.8 };
cout << endl;
cout << "La fonction 'accumulate' du header <numeric> calcule des valeurs" << endl;
cout << "dans un tableau de type 'array'" << endl << endl;
 
cout << "la liste est :" << endl;
 
for (auto iter = begin(valeurs); iter != end(valeurs); ++iter)
cout << *iter << " ";
cout << endl << endl;
 
//On calcule la somme des valeurs du tableau
auto total = accumulate(begin(valeurs), end(valeurs), 0.0);
 
cout << "La somme des elements du tableau est " << total << endl;
 
_getch();
return 0;
} 

 

    La fonction accumulate() retourne la somme des éléments dans le domaine défini par les deux premiers arguments passés à la fonction. Ceux-ci sont les itérateurs spécifiant le premier et le dernier plus un éléments de la liste.

   Le troisième élément passé en paramètre est la valeur initiale devant être utilisée pour le calcul de la somme. Cette valeur doit être spécifiquement indiquée. Si par exemple nous avions mis une valeur de 10.0 à la place de 0.0 comme troisième paramètre, cette valeur aurait été ajoutée à la somme des éléments du tableau et aurait donné 23.5 à la place de 13.5 comme résultat.

   Ce qui nous donne comme résultat dans la console :  

 

 

   Remarquez que la fonction accumulate() aurait pu être utilisée avec tout autre type de conteneur séquentiel autre que ''string'' et qu'elle est compatible pour tout domaine d'éléments de conteneurs qui supportent l'addition, et par extension, avec les objets de tous types de classes qui définissent la fonction operator+().

 


 7 – Opérations sur les conteneurs séquentiels (ajout d'éléments)

  7.1 – Utilisation de fa fonction assign() (conteneurs séquentiels uniquement)

 

 

   L'opérateur d'assignation nécessite que les opérandes de gauche et de droite aient le même type.

   L'opérateur ''assign'' copie tous les éléments de l'opérande de droite dans l'opérande de gauche.

   Les conteneurs séquentiels (excepté array) définissent un membre nommé ''assign'' qui nous permet d'assigner un conteneur ou des éléments de conteneur vers un autre conteneur compatible. wink

   Nous avons vu dans le paragraphe 6.1 la copie d'un conteneur dans un autre par la conversion d'éléments const char en strings.

   Nous pouvons faire la même chose avec la fonction assign(). Par exemple :

vector<const char*> heros = { ''Rabidja'', ''Wiwi'', ''Aron'' };
list<string> noms;

   Nous ne pouvons pas écrire noms = heros car les types diffèrent mais nous pouvons convertir les éléments const char en strings en écrivant :

noms.assign(heros.cbegin(), heros.cend() );

   Une seconde version de la fonction assign permet d'assigner ou de remplacer les éléments d'un conteneur par un nombre d'éléments spécifiés dans un autre conteneur. smiley

   Exemple :

list<string> maListe;
maListe.assign(16, ''Legends of Meruvia''); ''Tiens... ça nous fait une histoire de plus !''

 

 

      7.2 – Utilisation de fa fonction swap()

 

   La fonction swap() échange les contenus de deux conteneurs de même type.

   Exemple :

vector<string> archers(10);
vector<string> archers2(20);
swap(archers, archers2);

   Avant la fonction swap(), le vector archers contient 10 chaînes non initialisées et le vector archers2 contient 20 chaînes non initialisées. Après l'appel de swap(), archers contiendra 20 chaînes et archer2, 10 chaînes.

 

 

      7.3 – Utilisation de fa fonction push_back()

 

   Nous savons depuis un certain temps maintenant que la fonction push_back() ajoute un élément à la fin d'un vector et nous l'avons utilisée dans quelques exemples ou exercices passés. wink

   Connaissant actuellement le nom des conteneurs séquentiels, nous pouvons être plus précis en ajoutant que mis à part les conteneurs array et forward_list, nous pouvons utiliser push_back() avec les autres types de conteneurs séquentiels, y compris le conteneur stringsmiley

 

   Exercice 2 :

   Ecrivez une fonction à qui l'on passe une référence sur une chaîne représentant un mot quelconque. Cette fonction devra mettre le mot au pluriel et heu... blush si vous vous en souvenez, vous ferez un test sur les mots qui forment leur pluriel avec un 'X'. Allez, je suis gentil, je vous les donne : chou, genou, hibou, bijou, joujou, caillou et pou. cheeky

   Pour les perfectionnistes, vous pouvez ajouter les cas où le mot possède déjà un 'S' ou un 'X' au singulier. angel

 

 

      7.4 – Utilisation de fa fonction push_front()

 

   Complémentairement à la fonction push_back(), utilisable par les conteneurs cités en (7.3), les conteneurs list, forward_list et deque supportent une fonction dénommée push_front().

   Comme son nom l'indique, cette fonction ajoute un élément ou une série d'éléments en début de liste du conteneur. angel

 

   Exemple :

forward_list<int> mesChiffres = { 4, 5, 6, 7, 8, 9, 10 };

for(int i(3); i != - 1; --i)
{
mesChiffres.push_front(i);
}

 

   Pour avoir une liste ordonnée vous voyez que nous sommes obligés de décrémenter i de 3 à 0. En effet, push_front(3) va placer 3 avant le chiffre 4, push_front(2) placera 2 avant le chiffre 3 et ainsi de suite. Si nous avions écrit notre (for) de la forme :

for(int i(0); i != 4; ++i), la liste obtenue aurait été : { 3, 2, 1, 0, 4, 5, 6, 7, 8, 9, 10 }

 

 

      7.5 – Ajouter un élément en un point spécifique d'un conteneur

 

   Nous avons vu que les fonctions push_back() et push_front() étaient très intéressantes pour insérer un élément en fin et au début d'un conteneur séquentiel. D'une manière plus générale, la fonction insert() permet d'insérer un ou plusieurs éléments à n'importe quel endroit d'un conteneur. wink

   La fonction insert() est supportée par les conteneurs ''vector'', ''deque'', ''list'' et ''string''. Comme il peut souvent être intéressant d'ajouter des élément au début d'un conteneur, le ou les éléments à insérer sont toujours insérés une position avant l'élément pointé par l'itérateur actuel.

   Soit par exemple une liste list<string> maListe = { ''Meruvia'' }; et un itérateur iter pointant sur ce nom, alors l'instruction :

maliste.insert(iter, ''Bonjour'');

   insèrera ''Bonjour'' juste avant ''Meruvia'' dans la liste.

 

   Nous voyons ainsi que sans nous préoccuper de quel conteneur séquentiel supporte l'instruction push_front, nous pouvons toujours utiliser la fonction insert() pour insérer un élément avant le premier élément d'un conteneur séquentiel (autre que array bien entendu).

   Nous pouvons donc dire qu'il y a équivalence entre les expressions suivantes :

maliste.push_front(''Bonjour'') <=> maliste.insert(maliste(begin), ''Bonjour'')

 

 

      7.6 – Ajouter une série d'éléments en un point spécifique d'un conteneur

 

   Soient les deux vectors de strings suivants :

vector<string> maListe1 = { "Bonsoir", "a", "vous" };

vector<string> maListe2 = { "Bonjour" };
 

   Nous voudrions obtenir { ''Bonjour'', ''a'', ''vous'' } dans le vector maListe2, c'est-à-dire aller chercher les mots ''a'' et ''vous'' dans maListe1 et les insérer dans maListe2.

   Partant de la fin de maListe1, les mots ''a'' et ''vous'' sont situés aux emplacements :

maListe1.end() - 2 et maListe1.end()

   Cette série de deux mots doit maintenant être insérée dans maListe2 à l'emplacement maListe2.begin() + 1. En effet, dans maListe2, ''Bonjour'' est, quant-à-lui, situé à l'emplacement maListe2.begin().

   D'où l'expression et les instructions suivantes : 

maListe2.insert(maListe2.begin() + 1, maListe1.end() - 2, maListe1.end());

for (auto iter = begin(maListe2); iter != end(maListe2); ++iter)
 
cout << *iter << " ";
 

   Donneront bien comme résultat :

 

 

   Notez que je n'ajoute pas de projet pour ces quelques lignes mais vous pouvez très bien en écrire un vous-même. wink

 

 

      7.7 – Opérations ''Emplace''

 

   Le standard C++ 11 a introduit trois nouveaux membres dénommés ''emplace_front'', ''emplace'' et ''emplace_back'' qui construisent des éléments plutôt que de les copier. Ces opérations correspondent aux opérations push_front, insert et push_back en ceci qu'elles ajoutent un élément au début, soit à une position donnée ou en fin d'un conteneur. wink

   Si nous appelons un membre push ou insert, nous passons des objets du type des éléments et ceux-ci sont copiés dans le conteneur. Cependant, lorsque nous appelons un membre emplace, nous passons les arguments du type des éléments à ajouter. Les membres emplace utilisent ces arguments pour construire un élément directement dans l'espace réservé par le conteneur.

   Soit un vector de type Achats_Ventes dénommé armes :

vector<Achats_Ventes> armes;

    Soit un constructeur achats_ventes comprenant six paramètres :

Achats_Ventes(std::string nomArme, std::string typeArme, uint degats, uint etat, double poids, double prix);
 

    emplace_back ajoutera des éléments armes en fin de vector :

armes.emplace_back("Epee longue d'acier du heros Rabidja", "Lame longue une main", 4, 600, 3.5, 175.0);
 

   On peut également utiliser push_back en lui passant un élément se référant à la classe Achats_Ventes

armes.push_back(Achats_Ventes("Lance cruelle perceuse d'ame", "Lance une main", 7, 1500, 4.5, 410.0));
 

   La différence entre ces deux appels de fonctions est que à l'appel de emplace_back, l'objet est directement créé dans l'espace réservé par le conteneur tandis que push_back créera un objet local temporaire dans le conteneur.

   Les arguments de la fonction emplace doivent bien entendu correspondre aux arguments du constructeur d'un objet Achats_Venteswink

 

 

      7.8 – Accéder aux éléments d'un conteneur séquentiel

 

   En plus des accès fournis par les quatre fonctions du header <iterator> (voir § 5.0), nous pouvons accéder à certains éléments d'un conteneur à l'aide de quelques autres fonctions. 

 

nom.back() retourne une référence sur le dernier élément de nom. Indéfini si le conteneur nom est vide. 
nom.front()   retourne une référence sur le premier élément de nom. Indéfini si le conteneur nom est vide. 
nom [n]  retourne une référence sur l'élément indexé par une valeur unsigned n.
Indéfini si nom >= nom.size().
nom.at (n) retourne une référence sur l'élément indexé par n. Si n est en dehors de son domaine de définition, une exception out_of_range sera levée.

 

   Les opérateurs at et [ ] ne sont valides que pour les conteneurs string, vector, deque et array.
   back n'est pas valide pour forward_list.
   En outre, l'opérateur at a ceci d'intéressant qu'il se comporte comme l'opérateur [ ] mais qu'il est plus fiable car il vérifie que l'indice qui lui est passé se situe dans l'intervalle des indices valides du conteneur.

 

 

   Exemple 4 :

      Projet 135ContSequentiels_4    

 

//Projet 135ContSequentiels_4
//Accès aux membres d'un conteneur
#include <iostream>
#include <conio.h>
#include <list>
 
using namespace std;
 
 
int main()
{
//Initialisation d'une liste de doubles
list<double> val = { 3.5, -2.1, -9.9, 6.7, 12.4, 21.6, -17.1, -11.3, 7.9, 1.8 };
 
cout << endl;
cout << "Creation d'une liste de 10 valeurs de type double" << endl;
cout << "Lecture de la liste :" << endl << endl;
for (auto iter = begin(val); iter != end(val); ++iter)
cout << *iter << " ";
 
cout << endl << endl;
cout << "On initialise une reference sur les premier et dernier element de la liste." << endl;
cout << "Ces elements sont ensuite modifies." << endl;
 
//On modifie les premier et dernier éléments de la liste
//en initialisant une référence sur ceux-ci.
if (!val.empty())
{
auto &premier = val.front();
premier = 4.9;
auto &dernier = val.back();
dernier--;
}
 
cout << "Lecture de la liste modifiee" << endl << endl;
 
for (auto iter = begin(val); iter != end(val); ++iter)
cout << *iter << " ";
 
cout << endl;
 
_getch();
return 0;
} 

 

     Ce qui donne dans la console :

 

 

 

 

   8 – Opérations sur les conteneurs séquentiels (suppression d'éléments)

      8.1 – Les membres pop_front et pop_back

 

   Les fonctions pop_front() et pop_back() suppriment respectivement les premier et dernier éléments d'un conteneur séquentiel. wink

   De même que push_front n'est pas valide pour les conteneurs vector et string, pop_front ne l'est pas non plus pour ces types. Quant à forward_list, celui-ci ne pouvant être consulté que du début vers la fin, l'opération pop_back est également impossible.

   Attention : ces deux opérations retournant ''void'', si vous voulez utiliser ultérieurement la valeur que vous vous apprêtez à supprimer, il faudra la sauver avant d'effectuer un popindecision

 

Opérations supprimant des éléments dans les conteneurs séquentiels
nom.pop_back()   Supprime le dernier élément de nom. Indéfini si nom est vide.
Retourne void.   
nom.pop_front() Supprime le premier élément de nom. Indéfini si nom est vide.
Retourne void.
nom.erase(iter) Supprime l'élément pointé par l'itérateur iter et retourne un itérateur sur l'élément suivant l'élément supprimé ou sur l'itérateur de fin du conteneur si iter pointe lui-même sur le dernier élément. 
nom.erase(iter, iter2)  Supprime la rangée des éléments pointés par iter1 et iter2. Retourne un itérateur sur l'élément suivant celui qui a été supprimé. 
nom.clear()   Supprime tous les éléments dans nom. Retourne void.

 

 

   Allez, un petit challenge (exercice 3) !

 

   Créez une liste de doubles (reprenez la première liste de l'exercice précédent) soit :

list<double> val = { 3.5, -2.1, -9.9, 6.7, 12.4, 21.6, -17.1, -11.3, 7.9, 1.8 };
 

   ainsi qu'une liste vide pour recevoir des nombres négatifs, soit :

list<double> neg;

 

   Vous devrez enlever les nombres négatifs de la liste ''val'' et les transférer dans la liste ''neg''.

   Utilisez la fonction emplace_back() pour créer les éléments dans la nouvelle liste ''neg'' plutôt que de faire une copie des éléments négatifs de ''val''. wink

   Affichez ensuite les deux listes. angel

 

 

   9 – Corrigés des exercices du chapitre 21

 

      Exercice 1 (projet Chap21Exercice_1)

 

   Dans cet exercice on demandait de refaire le projet 125Transtypage du chapitre 21 en écrivant les lignes d'affichage des objets nombreUn et nombreDeux sans implémenter des fonctions membres d'accès publique.

   Il nous fallait donc surcharger l'opérateur de sortie ''<<'' en implémentant une fonction operator>>.

   Le code :

 

      Fichier Chap21Ex_1.h  

 

//Projet Chap21Exercice_1
//Conversion d'une valeur de type unsigned en objet de la classe Nombre
//Implémentation d'une fonction de surcharge de l'opérateur <<
//Fichier Chap21Ex_1.h
 
#ifndef DEF_NOMBRE
#define DEF_NOMBRE
 
#include<iostream>
 
class Nombre
{
 
public:
// Constructeurs
Nombre() { _nb = 0; }
Nombre(size_t val) { _nb = val; }
 
// Destructeur
~Nombre() {}
 
//Déclaration d'une fonction amie de l'opérateur I/O operator<<
friend std::ostream &operator<< (std::ostream &out, Nombre &rhs);
 
private:
//Donnée membre privée
size_t _nb;
 
};
 
 
inline
std::ostream &operator<< (std::ostream &out, Nombre &rhs)
{
out << rhs._nb; //Une fonction amie a accès aux éléments privés de la classe
return out;
}
 
#endif 

 

   Dans le fichier header nous déclarons une fonction amie operator<< à l'intérieur de la classe et nous avons vu que sa définition devait se faire à l'extérieur de la classe. Pour ceux qui n'auraient pas bien suivi, nous rappelons que les arguments devant être passés à la fonction sont bien entendu une référence au flux de sortie ainsi qu'une référence à une variable de type Nombreangel

   Vous remarquerez également que nous n'avons pas besoin ici de fonction membre d'acquisition de données getVal(). En effet, operator<<() étant une fonction amie, elle a accès à tous les éléments privés de la classe et c'est pourquoi nous pouvons écrire out << rhs._nb à la place de rhs.get_Val(). wink

 

      Fichier Chap21Ex_1.cpp   

 

//Projet Chap21Exercice_1
//Conversion d'une valeur de type unsigned en objet de la classe Nombre
//Implémentation d'une fonction de surcharge de l'opérateur <<
//Fichier Chap21Ex_1.cpp
 
#include <iostream>
#include "Chap21Ex_1.h"
#include <conio.h>
 
using namespace std;
using uint = unsigned;
 
int main()
{
//Déclaration d'une variable de type unsigned
uint maVariable = 15;
 
//Le constructeur par défaut initialise un objet à 0.
Nombre nombreUn;
//Le constructeur surchargé initialise un second objet à 10
Nombre nombreDeux(10);
 
//On affiche la valeur de maVariable
cout << endl;
cout << "Valeur de la variable 'maVariable de type unsigned = " << maVariable;
cout << endl;
//On affiche les valeurs des objets à l'aide de l'operateur surchargé operator<<
cout << "Valeur de l'objet 'nombreUn' = " << nombreUn << endl;
cout << "Valeur de l'objet 'nombreDeux' = " << nombreDeux << endl;
 
//Transtypage
cout << endl;
cout << "Maintenant on donne la valeur de maVariable a chaque objet" << endl;
 
cout << "par un transtypage d'un type unsigned vers chaque objet : " << endl << endl;
nombreUn = maVariable;
nombreDeux = maVariable;
 
//On affiche les valeurs modifiées des objets à l'aide de l'operateur surchargé
// operator<<
 
cout << "Valeur de l'objet 'nombreUn' = " << nombreUn << endl;
cout << "Valeur de l'objet 'nombreDeux' = " << nombreDeux << endl;
 
_getch();
return 0;
} 

 

     Voilà, la différence notable entre ce fichier et le fichier Transtypage_MainFile.cpp du chapitre précédent est que dans celui-ci, nous n'avons plus besoin de faire appel à une fonction membre d'acquisition de données pour accéder à la valeur des objets mais que nous pouvons maintenant les afficher directement sous la forme :

cout << nombreUn;
cout << nombreDeux;

   et ceci, grâce à la surcharge de l'opérateur de sortie ''<<''.

 


      Exercice 2 (projet Chap21Exercice_2)

 

   Pour cet exercice on demandait de modifier le projet 127ObjetConst en créant un pointeur dans le tas sur un objet non const et un autre sur un objet const et essayer de modifier un élément des deux objets.

   Le code :

 

   Le fichier Chap21Ex_2.h étant identique au fichier ObjetNonConst.h du projet 127ObjetnNonConst du chapitre 21, il ne sera pas reproduit ici.

   Fichier Chap21Ex_2MainFile.cpp  

 

//projet Chap21Exercice_2
//Fichier : Chap21Ex_2MainFile.cpp
//Création d'un pointeur sur un objet constant de type Player
 
#include "Chap21Ex_2.h"
#include <string>
#include <conio.h>
 
using namespace std;
 
int main()
{
//Création d'un pointeur sur un objet non constant de type Player
Player *heros = new Player("Aria", "Elfe", "Female");
 
//Création d'un pointeur sur un objet constant de type Player
const Player *secondHeros = new Player("Elwyn", "Elfe", "Male");
 
cout << endl;
cout << "Creation d'un pointeur sur un objet non constant de type Player : " << endl << endl;
 
//On lit les membres de l'objet heros
cout << "Nom : ";
heros->get_Name();
cout << endl;
cout << "Race : ";
 
heros->get_Race();
cout << endl;
cout << "Sexe : ";
heros->get_Sex();
cout << endl << endl;
 
//On modifie le nom de notre héroïne à l'aide du pointeur heros
//Ici pas de problème, objet non constant.
heros->set_Name("Thiellawen");
 
//Lecture du nom modifié
cout << "Un membre de l'objet peut etre modifie car l'objet est non constant !";
cout << endl;
cout << "Nom : ";
heros->get_Name();
cout << endl << endl;
 
cout << "Creation d'un pointeur sur un objet constant de type Player : " << endl << endl;
 
//On lit les membres de l'objet secondheros
cout << "Nom : ";
secondHeros->get_Name();
cout << endl;
cout << "Race : ";
secondHeros->get_Race();
cout << endl;
cout << "Sexe : ";
secondHeros->get_Sex();
cout << endl << endl;
 
cout << "On tente de modifier le nom du heros a l'aide du pointeur secondHeros" << endl;
cout << "Impossible car pointeur sur un objet Player constant ! " << endl;
cout << "L'objet etant constant, il ne peut plus etre modifie !" << endl;
//secondHeros->set_Name("Elwen");
 
delete heros;
delete secondHeros;
 
_getch();
return 0;
} 

 

   Voilà, rien de compliqué ici, et nous n'oublions pas de libérer la mémoire en fin de programme.

   Ce qui nous donne dans la console :

 

  

 

   Je pense que c'est tout en ce qui concerne ce chapitre mais vu que nous n'aurons pas assez d'un chapitre pour faire le tour des conteneurs séquentiels ainsi que de leurs adaptateurs associés, nous continuerons donc leur étude dans les chapitres 23 et 24. wink

   Les corrigés des exercices de ce chapitre seront présentés à la fin du chapitre 23.

      @ bientôt pour le chapitre 23 – Les conteneurs séquentiels (2).                                                                     

            Gondulzak.  angel

 
 
 

Connexion

CoalaWeb Traffic

Today47
Yesterday178
This week547
This month4221
Total1743428

25/04/24