Patrons de classe et conteneurs STL
Patrons de classe :
Un patron de classe est un modèle que le compilateur utilise pour créer des classes au besoin.
Les patrons de classe sont très utiles pour la réutilisation des classes conteneurs dont le rôle est de regrouper des objets suivant une certaine structure. Par exemple : liste, tableau, arbre, pile, etc.
Exemple de la classe Point :
#include <iostream.h>
#include <math.h>
// classe des points dont les coordonnées sont des entiers class PointEntier {
private : int x, y ; public :
PointEntier (int a = 0, int b = 0) { x = a, y = b ;
}
double longueur () const { return sqrt( x * x + y * y);
}
friend void afficher(const PointEntier &) ; };
void afficher(const PointEntier & P) {
cout << "<x = " << P.x << ", y = " << P.y
<< ", longueur = " << P.longueur() << ">\n\n";
}
// classe des points dont les coordonnées sont des réels class PointReel {
private :
double x, y ; public :
PointReel (double a = 0, double b = 0) {
x = a, y = b ; }
double longueur () const { return sqrt( x * x + y * y);
}
friend void afficher(const PointReel &) ; };
void afficher(const PointReel & P) {
cout << "<x = " << P.x << ", y = " << P.y
<< ", longueur = " << P.longueur() << ">\n\n";
}
// tests de ces 2 classes void demo1() {
cout << "Demo1 : 2 classes des points :
points entiers et points reels :\n\n";
PointEntier A(5, 7);
afficher(A);
PointReel B(12.86, 21.15);
afficher(B);
cout << "\nFin de demo1\n\n";
}
// Pour avoir les coordonnés du point de n’importe quel type T, on définit un // patron de classe
template <class T>
class Point { private : T x, y ; public :
// cas de définition directe Point (T a = 0, T b = 0) { x = a, y = b ;
}
// cas de définition reportée double longueur () const ; //cas des fonctions amies
friend void afficher (const Point<T> &) ;
// sous linux avec g++ il faut ajouter <> après le nom de // la fonction sinon le compilateur va considérer la fonction // comme non-template :
// friend void afficher <> (const Point<T> &) ;
};
template <class T> double Point<T>::longueur() const { return sqrt( x * x + y * y);
}
template <class T> void afficher(const Point<T> & P) { cout << "<x = " << P.x << ", y = " << P.y
<< ", longueur = " << P.longueur() << ">\n\n";
}
// test du patron de classe Point void demo2() {
cout << "Demo2 : patron de classe (1 seule classe Point) :\n\n";
Point <int> C(5,7);
afficher(C);
Point <double> D(12.86, 21.15);
afficher(D);
cout << "\nCa marche!, fin de demo2\n\n";
}
void main() { demo1();
demo2();
}
/* Exécution :
Demo1 : 2 classes des points : points entiers et points reels :
<x = 5, y = 7, longueur = 8.60233>
<x = 12.86, y = 21.15, longueur = 24.7528>
Fin de demo1
Demo2 : patron de classe (1 seule classe Point) :
<x = 5, y = 7, longueur = 8.60233>
<x = 12.86, y = 21.15, longueur = 24.7528>
Ca marche!, fin de demo2 Press any key to continue
*/
Exemple sur une pile générique
:#include <iostream.h>
#include <iomanip.h>
template <class T>
class Pile { private:
int maxElem;
int nbElem;
T * p;
public:
Pile(int max = 50) { maxElem = max;
p = new T[maxElem];
nbElem = 0;
}
int estVide() const { return nbElem == 0;
}
void empiler(T unElem) { p[nbElem++] = unElem;
}
T depiler() {
return p[--nbElem];
}
T consulterSommet() { return p[nbElem-1];
}
friend ostream& operator << (ostream& sortie, const Pile<T>& pile);
};
template <class T>
ostream& operator << (ostream& sortie, const Pile <T>& pile){
if (pile.estVide())
sortie << "pile vide\n";
else {
sortie << "Contenu de la pile :\n";
for (int i = 0 ; i < pile.nbElem ; i++) sortie << i << ") " << pile.p[i] << endl;
}
return sortie;
}
void demoPileEntiers() { Pile<int> pile(50);
cout << pile ;
for (int k = 1 ; k <= 20; k++) if (20 % k == 0)
pile.empiler(k);
cout << pile ;
cout << "Au sommet : " << pile.consulterSommet() << endl << endl;
}
void demoPileCaracteres() { Pile<char> pile(10);
cout << pile ; pile.empiler('A');
pile.empiler('E');
pile.empiler('Y');
pile.empiler('S');
cout << pile ;
cout << "Au sommet : " << pile.consulterSommet() << endl << endl;
}
class Point { private:
int x, y ; public:
Point(int x=0, int y = 0) {
this->x =x, this->y = y;
}
friend ostream& operator << (ostream& sortie, const Point& a);
};
ostream& operator << (ostream& sortie, const Point& a) { sortie << "<" << a.x << ", " << a.y << ">\n";
return sortie;
}
void demoPilePoints() { Pile<Point> pile(5);
cout << pile ;
pile.empiler(Point(5,1));
pile.empiler(Point(4,9));
pile.empiler(Point(6,2));
cout << pile ;
cout << "Au sommet : " << pile.consulterSommet() << endl;
}
void main() {
demoPileEntiers();
demoPileCaracteres();
demoPilePoints();
}
/* Exécution:
pile vide
Contenu de la pile : 0) 1
1) 2 2) 4 3) 5 4) 10 5) 20
Au sommet : 20
pile vide
Contenu de la pile : 0) A
1) E 2) Y 3) S
Au sommet : S pile vide
Contenu de la pile : 0) <5, 1>
1) <4, 9>
2) <6, 2>
Au sommet : <6, 2>
Press any key to continue
*/
Traitement d’exceptions :
Le langage C++ offre une gestion efficace des erreurs pouvant apparaître lors de l’exécution d’un programme. Par exemple :
#include <iostream.h>
void main() {
int c, a=1, b=0 ;
c=a/b ; // division par zéro cout << "c : " << c << endl ; }
Lors de son exécution, ce programme génère l’erreur suivante :
Floating exception (core dumped)
En C et C++, nous pouvions inclure dans le programme, un message d’erreur plus informatif, et un arrêt automatique pour sortir définitivement du programme : void main() {
int c, a=1, b=0 ;
// test si le dénominateur est égal à zéro if (b==0) {
cerr << "attention division par zéro ! " << endl ; exit(1) ;
}
// si le dénominateur n’est pas nul, suite des instructions c=a/b ;
cout << "c : " << c << endl ; }
En plus de cette technique
,
le C++ a introduit un nouveau mécanisme de gestion des erreurs. Ce mécanisme est connu sous le nom de traitements des exceptions.
#include <iostream.h>
#include <string> // classe string de la librairie STL
using namespace std ;// pour utiliser la librairie STL (explication plus bas) int test_denominateur(int a, int b) {
string x
= "division par zero" ;if (b==0) {
throw x
; }return a/b ; // division entière.
}
void main() {
int c=0,a=1,b=0 ;
try {
c=test_denominateur(a,b) ; //division par zero ! cout << "c : " <<c << endl ;
}
catch (string)
{cout << "test_denominateur : division par zero! "<<endl ; }
cout << "suite du programme! \n" ; }
/*Exécution :
test_denominateur : division par zero!
suite du programme ! */
1. On commence par construire un objet x de type quelconque qui représente l’erreur ; dans l’exemple x est du type string.
2. On lance une exception sur cet objet grâce au mot-clé réservé, throw.
Pour cet exemple, cette exception est lancée dans le cas ou le dénominateur est nul.
3. Cette expression est alors enfermée dans un bloc try, qui peut générer une erreur. Nous avons inclus la fonction qui peut générer une erreur, test_denominateur, dans ce bloc try.
4. Un bloc catch essaie d’attraper l’objet x, représentant l’erreur ; puisque x est de type string, nous avons défini un catch de ce type là.
5. S’il ne parvient pas, une foncion spéciale (terminate) est appelé pour terminer l’exécution du programme. Par exemple, dans le cas ou un catch spécifique à x (donc un string) n’a pas été défini.
Conteneurs STL :
Bibliothèque de modèles standards (STL – Standard Template Library)
STL est une bibliothèque de classes du type container (comme par exemple des listes chaînées), d'algorithmes et d'outils d'itération.
Un conteneur est une structure qui permet d'organiser un ensemble d'objets du même type en séquence. Voici par exemple différents types de conteneurs:
les listes (list),
les vecteurs (vector),
les ensembles (set),
les listes d'associations (map).
La notion de conteneur est importante. Il s'agit de structures algorithmiques permettant d'organiser un ensemble de données en séquence, puis de parcourir ces données.
La définition d'un conteneur est en fait indépendante du type des objets contenus. Les opérations que l'on peut effectuer sur un conteneur ne dépendent pas, du point de vue "mathématique", de ce type.
Les classes de conteneurs définies par la STL admettent donc toutes un paramètre template, qui, lors de l'instanciation d'un conteneur, sert à indiquer le type des objets que contiendra cette instance de conteneur.
STL est une bibliothèque standard du C++. Ce qui veut dire qu'il n'y a pas besoin d'options de compilation ou de Makefile compliqué! D'autre part, tout le code est contenu dans les fichiers header. Pour se servir de la STL, il suffit de faire des #include!
Namespaces :
La bibliothèque standard se trouve entièrement dans le namespace std. Un namespace (espace de nom ) est un moyen de lutter contre les conflits de noms, par exemple le type list s'appelle en réalité std::list. Cependant pour ne pas alourdir les exemples de code (et parce que en général, l'emploi de directives using permet ce raccourci), nous omettrons le std::.
Certains noms sont assez communs et peuvent avoir été utilisés par plusieurs librairies. Par exemple, beaucoup de librairies ont une classe de base abstraite qui porte le nom class Objet. Si vous essayez d'inclure les fichiers d'entêtes de deux librairies définissant une telle classe, vous aurez un conflit de nom, les références au type Objet étant alors ambigues.
Les namespace sont une solution à ce problème. Le fichier suivant namespace Lib // ouverture du namespace
{
class Toto {
// ...
};
}; // fermeture du namespace
définit le type Lib::Toto. Cependant quand aucune ambiguïté n'existe, le programmeur peut utiliser le type Toto directement.
Pour utiliser le contenu du namespace Lib, il faut inclure using namespace Lib;
La notion d'itérateur :
L'itérateur est une généralisation des pointeurs. Il permet de parcourir en séquence les éléments d'un conteneur. Pour comprendre l'analogie, regardons le code C++ classique suivant:
1 void fonction(char* t,const int& taille) 2 {
3 for(int i=0;i<taille;i++) 4 {
5 t[i] = t[i]+2;
6 } 7 }
Afin d'optimiser un tel parcours de tableau (pour éviter à la ligne 5, de calculer deux fois un déplacement de i par rapport à l'adresse de base de t), on peut remplacer ce code par le code suivant:
void fonction(char* t,const int& taille) {
for(char *p = t,*pstop = t+taille;p < pstop;p++) {
*p = (*p)+2;
} }
Un itérateur correspond au pointeur p. il "pointe" sur un objet, et sait passer à l'objet suivant dans le conteneur. Un itérateur est donc lié à un conteneur en particulier. Il a notamment besoin de connaître le type des objets du conteneur (pour avoir la taille et savoir "passer au suivant").
Pour cela, le type iterator est un type membre des classes conteneurs. La déclaration d'un itérateur sur un vecteur d'entiers se fait donc de la manière suivante:
vector<int>::iterator i;
Pour que cette itérateur pointe sur le premier élément d'une liste donnée, on utilise la méthode begin() de ce vecteur comme ci-dessous:
vector<int> v;
vector <int>::iterator i = v.begin();
En effectuant des ++i (ou des i++), on se déplacera alors le long du vecteur.
Mais il faut alors savoir où s'arrêter. Dans l'exemple C précédent, on avait une variable pstop qui indiquait la fin du tableau. L'équivalent en STL, c'est l'itérateur retourné par la méthode end() d'un conteneur. Ainsi la traduction en STL de l'exemple précédent est:
void fonction(vector<char>& t) {
for (vector<char>::iterator i = t.begin(),istop=t.end();
i != istop;++i) {
*i = (*i)+2;
} }
L'itérateur end() est en fait assez spécial. Il pointe sur "après" le dernier élément du conteneur. Il y a plusieurs raisons et avantages à cela:
Les fonctions qui recherchent un objet dans un conteneur retournent un itérateur sur cet objet. Quand elles ne l'ont pas trouvé, elles l'indiquent en retournant end().
Les insertions d'objets se font en indiquant une position grâce à un itérateur. L'objet est alors inséré "avant" l'itérateur. Pour insérer au début, on insère avant begin() et pour insérer en fin, on insère avant end().
Pour tester si un conteneur est vide, on peut utiliser
(v
.
begin()==v.
end())Ce test est défini inline pour tout les conteneurs par la fonction membre classique bool empty().
Quand on essaye d'incrémenter un itérateur qui pointe sur end(), sa valeur devient indéfinie.
Enfin, pour accéder à la valeur de l'objet "pointé" par l'itérateur, il suffit de le déréférencer avec * ou d'utiliser la notation ->.
Classe list :
La classe list est un conteneur ou chaque élément de la liste a son propre segment de mémoire et pointe son prédécesseur et son successeur (liste doublement chainée).
Il faut inclure :
#include <list>
using namespace std ;
Exemple :
// fichier list.cpp (exemple des listes : conteneur list)
#include <iostream.h>
#include <list>
using namespace std;
/* afficher le contenu d'une liste utilisant des itérateurs */
void afficher(list<int> liste, char * message) { if ( liste.size() == 0 )
cout << message << " est vide\n";
else {
cout << "Contenu de " << message << ":\n";
for (list<int>::iterator il = liste.begin();
il != liste.end(); il++) cout << *il << endl;
cout << endl;
// première et dernière valeur de la liste
cout << "Premier element : " << liste.front() << endl;
cout << "Dernier element : " << liste.back() << endl;
} }
void demo1() {
list<int> liste1; // liste VIDE au début cout << "\n\nDemo 1:\n\n";
afficher(liste1, "liste1 au debut");
// ajouter des multiples de 5 entre 1 et 50 au début de la liste for (int k = 1 ; k <= 50 ; k++)
if (k % 5 == 0)
liste1.insert(liste1.end(), k);
afficher(liste1, "liste1 avec des multiples de 5 entre 1 et 50");
list<int>::iterator zz = liste1.end();
zz--;
cout << " ****************** " << * zz << " *********** \n";
// modifier la valeur du 1er élément liste1.front() += 70;
// modifier la valeur du dernier élément liste1.back() *= 2;
// supprimer tous les 25 de la liste liste1.remove(25);
afficher(liste1, "liste1 modifiee");
// trier (sort) la liste liste1.sort();
afficher(liste1, "liste1 apres le tri");
// supprimer le début et la fin de la liste:
liste1.pop_front();
liste1.pop_back();
afficher(liste1, "liste1 apres avoir retire le 1er et le dernier");
// ajouter au début et à la fin de la liste liste1.push_front(30);
liste1.push_front(45);
liste1.push_back (15);
liste1.push_back (50);
liste1.push_back (30);
afficher(liste1, "liste1 apres avoir ajoute quelques val.
au debut et a la fin");
liste1.sort();
afficher(liste1,"liste1 apres encore trie");
// enlever des valeurs répétées liste1.unique();
afficher(liste1, "liste1 apres avoir enleve des val. doubles");
}
// une manière pour créer une liste à partir d'un tableau void creer(list<int> & liste, int tableau[], int nbElem) { for (int i = 0 ; i < nbElem; i++)
liste.insert(liste.end(), tableau[i]);
}
void demo2() {
cout << "\n\nDemo 2:\n\n";
int age1[] = { 23, 15, 20, 18, 22, 40, 18, 32 }, age2[] = { 15, 41, 22, 18, 26 };
list<int> liste1, liste2;
creer(liste1, age1, 8);
creer(liste2, age2, 5);
afficher(liste1, "liste 1 apres la creation");
afficher(liste2, "liste 2 apres la creation");
getchar();
liste1.sort();
liste2.sort();
// fusion de 2 listes liste1.merge(liste2);
afficher(liste1, "liste 1 apres la fusion avec liste 2");
}
void main() {
demo1();
demo2();
}
/* Exécution:
Demo 1:
liste1 au debut est vide
Contenu de liste1 avec des multiples de 5 entre 1 et 50:
5 10 15 20 25 30 35 40 45 50
Premier element : 5
Dernier element : 50
****************** 50 ***********
Contenu de liste1 modifiee:
75 10 15 20 30 35 40 45 100
Premier element : 75 Dernier element : 100
Contenu de liste1 apres le tri:
10 15 20 30 35 40 45 75 100
Premier element : 10 Dernier element : 100
Contenu de liste1 apres avoir retire le 1er et le dernier:
15 20 30 35 40 45 75
Premier element : 15 Dernier element : 75
Contenu de liste1 apres avoir ajoute quelques val. au debut et a la fin:
45 30 15 20 30 3540 45
75 15 50 30
Premier element : 45 Dernier element : 30
Contenu de liste1 apres encore trie:
15 15 20 30 30 30 35 40 45 45 50 75
Premier element : 15 Dernier element : 75
Contenu de liste1 apres avoir enleve des val. doubles:
15 20 30 35 40 45 50 75
Premier element : 15 Dernier element : 75 Demo 2:
Contenu de liste 1 apres la creation:
23 15 20 18 22 40 18 32
Premier element : 23 Dernier element : 32
Contenu de liste 2 apres la creation:
15 41 22 18 26
Premier element : 15 Dernier element : 26
Contenu de liste 1 apres la fusion avec liste 2:
15 15 18 18 18 20 22 22 23 2632 40 41
Premier element : 15 Dernier element : 41
*/
Classe vector :
La classe vector est le conteneur le plus pratique de la STL. Il encapsule efficacement les tableaux dynamiques en gérant la réallocation et les recopies. Un vecteur connaît 2 valeurs:
sa taille: le nombre d'éléments qu'il contient,
sa capacité: le nombre d'éléments qu'il peut contenir.
Il faut inclure :
#include <vector>
using namesapace std ;
Quand l'ajout d'un élément entraîne (taille>capacité) alors le vecteur se réalloue en doublant sa capacité (stratégie classique). Pour accéder directement aux éléments d'un vecteur, en lecture ou en écriture, on utilise la notation [] classique:
Attention, aucun contrôle de dépassement d'indice n'est effectué (sinon la classe serait trop lente et pas assez générique). Si vous tentez d'accéder un élément au delà de la taille du tableau, le programme risque de planter.
Exemple :
// fichier vect.cpp (exemple des vecteurs : conteneur vector)
#include <iostream.h>
#include <vector>
using namespace std;
class Cercle { private:
double rayon;
public:
Cercle(double rayon = 5.0) { this->rayon = rayon;
}
double surface() {
return 3.14159* rayon*rayon;
}
void afficher(char * mess = "") {
cout << mess << "<rayon: " << rayon
<< ", surface: " << surface() << ">\n";
} };
// une manière pour afficher
void afficher(vector<Cercle> vect, char * message) { if ( vect.size() == 0 )
cout << message << " est vide\n";
else {
int rang = 0;
cout << "Contenu de " << message << " de taille "
<< vect.size() << " :\n";
for ( vector<Cercle>::iterator iv = vect.begin();
iv != vect.end(); iv++)
{ cout << ++ rang << ") ";
iv -> afficher();
} cout << endl;
} }
// une autre manière pour afficher
void afficher(char * message, vector<Cercle> vect) { if ( vect.size() == 0 )
cout << message << " est vide\n";
else {
int rang = 0;
cout << "(Acces via []) Contenu de " << message << " de taille " << vect.size() << " :\n";
for ( int i = 0 ; i < vect.size() ; i++) { cout << ++ rang << ") ";
vect[i].afficher();
} cout << endl;
} }
// troisième manière pour afficher void afficher(vector<Cercle> vect) { if ( vect.size() == 0 )
cout << "Vecteur est vide\n";
else {
int rang = 0;
cout << "Vecteur de taille " << vect.size() << " :\n";
for(vector<Cercle>::reverse_iterator iv =vect.rbegin();
iv != vect.rend(); iv++)
{ cout << ++ rang << ")";
iv -> afficher();
} cout << endl;
} }
void main() {
vector<Cercle> vect1;
afficher(vect1, "vect 1 au debut");
vect1.insert(vect1.begin(), Cercle(10.2));
vect1.insert(vect1.end(), Cercle(6.8));
vect1.insert(vect1.end(), Cercle(12.4));
vect1[0].afficher("premier cercle: ");
afficher(vect1, "vect 1 apres quelques ajouts");
afficher("vect 1 (par autre maniere)", vect1);
cout << "\nParcourir en sens inverse:\n";
afficher(vect1);
}
/* Exécution:
vect 1 au debut est vide
premier cercle: <rayon: 10.2, surface: 326.851>
Contenu de vect 1 apres quelques ajouts de taille 3 : 1) <rayon: 10.2, surface: 326.851>
2) <rayon: 6.8, surface: 145.267>
3) <rayon: 12.4, surface: 483.051>
(Acces via []) Contenu de vect 1 (par autre maniere) de taille 3 : 1) <rayon: 10.2, surface: 326.851>
2) <rayon: 6.8, surface: 145.267>
3) <rayon: 12.4, surface: 483.051>
Parcourir en sens inverse:
Vecteur de taille 3 :
1) <rayon: 12.4, surface: 483.051>
2) <rayon: 6.8, surface: 145.267>
3) <rayon: 10.2, surface: 326.851>
*/
Classe set :
La classe set est un conteneur associatif ou les éléments sont triés suivant un certain critère. Ce critère est une sorte de fonction qui compare suivant la valeur des éléments. Par défaut, cette fonction utilise l’opérateur de comparaison <, cependant, une autre fonction de comparaison, se basant sur d’autres critères peut être définie pour réaliser cette opération.
Exemple :
/* fichier set.cpp (exemple des ensembles : conteneur set) But de ce programme :
- créer deux ensembles VIDE d'entiers au début et de les afficher - ajouter des multiples de 5 entre 1 et 50 dans ens1 et de l'afficher - ajouter des multiples de 8 entre 20 et 90 dans ens2 et de l'afficher - afficher des entiers "communs" de ces deux ensembles
- déterminer et retourner l'ensemble qui est l'union de ens1 et ens2 - afficher son contenu
Quelques fonctions membres pour traiter un ensemble : size() : le nombre d'éléments
insert (nouvElem) : ajouter un nouvel élément à l'ensemble
count (elem) : oui (1) ou non (0) l'élément appartient à l'ensemble
*/
#include <iostream.h>
#include <set>
using namespace std;
void afficher(set<int> ensemble, char * message) { if ( ensemble.size() == 0 )
cout << message << " est vide\n";
else {
int rang = 0;
cout << "Contenu de " << message << ":\n";
for ( set<int>::iterator ie = ensemble.begin();
ie != ensemble.end(); ie++)
cout << ++ rang << ") " << *ie << endl;
cout << endl;
}
}
void creer(set<int> & ensemble, int borne1, int borne2, int nombre) { for (int k = borne1; k <= borne2 ; k++)
if ( k % nombre == 0 )
ensemble.insert(k);
}
void afficher(set<int> e1, set<int> e2, char * message) { cout << "\n" << message << ":\n";
int k = 0;
for ( set<int>::iterator ie = e1.begin(); ie != e1.end(); ie++) if ( e2.count(*ie) )
cout << ++k << ") " << *ie << endl;
cout << endl;
}
set<int> reunion(set<int> e1, set<int> e2) { set<int> reuni = e1; // premier ensemble
for ( set<int>::iterator ie = e2.begin(); ie != e2.end(); ie++) if ( !e1.count(*ie) )
reuni.insert(*ie);
return reuni;
}
void main() {
set<int> ens1, ens2 ; // 2 ensembles VIDES au début afficher(ens1, "l'ensemble 1 au debut");
afficher(ens2, "l'ensemble 2 au debut");
creer (ens1, 1, 50, 5);
creer (ens2, 20, 90, 8);
afficher(ens1, "l'ensemble des multiples de 5 entre 1 et 50");
afficher(ens2, "l'ensemble des multiples de 8 entre 20 et 90");
afficher(ens1, ens2, "intersection de ens1 et ens2");
afficher( reunion(ens1, ens2), "la reunion de ens1 et ens2");
}
/* Exécution:
l'ensemble 1 au debut est vide l'ensemble 2 au debut est vide
Contenu de l'ensemble des multiples de 5 entre 1 et 50:
1) 5 2) 10 3) 15 4) 20 5) 25 6) 30 7) 35 8) 40 9) 45
10) 50
Contenu de l'ensemble des multiples de 8 entre 20 et 90:
1) 24 2) 32 3) 40 4) 48 5) 56 6) 64 7) 72 8) 80 9) 88
intersection de ens1 et ens2:
1) 40
Contenu de la reunion de ens1 et ens2:
1) 5 2) 10 3) 15 4) 20 5) 24 6) 25 7) 30 8) 32 9) 35 10) 40 11) 45 12) 48 13) 50 14) 56 15) 64 16) 72 17) 80 18) 88
*/