Vous êtes sur la page 1sur 44

TD 1 : Rappels de programmation C et orientée objet

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.

1.1 Rappels chaîne du C, const.

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 :

• une fonction length rendant une taille pour la chaîne de caractère.


• une fonction newcopy prenant une chaîne de caractère en argument et rendant une nouvelle
copie de cette chaîne de caractères.

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

NB : protection double include + namespace.

1
1.1 Rappels chaîne du C, const. TD 1

size_t length (const char *str);


Avec size_t un entier non signé représentant une taille, #include <cstddef> ou #include <cstring>
pour le voir. Sa taille est plateforme dépendante, généralement compatible avec la taille d'un pointeur
(e.g. size_t prend 8 bytes sur une x64). L'argument est certainement const, on ne souhaite pas le
modier.

char * newcopy (const char *str);


Le type de retour peut être const char* si on préfère. L'argument est certainement const, on ne souhaite
pas le modier.

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

// avec <= pour attraper le dernier ’\0’ 40


for (size_t i=0; i <= n ; ++i) { 41
dest[i] = src[i]; 42
} 43
44
return dest; 45
} 46
47
// version index + malloc 48
char * newcopy3 (const char *src) { 49
size_t n = length(src); 50
// void * malloc (size_t sz) 51
// pour allouer un tableau, multiplier la taille (sizeof) par le nombre de 52
cellules
// la signature force un cast pour remonter de void * vers le vrai type voulu 53
char * dest = (char *) malloc( (n+1)*sizeof(char) ); 54
55
// avec <= pour attraper le dernier ’\0’ 56
for (size_t i=0; i <= n ; ++i) { 57
dest[i] = src[i]; 58
} 59
60
return dest; 61
} 62
63
// une version pointeurs 64
// il y a beaucoup de variantes. 65
char * newcopy4 (const char *src) { 66
size_t n = length(src); 67
char * dest = new char[n+1]; 68
69
char *cd=dest ; 70
while (true) { 71
*cd = *src; 72
if (! *src) { 73
break; 74
} 75
++cd; 76
++src; 77
} 78
return dest; 79
} 80
81
// version memcpy 82
// *toujours* la version à recommander en production 83
// habite dans #include <cstring> 84
char * newcopy (const char *src) { 85
size_t n = length(src); 86
char * dest = new char[n+1]; 87
88
// dest, source, nombre de bytes = n+1 avec le \0 89
memcpy(dest,src,n+1); 90
91
return dest; 92
} 93
94
} 95

On n'hésite pas faire des dessins pour la version pointeurs.

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

A la n du programme, str est libéré.

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.

Pour la détecter, on utilise valgrind, avec l'option -leak-check=full.

Question 4. Expliquez comment compiler séparément ces chiers puis les assembler (linker) faire
un programme exécutable, et l'exécuter.

Invoking: GCC C++ Compiler

g++ -std=c++1y -O0 -g3 -Wall -c -o "src/strutil.o" "../src/strutil.cpp"

g++ -std=c++1y -O0 -g3 -Wall -c -o "src/exo1.o" "../src/exo1.cpp"

Invoking: GCC C++ Linker


g++ -o "td1" ./src/exo1.o ./src/strutil.o

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

• -std=c++1y : dialecte utilisé, on pourrait avoir -std=c++11, -std=c++14, -std=c++17 . . . La


version 1y veut dire >= à c++11 sur notre compilo.

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

• -c : arrêter la compil sur la production du .o

• -o : output le, par défaut même nom que le source avec .o

• le chier .cpp à compiler

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.

Au link, on assemble tous les morceaux, et on cherche un main.

• -o : output le, l'exe qu'on va produire

• tous les .o construits à la compilation

• les bibliothèques statiques libFoo.a

• les bibliothèques dynamiques -lFoo pour libFoo.so/.dylib/.DLL

Pas de dialecte à passer, ni de niveau d'optimisation. Cependant sur compilo récent des options comme
-fwhole-program font un link optimisé.

1.2 Une classe String

La gestion manuelle des pointeurs et de la mémoire pose des problèmes :

• 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

• branchement sur une mémoire non alllouée,

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

On doit pouvoir s'en servir de la manière suivante :

String str = "Hello";


size_t len = str.length();

Donc on avance ligne par ligne dans le programme, et on ajoute ce qu'il faut à la classe String.

Cadre général, on déclare une classe dans le bon namespace :

#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 :

• les include, le namespace englobant

• 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

• pour le reste, cf les commentaires dans le code ci dessus

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 include, le namespace englobant

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

La signature standard pour un type MyClass :

ostream & operator << (ostream & os, const MyClass & o)

Donc on instancie ici pour String :

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

std::cout << str

, ça rend un ostream & o1, on traite

o1 << " et "

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.

Le corps de l'opération est assez simple :

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 :

os << (void *) s.str ;

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

Qu'ache actuellement par exemple le programme suivant ?

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

Citer d'autres problèmes que cela peut poser.

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

Il faut dessiner la mémoire et éxécuter pas à pas pour expliquer.

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

Question 9. Quels autres opérateurs pourrait-on orir sur String ?

NB : on va les demander en TME.

Opérateurs de comparaison : operator<, operator==

Opérateur de concaténation : operator+

Opérateur de multiplication (par un scalaire) : 2 ∗ “to00 = “toto00


Opérateur d'accès indicé : operator[] . . .

11
TME 1 : Programmation, compilation et exécution en C++
Objectifs pédagogiques :
• mise en place
• classe simple
• opérateurs
• compilation, debugger, valgrind

1.1 Plan des séances

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.

1.2 Prise en main

Eclipse (CDT) : un environnement de développement (IDE) conguré pour C/C++.


Cet environnement graphique gèrera votre projet localement sur votre compte PPTI. Il vous permet-
tra d'éditer les sources, de les compiler et de les exécuter. Son utilisation est fortement recommandée
(instructions précises dans les supports), mais d'autres outils peuvent aussi être utilisés (NetBeans,
Visual Studio Code, . . . ).

Question 1. Lancement d'Eclipse

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++

Démarrer eclipse puis construire un File->New->C/C++ Project; on va utiliser le C++ Managed


Build assez facile d'emploi et bien pris en charge par l'IDE comme système de build. On va
construire pour commencer un projet hébergeant simplement un éxécutable, prenez le template
fourni Hello World. Assurez-vous que la chaîne de compilation sélectionnée est bien Linux GCC.
Appelez le projet TME0. On peut valider les options par défaut sur les autres onglets.

On a à présent un projet avec un main C++ qui ache un message.

Question 3. Compiler le projet

Sélectionner le menu Project->Build Project.

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.

On constate une bonne qualité du soulignement au cours de la frappe, et l'auto-complétion disponible


sur ctrl-espace.

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 ;

Question 5. Découverte du debugger

Recopier le code suivant dans votre main et lancer le programme.

for (size_t i=9; i >= 0 ; i--) {


if (tab[i] - tab[i-1] != 1) {
cout << "probleme !";
}
}

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)

• Step into, Step Over, Step Return : exécution ligne à ligne

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.

Question 6. Visualisation de l'état sous debugger

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.

c'est un size_t donc il ne sera jamais négatif.

on utilise le type int à la place.

Question 7. Valgrind et détection des fautes mémoire

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.

Lire cette page http://valgrind.org/docs/manual/mc-manual.html#mc-manual.errormsgs décrivant


les erreurs détectées par cet outil.

13
1.3 Réalisation d'une classe String TME 1

on doit avoir des erreurs de mémoire non initialisée.

Standard C++ dans Eclipse CDT.


Par défaut la version du C++ utilisée est celle par défaut de notre compilateur, ce qui est insusant
pour notre usage.
Il faut congurer le dialecte pour la version "-std=c++1y", soit la plus récente disponible. Malheureuse-
ment il faut faire ce réglage dans deux endroits :

• 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

 "Window -> Preferences -> C/C++ -> Build -> Settings"

 Ouvrir l'onglet "Discovery".

 Sélectionner dans la liste le "CDT GCC Built-in compiler Settings"

 Ajouter un "-std=c++1y" au ags, de façon à avoir


${COMMAND} ${FLAGS} -std=c++1y -E -P -v -dD "${INPUTS}"

• 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:

 "Project properties -> C/C++ Build -> Settings"

 Sous le "GCC C++ compiler" on trouve une rubrique "Dialect"

 sélectionner : "-std=c++1y" dans le menu déroulant.

Attention, il faut faire le premier réglage dans tout nouveau workspace, et le deuxième pour chaque
nouveau projet.

1.3 Réalisation d'une classe String

Question 8. Dénir et tester les fonctions length et newcopy du TD1.

Question 9. Implanter la classe String conformément aux instructions du TD 1.

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.

On commencera par réaliser et tester :

• Constructeur par copie de l'argument

• Destructeur qui invoque delete

• Ajout dans un ux / impression de la String

• Constructeurs par copie, operator= redénis

Inclut aussi la réponse aux question suivantes.

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

Ajouter (en s'appuyant sur compare) :


• Un opérateur de comparaison d'égalité bool operator==(const String &a,const String &b)
symétrique, déclaré friend et en dehors de la classe String.

• 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 un "delete[]" dans String,

• commentez une copie, i.e. au lieu de faire la copie, contentez de l'aecter à l'attribut de la
classe.

1.4 Utilisation de shared_ptr

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.

Lisez la documentation dehttp://www.cplusplus.com/reference/memory/shared_ptr/ et http://


www.cplusplus.com/reference/memory/shared_ptr/shared_ptr/.
Question 15. Dans une copie de votre classe String, que l'on appelera StringPtr, substituer au
const char * un std::shared_ptr<const char> qui est déni dans le header <memory>. Mettez à
jour la gestion mémoire au sein de la classe (constructeur, destructeur, copie, . . . ) : on ne garde
que le constructeur par copie d'un const char *, le comportement par défaut des autres opérations
est déjà celui qu'on souhaite. Corriger le code pour qu'il compile et fonctionne bien. Pour obtenir
le pointeur nu (const char *) à partir d'un shared_ptr<const char> ptr on utilise ptr.get().

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

// encore dans namespace pr. 46


47
// déclaration d’un opérateur + symétrique (non membre) 48
StringPtr operator+ (const StringPtr &s1, const StringPtr & s2); 49
// déclaration d’une sérialisation par défaut (toStringPtr du c++) 50
std::ostream & operator << (std::ostream &os, const pr::StringPtr & s); 51
// comparaison d’égalité 52
bool operator== (const StringPtr &s1, const StringPtr & s2); 53
54
} // fin namespace pr 55
56
#endif /* StringPtr_H_ */ 57

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

Question 16. Si on lance le programme sous Valgrind on rencontre un problème d'incohérence


new[]/delete, qui gène l'utilisation. Expliquez ce qui se passe. Pour corriger le problème, faites une
recherche sur StackOverow pour les mots clés "shared_ptr array C++", on doit y proposer (vu
que l'on n'a pas de compilateur C++17 à la PPTI) d'utiliser le constructeur à deux arguments de
shared_ptr pour lui passer une fonction à invoquer pour la désallocation. Adopter une des syntaxes
proposées, et s'assurer que le problème est résolu.

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; }.

La lambda fournie résout le problème, cf ce SO pour plus de détails : https://stackoverflow.com/


questions/13061979/shared-ptr-to-an-array-should-it-be-used
Ca se passe juste dans le ctor de StringPtr dans le corrigé ligne 9 du CPP.

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.

1.1 Vecteur : stockage contigü.

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.

Question 1. Ecrivez une classe Vector<T> en respectant les contraintes suivantes :

• 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

On fournira pour cette classe les operateurs et fonctions suivantes :

• Vector(int size=10) : un constructeur qui prend la taille d'allocation initiale


• Ṽector() un destructeur
• operator[] dans ses deux variantes (const ou non), pour consulter ou modier le contenu.
• size_t size() const la taille actuelle (nombre d'éléments)
• bool empty() const vrai si la taille actuelle est 0
• void push_back (const T& val) : ajoute à la n du vecteur, peut nécessiter réallocation et
copie.

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

while (it != vec.end()) { 71


os << *it; 72
++it; 73
if (it != vec.end()) { 74
os << ", "; 75
} 76
} 77
os << "]"; 78
return os; 79
} 80
81
} /* namespace pr */ 82
83
#endif /* SRC_VECTOR_H_ */ 84

1.2 Liste : stockage par Chainon.

Une liste simplement chaînée stocke les données dans des chaînons.

Question 2. Ecrivez une classe Liste<T>.

• Une Liste est munie d'un pointeur vers le premier chaînon qui la constitue (ou nullptr si elle
est vide)

• La liste ne stocke pas sa taille, il faut la recalculer

• Un Chainon est composé d'un attribut de type T (le data) et d'un pointeur vers le prochain
Chainon (ou nullptr).

On fournira pour ce cette classe les operateurs et fonctions suivantes :

• List() : un constructeur par défaut pour la liste vide


• L̃ist() un destructeur, qui libère tous les chaînons
• size_t size() const la taille actuelle (nombre d'éléments)
• bool empty() const vrai si la liste est vide
• void push_back (const T& val) : ajoute à la n de la liste
• void push_front (const T& val) : ajoute en tête de la liste

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

// default ctor = end() 76


ListIte (Chainon * cur=nullptr) : cur(cur) {} 77
78
T & operator* () const { 79
return cur->data; 80
} 81
T * operator-> () const { 82
return & cur->data; 83
} 84
85
ListIte & operator++ () { 86
cur = cur->next; 87
return *this; 88
} 89
bool operator!= (const ListIte &o) const { 90
return cur != o.cur; 91
} 92
}; 93
typedef ListIte iterator; 94
class ConstListIte { 95
const Chainon * cur; 96
public: 97
// default ctor = end() 98
ConstListIte (const Chainon * cur=nullptr) : cur(cur) {} 99
100
const T & operator* () const { 101
return cur->data; 102
} 103
const T * operator-> () const { 104
return & cur->data; 105
} 106
107
ConstListIte & operator++ () { 108
cur = cur->next; 109
return *this; 110
} 111
bool operator!= (const ConstListIte &o) const { 112
return cur != o.cur; 113
} 114
}; 115
116
typedef ConstListIte const_iterator; 117
118
const_iterator begin() const { 119
return tete; 120
} 121
const_iterator end() const { 122
return nullptr; 123
} 124
iterator begin() { 125
return tete; 126
} 127
iterator end() { 128
return nullptr; 129
} 130
}; 131
132
template<typename T> 133
std::ostream & operator<< (std::ostream & os, const List<T> & vec) { 134

26
1.3 Itérateurs. TD 2

os << "["; 135


typename List<T>::const_iterator it = vec.begin(); 136
while (it != vec.end()) { 137
os << *it; 138
++it; 139
if (it != vec.end()) { 140
os << ", "; 141
} 142
} 143
os << "]"; 144
return os; 145
} 146
147
} /* namespace pr */ 148
149
#endif /* SRC_LIST_H_ */ 150

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 ?

Donc l'itération la plus standard est sûrement le foreach,

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

On voit qu'il faut :

• begin et end membres du conteneur, fonctions qui rendent des itérateurs

• L'itérateur doit supporter

 la comparaison d'inégalité (operator !=),

 l'opérateur de déréférencement (operator*, parfois confortable d'avoir aussi operator->)

 le préincrément, (operator++)

Question 4. Ajoutez ces mécanismes à votre Liste. En quoi consiste un itérateur ?

27
1.3 Itérateurs. TD 2

Cf le code fourni. On présente la version non const dans un premier temps.

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.

Question 5. Ajoutez ces mécanismes à votre Vector. En quoi consiste un itérateur ?

Cf le code fourni. On présente la version non const dans un premier temps.

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.

Question 6. Expliquez la diérence entre les iterator et const_iterator. Expliquez comment il


faut modier Vector et List pour permettre des itérations const.

Les signatures const/non const interfèrent pas mal, on est forcés de dupliquer un peu le code des
itérateurs.

Sur Vector c'est juste rajouter les begin/end const essentiellement.

Sur Liste il faut aussi dupliquer l'itérateur.

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.

Une version de base.

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

std::cout << std::endl; 25


} 26
27
28
int main2 () { 29
Vector<int> vec (5); 30
31
for (int i=0; i < 10 ; i++) { 32
vec.push_back(i); 33
} 34
35
std::cout << vec << std::endl; 36
37
38
show_const2(vec); 39
40
show2(vec); 41
42
std::cout << vec[4] << std::endl; 43
44
return 0; 45
} 46

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

template< template<typename> class Container, typename T> 34


int test (Container<T> & vec) { 35
36
for (int i=0; i < 15 ; i++) { 37
vec.push_back(i); 38
} 39
40
std::cout << vec << "size " << vec.size() <<std::endl; 41
42
43
show_const(vec); 44
45
show(vec); 46
47
std::cout << vec[4] << std::endl; 48
49
return 0; 50
} 51
52
int main () { 53
Vector<int> vec; 54
test(vec); 55
List<int> list; 56
test(list); 57
} 58

30
TME 2 : Conteneurs, Itérateurs, Lib Standard
Objectifs pédagogiques :
• conteneurs
• itérateurs
• map

1.1 Liste, Vecteur

Question 1. En suivant les indications du TD 2, implanter les classes génériques Liste<T> et


Vector<T> et leurs itérateurs. Elles doivent pouvoir s'utiliser avec ce main (générique) :

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

C'est les même qu'au TD.

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

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
while (it != vec.end()) { 71
os << *it; 72
++it; 73
if (it != vec.end()) { 74
os << ", "; 75
} 76
} 77
os << "]"; 78
return os; 79
} 80
81
} /* namespace pr */ 82
83
#endif /* SRC_VECTOR_H_ */ 84

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

1.2 std::vector, std::pair

On part du programme suivant, qui essaie de compter combien de mots diérents sont utilisés dans
un livre.

Compte le nombre mots différents


1
#include <iostream> 2
#include <fstream> 3
#include <regex> 4
#include <algorithm> 5
#include <vector> 6
#include <chrono> 7
8
using namespace std; 9
10
int main_p () { 11
vector<string> words; 12
words.reserve(5000); 13
14
ifstream input = ifstream("/tmp/WarAndPeace.txt"); 15
16
std::chrono::steady_clock::time_point start = std::chrono::steady_clock::now(); 17
std::cout << "Parsing War and Peace" << endl; 18
19
std::string s; 20
// une regex qui reconnait les caractères anormaux (négation des lettres) 21
regex re( R"([^a-zA-Z])"); 22
while (input >> s) { 23
// élimine la ponctuation et les caractères spéciaux 24
s = regex_replace ( s, re, ""); 25
// passe en lowercase 26
std::transform(s.begin(),s.end(),s.begin(),::tolower); 27
28
// cherchons si le mot est déjà présent 29
auto it = words.begin(); 30
while (it != words.end()) { 31
if (*it == s) { 32
// trouvé 33
break; 34
} 35
++it; 36
} 37
if (it != words.end()) { 38
// déjà trouvé 39
continue; 40
} else { 41
words.push_back(s); 42
} 43
} 44
input.close(); 45
46
std::cout << "Finished Parsing War and Peace" << endl; 47

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 2. Exécutez le programme sur le chier WarAndPeace.txt fourni. Combien y a-t-il de


mots diérents ?

Sur ma machine, j'ai

Parsing War and Peace


Finished Parsing War and Peace
Parsing with took 17751ms.
Found a total of 20333 words.

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.

Question 4. Que penser de la complexité algorithmique de ce programme ? Quelles autres struc-


tures de données de la lib standard aurait-on pu utiliser ?

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

1.3 Table de Hash

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.

On rappelle les principes d'une table de hash :

• La table stocke un vecteur de taille relativement grande appelé buckets.


• Dans chaque case du vecteur, on trouve une liste de paires (clé,valeur)

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

• On cherche dans le bucket d'indice hash(k) % buckets.size(), un élément de la liste dont la


clé serait égale (au sens de ==) avec la clé k recherchée.

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.

On pourra dans l'ordre :

• dénir le cadre de la classe, ses paramètres génériques

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

• il porte une réference vers la table buckets


• il porte un indice ou un itérateur vit dans cette table, désignant la liste (bucket) qu'il explore
actuellement

• 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 les opérations begin() et end() de la table de hash.


• Dénir iterator find (const K & key) qui rend un itérateur correctement positionné, ou
end() si la clé n'est pas trouvée. Pour calculer la valeur de hash de l'objet key , on utilisera
size_t h = std::hash<K>()(key);. Cette fonction ne doit pas itérer toute la table, seulement
le bucket approprié.

• 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

1.4 Mots les plus fréquents

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.

Corrigé de cette série de questions dans le code à la n.

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.

exemple sort et lambda


#include <vector> 1
#include <string> 2
#include <algorithm> 3
4
class Etu { 5
public : 6
std::string nom; 7
int note; 8
}; 9
10
int main_sort () { 11
std::vector<Etu> etus ; 12
// plein de push_back de nouveaux étudiants dans le désordre 13
14
// par ordre alphabétique de noms croissants 15
std::sort(etus.begin(), etus.end(), [] (const Etu & a, const Etu & b) { return a.nom < 16
b.nom ;});
// par notes décroissantes 17
std::sort(etus.begin(), etus.end(), [] (const Etu & a, const Etu & b) { return a.note 18
> b.note ;});
return 0; 19
} 20

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

while (it != counts.end()) { 59


if (it->first == s) { 60
break; 61
} 62
++it; 63
} 64
if (it != counts.end()) { 65
it->second++; 66
} else { 67
counts.push_back(make_pair(s,1)); 68
} 69
} 70
input.close(); 71
end = std::chrono::steady_clock::now(); 72
std::cout << "Parsing with pair<string,int> took " 73
<< std::chrono::duration_cast<std::chrono::milliseconds>(end - start). 74
count()
<< "ms.\n"; 75
76
printFrequency("war",map); 77
printFrequency("peace",map); 78
printFrequency("toto",map); 79
80
start = std::chrono::steady_clock::now(); 81
std::cout << "Most frequent" << endl; 82
std::vector<pr::HashMap<std::string,int>::Entry * > entries; 83
entries.reserve(map.size()); 84
for (auto it = map.begin() ; it != map.end() ; ++it) { 85
entries.push_back(& (*it) ); 86
} 87
std::sort(entries.begin(),entries.end(), [] (auto a,auto b) { return a->value > b 88
->value ;});
int line=0; 89
for (const auto & val : entries) { 90
std::cout << val->key << "->" << val->value << " ,"; 91
if (++line % 10 == 0) { 92
std::cout << endl; 93
} 94
if (line == 100) { 95
break; 96
} 97
} 98
std::cout << std::endl; 99
end = std::chrono::steady_clock::now(); 100
std::cout << "Sorting by frequency took " 101
<< std::chrono::duration_cast<std::chrono::microseconds>(end - start).count() 102
<< "us.\n"; 103
104
return 0; 105
} 106

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

Je ne l'ai pas codé désolé, c'est une question bonus.

Bilan : il faut recalculer tous les hash et les modulo.

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

Vous aimerez peut-être aussi