Explorer les Livres électroniques
Catégories
Explorer les Livres audio
Catégories
Explorer les Magazines
Catégories
Explorer les Documents
Catégories
Objectifs pédagogiques :
• mémoire, pointeur, classe
• syntaxe générale du c++
• constructeur, destructeur
• opérateurs
Introduction
Dans ce premier TD, nous allons réaliser en C++ une classe String appuyée par une représen-
tation interne à l'aide d'une chaîne de caractères du C : un pointeur char * vers un espace mé-
moire zéro terminé. Cet exercice est à vocation pédagogique, on préférera en pratique utiliser la
std::string standard du header <string> et les fonctions de manipulation de chaînes du C rangées
dans <cstring>.
Pour être sûr de ne pas entrer en conit avec d'autres applications, nous utiliserons le namespace
pr pour notre implémentation.
Les chaînes de caractères du C, sont représentées par un pointeur de caractère qui cible une zone
mémoire contenant la chaîne, et se terminant par un ’\0’.
On souhaite implanter nos propres petites fonctions utilitaires travaillant avec ces chaînes. Cet
exercice est à vocation pédagogique, en pratique on préférera utiliser les fonctions standard du C
(strcat,strcmp,strcpy...) qui se trouvent dans le header standard <cstring> en C++.
On considère :
Question 1. Quel sont donc les signatures de ces fonction ? Où et comment les déclarer, sachant
qu'on veut en faire des fonctions utilitaires rangées dans le namespace "pr".
chier strutil.h
strutil.h
#ifndef SRC_STRUTIL_H_ 1
#define SRC_STRUTIL_H_ 2
3
#include <cstring> 4
5
namespace pr { 6
7
size_t length (const char *str); 8
9
char * newcopy (const char *str); 10
11
} 12
13
14
#endif /* SRC_STRUTIL_H_ */ 15
1
1.1 Rappels chaîne du C, const. TD 1
Question 2. Donnez deux implantations de ces fonctions. Pour la fonction length, comparer
une version utilisant la notation str[i] à une version en pointeurs pur. Pour l'allocation, comparer
l'utilisation de l'opérateur new du C++ à malloc. Pour la copie comparer une invocation à memcpy
à une boucle. Où placer ce code d'implantation ?
chier strutil.cpp
strutil.cpp
#include "strutil.h" 1
2
#include <cstdlib> 3
4
namespace pr { 5
6
7
// version index 8
size_t length2 (const char *str) { 9
size_t ret = 0; 10
for (int i=0 ; str[i] ; ++i) { 11
++ret; 12
} 13
return ret; 14
} 15
16
// version pointeurs 17
size_t length3 (const char *str) { 18
size_t ret = 0; 19
for ( ; *str ; ++str) { 20
++ret; 21
} 22
return ret; 23
} 24
25
// version pointeurs + arithmétique (sans compteur) 26
size_t length (const char *str) { 27
const char *cp=str; 28
for ( ; *cp ; ++cp) { 29
} 30
return cp-str; 31
} 32
33
34
// version index 35
char * newcopy2 (const char *src) { 36
size_t n = length(src); 37
char * dest = new char[n+1]; 38
39
2
1.1 Rappels chaîne du C, const. TD 1
3
1.1 Rappels chaîne du C, const. TD 1
(Les ++i au lieu de i++, c'est plus du style qu'autre chose, mais l'opérateur post xé en général induit
un temporaire, pas le préxé. Bonne habitude en C++, pour la manipulation e.g. des itérateurs ça
peut faire une diérence. Ce n'est pas important.)
Les tests de n de boucle etc. sur ∗cp ou cp est un char∗, s'expandent en ∗cp! =0 \00 , mais la plupart
des programmeurs C préfèrent la syntaxe plus compacte. Toute valeur diérente de 0 est vrai dans les
tests.
Le new[] du C++ est mieux typé que malloc : le type et la cardinalité sont donnés. Ici sizeof (char)
vaut 1, on le sait, mais c'est la syntaxe générale d'un malloc qui est utilisée.
memcpy est plus ecace que la boucle, dès qu'on a un peu de distance à copier. La copie est faite par
blocs et gérée par l'OS, c'est ce qu'on peut faire de mieux pour copier le contenu de la mémoire d'un
endroit à un autre. C'est une fonction essentielle, son API pointeur+ size_t en bytes pour la taille est
caractéristique du C.
La dernière version du code à chaque fois est sans doute la plus proche de strlen/strcpy. Les vraies
versions intègrent du code pour faire les tests mot par mot (e.g. 4 bytes) au lieu de char par char.
Question 3. Ecrivez un programme dans un chier exo1.cpp qui construit une copie d'une chaîne
"Hello World", puis ache les deux chaines, leurs addresses, leur longueurs, et sort proprement
(pas de fuites mémoire).
chier exo1.cpp
exo1.cpp
/* 1
* exo1.cpp 2
* 3
* Created on: Sep 11, 2018 4
* Author: ythierry 5
*/ 6
7
8
#include "strutil.h" 9
10
#include <iostream> 11
12
using namespace pr; 13
using namespace std; 14
15
16
int main12 () { 17
const char * str = "Hello"; 18
char * copy = newcopy(str); 19
20
cout << str << length(str) << endl; 21
cout << copy << length(copy) << endl; 22
23
// A ajouter dans un deuxieme temps 24
delete [] copy; 25
26
return 0; 27
} 28
Donc les using permettent d'alléger la syntaxe, sinon il faut pr::length et std::cout, std::endl.
On fait return 0 si pas d'erreur, les codes de retour diérents de 0 indiquent un problème (convention
Unix).
4
1.2 Une classe String TD 1
Les litéraux comme Hello sont stockés directement dans le segment de code, ils sont non modiables
(const char *), et ont une durée de vie égale à celle du programme (sauf s'il vient d'une lib dynammique
chargée par dlopen, et qu'on la dlclose intempestivement.)
Sans le delete[] à la n, on a une fuite mémoire, le new[]/malloc de newcopy n'a pas de delete[]/new
symétrique.
Question 4. Expliquez comment compiler séparément ces chiers puis les assembler (linker) faire
un programme exécutable, et l'exécuter.
Execution
./td1
Valgrind
valgrind --leak-check=full --track-origins=yes ./td1
Donc à la compil, on produit un .o (chier binaire compilé) par source .cpp, avec les ags
• -O0 : niveau d'optimisation, -O0 pour pouvoir debugger, -O2 ou -O3 en production
• -Wall : active tous les warnings de base, il y a d'autres warnings cela dit (cf -Wextra. . . )
En pratique on rajoute des ags pour que le compilo garde la trace des chiers (include) dont dépendent
le source compilé, il faut reconstruire le .o si n'importe lequel de ces chiers inclus transitivement
changent.
Pas de dialecte à passer, ni de niveau d'optimisation. Cependant sur compilo récent des options comme
-fwhole-program font un link optimisé.
• accès hors d'une zone allouée (par dépassement, ou par accès à un pointeur non initialisé, ou
par deref de nullptr),
5
1.2 Une classe String TD 1
• double désallocation.
La structure de classe permet d'éviter un certain nombre de ces erreurs, en encapsulant ces com-
portements, de manière à assurer par exemple que les allocations et désallocations sont cohérentes.
Le reste de cet exercice consiste à écrire une classe String qui se comporte bien.
Question 5. Dans un chier String.h, dénir une classe String minimale munie d'un atribut
logeant un pointeur vers une chaîne du C et d'un constructeur permettant de positionner cet attribut.
Ajoutez une opération membre length" qui calcule la longueur de la chaîne. Donnez également le
code de l'implantation de la classe, qu'on placera dans String.cpp.
Donc on avance ligne par ligne dans le programme, et on ajoute ce qu'il faut à la classe String.
#ifndef STRING_H_ 1
#define STRING_H_ 2
3
#include <cstddef> // pour size_t 4
#include <iosfwd> // pour ostream 5
6
// pas de "using" dans les .h 7
namespace pr { 8
9
class String { 10
// private tant qu’on a rien dit 11
const char * str; 12
public: 13
// tout ce qui suit est public 14
// un ctor 15
String (const char * ori); 16
// taille, membre public et const, i.e. ne modifie pas *this 17
size_t length() const; 18
19
}; // attention au point virgule pour terminer la déclaration de classe ! 20
21
22
} // fin namespace pr 23
24
#endif /* STRING_H_ */ 25
On note :
• visibilité : private tant qu'on n'a rien dit (accès aux membres de la classe seulement), public = tout
le monde voit, protected = membres et membres des classes dérivées (mais du namespace/package
comme en Java)
• la classe n'a pas de notion de visibilité comme en Java, la notion de visibilité ne s'applique qu'aux
membres des classes C++. Le reste se fait avec des namespace.
• l'attribut est déclaré const char *, et sera donc non modiable; on pourra changer de représenta-
tion interne (str = autrechose OK) mais pas modier str (str[0] =0 \0 NOK). C'est un choix qui
est fait ici, qui nous rapproche des String de Java, immuables. La std::string est au contraire
modiable.
• l'invocation fournie String str = “Hello”; indique l'existence d'un constructeur prenant un
const char * en argument.
6
1.2 Une classe String TD 1
Et pour l'implantation :
#include "String.h" 1
#include "strutil.h" // pour length et newcopy 2
3
#include <iostream> 4
5
namespace pr { 6
7
String::String (const char * ori) : str(ori) { 8
} 9
10
size_t String::length () const { 11
return pr::length(str); 12
} 13
14
} // fin namespace pr 15
16
#endif /* STRING_H_ */ 17
On note :
• les opérations de String sont rangées dans son namespace, i.e. toute classe dénit implicitement
un namespace.
• l'opérateur :: permet de qualier les noms, sans plus de précision on cherche dans le namespace
courant
• using std; importe les noms dans le namespace courant; au lieu de std::cout on peut écrire
juste cout.
• sur le constructeur, on a une liste d'initialisation (entre le ':' et l'accolade ouvrante du ctor).
Les attributs n'ont pas de valeur par défaut en C++ (non initialisé par défaut). Cette liste
d'initialisation peut comporter plusieurs initialisations (séparés par des virgules), on y place
également les appels aux constructeurs des classes parentes d'une classe dérivée. Enn (stan-
dard c++11), on peut déléguer à un autre constructeur à cet endroit.
• sur length, pas de grosse surprise, on délégue sur la fonction de exo 1. Il faut include stru-
til.h. Mais si on fait juste return length(str), on a un StackOVerow (on va par défaut trouver
String::length) ! En qualiant avec pr:: on tombe sur la bonne fonction de exo1.
Question 6. Ajoutez les mécanismes permettant d'imprimer une String à la manière C++ stan-
dard.
ostream & operator << (ostream & os, const MyClass & o)
ostream & operator << (ostream & os, const String & s)
L'opérateur est binaire et prend deux arguments, ce qui est à sa gauche, et ce qui est à sa droite. A
gauche donc, on s'attends à trouver un ostream &, c'est à dire un ux de sortie, sur lequel on peut
écrire. Le ux en question pourrait être une sortie standard, std::cout ou std::cerr (soit les stdout/stderr
du C++). Mais ça pourrait également être un ux sur un chier de sortie (voir <fstream>), ou un ux
dans une zone mémoire (stringstream, déclaré dans <sstream>).
7
1.2 Une classe String TD 1
Les deux arguments sont passés par référence ; le ux va être modié (on va pousser des choses dedans),
donc il n'est pas const. Au contraire l'achage ne doit pas modier la String, on peut donc utiliser une
const ref.
Par convention, l'opération rend le ux sur lequel on travaille, de manière à pouvoir chaîner les appels :
std::cout << str << " et " << str.length() << std::endl;
Donc on résoud
etc. . .
Niveau déclaration, comme l'opérande qui est à gauche est un stream standard, on ne peut pas l'enrichir
(i.e. dénir un operator membre de ostream). On déclare donc l'opérateur de façon externe, dans le
même namespace que String, mais à côté de la classe.
std::ostream & operator << (std::ostream & os, const pr::String & s) { 1
return os << s.str ; 2
} 3
Ce code utilise le fait que les char * du C ont déjà un cas particulier dans le standard, plus précisément
ajouter un char * ou un const char * dans un ux l'interprète comme un string du C. Si on veut
acher vraiment l'addresse (et pas le contenu de la string) on peut faire :
Notons aussi que le standard dénit déjà operator pour tous les types simples et pas mal des types de
la lib standard (e.g. std::string).
Le corps de l'opération accède directement à l'attribut str de la String, qui est private dans le contexte
de l'opérateur. Pour résoudre ce problème on ajoute la déclaration friend, dans la déclaration de la
classe. Toute fonction déclarée friend peut accéder aux champs privés de la classe.
C'est à la classe de déclarer ses amis (elle rompt ainsi sélectivement son encapsulation), on ne peut pas
se déclarer ami sans modier la classe dont on veut être l'ami.
Question 7. Le code actuellement proposé ne copie pas la chaîne passée en argument. Rappelez
en quoi consiste le comportement par défaut qui est généré par le compilateur pour les opérations :
// dtor 1
virtual ~String(); 2
// par copie de ori 3
String(const String &ori); 4
// mise à jour du contenu 5
String & operator=(const String & other); 6
String getAlphabet() { 1
char [27] tab; 2
for (int i=0; i < 26 ; ++i) { 3
8
1.2 Une classe String TD 1
tab[i] = ’a’+i; 4
} 5
tab[26]=’\0’; 6
return String(tab); 7
} 8
int main() { 9
String abc = getAlphabet(); 10
std::cout << abc << std::endl; 11
} 12
Comportement par défaut du dtor = vide (mais invoque implicitement le super destructeur si héritage).
Comportement par défaut du constructeur par copie : aecter chacun des champs/attributs par copie.
Comportement par défaut de opérateur = : aecter par = tous les attributs de la classe.
Problèmes : la String ne maîtrise pas sa mémoire, elle n'encapsule pas correctement son comportement.
Exemple de problème sur l'exemple, le tableau est stack alloc dans le frame de getAlphabet, donc se
fait nettoyer quand on pop le contexte. Notre String se retrouve à pointer qq part (d'illégal) sur la pile,
ce qui provoque une faute mémoire quand va essayer de la coller dans cout (car on va lire la mémoire
pointée, et même brancher sur son contenu en cherchant des '0').
Donc la faute, c'est qu'on a f ree la mémoire que je croyais pouvoir adresser. Autres fautes possibles,
on me modie le contenu pointé (rupture d'encapsulation).
Question 8. Modiez la classe String, pour qu'au contraire, elle soit responsable de la mémoire
qu'elle pointe. Si l'on se contentait de ne modier que le destructeur et le constructeur, expliquez
les problèmes que cela pose sur cet exemple.
int main() { 1
String abc = "abc"; 2
{ 3
String bcd(abc); 4
} 5
6
std::cout << abc << std::endl; 7
8
String def = "def"; 9
def = abc; 10
11
std::cout << abc << " et " << def << std::endl; 12
} 13
Pour qu'elle soit responsable de sa mémoire, on va copier à la construction le char * qui m'est passé
dans une zone nouvellement allouée. Et donc libérer cette mémoire quand on la détruit.
On ajoute donc :
String::~String() { 1
delete [] str; 2
} 3
String::String(const char *cstr) { 4
str = newcopy(cstr); 5
} 6
9
1.2 Une classe String TD 1
Si on en reste là et qu'on a les versions par défaut du reste, on a des tas de problèmes. (NB : le compilo
ne générera PAS de comportement par défaut si on fournit une des briques : ctor par copie, operator=,
dtor. . . ). Donc ici les problèmes ce serait des problèmes de compilation heureusement.
int main() { 1
String abc = "abc";// copie donc dans un bloc nouveau, on a fait le ctor 2
{ 3
String bcd(abc); // ctor par copie par défaut = aliasing ! on pointe la même zone 4
} // fin de scope de bcd => dtor de bcd invoqué 5
6
std::cout << abc << std::endl; // faute mémoire, la zone est déjà free 7
8
String def = "def"; // copie donc, on a fait le ctor 9
def = abc; // fuite mémoire, on perd l’addresse stockée dans def 10
11
std::cout << def << std::endl; // faute mémoire, la zone est déjà free 12
} // faute mémoire double free sur dtor de abc, triple free avec le dtor de def 13
On corrige avec la classe complètée ainsi (il y a des choses non demandées, corriger les ctor/dtor/oper-
ator= etc. . . )
String.h
#ifndef STRING_H_ 1
#define STRING_H_ 2
3
#include <cstddef> // pour size_t 4
#include <iosfwd> // pour ostream 5
6
// pas de "using" dans les .h 7
8
namespace pr { 9
10
class String { 11
const char * str; 12
13
// déclaration d’accès friend 14
friend std::ostream & operator << (std::ostream &os, const pr::String & s); 15
16
public: 17
// ctor (par copie en fin de TD) de cstr 18
// + déclaration d’une conversion : const char * -> const String & 19
String(const char *cstr=""); 20
// dtor 21
virtual ~String(); 22
// par copie de ori 23
String(const String &ori); 24
// mise à jour du contenu 25
String & operator=(const String & other); 26
27
// taille 28
size_t length() const; 29
}; 30
31
// encore dans namespace pr. 32
33
// déclaration d’une sérialisation par défaut (toString du c++) 34
std::ostream & operator << (std::ostream &os, const pr::String & s); 35
10
1.2 Une classe String TD 1
36
} // fin namespace pr 37
38
#endif /* STRING_H_ */ 39
String.cpp
#include "String.h" 1
#include "strutil.h" // pour length et newcopy 2
3
#include <iostream> 4
5
namespace pr { 6
7
// par délégation sur le ctor qui prend un const char * 8
String::String (const String & ori) : String(ori.str) { 9
} 10
11
String::String(const char *cstr) { 12
// if (début du TD) 13
str = cstr; 14
15
// else (fin du TD) 16
str = newcopy(cstr); 17
} 18
19
String::~String() { 20
delete [] str; 21
} 22
23
String & String::operator=(const String & other) { 24
if (this != &other) { 25
delete[] str; 26
str = newcopy(other.str); 27
} 28
return *this; 29
} 30
31
size_t String::length () const { 32
return pr::length(str); 33
} 34
35
std::ostream & operator << (std::ostream & os, const pr::String & s) { 36
return os << s.str ; 37
} 38
39
} // namespace pr 40
11
TME 1 : Programmation, compilation et exécution en C++
Objectifs pédagogiques :
• mise en place
• classe simple
• opérateurs
• compilation, debugger, valgrind
Cette UE de programmation avancée suppose une familiarité avec le C et un langage O-O comme
Java (syntaxe de base, sémantique), les premières séances sont l'occasion de se remettre à niveau.
Cet énoncé contient donc plusieurs encadrés, en gris, qui rappellent les notions clés à appliquer.
Comme le niveau est assez hétérogène, prenez le temps de bien absorber les concepts si nécessaire,
nous pouvons prendre un peu de retard. Les TME proposent souvent des extensions dont les
réalisations sont optionelles (bonus) mais qui permettent d'aller un peu plus loin.
Vous soumettrez l'état de votre TME à la n de chaque séance, en poussant votre code sur un git.
La procédure que l'on va mettre en place ce semestre n'étant pas encore opérationnelle pour cette
séance, créez vous un git ou contentez vous de zipper le dossier "src" pour cette séance.
Attention, plusieurs versions d'Eclipse cohabitent à la PPTI. Il faut utiliser une version pour
développement CDT, qu'on a déployé dans /usr/local/eclipseCPP/. La manière la plus sûre
est d'ouvrir un terminal et d'y entrer la commande : /usr/local/eclipseCPP/eclipse.
Si c'est la première fois que vous utilisez Eclipse, celui-ci vous demande le nom du répertoire
workspace où il placera par défaut vos projets. On recommande d'utiliser un nouveau dossier
~/workspacePR comme dossier de workspace qui va loger les projets.
Question 2. Contruire un nouveau projet C++
La console CDT (un des onglets dans la partie basse de l'IDE) montre les instructions de compi-
lation qui ont été faites. On y voit une étape de compilation et une étape de link.
Eclipse a conguré une version compilée en mode Debug par défaut. Il place les makele qu'il a
engendré et les chiers produits par la compilation dans un dossier Debug. A priori, on n'édite pas
directement ces chiers, mais plutôt les Properties du projet (clic droit sur le projet, dernier item).
Il propose aussi une version Release, compilée avec des ags plus optimisés, utiliser la èche/tri-
angle à côté du marteau (build) dans le ruban d'outils en haut de l'IDE pour basculer sur cette
12
1.2 Prise en main TME 1
conguration. Si vous ne voyez pas cet outil, assurez vous d'avoir basculé en Persepctive C/C++
(avec le bouton dans le coin en haut à droite de eclipse). On va rester en conguration Debug pour
l'instant.
Un nouvel élément Binaries est visible, il contient les binaires qu'on vient de construire. On peut
clic-droit sur un binaire et faire Run As. . . ->Local C/C++ application. Cela lance le programme,
dans la console d'eclipse.
Question 4. Ajoutez dans le main un un tableau tab" de dix entiers que vous remplirez avec les
entiers 0 à 9. Achez le contenu du tableau.
Certaines fautes ne sont pas soulignées au cours de la frappe, mais seulement si on lance le build com-
plet. Par exemple cette erreur d'utilisation d'une variable non initialisée ne sera indiquée qu'après
une compilation.
char * a;
cout << a ;
Si probleme s'ache, on sait qu'on a un souci. Double-cliquez dans la marge de l'éditeur, sur la
ligne qui contient l'achage de probleme, l'outil crée un Breakpoint pour le debug.
Cliquez sur le binaire, mais cette fois-ci faites Debug As..->Local C++ Application au lieu de
Run As. Accepter de basculer en perspective Debug.
La ligne verte indique la position actuelle dans le code, on la fait évoluer en utilisant les outils dans
le ruban du haut.
• Continue : poursuit l'exécution jusqu'au prochain breakpoint (qu'on place en double clic dans
la marge)
Avec le breakpoint positionné sur notre message, avancer jusqu'à l'atteindre, puis inspecter les
valeurs des variables (à droite). Pour l'achage des pointeurs sous forme de tableau, faire un clic
droit sur la variable et demander le mode tableau.
Quel est le contenu des cellules du tableau ? Combien vaut i ? Modier la déclaration du type de
i, pour que la boucle aie eectivemet lieu 10 fois.
A présent, sans avoir complétement debuggé le problème, lancer une analyse avec valgrind. Sélec-
tionner le binaire, et faire Proling Tools->Prole with Valgrind. Il doit vous aider à détecter vos
erreurs (fautes mémoires et fuites mémoire). Les résultats sont visible dans l'onglet Valgrind.
13
1.3 Réalisation d'une classe String TME 1
• Pour l'éditeur/correction à la volée des erreurs, on règle une préférence globale, valable pour tous
les projets d'un workspace donné. Naviguer vers
• Pour le compilateur à proprement parler, on doit faire un réglage pour chaque projet séparément.
En partant d'un clic droit sur le projet, accéder à ses propriétés:
Attention, il faut faire le premier réglage dans tout nouveau workspace, et le deuxième pour chaque
nouveau projet.
Au fur et à mesure de la réalisation de la classe, construire un main qui teste les éléments réalisés.
S'assurer qu'il ne provoque aucune faute mémoire sous valgrind.
String.h
#ifndef STRING_H_ 1
#define STRING_H_ 2
3
#include <cstddef> // pour size_t 4
#include <iosfwd> // pour ostream 5
6
// pas de "using" dans les .h 7
8
namespace pr { 9
10
class String { 11
const char * str; 12
14
1.3 Réalisation d'une classe String TME 1
13
// déclaration d’accès friend 14
friend String operator+ (const String &s1, const String & s2); 15
friend bool operator== (const String &s1, const String & s2); 16
friend std::ostream & operator << (std::ostream &os, const pr::String & s); 17
18
public: 19
// ctor par copie de cstr 20
// + déclaration d’une conversion : const char * -> const String & 21
// + ctor par défaut à chaîne vide 22
String(const char *cstr=""); 23
// dtor 24
virtual ~String(); 25
// par copie de ori 26
String(const String &ori); 27
// mise à jour du contenu 28
String & operator=(const String & other); 29
30
// accès (non modifiable !) 31
char operator[] (size_t index) const; 32
// accès en modification (illégal, on stocke un const char *) 33
// permettrait de faire : str[0]=’a’; 34
// char & operator[] (size_t index); 35
36
// taille 37
size_t length() const; 38
// égalité logique 39
bool operator<(const String & o) const; 40
}; 41
42
// encore dans namespace pr. 43
44
// déclaration d’un opérateur + symétrique (non membre) 45
String operator+ (const String &s1, const String & s2); 46
// déclaration d’une sérialisation par défaut (toString du c++) 47
std::ostream & operator << (std::ostream &os, const pr::String & s); 48
// comparaison d’égalité 49
bool operator== (const String &s1, const String & s2); 50
51
} // fin namespace pr 52
53
#endif /* STRING_H_ */ 54
String.cpp
#include "String.h" 1
#include "strutil.h" // pour length et newcopy 2
3
#include <iostream> 4
5
namespace pr { 6
7
8
String::~String() { 9
delete [] str; 10
} 11
12
String::String (const String & ori) : String(ori.str) { 13
} 14
15
1.3 Réalisation d'une classe String TME 1
15
String & String::operator=(const String & other) { 16
if (this != &other) { 17
delete[] str; 18
str = newcopy(other.str); 19
} 20
return *this; 21
} 22
23
String::String(const char *cstr) { 24
str = newcopy(cstr); 25
} 26
27
size_t String::length () const { 28
return pr::length(str); 29
} 30
31
String operator+ (const String &s1, const String & s2) { 32
size_t sz1 = s1.length(); 33
size_t sz2 = s2.length(); 34
// + 1 pour le ’\0’ 35
char res [sz1+ sz2 +1]; 36
37
if (true) { 38
// version memcpy 39
memcpy(res,s1.str,sz1); 40
// + 1 pour le ’\0’ 41
memcpy(res+sz1,s2.str,sz2+1); 42
} else { 43
// version à la main 44
char * pr = res; 45
for (const char * p1=s1.str ; *p1 ; ++p1,++pr) { 46
*pr = *p1; 47
} 48
for (const char * p2=s2.str ; *p2 ; ++p2,++pr) { 49
*pr = *p2; 50
} 51
*pr = ’\0’; 52
} 53
54
return String (res); 55
} 56
57
bool operator==(const String & a,const String &b) { 58
return ! compare(a.str,b.str); 59
} 60
61
bool String::operator<(const String & o) const { 62
return compare(str,o.str) < 0; 63
} 64
65
char String::operator[] (size_t index) const { 66
return str[index]; 67
} 68
69
// Version modifiable, illégale si on stocke un const char * 70
//char & String::operator[] (size_t index) { 71
// // Cette ligne induit un cast qui perd le const du char pointé 72
// return str[index]; 73
16
1.3 Réalisation d'une classe String TME 1
//} 74
75
std::ostream & operator << (std::ostream & os, const pr::String & s) { 76
return os << s.str ; 77
} 78
79
} // namespace pr 80
Question 10. Ajouter une fonction utilitaire compare aux fonctions utilitaires rangées dans stru-
til.h. Elle doit se comporter comme la fonction strcmp standard, elle prend deux chaînes a et b du
C en argument, et elle rend une valeur négative si a < b, 0 si les deux chaînes sont logiquement
égales, et une valeur positive sinon.
Plus précisément, on itére sur les deux chaînes simultanément (avec deux pointeurs) tant que les
valeurs pointées sont égales et que la première est diérente de '\0'. On compare ensuite les carac-
tères pointés (on peut faire simplement la diérence de leurs valeurs).
strutil.h
#ifndef SRC_STRUTIL_H_ 1
#define SRC_STRUTIL_H_ 2
3
#include <cstring> 4
5
namespace pr { 6
7
size_t length (const char *str); 8
9
char * newcopy (const char *str); 10
11
int compare (const char *a, const char * b); 12
13
} 14
15
16
#endif /* SRC_STRUTIL_H_ */ 17
strutil.cpp
#include "strutil.h" 1
2
#include <cstdlib> 3
4
namespace pr { 5
6
7
// version pointeurs + arithmétique (sans compteur) 8
size_t length (const char *str) { 9
const char *cp=str; 10
for ( ; *cp ; ++cp) { 11
} 12
return cp-str; 13
} 14
15
16
17
1.4 Utilisation de shared_ptr TME 1
// version memcpy 17
// *toujours* la version à recommander en production 18
// habite dans #include <cstring> 19
char * newcopy (const char *src) { 20
size_t n = length(src); 21
char * dest = new char[n+1]; 22
23
// dest, source, nombre de bytes = n+1 avec le \0 24
memcpy(dest,src,n+1); 25
26
return dest; 27
} 28
29
30
int compare (const char *cp, const char * cq) { 31
for ( ; *cp == *cq && *cp; ++cp,++cq) { 32
} 33
return *cp - *cq; 34
} 35
36
} 37
Question 11. Pour la comparaison entre deux String, on peut proposer soit des opérateurs mem-
bres, soit des fonctions extérieures à la classe (plus symétriques).
• Un opérateur fournisssant une relation d'ordre bool operator<(const String & b) const mem-
bre de la classe String.
Question 12. Pour l'accès aux caractères de la String on peut réaliser un opérateur [].
Dénissez char operator[](size_t s)const. Peut-on modier la String par ce biais ? Quelle serait
la signature d'une variante permettant de modier le contenu de la String ? Essayez de la dénir,
quel problème de typage (const-ness) se pose ?
Question 13. Pour la construction de String plus longues, on peut proposer un String operator+(const
String & a, const String &b) symétrique (non membre). L'algorithme consiste à calculer la longueur
totale du résultat, allouer (sur le stack) un tableau de cette taille, et construire une String avec ce
résultat.
Question 14. Lancer votre programme de test sous valgrind. Si vous n'avez pas d'erreurs, intro-
duisez en délibérément, puis détectez là avec Valgrind, interprétez le message qu'il produit.
• commentez une copie, i.e. au lieu de faire la copie, contentez de l'aecter à l'attribut de la
classe.
Cette partie du TME est considérée comme du "bonus" pour ceux qui sont le plus à l'aise ou qui
sont déjà familiers avec le C++.
Avec les String que l'on a déni, deux String ne peuvent pas partager leur espace de stockage,
même si elles ont le même contenu. Comme le contenu de la String est stocké de façon const,
partager la représentation des String (en lecture seule donc) ne devrait pas poser de problème. Mais
la gestion mémoire est plus complexe, à quel moment libérer une chaîne pointée ? Une solution
18
1.4 Utilisation de shared_ptr TME 1
possible consiste à utiliser un compteur de références, où la chaîne est désallouée quand elle n'est
plus référencée, mais n'est pas copiée e.g. quand on aecte une String à une autre.
StringPtr.h
#ifndef StringPtr_H_ 1
#define StringPtr_H_ 2
3
#include <cstddef> // pour size_t 4
#include <iosfwd> // pour ostream 5
#include <memory> 6
7
// pas de "using" dans les .h 8
9
namespace pr { 10
11
class StringPtr { 12
std::shared_ptr<const char> str; 13
14
// déclaration d’accès friend 15
friend StringPtr operator+ (const StringPtr &s1, const StringPtr & s2); 16
friend bool operator== (const StringPtr &s1, const StringPtr & s2); 17
friend std::ostream & operator << (std::ostream &os, const pr::StringPtr & s); 18
19
public: 20
// ctor par copie de cstr 21
// + déclaration d’une conversion : const char * -> const StringPtr & 22
// + ctor par défaut à chaîne vide 23
StringPtr(const char *cstr=""); 24
25
// ELIMINES : shared_ptr est copy et assignable constructible, et gère sa mémoire. 26
// dtor 27
// virtual ~StringPtr(); 28
// par copie de ori 29
// StringPtr(const StringPtr &ori); 30
// mise à jour du contenu 31
// StringPtr & operator=(const StringPtr & other); 32
33
// accès (non modifiable !) 34
char operator[] (size_t index) const; 35
// accès en modification (illégal, on stocke un const char *) 36
// permettrait de faire : str[0]=’a’; 37
// char & operator[] (size_t index); 38
39
// taille 40
size_t length() const; 41
// égalité logique 42
bool operator<(const StringPtr & o) const; 43
}; 44
45
19
1.4 Utilisation de shared_ptr TME 1
StringPtr.cpp
#include "StringPtr.h" 1
#include "strutil.h" // pour length et newcopy 2
3
#include <iostream> 4
5
namespace pr { 6
7
8
StringPtr::StringPtr(const char *cstr) : str(newcopy(cstr),[](char*p){ delete[] p;}) { 9
} 10
11
size_t StringPtr::length () const { 12
return pr::length(str.get()); 13
} 14
15
StringPtr operator+ (const StringPtr &s1, const StringPtr & s2) { 16
size_t sz1 = s1.length(); 17
size_t sz2 = s2.length(); 18
// + 1 pour le ’\0’ 19
char res [sz1+ sz2 +1]; 20
21
if (true) { 22
// version memcpy 23
memcpy(res,s1.str.get(),sz1); 24
// + 1 pour le ’\0’ 25
memcpy(res+sz1,s2.str.get(),sz2+1); 26
} else { 27
// version à la main 28
char * pr = res; 29
for (const char * p1=s1.str.get() ; *p1 ; ++p1,++pr) { 30
*pr = *p1; 31
} 32
for (const char * p2=s2.str.get() ; *p2 ; ++p2,++pr) { 33
*pr = *p2; 34
} 35
*pr = ’\0’; 36
} 37
38
return StringPtr (res); 39
} 40
41
bool operator==(const StringPtr & a,const StringPtr &b) { 42
if (a.str.get() == b.str.get()) { 43
return true; 44
20
1.4 Utilisation de shared_ptr TME 1
} 45
return ! compare(a.str.get(),b.str.get()); 46
} 47
48
bool StringPtr::operator<(const StringPtr & o) const { 49
if (str == o.str) { 50
return false; 51
} 52
return compare(str.get(),o.str.get()) < 0; 53
} 54
55
char StringPtr::operator[] (size_t index) const { 56
return str.get()[index]; 57
} 58
59
// Version modifiable, illégale si on stocke un const char * 60
//char & StringPtr::operator[] (size_t index) { 61
// // Cette ligne induit un cast qui perd le const du char pointé 62
// return str[index]; 63
//} 64
65
std::ostream & operator << (std::ostream & os, const pr::StringPtr & s) { 66
return os << s.str ; 67
} 68
69
} // namespace pr 70
Le problème eectivement c'est que "shared_ptr" libère à l'aide de "delete" alors qu'on alloue avec
new[] les char de la chaîne.
Le standard C++17 résoudra ce problème de shared_ptr, en attendant (sur nos machines) utiliser le con-
structeur à deux arguments deshared_ptr pour lui passer une fonction à invoquer pour la désallocation.
On pourra lui passer la lambda suivante : [](char *p) { delete[] p; }.
21
TD 2 : Conteneurs, Itérateurs
Objectifs pédagogiques :
• introduction à la lib standard
• gestion mémoire, allocations
• itérateurs du C++, range
• vecteur<T>, liste<T>, map<K,V>
Introduction
Dans ce TD, nous allons réaliser en C++ des classes Vector<T>,List<T>,Map<K,V> orant le con-
fort habituel de ces conteneurs classiques. Cet exercice est à vocation pédagogique, on préfér-
era en pratique utiliser les classes standard, qu'on trouvera dans les header <vector>, <list>,
<unordered_map>.
L'objectif de l'exercice est de bien comprendre les conteneurs standards du c++ et leur API.
Pour être sûr de ne pas entrer en conit avec d'autres applications, nous utiliserons le namespace
pr pour notre implémentation.
Un vecteur stocke de façon contigüe en mémoire des données. C'est une des structures de données
les plus simples, mais pour cette raison ça reste un bon choix pour de nombreuses applications.
• Le vecteur est muni d'un pointeur vers l'espace mémoire alloué pour stocker les données
• Le vecteur est muni d'une taille size, qui indique le remplissage actuel
• Le vecteur a une taille d'allocation, toujours supérieure ou égale à size
• Les objets de type T ajoutés sont copiés dans l'espace mémoire géré par le vecteur
Vector.h
#ifndef SRC_VECTOR_H_ 1
#define SRC_VECTOR_H_ 2
3
#include <cstddef> 4
#include <ostream> 5
6
namespace pr { 7
8
template <typename T> 9
class Vector { 10
size_t alloc_sz; 11
22
1.1 Vecteur : stockage contigü. TD 2
T * tab; 12
size_t sz; 13
14
void ensure_capacity(size_t n) { 15
if (n >= alloc_sz) { 16
// double alloc size 17
alloc_sz *= 2; 18
T * newtab = new T[alloc_sz]; 19
for (size_t ind=0; ind < sz ; ind++) { 20
newtab[ind] = tab[ind]; 21
} 22
delete[] tab; 23
tab = newtab; 24
} 25
} 26
27
public: 28
Vector(int size=10): alloc_sz(size),sz(0) { 29
tab = new T [alloc_sz]; 30
} 31
virtual ~Vector() { 32
delete [] tab; 33
} 34
35
const T & operator[] (size_t index) const { 36
return tab[index]; 37
} 38
T& operator[] (size_t index) { 39
return tab[index]; 40
} 41
42
void push_back (const T& val) { 43
ensure_capacity(sz+1); 44
tab[sz++]=val; 45
} 46
47
size_t size() const { return sz ; } 48
49
typedef const T* const_iterator; 50
typedef T* iterator; 51
52
const_iterator begin() const { 53
return tab; 54
} 55
const_iterator end() const { 56
return tab+sz; 57
} 58
iterator begin() { 59
return tab; 60
} 61
iterator end() { 62
return tab+sz; 63
} 64
}; 65
66
template<typename T> 67
std::ostream & operator<< (std::ostream & os, const Vector<T> & vec) { 68
os << "["; 69
typename Vector<T>::const_iterator it = vec.begin(); 70
23
1.2 Liste : stockage par Chainon. TD 2
Une liste simplement chaînée stocke les données dans des chaînons.
• Une Liste est munie d'un pointeur vers le premier chaînon qui la constitue (ou nullptr si elle
est vide)
• Un Chainon est composé d'un attribut de type T (le data) et d'un pointeur vers le prochain
Chainon (ou nullptr).
List.h
#ifndef SRC_LIST_H_ 1
#define SRC_LIST_H_ 2
3
#include <cstddef> 4
#include <ostream> 5
6
namespace pr { 7
8
template <typename T> 9
class List { 10
11
class Chainon { 12
public : 13
T data; 14
Chainon * next; 15
Chainon (const T & data, Chainon * next=nullptr):data(data),next(next) {}; 16
24
1.2 Liste : stockage par Chainon. TD 2
}; 17
Chainon * tete; 18
19
20
public: 21
List(): tete(nullptr) { 22
} 23
virtual ~List() { 24
for (Chainon * c = tete ; c ; ) { 25
Chainon * tmp = c->next; 26
delete c; 27
c = tmp; 28
} 29
} 30
31
const T & operator[] (size_t index) const { 32
auto it = begin(); 33
for (size_t i=0; i < index ; i++) { 34
++it; 35
} 36
return *it; 37
} 38
T& operator[] (size_t index) { 39
auto it = begin(); 40
for (size_t i=0; i < index ; i++) { 41
++it; 42
} 43
return *it; 44
} 45
46
void push_back (const T& val) { 47
if (tete == nullptr) { 48
tete = new Chainon(val); 49
} else { 50
Chainon * fin = tete; 51
while (fin->next) { 52
fin = fin->next; 53
} 54
fin->next = new Chainon(val); 55
} 56
} 57
58
void push_front (const T& val) { 59
tete = new Chainon(val,tete); 60
} 61
62
bool empty() const { 63
return tete == nullptr; 64
} 65
66
size_t size() const { 67
size_t sz = 0; 68
for (auto it = begin() ; it != end() ; ++it,++sz); 69
return sz; 70
} 71
72
class ListIte { 73
Chainon * cur; 74
public: 75
25
1.2 Liste : stockage par Chainon. TD 2
26
1.3 Itérateurs. TD 2
1.3 Itérateurs.
Question 3. Quelles opérations doit fournir un itérateur du C++ ? Comment itérer de façon
standard sur un conteneur ?
1
vector<int> vec; 2
// on suppose qu’on ajoute des entiers dans vec 3
4
// boucle "foreach" 5
for (int i : vec) { 6
// body 7
} 8
9
// Expansion C++11 10
{ 11
for(auto it = vec.begin(),_end = vec.end(); it != _end; ++it) 12
{ 13
int i = *it; 14
// body 15
} 16
} 17
le préincrément, (operator++)
27
1.3 Itérateurs. TD 2
L'itérateur pointe naturellement un chainon cur ; operator* rend le data stocké dans le chaînon, et
operator++ fait cur = cur->next.
Begin rend un itérateur cur=tete de liste, end rend un itérateur cur=nullptr.
L'itérateur est tout bêtement un pointeur dans le tableau, pas besoin de dénir de classe. Un bon
typedef (dans la partie public) est cependant une bonne idée, pour que Vector<T>::iterator soit déni
pour les clients.
Begin rend un pointeur sur le début de la zone du tableau, end rend un pointeur un au delà du dernier
élément.
Les signatures const/non const interfèrent pas mal, on est forcés de dupliquer un peu le code des
itérateurs.
Question 7. Ecrivez une fonction qui ache le contenu d'un Vector. Comment la modier à l'aide
de template pour permettre d'acher également une Liste ? Utilisez la fonction dans un main.
main.cpp
#include "Vector.h" 1
#include <iostream> 2
3
4
using namespace std; 5
using namespace pr; 6
7
8
void show_const2 (const Vector<int> & vec) { 9
std::cout << "const:" << std::endl; 10
for (const auto & val : vec) { 11
std::cout << val; 12
} 13
std::cout << std::endl; 14
} 15
16
void show2 (Vector<int> & vec) { 17
std::cout << "avant:" << std::endl; 18
for (auto & val : vec) { 19
std::cout << val << " "; 20
++val; 21
} 22
std::cout << "apres:" << std::endl; 23
show_const2(vec); 24
28
1.3 Itérateurs. TD 2
version templatiée :
main.cpp
#include "List.h" 1
#include "Vector.h" 2
#include <iostream> 3
4
5
using namespace std; 6
using namespace pr; 7
8
9
template< template<typename> class Container, typename T> 10
void show_const (const Container<T> & vec) { 11
std::cout << "const:" << std::endl; 12
for (const auto & val : vec) { 13
std::cout << val; 14
} 15
std::cout << std::endl; 16
} 17
18
19
template< template<typename> class Container, typename T> 20
void show (Container<T> & vec) { 21
std::cout << "avant:" << std::endl; 22
for (auto & val : vec) { 23
std::cout << val << " "; 24
++val; 25
} 26
std::cout << "apres:" << std::endl; 27
show_const(vec); 28
std::cout << std::endl; 29
} 30
31
32
33
29
1.3 Itérateurs. TD 2
30
TME 2 : Conteneurs, Itérateurs, Lib Standard
Objectifs pédagogiques :
• conteneurs
• itérateurs
• map
main_template.cpp
#include "List.h" 1
#include "Vector.h" 2
#include <iostream> 3
4
using namespace std; 5
using namespace pr; 6
7
8
template< template<typename> class Container, typename T> 9
void show_const (const Container<T> & vec) { 10
std::cout << "const:" << std::endl; 11
for (const auto & val : vec) { 12
std::cout << val; 13
} 14
std::cout << std::endl; 15
} 16
17
18
template< template<typename> class Container, typename T> 19
void show (Container<T> & vec) { 20
std::cout << "avant:" << std::endl; 21
for (auto & val : vec) { 22
std::cout << val << " "; 23
++val; 24
} 25
std::cout << "apres:" << std::endl; 26
show_const(vec); 27
std::cout << std::endl; 28
} 29
30
31
32
template< template<typename> class Container, typename T> 33
int test (Container<T> & vec) { 34
35
for (int i=0; i < 15 ; i++) { 36
vec.push_back(i); 37
} 38
39
std::cout << vec << "size " << vec.size() <<std::endl; 40
41
show_const(vec); 42
show(vec); 43
44
return 0; 45
} 46
31
1.1 Liste, Vecteur TME 2
47
int main_t () { 48
Vector<int> vec; 49
test(vec); 50
std::cout << vec[4] << std::endl; 51
vec[4]++; 52
std::cout << vec[4] << std::endl; 53
54
List<int> list; 55
test(list); 56
return 0; 57
} 58
Vector.h
#ifndef SRC_VECTOR_H_ 1
#define SRC_VECTOR_H_ 2
3
#include <cstddef> 4
#include <ostream> 5
6
namespace pr { 7
8
template <typename T> 9
class Vector { 10
size_t alloc_sz; 11
T * tab; 12
size_t sz; 13
14
void ensure_capacity(size_t n) { 15
if (n >= alloc_sz) { 16
// double alloc size 17
alloc_sz *= 2; 18
T * newtab = new T[alloc_sz]; 19
for (size_t ind=0; ind < sz ; ind++) { 20
newtab[ind] = tab[ind]; 21
} 22
delete[] tab; 23
tab = newtab; 24
} 25
} 26
27
public: 28
Vector(int size=10): alloc_sz(size),sz(0) { 29
tab = new T [alloc_sz]; 30
} 31
virtual ~Vector() { 32
delete [] tab; 33
} 34
35
const T & operator[] (size_t index) const { 36
return tab[index]; 37
} 38
T& operator[] (size_t index) { 39
return tab[index]; 40
} 41
42
32
1.1 Liste, Vecteur TME 2
List.h
#ifndef SRC_LIST_H_ 1
#define SRC_LIST_H_ 2
3
#include <cstddef> 4
#include <ostream> 5
6
namespace pr { 7
8
template <typename T> 9
class List { 10
11
class Chainon { 12
public : 13
T data; 14
33
1.1 Liste, Vecteur TME 2
Chainon * next; 15
Chainon (const T & data, Chainon * next=nullptr):data(data),next(next) {}; 16
}; 17
Chainon * tete; 18
19
20
public: 21
List(): tete(nullptr) { 22
} 23
virtual ~List() { 24
for (Chainon * c = tete ; c ; ) { 25
Chainon * tmp = c->next; 26
delete c; 27
c = tmp; 28
} 29
} 30
31
void push_back (const T& val) { 32
if (tete == nullptr) { 33
tete = new Chainon(val); 34
} else { 35
Chainon * fin = tete; 36
while (fin->next) { 37
fin = fin->next; 38
} 39
fin->next = new Chainon(val); 40
} 41
} 42
43
void push_front (const T& val) { 44
tete = new Chainon(val,tete); 45
} 46
47
bool empty() const { 48
return tete == nullptr; 49
} 50
51
size_t size() const { 52
size_t sz = 0; 53
for (auto it = begin() ; it != end() ; ++it,++sz); 54
return sz; 55
} 56
57
class ListIte { 58
Chainon * cur; 59
public: 60
// default ctor = end() 61
ListIte (Chainon * cur=nullptr) : cur(cur) {} 62
63
T & operator* () const { 64
return cur->data; 65
} 66
T * operator-> () const { 67
return & cur->data; 68
} 69
70
ListIte & operator++ () { 71
cur = cur->next; 72
return *this; 73
34
1.1 Liste, Vecteur TME 2
} 74
bool operator!= (const ListIte &o) const { 75
return cur != o.cur; 76
} 77
}; 78
typedef ListIte iterator; 79
class ConstListIte { 80
const Chainon * cur; 81
public: 82
// default ctor = end() 83
ConstListIte (const Chainon * cur=nullptr) : cur(cur) {} 84
85
const T & operator* () const { 86
return cur->data; 87
} 88
const T * operator-> () const { 89
return & cur->data; 90
} 91
92
ConstListIte & operator++ () { 93
cur = cur->next; 94
return *this; 95
} 96
bool operator!= (const ConstListIte &o) const { 97
return cur != o.cur; 98
} 99
}; 100
101
typedef ConstListIte const_iterator; 102
103
const_iterator begin() const { 104
return tete; 105
} 106
const_iterator end() const { 107
return nullptr; 108
} 109
iterator begin() { 110
return tete; 111
} 112
iterator end() { 113
return nullptr; 114
} 115
}; 116
117
template<typename T> 118
std::ostream & operator<< (std::ostream & os, const List<T> & vec) { 119
os << "["; 120
typename List<T>::const_iterator it = vec.begin(); 121
while (it != vec.end()) { 122
os << *it; 123
++it; 124
if (it != vec.end()) { 125
os << ", "; 126
} 127
} 128
os << "]"; 129
return os; 130
} 131
132
35
1.2 std::vector, std::pair TME 2
} /* namespace pr */ 133
134
#endif /* SRC_LIST_H_ */ 135
On part du programme suivant, qui essaie de compter combien de mots diérents sont utilisés dans
un livre.
36
1.3 Table de Hash TME 2
48
std::chrono::steady_clock::time_point end = std::chrono::steady_clock::now(); 49
std::cout << "Parsing with took " 50
<< std::chrono::duration_cast<std::chrono::milliseconds>(end - start).count() 51
<< "ms.\n"; 52
53
std::cout << "Found a total of " << words.size() << " words." << std::endl; 54
55
return 0; 56
} 57
Question 3. Modiez le programme pour qu'il calcule le nombre d'occurrences de chaque mot.
Pour cela, on adaptera le code pour utiliser un vecteur qui stocke des pair<string,int> au lieu de
stocker juste des string. Acher le nombre d'occurrences des mots war, peace et toto.
On doit trouver :
Frequency : war:298
Frequency : peace:114
Word :toto not found.
Oui, ben c'est pas terrible, on est en O(N 2 ), pour chaque mot rencontré, au pire des cas on scanne tous
les mots qu'on a déjà vus.
Le hash (unordered_map) ore du O(N ) si on a pas trop de collisions, et pas besoin de réindexer
(dimensionnement initial adéquat).
On souhaite à présent implanter une table de hash assez simple, en appui sur les classes Liste<T>
et Vector<T> qu'on a développé plus haut.
• Pour rechercher une entrée à partir d'une clé k, on hash la clé, ce qui rend un entier hash(k)
dont la valeur dépend du contenu de la clé.
37
1.3 Table de Hash TME 2
• Si on le trouve, on peut exhiber la valeur qui lui est associée, sinon c'est qu'il n'est pas présent
dans la table.
Question 5. Ecrire une classe générique HashTable<K,V> où K est un paramètre qui donne le type
de la clé, et V donne le type des valeurs.
• dénir un type Entry pour contenir les paires clés valeur ; les clés sont stockées de façon const,
pas les valeurs
• ajouter un attribut typé Vector< List< Entry > > dans la classe
• dénir un constructeur prenant une taille, qui initialise le vecteur avec des listes vides dans
chaque bucket
Ensuite, dénir un itérateur (version non const), son opération de déréférencement, sa comparaison,
son incrément.
• lit de liste, qui pointe l'élément courant dans la liste que désigne l'itérateur
il porte un itérateur
• pour l'incrémenter, on incrémente d'abord lit, si l'on est au bout de la liste, on se décale sur
vit à la recherche d'une case non vide. lit devient alors la tête de cette liste.
• pour la comparaison d'égalité, on peut se contenter de comparer les itérateurs lit
• le déréférencement rend une Entry , celle qui est pointée par lit
Enn dénir les méthodes utilisant ces itérateurs :
• Dénir std::pair<iterator,bool> insert (const Entry & entry) qui essaie d'insérer la paire
clé valeur fournie dans la table.
Si la clé était déjà présente dans la table, le booléen rendu est faux, et l'itérateur pointe
l'entrée qui existait déjà dans la table (qui n'a pas été modiée).
Si la clé n'était pas encore présente, le booléen rendu est vrai, et l'itérateur pointe l'entrée
qui vient d'être ajoutée.
HashMap.h
#ifndef SRC_HASHMAP_H_ 1
#define SRC_HASHMAP_H_ 2
3
#include <cstddef> 4
#include <ostream> 5
6
#include "List.h" 7
#include "Vector.h" 8
9
namespace pr { 10
11
template <typename K, typename V> 12
class HashMap { 13
14
public: 15
38
1.3 Table de Hash TME 2
class Entry { 16
public : 17
const K key; 18
V value; 19
}; 20
private : 21
22
typedef Vector<List<Entry> > buckets_t; 23
// storage for buckets table 24
buckets_t buckets; 25
// total number of entries 26
size_t sz; 27
28
public: 29
HashMap(size_t size): buckets(size),sz(0) { 30
for (size_t i =0; i < size ; i++) { 31
buckets.push_back(List<Entry>()); 32
} 33
} 34
35
class HashMapIte { 36
buckets_t & table; 37
size_t index; 38
typename List<Entry>::iterator cur; 39
public: 40
// begin() ctor 41
HashMapIte (buckets_t & table) :table(table),index(0) { 42
cur = table[0].end(); 43
for (index=0; index < table.size() ; index++) { 44
if (! table[index].empty()) { 45
cur = table[index].begin(); 46
break; 47
} 48
} 49
} 50
51
HashMapIte (buckets_t & table, size_t index, typename List<Entry>::iterator 52
cur) : table(table),index(index),cur(cur) {}
53
Entry & operator* () const { 54
return *cur; 55
} 56
Entry * operator-> () const { 57
return & (*cur); 58
} 59
HashMapIte & operator++ () { 60
++cur; 61
while (index < table.size() && ! (cur != table[index].end())) { 62
++index; 63
cur = table[index].begin(); 64
} 65
return *this; 66
} 67
bool operator!= (const HashMapIte &o) const { 68
return cur != o.cur; 69
} 70
}; 71
typedef HashMapIte iterator; 72
73
39
1.3 Table de Hash TME 2
iterator begin() { 74
return HashMapIte(buckets); 75
} 76
iterator end() { 77
return HashMapIte(buckets,buckets.size(),nullptr); 78
} 79
80
iterator find (const K & key) { 81
size_t h = std::hash<K>()(key); 82
size_t target = h % buckets.size(); 83
for (auto it = buckets[target].begin() ; it != buckets[target].end() ; ++ it 84
) {
if (it->key == key) { 85
// Found it ! 86
return iterator(buckets,target,it); 87
} 88
} 89
// miss 90
return end(); 91
} 92
93
std::pair<iterator,bool> insert (const Entry & entry) { 94
size_t h = std::hash<K>()(entry.key); 95
size_t target = h % buckets.size(); 96
97
for (auto it = buckets[target].begin() ; it != buckets[target].end() ; ++ it 98
) {
if (it->key == entry.key) { 99
// Found it ! 100
return std::make_pair(iterator(buckets,target,it),false); 101
} 102
} 103
// not found, do insertion (in front) 104
buckets[target].push_front(entry); 105
++sz; 106
return std::make_pair(iterator(buckets,target,buckets[target].begin()),true) 107
;
} 108
109
size_t size() const { return sz ; } 110
111
}; 112
113
template<typename K, typename V> 114
std::ostream & operator<< (std::ostream & os, HashMap<K,V> & vec) { 115
os << "["; 116
typename HashMap<K,V>::iterator it = vec.begin(); 117
int perline = 0; 118
while (it != vec.end()) { 119
os << it->key << " -> " << it->value ; 120
++it; 121
if (it != vec.end()) { 122
os << ", "; 123
} 124
perline++; 125
if (perline == 10) { 126
perline =0; 127
os << std::endl; 128
} 129
40
1.4 Mots les plus fréquents TME 2
} 130
os << "]"; 131
return os; 132
} 133
134
} /* namespace pr */ 135
136
#endif /* SRC_HASH_H_ */ 137
Question 6. Utiliser une table de hash associant des entiers (le nombre d'occurrence) aux mots,
et reprendre les question où l'on calculait de nombre d'occurrences de mots avec cette nouvelle
structure de donnée.
On a beau avoir fait une hashtable un peu naive, on doit avoir divisé les temps par 10 à 100.
Question 7. Après avoir chargé le livre, initialiser un std::vector<pair<string,int> >, par copie
des entrées dans la table de hash. On pourra utiliser le contructeur par copie d'une range : vector
(InputIterator first, InputIterator last).
Question 8. Ensuite trier ce vecteur par nombre d'occurrences décroissantes à l'aide de std::sort.
On lui passe les itérateurs de début et n de la zone à trier, et un prédicat binaire. Voir l'exemple
suivant.
41
1.4 Mots les plus fréquents TME 2
main_hash.h
1
#include "HashMap.h" 2
#include <iostream> 3
#include <fstream> 4
#include <regex> 5
#include <algorithm> 6
#include <vector> 7
#include <chrono> 8
9
using namespace std; 10
using namespace pr; 11
12
13
void printFrequency (const string & word, HashMap<string,int> & map) { 14
auto it = map.find(word); 15
if (it != map.end()) { 16
std::cout << "Frequency : " << it->key << ":" << it->value << endl; 17
} else { 18
std::cout << "Word :"<< word << " not found." << endl; 19
} 20
21
} 22
23
int main () { 24
25
HashMap<std::string,int> map(2<<18); // 256 k word capacity 26
ifstream input = ifstream("/tmp/WarAndPeace.txt"); 27
28
std::chrono::steady_clock::time_point start = std::chrono::steady_clock::now(); 29
std::cout << "Parsing War and Peace" << endl; 30
std::string s; 31
regex re( R"([^a-zA-Z])"); 32
while (input >> s) { 33
s = regex_replace ( s, re, ""); 34
std::transform(s.begin(),s.end(),s.begin(),::tolower); 35
auto ret = map.insert({s,1}); 36
if (! ret.second) { 37
(*ret.first).value++; 38
} 39
} 40
input.close(); 41
// std::cout << "Counts :" << map << endl; 42
std::cout << "Finished Parsing War and Peace" << endl; 43
44
std::chrono::steady_clock::time_point end = std::chrono::steady_clock::now(); 45
std::cout << "Parsing with hash took " 46
<< std::chrono::duration_cast<std::chrono::milliseconds>(end - start).count() 47
<< "ms.\n"; 48
49
start = std::chrono::steady_clock::now(); 50
std::cout << "Parsing War and Peace" << endl; 51
vector<pair<string,int> > counts; 52
counts.reserve(5000); 53
input = ifstream("/tmp/WarAndPeace.txt"); 54
while (input >> s) { 55
s = regex_replace ( s, re, ""); 56
std::transform(s.begin(),s.end(),s.begin(),::tolower); 57
auto it = counts.begin(); 58
42
1.4 Mots les plus fréquents TME 2
Question 9. Si la table de hash est trop pleine, on aura beaucoup de collisions de hash, c'est à dire
que les listes stockées dans chaque bucket deviennent de plus en plus longues. Ecrivez une fonction
grow dans votre table de hash, qui double la taille du vecteur de buckets, et réinsère les éléments
dans le résultat. Quelle est la complexité de cette réindexation ?
43
1.4 Mots les plus fréquents TME 2
Donc on initialise une nouvelle table, on itère sur this, on insère tout ce qu'on rencontre dans la nouvelle
table (avec insert).
Vu qu'on sait ce qu'on est en train de faire, et que la propriété de clé est présente initialement, on peut
se contenter de push_front sur le bon bucket, et ne jamais itérer sur une liste dans l'algo.
Ca reste une opération chère, en mémoire (le double d'une taille déjà calculé pour être un peu grosse)
et en temps (rehasher toutes les clés).
44