Comme je vous l'ai dit plus haut,
cin
dispose de méthodes nous permettant de savoir si la dernière opération s'est bien déroulée. Et parmi celles-ci, il y a
fail()
, qui retourne
true
si une erreur s'est produite lors de la dernière opération, ce qui inclus les opération d'extraction du flux (lorsque l'on demande à l'utilisateur de saisir une information).
Un exemple pour illustrer cela :
Code : C++ 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27 | #include <iostream>
#include <cstdlib>
#include "fonctions_saisie.hpp" // Ce fichier contient le prototype de la fonction vider_buffer()
using namespace std;
int main(int argc, char **argv)
{
unsigned short int age;
cout << "Entrez votre âge : ";
cin >> age;
if(cin.eof() || cin.bad()) // S'il y a une erreur interne au flux, qui n'est pas provoqué par l'utilisateur (pour une fois...)
{
cerr << "Une erreur interne est survenue." << endl;
}
else if(cin.fail()) // Si fail() retourne true
{
cerr << "Erreur, saisie incorrecte." << endl;
}
vider_buffer(); // On remet cin dans un état valide et vide le buffer
return EXIT_SUCCESS;
}
|
Voici ce que l'on obtient lors de l'exécution :
Code : Console - Sortie | Entrez votre âge : n'importe
Erreur, saisie incorrecte. |
Il est important de tester bad()
avant fail()
car fail()
peut retourner true
alors qu'il n'y a eu aucune erreur de saisie.
Une solution à nos problèmes
Validation tolérante
Il nous suffit juste de mettre le code dans une boucle pour nous assurer que l'utilisateur entre une information correcte (le reste du code ne change pas).
Il ne faut pas oublier de vider le buffer après chaque demande de saisie sinon on crée une boucle infinie en cas de saisie incorrecte.
Code : C++ 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45 | #include <iostream>
#include <cstdlib>
#include "fonctions_saisie.hpp"
using namespace std;
int main(int argc, char **argv)
{
unsigned short int age;
while(true) // On crée une boucle, dont on ne sortira que lorsque l'utilisateur aura entré des informations correctes
{
cout << "Entrez votre âge : ";
cin >> age;
if(cin.eof() || cin.bad())
{
cerr << "Une erreur interne est survenue." << endl;
if(cin.eof()) // Si c'est la fin du flux d'entrée, il faut sortir de la boucle
{
break;
}
vider_buffer(); // On remet cin dans un état valide et vide le buffer
continue;
}
else if(cin.fail())
{
cerr << "Erreur, saisie incorrecte." << endl;
vider_buffer(); // On remet cin dans un état valide et vide le buffer
continue;
}
break; // On ne sort de la boucle que s'il n'y a eu aucune erreur ou que c'est la fin du flux d'entrée
}
cout << "Vous avez " << age << " ans." << endl;
return EXIT_SUCCESS;
}
|
Voici ce que l'on obtient à l'exécution :
Code : Console | Entrez votre âge : n'importe
Erreur, saisie incorrecte.
Entrez votre âge : 20
Vous avez 20 ans. |
Avant que vous ne criiez tous "victoire !" en coeur, j'aimerais attirer votre attention sur un petit détail. En effet, ce programme s'assure que
cin
réussi bien à extraire un nombre du buffer puis le vide, mais imaginons que l'utilisateur entre un nombre suivi par autre chose. Au lieu de spéculer sur les résultats, faisons un test :
Code : Console | Entrez votre âge : n'importe
Erreur, saisie incorrecte.
Entrez votre âge : 14a
Vous avez 14 ans. |
Le résultat serait le même si l'on avait entré n'importe quelle chaîne commençant par un nombre puis suivi d'un caractère autre qu'un chiffre (comme "14-8" ou "14kjhyt=$").
On a bien un résultat mais l'utilisateur a quand même entré quelque chose qui pourrait ne pas avoir de sens (je ne pense pas que "14kjhyt=$" soit une réponse pleine de sens

).
Validation un peu plus stricte
Comme je l'ai dit plus haut, le code précédent s'assurait que
cin
avait réussi à extraire un nombre, sans se soucier de ce qu'il y avait après et c'est ce comportement qu'il va nous falloir changer.
Heureusement pour nous, il existe une fonction permettant de savoir si la fin d'un flux a été atteinte,
eof()
mais malheureusement, pour
cin
, il n'indique pas qu'il n'y a plus de données à lire, mais la fin du flux d'entrée (qui est une erreur lors de l'exécution).
Peu importe, il suffit de trouver une classe de flux d'entrée dont le comportement vis à vis de
eof()
n'est pas celui de
cin
et pour cela, direction la
documentation 
.
Il existe donc diverses classes permettant de manipuler les flux d'entrée, les voici :
- istream : la classe d'entrée basique, dont dérivent toutes les autres. Elle permet d'extraire des données de n'importe quel flux d'entrée (cin
est une instance de istream
connecté au flux d'entrée standard).
- iostream (hérite de istream
) : est une classe permettant d'extraire, mais aussi d'écrire des données dans n'importe quel flux.
- fstream (hérite de iostream
) : est une classe permettant d'extraire et d'écrire des données dans un fichier.
- stringstream (hérite de iostream
) : est une classe permettant d'extraire et d'écrire des données dans un objet de type string
.
- ifstream (hérite de istream
) : est une classe permettant d'extraire des données d'un fichier.
- istringstream (hérite de istream
) : est une classe permettant d'extraire des données d'un objet de type string
.
Parmi toutes ces classes, il y en a deux qui pourraient nous intéresser,
stringstream
et
istringstream
. Etant donné que nous ne ferons qu'extraire des données, nous utiliserons
istringstream
.
En regardant la documentation sur
istringstream
, on s'aperçoit qu'on ne peut pas extraire de données dans un objet de ce type avec
cin
, mais qu'il manipule un
string
qui lui peut être utilisé avec
cin
.
Il nous faut donc instancier un objet de type
string
, extraire des données de
cin
dans cet objet, instancier un objet de type
istringstream
, lui donner l'objet de type
string
et en extraire les informations.
Donc voici ce que nous donne cette méthode :
Code : C++ 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62 | #include <iostream>
#include <cstdlib>
#include <string> // Pour pouvoir utiliser la classe string
#include <sstream> // Pour pouvoir manipuler la classe istringstream
#include "fonctions_saisie.hpp"
using namespace std;
int main(int argc, char **argv)
{
unsigned short int age;
string temp; // On crée une variable temporaire
while(true)
{
cout << "Entrez votre âge : ";
cin >> temp;
if(cin.eof() || cin.bad())
{
cerr << "Une erreur interne est survenue." << endl;
if(cin.eof()) // Si c'est la fin du flux d'entrée, il faut sortir de la boucle
{
break;
}
vider_buffer(); // On remet cin dans un état valide et vide le buffer
continue;
}
else if(cin.fail())
{
cerr << "Erreur, saisie incorrecte." << endl;
vider_buffer(); // On remet cin dans un état valide et vide le buffer
continue;
}
vider_buffer(); // On remet cin dans un état valide et vide le buffer
istringstream stream(temp);
stream >> age;
if(stream.fail() || !stream.eof()) // Si l'on est pas arrivé à extraire les données ou que l'on a pas atteint la fin du flux
{
cerr << "Erreur, saisie incorrecte." << endl; // L'utilisateur a fait une erreur de saisie
}
else
{
break; // Sinon on ne sort de la boucle
}
}
cout << "Vous avez " << age << " ans." << endl;
return EXIT_SUCCESS;
}
|
Le gentil Windows et le méchant Linux
Heu tu ne t'es pas trompé ? C'est pas le gentil Linux et le méchant Windows ?
Et bien pour une fois, non

!
Voici la sortie que produit le programme sous Windows :
Code : Console - Sortie sous Windows | Entrez votre âge : 77228
Erreur, saisie incorrecte.
Entrez votre âge : -10
Erreur, saisie incorrecte.
Entrez votre âge : 14
Vous avez 14 ans. |
C'est exactement le comportement attendu du programme, il demande à l'utilisateur d'entrer un
entier court non signé, c'est à dire compris entre 0 et 65 535.
Code : C++ - Sortie sous Linux | Entrez votre âge : 77228
Erreur, saisie incorrecte.
Entrez votre âge : -10
Vous avez 65526 ans.
|
Suivant le système d'exploitation, on a un comportement différent, ce qui est assez gênant puisque l'on voudrait que notre méthode marche tout le temps.
Ce "phénomène" ne se produit qu'avec les types
non signés ce qui, heureusement pour nous, ne concerne pas beaucoup de types, à savoir :
- unsigned char
: caractères non signés, sur 1 octet. Ils peuvent prendre des valeurs comprises entre 0 et 255.
- unsigned short int
: entiers non signés courts, sur 2 octets. Ils peuvent prendre des valeurs comprises entre 0 et 65 535.
- unsigned int
: entiers non signés, sur 2 ou 4 octets (suivant si votre programme est compilé en 16 ou 32 bits). Ils peuvent prendre des valeurs comprises entre 0 et 65635 ou 4 294 967 295.
- unsigned long int
: entiers non signés longs, sur 4 octets. Ils peuvent prendre des valeurs comprises entre 0 et 4 294 967 295.
- unsigned long long int
: entiers non signés long long, sur 8 octets. Ils peuvent prendre des valeurs comprises entre 0 et 18 446 744 073 709 551 615.
Les valeurs données ne sont pas forcément celles qui seront en vigueur sur votre système, car elles dépendent du système d'exploitation, de l'architecture de votre ordinateur et du compilateur que vous utilisez.
Il est aussi à savoir que dans la norme C++, le type
unsigned long long int
est optionnel, il n'est donc peut être pas supporté avec le compilateur que vous utilisez (il est néanmoins disponible avec beaucoup de compilateurs, ce qui inclut g++ de
GCC, sous Windows et Linux et Visual C++ de Microsoft).
D'accord, mais comment on va faire pour être sûr que l'utilisateur n'entre pas de nombres négatifs ?
La solution que je vous propose est de regarder dans l'instance de
string
que l'on utilise (la variable
temp
).
Grâce à la méthode
find_first_not_of() de
string
, nous pouvons déterminer si la chaîne de caractères contient autre chose que des nombres, voici son prototype :
Code : C++ - Prototype de find_first_not_of() | size_t find_first_not_of(const string &str, size_t pos = 0);
|
find_first_not_of()
retourne la position du premier caractère qui n'est pas un de ceux donnés en paramètre. La valeur
string::npos
est retourné si aucun caractère n'est trouvé.
Paramètres :
- str
: liste des caractères à trouver dans la chaîne
- pos
: la position à partir de laquelle chercher. Par défaut 0.
Voici ce que cela donne :
Code : C++ 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61 | #include <iostream>
#include <cstdlib>
#include <limits>
#include <string>
#include <sstream>
#include "fonctions_saisie.hpp"
using namespace std;
int main(int argc, char **argv)
{
unsigned short int age;
string temp;
while(true)
{
cout << "Entrez votre âge : ";
cin >> temp;
if(cin.eof() || cin.bad())
{
cerr << "Une erreur interne est survenue." << endl;
if(cin.eof())
{
break;
}
vider_buffer();
continue;
}
else if(cin.fail() || temp.find_first_not_of("0123456789") != string::npos) // S'il y a une erreur de saisie ou que l'entrée contient autre chose que des chiffres
{
cerr << "Erreur, saisie incorrecte." << endl;
vider_buffer();
continue;
}
istringstream stream(temp);
stream >> age;
if(stream.fail() || !stream.eof())
{
cerr << "Erreur, saisie incorrecte." << endl;
}
else
{
break;
}
}
cout << "Vous avez " << age << " ans." << endl;
return EXIT_SUCCESS;
}
|
Voilà ce que produit ce code sous Linux :
Code : Console - Sortie sous Linux | Entrez votre âge : -10
Erreur, saisie invalide.
Entrez vote âge : 20
Vous avez 20 ans. |
On a donc bien le résultat attendu sous Linux maintenant.
C'est génial mais ça fait beaucoup de code à copier/coller à chaque fois. Tu n'aurais pas mieux ?
Nous pouvons créer des fonctions ayant pour but de saisir différents types de variables.
- saisir_short_int()
pour les variables de type short int
.
- saisir_unsigned_short_int()
pour les variables de type unsigned short int
.
- saisir_int()
pour les variables de type int
.
- saisir_unsigned_int()
pour les variables de type unsigned int
.
- ...
- saisir_double()
pour les variables de type double
.
- saisir_string()
pour les variables de type string
(eh oui, même pour ce type, il peut y avoir une erreur sur le flux).
On donnerait en paramètre à ces fonctions la variable à modifier (par un passage par référence par exemple) et le message à afficher ("Entrez votre âge : ", "Entrez le nombre d'objets à acheter : ", etc). Elles retourneraient un booléen pour indiquer le succès ou l'échec de l'opération.
Voici deux de ces fonctions (le reste réside essentiellement en un copier/coller) :
Code : C++ - Les fonctions saisir_int et saisir_unsigned_int 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101 | bool saisir_short_int(short int &variable, const string &message)
{
string temp;
while(true)
{
cout << message;
cin >> temp;
if(cin.bad() || cin.eof())
{
cerr << "Une erreur interne est survenue" << endl;
if(cin.eof())
{
return false;
}
vider_buffer();
continue;
}
else if(cin.fail())
{
cerr << "Erreur, saisie incorrecte." << endl;
vider_buffer();
continue;
}
vider_buffer();
istringstream stream(temp);
stream >> variable;
if(stream.fail() || !stream.eof())
{
cerr << "Erreur, saisie incorrecte." << endl;
}
else
{
return true;
}
}
return false;
}
bool saisir_unsigned_short_int(unsigned short int &variable, const string &message)
{
string temp;
while(true)
{
cout << message;
cin >> temp;
if(cin.bad() || cin.eof())
{
cerr << "Une erreur interne est survenue" << endl;
if(cin.eof())
{
return false;
}
vider_buffer();
continue;
}
else if(cin.fail() || temp.find_first_not_of("0123456789") != string::npos)
{
cerr << "Erreur, saisie incorrecte." << endl;
vider_buffer();
continue;
}
vider_buffer();
istringstream stream(temp);
stream >> variable;
if(stream.fail() || !stream.eof())
{
cerr << "Erreur, saisie incorrecte." << endl;
}
else
{
return true;
}
}
return false;
}
|
Les types char et unsigned char
Peut-être que certains auront l'idée de créer les fonctions
saisir_char()
et
saisir_unsigned_char()
de la même manière que les autres fonctions (les deux fonctions sont identiques car
unsigned char
représente un caractère, et non un nombre). Voilà ce que donne un programme utilisant une de ces deux fonctions :
Code : Console - Sortie | Entrez un caractère : a
Erreur, saisie incorrecte.
Entrez un caractère : b
Erreur, saisie incorrecte.
Entrez un caractère : a
Erreur, saisie incorrecte.
... |
Le programme entre dans une boucle infinie. Après quelques tests, on s'aperçoit que c'est l'instruction
stream.eof()
qui retourne toujours
false
(et ce pour de raisons qui restent toujours obscures).
Il faut donc s'affranchir de cette méthode pour ces deux types.
Oui, mais comment on fait étant donné qu'on en a besoin ?
Il ne faut pas perdre de vue que l'on avait utilisé
istringstream
pour pouvoir convertir une chaîne de caractères en un autre type.
Mais là, ce que nous voulons, c'est simplement un caractère, et miracle nous en avons grâce à l'objet
temp
puisque c'est une chaîne de caractères.
Il nous suffit de vérifier que cette chaîne ne contienne qu'un seul caractère (en plus du
'\0'
) et de prendre ce caractère via la méthode
at() de la classe
string
qui permet de récupérer un caractère à une position donnée.
Les fonctions pour ces deux types deviennent encore plus simples que les autres :
Code : C++ 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55 | bool saisir_char(char &variable, const string &message)
{
string temp;
while(true)
{
cout << message;
cin >> temp;
if(cin.bad() || cin.eof())
{
cerr << "Une erreur interne est survenue" << endl;
if(cin.eof())
{
return false;
}
vider_buffer();
continue;
}
else if(cin.fail() || temp.length() != 1)
{
cerr << "Erreur, saisie incorrecte." << endl;
vider_buffer();
continue;
}
vider_buffer();
variable = temp.at(0);
return true;
}
return false;
}
bool saisir_unsigned_char(unsigned char &variable, const string &message)
{
char caractere;
if(saisir_char(caractere, message))
{
variable = static_cast<unsigned char>(caractere); // On converti le caractère en unsigned char
return true;
}
return false;
}
|
Et attends ! C'est quoi ce static_cast<unsigned char>(caractere)
?
Pour stocker les caractères, on utilise le type
char
, qui peut prendre des valeurs allant de -128 à 127. Chacune de ces valeur est associée à un caractère, c'est la norme ASCII. Or cette norme définit tous les caractères comme ayant une représentation entière comprise entre 0 et 127. On peut donc convertir un
char
en
unsigned char
sans avoir peur de perdre quoi que ce soit. Et pour ce faire, on utilise le
casting, si vous ne savez pas ce que c'est ou que vous voulez en savoir plus, je vous suggère un tutoriel du site du zér0,
[C++] Les conversions de types, de shareman.
Voilà qui met fin à ce tutoriel.
Quoi c'est pas fini ?! Pourtant, on a solutionné tous les problèmes !
Et bien non, il reste encore quelque chose à voir. Vous vous souvenez de ce que j'ai dit au début, sur une éventuelle attaque au « buffer overflow » ? Et bien nous allons voir ça tout de suite.