La saisie sécurisée en C++

La saisie sécurisée en C++

Mis à jour le mardi 19 novembre 2013

Quand on débute le C++, on découvre avec émerveillement la simplicité des fonctions d'entrée/sortie par rapport au C, plus besoin de spécifier le type de la variable passée en paramètre, plus besoin de mettre des "&" de partout.

Prenons par exemple le code suivant, que l'on rencontre très couramment :

#include <iostream>
#include <cstdlib>

using namespace std;

int main()
{
    unsigned short int age;

    cout << "Entrez votre age : ";
    cin >> age;

    char nom[15];

    cout << "Entrez votre nom : ";
    cin >> nom;

    cout << endl << "Bonjour " << nom << ", vous avez " << age << " ans." << endl;

    return EXIT_SUCCESS;
}

Les petits malins auront vite fait de remarquer qu'il y a quelques failles dans l'utilisation de ces méthodes. En effet, il suffit d'entrer tout sauf un nombre pour que le programme ne fonctionne plus du tout comme prévu.

Entrez votre age : n'importe
Entrez votre nom : 
Bonjour ╝[¼t@'@, vous avez 32509 ans

Et là nous nous retrouvons face à non pas un, mais deux problèmes bien ennuyeux, nous n'avons ni l'âge de l'utilisateur, ni son nom :(.

A ce stade, vous avez trois possibilités :

  1. Arrêter de programmer.

  2. Arrêter d'interagir avec l'utilisateur (ce qui limiterait un peu vos programmes).

  3. Utiliser la saisie sécurisée afin que ce genre de désagréments ne puisse pas survenir.

Fonctionnement de cin

Bien, si vous en êtes à lire ces lignes c'est que vous avez opté pour la troisième solution, alors attaquons le sujet :pirate: .

Pour comprendre ce que l'on a obtenu, il faut en savoir plus sur cin .

L'état du flux

cin permet de manipuler l'entrée standard, qui est généralement (mais pas toujours) le clavier, il est ce que l'on appelle un flux d'entrée, quelque chose qui permet de manipuler les informations saisies par l'utilisateur. Lorsque l'on réalise une opération avec (comme par exemple cin >> age; ), il extrait des informations du flux d'entrée (il les sort du flux pour les mettre dans la variable en quelques sortes).
De plus, à chaque fois que l'on demande à cin d'extraire des données du flux, ce dernier enregistre des informations sur son état, à savoir le succès ou l'échec de l'opération d'extraction. Si tout s'est déroulé normalement, l'état sera bon, sinon le code nous indiquera la ou les erreurs survenues.

Voici les différentes méthodes disponibles :

  • good() : retourne true si le flux est valide, false dans le cas contraire.

  • bad() : retourne true s'il y a eu une erreur sur le flux, false dans le cas contraire.

  • fail() : retourne true s'il y a eu une erreur sur le flux ou que la dernière opération a échouée, false dans le cas contraire. Donc si bad() retourne true alors fail() retourne true (mais si bad() retourne false , fail() peut retourner true ).

  • eof() : retourne true si la fin du flux a été rencontrée lors d'une opération d'extraction (si le flux est un fichier, c'est tout a fait normal mais si le flux est l'entrée standard, c'est que l'on ne peut plus rien demander à l'utilisateur).

Ceci est très pratique car on pourra savoir ce qui s'est passé lors de la dernière opération. Mais il faut aussi savoir qu'avant toute extraction, cin fait une vérification sur son état, s'il y a une erreur (donc si good() retourne false ), alors il ne fait rien.
Dans notre exemple, comme il y a eu une erreur lorsqu'on a saisi n'importe alors qu'on nous demandait un nombre, l'état de cin est passé à « invalide » et il ne demande rien lorsque l'on voudrait avoir le nom de l'utilisateur.

Pour solutionner ce problème, il suffit d'utiliser la méthode clear() qui va effacer toutes les erreurs et donc permettre à cin de faire son boulot par la suite. Voici son prototype :

void clear(iostate state = goodbit);

clear() permet de définir l'état d'un flux.

Paramètres :

  • state : l'état à attribuer au flux, par défaut goodbit , soit aucune erreur.

On peut donc l'utiliser dans notre programme ce qui permet de continuer à demander la saisie à l'utilisateur même après une erreur.
Le code devient donc :

#include <iostream>
#include <cstdlib>

using namespace std;

int main()
{
    unsigned short int age;

    cout << "Entrez votre age : ";
    cin >> age;
    cin.clear();

    char nom[15];

    cout << "Entrez votre nom : ";
    cin >> nom;
    cin.clear();

    cout << endl << "Bonjour " << nom << ", vous avez " << age << " ans." << endl;

    return EXIT_SUCCESS;
}

Ce code produit la sortie suivante :

Entrez votre âge : n'importe
Entrez votre nom : 
Bonjour n'importe, vous avez 32509 ans.

Encore une fois nous n'avons pas réussi à récupérer le nom de l'utilisateur, mais nous avons tout de même quelque chose de stocké dans la variable nom , ce que nous avons tapé lorsque le programme nous a demandé notre âge.
Et pour comprendre ce qu'il s'est passé, il nous faut en savoir encore plus sur le fonctionnement de cin .

Une affaire de buffer

Contrairement à ce que l'on pourrait penser, lorsque l'on demande une information via l'objet cin , on ne demande pas à l'utilisateur de saisir quelque chose au clavier. cin regarde dans une zone en mémoire, où est stockée tout ce que l'utilisateur a entré pour notre programme, appelée buffer, qui représente le flux.
Lorsque cin veut extraire une information, il regarde dans le buffer, si celui-ci n'est pas vide, il tente d'extraire les données, sinon il demande à l'utilisateur d'entrer quelque chose au clavier, qui va donc se retrouver dans le buffer, puis il tente d'extraire des données ensuite. Si l'extraction réussit, les informations extraites sont retirées du buffer et la variable est modifiée, sinon, les informations restent dans le buffer et la variable reste inchangée.

D'accord mais qu'est-ce que ça peut bien nous faire de savoir comment ça marche ?

Et bien maintenant que l'on sait tout ça, il faut aller faire un tour par... la documentation :D .

Il est clairement indiqué dans cette dernière qu'après une opération d'extraction (réussie ou non), cin ne supprime pas ce qui reste dans le buffer. Donc le contenu non supprimé sera alors utilisé lors de la prochaine demande d'informations à l'utilisateur.
Dans notre cas, cin ne parvient pas à extraire un nombre du buffer pour le stocker dans la variable age donc il ne la modifie pas. Et lorsque l'on demande le nom de l'utilisateur, il extrait ce que contenait le buffer et le place dans la variable nom .

Pour résoudre ce problème, il nous faut vider le buffer après chaque opération d'extraction. Il existe justement une méthode permettant de vider le buffer, ignore() , dont le prototype est le suivant :

istream &ignore(streamsize n = 1, int delim = EOF);

ignore() permet d'extraire des caractères du buffer (en les ignorant). L'extraction se termine lorsque n caractères ont été extrais ou que le caractère delim est rencontré (qui est aussi extrait). La valeur retournée est une référence vers l'objet qui a utilisé la méthode.

Paramètres :

  • n : le nombre de caractères à extraire. Par défaut 1 seul.

  • delim : le caractère auquel s'arrêter s'il est rencontré. Par défaut c'est le caractère de fin de ligne.

C'est bien beau mais quelle valeur on va donner à n pour être sûr que le buffer soit vidé en entier

Et bien là encore, la librairie standard a tout prévu.
La classe numeric_limits permet, entre autres, de connaitre la valeur maximale que peut prendre une variable d'un type numérique donné via la méthode statique numeric_limits<T>::max() , dont voici le prototype :

T max();

numeric_limits<T>::max() retourne une variable de type T dont la valeur est la valeur la plus grande que puisse prendre une variable de ce type.

La librairie standard fournit aussi la classe streamsize qui représente la taille d'un flux.
Pour vider entièrement notre buffer, on fera donc cin.ignore(numeric_limits<streamsize>::max()); (numeric_limits<streamsize>::max() retourne la taille maximale d'un flux, donc la taille maximale du buffer).

Première approche

Notre programme devient donc :

#include <iostream>
#include <cstdlib>
#include <limits> // Permet d'avoir accès à la classe numeric_limmits

using namespace std;

int main(int argc, char **argv)
{
    unsigned short int age;

    cout << "Entrez votre âge : ";
    cin >> age;
    cin.clear(); // Il faut quand même utiliser clear() car ignore() est une méthode d'extraction
    cin.ignore(numeric_limits<streamsize>::max());

    char nom[15];

    cout << "Entrez votre nom : ";
    cin >> nom;
    cin.clear(); // Il faut quand même utiliser clear() car ignore() est une méthode d'extraction
    cin.ignore(numeric_limits<streamsize>::max());

    cout << endl << "Bonjour " << nom << ", vous avez " << age << " ans." << endl;

    return EXIT_SUCCESS;
}

Voici la sortie obtenue :

Entrez votre âge : n'importe
Entrez votre nom : ooprog

Bonjour ooprog, vous avez 32509 ans.

Cette fois-ci on demande bien à l'utilisateur de saisir son nom même s'il entre un âge invalide.

Approche plus sécuritaire

On pourrait se demander ce qu'il advient lorsque l'on utilise deux fois consécutivement cette méthode. Et bien essayons !

#include <iostream>
#include <cstdlib>
#include <limits>

using namespace std;

int main(int argc, char **argv)
{
    unsigned short int age;

    cin >> age;

    /* Premier vidage */
    cin.clear();
    cin.ignore(numeric_limits<streamsize>::max());

    /* Second vidage */
    cin.clear();
    cin.ignore(numeric_limits<streamsize>::max());

    return EXIT_SUCCESS;
}

Résultat, le programme se met en pause. Afin d'éviter cela nous avons deux solutions :

  1. Faire attention quant à l'utilisation de la méthode ignore().

  2. Faire en sorte de ne vider le buffer que lorsqu'il n'est pas vide.

Il va de soi que la deuxième solution est la meilleure. Aussi nous faut-il un moyen de savoir si le buffer est vide.
cin ne dispose d'aucune méthode retournant la taille du buffer mais par contre, on peut se déplacer à l'intérieur, via la méthode seekg(), dont voici le prototype :

istream &seekg(streamoff off, ios_base::seekdir dir);

seekg() permet de se positionner dans le buffer. La valeur retournée est une référence vers l'objet qui a utilisé la méthode. De plus si l'on essaie d'aller à une position qui n'existe pas, l'état est modifié et on pourra le savoir en utilisant la méthode fail() .

Paramètres :

  • off : la position à laquelle se rendre à partir de dir .

  • dir : point de départ du déplacement, peut prendre les valeurs ios::beg , ios::cur ou ios::end , pour commencer respectivement à partir du début, de la position courante ou de la fin.

Donc pour savoir si le buffer est vide, on pourra faire :

cin.seekg(0, ios::end); // On se positionne à la fin du buffer

if(!cin.fail())
{
    /* Le buffer n'est pas vide */
}

else
{
    /* Le buffer est vide */
}

Donc pour vider notre buffer uniquement s'il est vide, on fait :

cin.clear();
cin.seekg(0, ios::end);

if(!cin.fail())
{
    cin.ignore(numeric_limits<streamsize>::max()); // Le flux a déjà un état valide donc inutile de faire appel à clear()
}

else
{
    cin.clear(); // Le flux est dans un état invalide donc on le remet en état valide
}

Voilà une méthode qui marche parfaitement.
J'en profite pour créer une fonction qui permettra de vider le buffer.

void vider_buffer()
{
    cin.clear();
    cin.seekg(0, ios::end);

    if(!cin.fail())
    {
        cin.ignore(numeric_limits<streamsize>::max()); // Le flux a déjà un état valide donc inutile de faire appel à clear()
    }

    else
    {
        cin.clear(); // Le flux est dasn un état invalide donc on le remet en état valide
    }
}

Maintenant que nous avons solutionné le second problème, il nous faut passer au premier, à savoir la validation des données entrées par l'utilisateur.

Petite digression

J'en profite pour ouvrir une petite parenthèse :
Il vous est sûrement déjà arrivé de vouloir faire une pause durant votre programme, demandant à l'utilisateur d'appuyer sur entrée. Pour ce faire, vous utilisez sûrement system("pause"); . Or, ne vous a-t-on jamais dit qu'il fallait éviter au maximum la fonction system() ? (et de tout façon maintenant c'est fait !).

Quel rapport avec ce qu'on fait ici ?

Et bien notre fonction vider_buffer() nous assure que le buffer est vidé après son utilisation. Donc un simple cin.ignore(numeric_limits<streamsize>::max()); va provoquer une demande d'entrée à l'utilisateur.

Donc voici une fonction dont vous pourrez abuser :

void pause()
{
    vider_buffer();

    cin.ignore(numeric_limits<streamsize>::max());
}

Voilà la parenthèse est finie et je ne veux plus jamais voir de system("pause"); dans vos codes !

Validation de l'entrée

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 :

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

Entrez votre âge : n'importe
Erreur, saisie incorrecte.

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

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

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 :

Entrez votre âge : n'importe
Erreur, saisie incorrecte.
Entrez votre âge : 14a
Vous avez 14 ans.

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

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 :D .
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 :

#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 :D !
Voici la sortie que produit le programme 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.

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 :

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 :

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

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

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 :

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 :

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.

La saisie des chaînes de caractères

Cette fois je ne laisse pas le suspense planer longtemps, nous allons nous attarder sur les chaîne de caractères.
En effet, jusqu'alors nous n'avons travailler qu'avec des variables numériques ou alors avec un seul caractère à la fois. Or je ne suis pas censé vous apprendre que les chaînes de caractères sont des tableaux de caractères.

Quelques problèmes de plus

Une question d'espaces

Essayons un petit programme :soleil: :

#include <iostream>
#include <cstdlib>

using namespace std;

int main()
{
    char nom[200];

    cout << "Entrez vos nom et prénom(s) : ";

    cin >> nom;

    cout << "Bonjour " << nom << "." << endl;

    return EXIT_SUCCESS;
}

Voici le résultat obtenu :

Entrez vos nom et prénom(s) : Pierre Dupond
Bonjour Pierre.

Et oui, la saisie a été tronquée.

La documentation est très claire sur le sujet, l'extraction s'achète dès que le flux rencontre un espace (que ce soit un espace "normal" ou un retour du chariot). C'est donc le premier problème qu'il nous faudra résoudre.

Une question d'espace

Non vous n'avez pas louché, c'est bien le même titre, à un caractère près.
J'espère ne rien vous apprendre en vous disant que les chaînes de caractères sont des tableaux de caractères et que ces tableaux ont des tailles fixes, car c'est de là que va venir notre prochain problème.

Testons le code suivant :

#include <iostream>
#include <cstdlib>

using namespace std;

int main()
{
    char nom[5];

    cout << "Entrez votre nom : ";
    cin >> nom;

    cout << "Bonjour " << nom << "." << endl;

    return 0;
}

Nous obtenons un résultat tout à fait normal a priori :

Entrez votre nom : Jacques
Bonjour Jacques.

Mais en réfléchissant quelques secondes, il y a quelque chose de troublant.
La chaîne de caractères "Jacques" devrait prendre 8 cases en mémoire (les 7 caractères de "Jacques" et le caractère '\0' ) or notre variable nom n'en fait que 5. Alors que s'est-il passé ?

Et bien cin a tout simplement écrit à la suite dans la mémoire, c'est à dire dans des cases mémoire n'étant pas réservées pour la variable nom (quel culot il a cet objet :lol: ).
Cela provoque deux problèmes :

  1. Nous écrivons dans un emplacement peut-être utilisé par un autre programme, ce qui pourrait gêner son exécution.

  2. Un hacker pourrait introduire du code dans la mémoire et le faire exécuter, ce qui pourrait s'avérer très dangereux pour notre ordinateur.

Il nous faudra donc résoudre ce problème aussi.

Récupérer une ligne

Cas général

Il existe trois fonctions permettant de récupérer une ligne entière :

  • get() : une méthode de cin qui permet de récupérer une ligne et de la stocker dans un tableau de caractères (char * )

  • getline() : une méthode de cin qui fonctionne comme get() sauf qu'elle supprime du flux le '\n' final.

  • getline() : une fonction qui permet de récupérer une ligne et de la stocker dans une instance de string (elle supprime aussi le '\n' final).

Etant donné que nos fonctions travaillent avec le type string , nous utiliserons getline() , dont voici le prototype :

istream &getline(istream &is, string &str, char delim = '\n');

Les paramètres sont :

  • is : le flux sur lequel opérer, nous utiliserons cin .

  • str : l'objet string à modifier.

  • delim : le caractère délimiteur. Par défaut '\n'

On peut utiliser getline() pour toutes les saisies, cela rend la validation encore plus stricte (avant, l'utilisateur pouvait entrer "27 n'importe quoi" sans qu'il n'y ait d'erreur).
Nos fonctions deviennent donc :

bool saisir_string(string &variable, const string &message)
{
    while(true)
    {
        cout << message;

        getline(cin, variable); // getline() ici est une fonction et non une méthode de la classe istream

        if(cin.bad() || cin.eof())
        {

            cerr << "Erreur, saisie incorrecte." << endl;

            if(cin.eof())
            {
                break;
            }

            vider_buffer();
        }

        else if(cin.fail())
        {
            cerr << "Erreur, saisie incorrecte." << endl;

            vider_buffer();
        }

        // L'utilisation de vider_buffer() n'est plus nécessaire car getline() supprime le caractère '\n'

        break;
    }

    return true;
}

bool saisir_int(int &variable, const string &message)
{
    string temp;

    while(saisir_string(temp, message)) // On utilise saisir_string sur temp pour obtenir ce que l'utilisateur a entré
    {
        istringstream stream(temp);
        stream >> variable;

        if(stream.fail() || !stream.eof())
        {
            cerr << "Erreur, saisie incorreecte." << endl;
        }

        else
        {
            return true;
        }
    }

    return false;
}

Comme vous pouvez le voir, on définit une fonction saisir_string() qui permet de réaliser la saisie sécurisée du type string (car il peut y avoir un problème au niveau de cin lors de l'opération). On utilise ensuite cette fonction dans toutes les autres.
Voilà qui résout notre premier problème, et permet une validation plus stricte :

Entrez votre âge : 28 ytg
Erreur, saisie incorrecte.
Entrez votre âge : 28
Vous avez 28 ans.

Cas particulier : les tableaux de caractères

Il reste encore le problème du "buffer overflow", qui ne s'applique que lorsqu'on veut stocker des informations dans un char * .
Nous pourrions bannir ce type de variable dans nos programmes et n'utiliser que string mais comme je l'ai déjà noté plus haut, en C++ aussi, les chaînes de caractères sont des tableaux de caractères (et en plus, ce ne serait pas marrant :p ).

On a vu deux méthodes de cin qui permettent de saisir une ligne et de la stocker dans un tableau de char , get() et getline() .

Nous allons utiliser getline() , dont voici le prototype :

istream &getline(char *s, streamsize n, char delim = '\n');

Les paramètres sont :

  • s : la chaîne de caractères dans laquelle stocker les données extraites.

  • n : la taille de la chaîne de caractères.

  • delim : le caractère auquel s'arrêter s'il est rencontré. Par défaut '\n' .

Au cas où vous demanderiez comment on va indiquer à l'utilisateur qu'il a entré trop de caractères, sâchez que getline() invalide cin si cela survient.
On va donc pouvoir utiliser cette méthode pour saisir des chaînes de caractères sans risquer de dépasser la taille de notre chaîne.

Nous allons donc créer une nouvelle fonction saisir_chaine_caracteres() dont voici le prototype :

bool saisir_chaine_caracteres(char *chaine, const unsigned int taille_tableau, const string &message);

Cette fonction retourne true en cas de succès ou false en cas d'erreur.

Les paramètres sont :

  • message : pointeur vers une chaîne de caractères valide.

  • taille_tableau : taille du tableau de caractères.

  • message : le message à afficher à l'utilisateur à chaque fois qu'il devra entrer quelque chose au clavier (une fois au minimum).

Mais attends, inutile de passer la taille maximale, il y a la fonction sizeof() qui retourne la taille d'une variable en octet, comme un char fait 1 octet, alors sizeof(chaine) retourne le nombre de cases du tableau non ?

Et ben non :p .
sizeof est un opérateur, c'est à dire que la taille de l'objet que l'on lui passe en paramètre est calculée durant la compilation et non l'exécution. Donc il est incapable de donner la taille des tableaux créés dynamiquement (lors de l'exécution).

Il y a la fonction strlen() qui retourne la taille d'une chaîne de caractères, on se limite à cette taille et le tour est joué !

Et ben re non :p .
strlen() est bien une fonction mais elle ne fait que rechercher le caractère '\0' . Donc si on lui donne en paramètre un tableau de caractères qui ne contient pas '\0' (comme on le fait quand on fait char nom[100]; puis strlen(nom); ), strlen() ne retournera pas une valeur cohérente.

On peut toujours supprimer l'ancien tableau et allouer un nouveau tableau avec une taille suffisante non ?

Encore non :p .
Dans ce cas ce sont les tableaux créés automatiquement (durant la compilation) qui poseront problème car on ne peut pas convertir un tableau de taille x en un tableau d'une autre taille s'il est créé par le compilateur (on a une erreur lors de la compilation du genre "cannot convert char[x] to char *").

On est donc obligé de demander la taille maximale alors ?

Exactement, et voici l'implémentation de la fonction :

bool saisir_chaine_caracteres(char *chaine, const unsigned int taille_tableau, const string &message)
{
    while(true)
    {
        cout << message;
        cin.getline(chaine, taille_tableau);

        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 << "Veuillez entrez au maximum " << (taille_tableau - 1) << " caractères." << endl;

            vider_buffer();

            continue;
        }

        vider_buffer();

        break;
    }

    return true;
}

Et voici le petit programme vu en introduction, mais utilisant nos fonctions :

#include "fonctions_saisir.hpp"
#include <iostream>
#include <cstdlib>

using namespace std;

int main()
{
    unsigned short int age;

    saisir_unsigned_short_int(age, "Entrez votre âge : ");

    char nom[15];

    saisir_chaine_caracteres(nom, 15, "Entrez vos nom et prénom(s) : ");

    /* Il serait préférable d'utiliser la classe string ici :
    string nom;

    saisir_string(nom, "Entrez vos nom(s) et prénom(s) : ");
    */

    cout << "Bonjour " << nom << ", vous avez " << age << " ans." << endl;

    return EXIT_SUCCESS;
}

Et le résultat :

Entrez vos nom et prénom(s) : Jean Philippe Delaunay
Veuillez entrer au maximum 14 caractères.
Entrez vos nom et prénom(s) : Jean Delaunay
Entrez votre âge : 24gfh
Erreur, saisie incorrecte.
Entrez votre âge : 33 adfr
Erreur, saisie incorrecte.
Entrez votre âge : 26
Bonjour Jean Delaunay, vous avez 26 ans.

C'est bien le résultat attendu, les variables contiennent les informations voulues et il n'y a plus de risque de buffer overflow, victoire !

Nous avons vu comment sécuriser nos programmes vis-à-vis des entrées utilisateurs. Avec des méthodes similaires, vous pouvez sécuriser les entrées effectuées avec les fichiers (n'utilisez pas de boucle car vous bloqueriez votre programme). Utilisez ces fonctions pour toutes les saisies de l'utilisateur si vous voulez des programmes sûrs.

Si vous connaissez les fonctions templates, vous pouvez très bien créer une fonction pour la saisie sécurisée de n'importe quel type (et n'oubliez pas de créer des spécifications pour les types numériques).

Pour les intéressés, vous pouvez aussi télécharger une suite de fonctions que j'ai codé qui contient tout ce qui est nécessaire pour faire de la saisie sécurisée en C++.
Son utilisation est très simple :

#include <cstdlib>
#include "input.hpp" // Fichier d'en-tête contenant les fonctions

using namespace std;
using namespace input; // Les fonctions sont dasn un namespace nommé input

int main()
{
    unsigned short int age;

    get(age, "Entrer votre âge : "); // Pour la plupart des types, on utilise get()

    char nom[15];

    get_string(nom, 15, "Entrez votre nom : "); // Seule exception, le type char *, que l'on doit manipuler avec get_string()

    cout << endl << "Bonjour " << nom << ", vous avez " << age << " ans." << endl;

    return EXIT_SUCCESS;
}

déroulement d'un cours

  • 1

    Dès aujourd'hui, vous avez accès au contenu pédagogique et aux exercices du cours.

  • 2

    Vous progressez dans le cours semaine par semaine. Une partie du cours correspond à une semaine de travail de votre part.

  • !

    Les exercices doivent être réalisés en une semaine. La date limite vous sera annoncée au démarrage de chaque nouvelle partie. Les exercices sont indispensables pour obtenir votre certification.

  • 3

    À l'issue du cours, vous recevrez vos résultats par e-mail. Votre certificat de réussite vous sera également transmis si vous êtes membre Premium et que vous avez au moins 70% de bonnes réponses.

L'auteur

Exemple de certificat de réussite
Exemple de certificat de réussite