Aller au contenu
Topos Uranos

Résumé

Tout ce qu’un programme calcule disparaît quand il se termine, à moins qu’il ne le laisse écrit dans un endroit qui lui survive. Cette leçon présente cet endroit, le fichier texte, et l’idée qui permet de l’utiliser sans rien apprendre de radicalement nouveau : un fichier se lit et s’écrit comme un flux, avec les mêmes opérations que le clavier et l’écran. On étudie std::ifstream et std::ofstream, la vérification qu’un fichier a été ouvert avant de l’utiliser et le répertoire de travail, qui décide où on le cherche ; la boucle de lecture correcte, while (std::getline(in, line)), face à la boucle avec eof(), qui traite une donnée de trop, et la raison exacte de cette donnée ; l’écriture avec std::println, le mode ajout et la fermeture à la sortie du bloc. Les formats de ligne terminent la leçon : séparer des champs par des virgules, les valider avec std::from_chars et décider, dans la spécification, que faire des lignes mal formées.

Objectifs d’apprentissage

  1. Ouvrir un fichier en lecture ou en écriture et vérifier qu’il a été ouvert avant de l’utiliser, en distinguant le fichier qui n’existe pas de celui qu’on cherche dans un autre répertoire.
  2. Lire un fichier ligne par ligne avec while (std::getline(in, line)) et expliquer, en suivant l’état du flux, pourquoi while (!in.eof()) traite une donnée de trop.
  3. Écrire des résultats dans un fichier avec std::println(out, …), en choisissant entre remplacer son contenu et ajouter à la fin.
  4. Traiter un fichier de valeurs séparées par des virgules dans un vecteur d’enregistrements, en validant chaque ligne et en signalant les lignes non valides avec leur numéro.

Un fichier est un flux

La persistance

Les variables d’un programme vivent dans la mémoire, et la mémoire d’un programme est libérée quand il se termine : l’exécution suivante commence sans se souvenir de rien. Pour qu’une donnée survive au programme qui l’a produite, elle doit rester sur un support permanent, et le support d’usage général est le fichier : une suite d’octets dotée d’un nom, enregistrée sur le disque, que le système d’exploitation conserve d’une exécution à l’autre. Un fichier texte est un fichier dont les octets sont des caractères, en UTF-8 comme on l’a vu dans la leçon Le texte n’est pas ASCII, organisés en lignes séparées par le caractère '\n'. C’est le format le plus modeste et le plus durable : n’importe quel éditeur l’ouvre, n’importe quel langage le lit, et une personne peut le relire à l’œil nu.

Or, C++ ne traite pas le fichier comme une chose différente du clavier. Depuis la leçon Entrée, sortie et erreurs, std::cin est un flux d’entrée : une source de caractères qui se consomment dans l’ordre, avec un état qui enregistre si la dernière opération a échoué. Un fichier ouvert en lecture est un autre flux d’entrée, de type std::ifstream (de input file stream) ; un fichier ouvert en écriture, un flux de sortie, de type std::ofstream. Tous deux sont déclarés dans l’en-tête <fstream>. Il s’ensuit que tout ce qu’on a appris sur std::cin dans la leçon Bien lire, l’opérateur >>, std::getline et l’état d’erreur, vaut sans changement pour un fichier.

Ouvrir et vérifier

Un fichier s’ouvre à la création du flux, avec le nom du fichier entre accolades : std::ifstream in{"grades.txt"};. L’ouverture peut échouer (le fichier n’existe pas, on n’a pas la permission de le lire, le nom est mal écrit), et l’échec n’arrête pas le programme ni ne produit aucun message : il laisse le flux dans un état d’erreur. Un flux en erreur, utilisé comme condition, vaut false ; la vérification est donc if (!in), et elle doit se faire avant la première lecture. Le programme suivant essaie d’ouvrir un fichier qui n’existe pas.

Programme en C++
#include <fstream>#include <iostream>#include <print>​int main(){    std::ifstream in{"grades.txt"};    if (!in) {        std::println(std::cerr, "Impossible d’ouvrir grades.txt");        return 1;    }    int first{};    in >> first;    std::println("Première note : {}", first);}
Sortie
Impossible d’ouvrir grades.txt

Le programme s’est terminé avec le code 1.

Trois détails méritent l’attention. Le premier est std::cerr (de <iostream>), le flux d’erreur : il écrit à l’écran comme la sortie standard, mais c’est un canal distinct, de sorte que les messages d’erreur ne se mélangent pas aux résultats quand la sortie est redirigée vers un fichier ; std::println accepte comme premier argument le flux dans lequel il doit écrire. Le deuxième est le return 1; : un code de terminaison non nul, comme on l’a vu dans la leçon Anatomie d’un programme, avertit celui ou celle qui a exécuté le programme que quelque chose s’est mal passé. Le troisième est ce qui serait arrivé sans la vérification : in >> first sur un flux en erreur ne fait rien, et le programme aurait écrit « Première note : 0 » en toute tranquillité. Un résultat faux qui a l’apparence du vrai est pire qu’un message d’erreur ; d’où la règle de cette leçon : tout fichier se vérifie à l’ouverture.

Chemins relatifs et répertoire de travail

Le nom "grades.txt" ne dit pas dans quel répertoire se trouve le fichier. Un tel nom est un chemin relatif, et il se résout à partir du répertoire de travail : le répertoire depuis lequel on exécute le programme, qui n’est pas nécessairement celui de l’exécutable ni celui du code source. Le programme suivant, enregistré dans le répertoire project à côté d’un fichier data.txt, ouvre ce fichier et écrit sa première ligne ; on l’a exécuté deux fois, d’abord depuis project, puis depuis le répertoire parent.

Terminal
$ cd project$ g++ -std=c++23 -Wall -Wextra -Wpedantic -Wconversion -Wshadow -g -fsanitize=address,undefined -fno-sanitize-recover=undefined main.cpp -o main$ ./mainPremière ligne : Bonjour depuis le fichier$ cd ..$ ./project/mainImpossible d’ouvrir data.txt

L’exécutable est le même, et le fichier est toujours à sa place ; la seule chose qui a changé est le répertoire de travail, et avec lui l’endroit où l’on cherche data.txt. C’est la cause la plus fréquente du « fichier introuvable ». Un éditeur qui exécute le programme d’un clic sur un bouton choisit de lui-même le répertoire de travail (dans Visual Studio Code, c’est en général le dossier ouvert dans l’éditeur, bien que cela dépende de la configuration de la tâche), de sorte qu’il convient d’exécuter depuis le terminal, dans le répertoire des données, tant qu’on ne sait pas ce que fait l’éditeur. La tentation inverse, écrire dans le code un chemin absolu comme "/home/ana/curso/data.txt" ou "C:\\Users\\Ana\\data.txt", résout le problème sur une seule machine et le crée sur toutes les autres : le programme cesse de fonctionner dès qu’une autre personne l’exécute ou que le répertoire est déplacé. Ce qui est recommandable, c’est le chemin relatif avec un répertoire de travail connu ou, mieux, le nom du fichier comme donnée fournie par l’utilisateur.