Bonjour,
j'ai un peu de temps à consacrer à votre question, essayons de le mettre à profil.
Si vous souhaitez apprendre à coder avec un C++ propre et au goût du jour, voici un aglomérat de remarques concernant votre programme. Coder correctement en C++ c'est :
- une meilleure lisibilité qui elle permet une meilleure transitivité de la pensée au code (j'écris ce que je pense), et du code à la pensée (le code décrit quoi faire au lieu de comment faire) ;
- être concis : moins de code = moins de busg et moins d'efforts à lire et à écrire ;
- des idiomes : il y a tant de façons différente d'opérer la même tâche ; utiliser la méthode idiomatique en C++ réduit le nombre de WTF/ligne, permet un code plus facile à lire (voir lisibilité ci-dessus) et une meilleure communication avec les autres développeurs C++.
Et quand on code propre, on écrit du code qui marche. Du premier coup (si !). Lanssons-nous.
1. Inclusions
Il faut inclure, tout ce qui est nécessaire, seulement ce qui est nécessaire, et préférer les inclusiosn C++ aux inclusions C.
Tous les standard headers du C (<math.h>, <string.h>, <stdlib.h>, etc.) ont été redéfinis en C++ : enlevez le ".h" à la fin du nom et préfixez d'un "c" (<cmath>, <cstring>, <cstdlib>, etc.). Notamment, ces nouvelles versions définissent leurs identifiants dans le namespace ::std. Voir ci-après.
Dans le code que vous donnez par exemple, vous n'utilisez pas ce qui est défini dans "math.h", ne l'incluez pas.
2. using namespace Considered harmful
Vous trouverez sur l'Internet de nombreux exemples de Vous trouverez aussi beaucoup d'articles valorisant les actions de l'Allemagne nazie (un point goldwin pour moi je vous prie) : n'écoutez pas sans esprit critique les conseils d'inconnus sur l'Internet.
En déclarant using namespace std;, vous importez dans le namespace global tous les identifiants définis dans ::std, c'est à dire beaucoup de choses, dont notamment beaucoup de mots très courants en programmation : swap, sort, array, vector, list, ... autant de mots que vous pourriez être amené à utiliser pour nommer une variable ou une fonction. En using namespace std;, vous courrez au devant de grands périls.
De plus, en déclarant using namespace std;, vous vous permettez d'utiliser des identifiants de ::std sans les préfixer de std::, ce qui rend moins évident pour vos lecteurs (y compris vous dans le futur) que vector est bel est bien std::vector et non une classe vector que vous avez défini ailleurs. Voir ce que je dis sur la lisibilité ci-dessus.
3. Formatter son code proprement
Choisissez un guide d'écriture et tenez-y vous ! Par exemple Google C++ Style Guide. Ou n'importe quel guide. Mais tenez-y vous.
Notamment :
- une ligne = une instruction = une ligne ;
- respecter le niveau d'indentation ;
- pas de ligne vide inutile ;
- déclarer les variables dans le plus petit scope possible, au plus près de leur première utilisation.
4. Divisez votre code en entités fonctionnelles
Ecrire un programme qui demande à l'utilisateur de saisir 10 entiers stockés dans un tableau ainsi qu'un entier V. Le programme doit rechercher si V se trouve dans le tableau et doit supprimer la première occurrence de V en décalant d'une case vers la gauche les éléments suivants et en rajoutant un 0 à la fin du tableau. Le programme doit ensuite afficher le tableau final.
L'énoncé vous demande de :
- lire des entiers saisis par l'utilisateur ;
- rechercher un entier dans un tableau d'entiers ;
- supprimer un élément d'un tableau (en décalant les éléments suivants).
Ces tâches ne devraient rien avoir à voir entre elles, et c'est l'agencement de ces tâches simples (donc courtes, lisibles, voir ci-avant) qui vous permettron d'écrire un programme.
5. Lire un entier saisi par l'utilisateur
Lorsque je lis "saisi par l'utilisateur", j'ai peur. L'utilisateur est un con. L'utilisateur peut saisir tout et n'importe quoi. Mais seul des entiers nous intéressent ici. Et c'est là que std::getline et std::stoi vont nous aider, en nous protégeant de tous les cas de figure (notamment, saisie de l'utilisateur erronée).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| #include <iostream>
#include <string>
#include <stdexcept>
// Prompts the user to input an integer value. Returns the first integer correctly input.
int prompt_integer(std::string prompt)
{
std::cout << prompt << std::flush;
for (std::string line ; std::getline(std::cin, line) ;) {
try {
return std::stoi(line);
} catch (std::exception&) {
std::cout << "invalid value: " << line << "\n" << prompt << std::flush;
}
}
return 0;
} |
L'interaction avec l"utilisateur est toujours complexe. Je vais vous laisser le soin de prendre le temps de comprendre cet exemple. Je ne vous expliquerai que la seconde ligne : la boucle for.
for (std::string line ; std::getline(std::cin, line) ;) { /*body */ }
Cet usage idiomatique casse l'habitude de la boucle for (initialisation ; test ; incrément) car le test modifie line. Suivons l'ordre d'exécution :
- Définition de la variable line (vide par défaut).
- getline lit une ligne de cin dans line et retourne true si la lecture s'est bien passée.
- S'il y a un problème de lecture (fin de fichier notamment, car oui l'entrée standart est un fichier qui peut se terminer), on sort de la boucle.
- Le /*body */ est exécuté (celà peut amener à return ... qui sort de la fonction et donc de la boucle).
- getline est appelée
- Le /*body */ est exécuté.
- ...
Une fois que ceci est compris, on comprend que la fonction prompt_integer demande à l'utilisateur de saisir un entier jusqu'à ce que celui-ci saisisse correctement un entier.
Voilà, le plus dur est fait. Il ne reste plus qu'à :
6. Rechercher un entier dans un tableau d'entier pour le supprimer
C'est comme sur l'appstore : il y a une fonction pour ça : std::remove.
1 2
| #include <algorithm>
std::remove(begin, end, value); |
Cela déplace l'element du tableau [begin ; end[ dont la valeur est value à la fin du tableau. Exactement ce que l'on veut. Il ne restera plus qu'à le supprimer après.
std::remove est en réalité une fonction template, begin et end peuvent avoir quasiment la forme que l'on veut : pointeurs, itérateurs, ...
7. Supprimer le dernier élément d'un tableau
Idem, y'a une fonction pour ça : la plupart des conteneurs de la bibliothèque standart ont une fonction membre erase qu'il suffit d'appeler. Cette fonction prend en paramettre la position de l'élément à supprimer qui s'avère être la valeur retournée par std::remove. La nature est bien faite !
Sauf qu'ici ... on ne veut pas supprimer à proprement parler notre valeur mais la remplacer par zéro. Qu'à cela ne tienne, std::remove retourne justement un itérateur sur l'élément qui a été déplacé à la fin du tableau. Il suffit d'écrire zéro dessus.
8 Emboiter le tout pour faire un programme
Si vous lisez ceci, c'est probablement que vous avez pris au sérieux ces conseils et que vous prenez le temps de les lire. Je vous donne donc ici mon implémentation de votre problème. Continuez à lire puis revenez dessus pour expérimenter.
On récapitule :
Ecrire un programme qui demande à l'utilisateur de saisir 10 entiers stockés dans un tableau ainsi qu'un entier V. Le programme doit rechercher si V se trouve dans le tableau et doit supprimer la première occurrence de V en décalant d'une case vers la gauche les éléments suivants et en rajoutant un 0 à la fin du tableau. Le programme doit ensuite afficher le tableau final.
Voici à quoi va donc ressembler notre main :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| int main()
{
// prompt array
std::array<int, 10> values = { 0 };
std::generate(std::begin(values),
std::end(values),
[](){ return prompt_integer("entier a stocker: "); }
);
// prompt value to search & destroy
const int persona_non_grata = prompt_integer("entier a supprimer: ");
// apply
find_and_remove_by_zeroing(persona_non_grata, values);
// print
std::cout << "Valeurs finales : ";
for (auto n : values) {
std::cout << n << ", ";
}
std::cout << "\n";
} |
Chaque élément de l'intitulé du problème est traduit par quelques lignes de code. Quelues précisions tout de même :
C'est équivalent à int values[10]; et il est parfaitement possible de définir values comme ça sans rien changer au reste du code ... mais franchement std::array est vraiment plus safe est plus riche ; à préférer.
std::generate(begin, end, generator);
Cela appelle generator() pour construire chaque élément de [begin ; end[ (voir doc de std::generate). Ici donc, chacun des éléments de values sera le resultat de l'appel à
[](){ return prompt_integer("entier a stocker: "); }
qui est une lambda : un objet anonyme s'utilisant de la même manière qu'une fonction ne prenant aucun argument (()) et retournant le résultat de prompt_integer("entier a stocker: "). Exactement ce qu'on veut.
C'est fonctionnellement équivalent à ce qui suit, mais c'est plus explicite (on dit clairement qu'on génère les valeurs) et si la taille de values change, le code de la génération n'a pas à changer.
1 2 3
| for (int i = 0 ; i < 10 ; ++i) {
values[i] = prompt_integer("entier a stocker: ");
} |
Enfin :
find_and_remove_by_zeroing(persona_non_grata, values);
Il ne reste plus qu'à la définir. Mais elle fait ce qu'elle dit faire.
9. Le mot de la fin
J'espère que ces conseils vous amèneront à écrire du meilleur code et vous fera apprécier le C++ récent plutôt que détester le "C with classes" qu'était le vieux C++. N'hésitez pas à poser des questions spécifiques sur certains aspects de ces exemples, et jouez avec le code pour essayer d'autres exercices (par exemple, ne plus lire 10 entiers mais un nombre d'entiers décidé par l'utilisateur).
Bonne chance.
Partager