Перейти к содержимому
Topos Uranos

Краткое содержание

Всё, что программа вычисляет, исчезает, когда она завершается, если только она не запишет это туда, где оно её переживёт. Этот урок представляет такое место, текстовый файл, и идею, которая позволяет пользоваться им, не изучая ничего принципиально нового: файл читается и записывается как поток, теми же операциями, что клавиатура и экран. Изучаются std::ifstream и std::ofstream, контроль того, что файл открылся, прежде чем им пользоваться, и рабочая папка, от которой зависит, где он ищется; правильный цикл чтения, while (std::getline(in, line)), в сравнении с циклом на eof(), который обрабатывает лишнее значение, и точная причина появления этого значения; запись с помощью std::println, режим дозаписи и закрытие при выходе из блока. Завершают урок форматы строк: разделение полей запятыми, их проверка корректности с помощью std::from_chars и решение, принимаемое в спецификации, что делать со строками неверного формата.

Цели обучения

  1. Открывать файл для чтения или для записи и проверять, что он открылся, прежде чем им пользоваться, отличая несуществующий файл от файла, который ищется в другой папке.
  2. Читать файл строка за строкой с помощью while (std::getline(in, line)) и объяснять, следя за состоянием потока, почему while (!in.eof()) обрабатывает лишнее значение.
  3. Записывать результаты в файл с помощью std::println(out, …), выбирая между заменой его содержимого и дозаписью в конец.
  4. Обрабатывать файл значений, разделённых запятыми, в вектор записей, проверяя корректность каждой строки и сообщая о неверных с указанием их номера.

Файл есть поток

Долговременное хранение

Переменные программы живут в памяти, а память программы освобождается, когда она завершается: следующий запуск начинается, ничего не помня. Чтобы значение пережило программу, которая его получила, оно должно остаться на постоянном носителе, и носителем общего назначения является файл: последовательность байтов с именем, хранящаяся на диске, которую операционная система сохраняет между запусками. Текстовый файл есть файл, байты которого являются символами, в UTF-8, как было показано в уроке Текст не сводится к ASCII, организованными в строки, разделённые символом '\n'. Это самый скромный и самый долговечный формат: его открывает любой редактор, читает любой язык, и человек может просмотреть его невооружённым глазом.

Однако C++ не считает файл чем-то отличным от клавиатуры. Начиная с урока Ввод, вывод и ошибки, std::cin есть поток ввода: источник символов, которые потребляются по порядку, с состоянием, фиксирующим, не закончилась ли неудачей последняя операция. Файл, открытый для чтения, есть другой поток ввода, типа std::ifstream (от input file stream); файл, открытый для записи, есть поток вывода, типа std::ofstream. Оба объявлены в заголовке <fstream>. Отсюда следует, что всё изученное о std::cin в уроке Надёжное чтение ввода, оператор >>, std::getline и состояние ошибки, без изменений годится и для файла.

Открыть и проверить

Файл открывается при создании потока, с именем файла в фигурных скобках: std::ifstream in{"grades.txt"};. Открытие может не удаться (файл не существует, нет права на его чтение, имя написано с ошибкой), и неудача не останавливает программу и не выводит никакого сообщения: она оставляет поток в состоянии ошибки. Поток в состоянии ошибки, использованный как условие, даёт false; поэтому контроль выполняется условием if (!in), и делать его нужно до первого чтения. Следующая программа пытается открыть несуществующий файл.

Программа на C++
#include <fstream>#include <iostream>#include <print>​int main(){    std::ifstream in{"grades.txt"};    if (!in) {        std::println(std::cerr, "Не удалось открыть grades.txt");        return 1;    }    int first{};    in >> first;    std::println("Первая оценка: {}", first);}
Вывод
Не удалось открыть grades.txt

Программа завершилась с кодом 1.

Три детали заслуживают внимания. Первая есть std::cerr (из <iostream>), поток ошибок: он пишет на экран, как стандартный вывод, но это отдельный канал, так что сообщения об ошибках не смешиваются с результатами, когда вывод перенаправляется в файл; std::println принимает первым аргументом поток, в который должен писать. Вторая есть return 1;: ненулевой код завершения, как было показано в уроке Анатомия программы, сообщает тому, кто запустил программу, что что-то пошло не так. Третья есть то, что произошло бы без этого контроля: in >> first на потоке в состоянии ошибки ничего не делает, и программа совершенно спокойно вывела бы «Первая оценка: 0». Ложный результат, похожий на истинный, хуже сообщения об ошибке; отсюда правило этого урока: каждый файл проверяется при открытии.

Относительные пути и рабочая папка

Имя "grades.txt" не говорит, в какой папке находится файл. Такое имя есть относительный путь, и оно разрешается относительно рабочей папки: папки, из которой запускается программа и которая не обязательно совпадает ни с папкой исполняемого файла, ни с папкой исходного кода. Следующая программа, сохранённая в папке project рядом с файлом data.txt, открывает этот файл и выводит его первую строку; её запустили дважды, сначала из project, а затем из родительской папки.

Терминал
$ cd project$ g++ -std=c++23 -Wall -Wextra -Wpedantic -Wconversion -Wshadow -g -fsanitize=address,undefined -fno-sanitize-recover=undefined main.cpp -o main$ ./mainПервая строка: Привет из файла$ cd ..$ ./project/mainНе удалось открыть data.txt

Исполняемый файл тот же, и файл данных остался на своём месте; изменилась лишь рабочая папка, а вместе с ней и место, где ищется data.txt. Это самая частая причина ситуации «не находит файл». Редактор, запускающий программу по кнопке, сам выбирает рабочую папку (в Visual Studio Code обычно это папка, открытая в редакторе, хотя это зависит от настройки задачи), так что, пока неизвестно, что делает редактор, лучше запускать программу из терминала, находясь в папке с данными. Противоположный соблазн, записать в коде абсолютный путь, например "/home/ana/curso/data.txt" или "C:\\Users\\Ana\\data.txt", решает проблему на одной машине и создаёт её на всех остальных: программа перестаёт работать, как только её запускает другой человек или папку перемещают. Рекомендуется относительный путь при известной рабочей папке или, ещё лучше, имя файла как данные, которые сообщает пользователь.