Самостоятельная работа 12: файлы

Кувшинов Д.Р.

2016


Общее оглавление


12 баллов

Цели и критерии

Цель: закрепление навыков работы с текстовыми файлами средствами Стандартной библиотеки C++.

Критерии полноты

  1. Реализовать требуемую операцию в виде отдельных функций, способных работать с потоками ввода-вывода (std::istream, std::ostream). Для простоты предполагается использование системной 8-битной кодировки “по умолчанию”.
  2. Имена входных файлов вводятся как параметры командной строки. Если программа вызвана без параметров, то имена файлов вводятся через консоль (стандартный поток ввода).
  3. Результат работы выводится в стандартный поток вывода (если в задании не указано иное). Сообщения об ошибках (если есть) выводятся в стандартный поток вывода ошибок (std::cerr или std::clog).
  4. Программа должна быть способна обрабатывать произвольное количество входных файлов за один сеанс (не должно быть явных внутренних ограничений на количество файлов).
  5. Составить набор тестовых файлов и команду вызова, сравнивающую результат их обработки с заранее заготовленным результатом. Если требуется, результат программы можно сначала записать в файл через перенаправление потока вывода в файл, затем сравнить его на равенство с заранее подготовленным файлом — например с помощью команды fc.

“Рецепт” с командой fc

Пусть в текущем каталоге есть два текстовых файла result.txt и ref.txt и мы хотим сравнить их на равенство средствами командной строки. В этом случае удобно воспользоваться следующей командой (синтаксис cmd.exe):

fc result.txt ref.txt >nul && echo pass || echo fail

Команда выведет pass, если файлы совпадают и fail, если не совпадают.

В случае двоичных файлов достаточно добавить параметр /b:

fc /b result.bin ref.bin >nul && echo pass || echo fail

Нужную команду можно сохранить в виде .cmd файла, чтобы не вводить её вручную. Задержка экрана выполняется командой pause.

По использованию объектов fstream см. также здесь.


Пример 1

Для конкатенации заданных входных файлов вывести последовательность строк, пропуская (т.е. не сохраняя в новом файле) подряд идущие повторяющиеся строки из конкатенации исходных файлов.

Если очередной входной файл заканчивается не переводом строки, то следующий файл начинать с новой строки.

Решение

Для решения данной задачи удобно определить класс, по мере надобности открывающий нужные нам файлы.

class File_sequence
{
public:
  /// Конструктор по умолчанию == пустая последовательность файлов.
  File_sequence();

  /// Инициализировать очередь имён, передав диапазон имён файлов.
  template <class InIt>
  File_sequence(InIt from, InIt to);

  /// Добавить в конец очереди имя ещё одного файла.
  void push(const std::string &filename);

  /// Обратиться к текущему файлу как к потоку ввода (эту ссылку нельзя сохранять).
  std::istream& access_once();

  /// Проверить состояние текущего файла.
  /// Возвращает false либо из-за ошибки чтения (access_once().fail()), либо из-за того, что очередь закончилась.
  operator bool();
};

Фактически, данный интерфейс предлагает всего две возможности: добавить имена файлов, конкатенацию которых мы хотим получить (второй конструктор и push), и обратиться к текущему файлу (access_once). Если файл “кончился”, то функция access_once откроет следующий доступный файл.

Использовать данный класс можно так:

  1. Создать объект класса и указать имена файлов, которые он должен будет читать. За это отвечает конструктор и функция push, которая позволяет добавлять имена по одному.
  2. В цикле прочитать все строки всех файлов с помощью функции getline.

Для удобства определим обёртку стандартной функции getline так:

/// Считать одну строку, замена std::getline для File_sequence.
inline File_sequence& getline(File_sequence &fs, std::string &line)
{
  std::getline(fs.access_once(), line);
  return fs;
}

Теперь можно читать строки с помощью обычной конструкции на основе цикла for:

File_sequence fs;
fs.push(filename);

for (string line; getline(fs, line);)
  cout << line << '\n'; // выводим файл построчно на экран

В случае последовательности файлов:

File_sequence fs;
// Читать файлы с именами 1.txt, 2.txt, ..., 100.txt.
for (int i = 1; i < 101; ++i)
  fs.push(to_string(i) + ".txt");

for (string line; getline(fs, line);)
  cout << line << '\n'; // выводим всё построчно на экран


Для реализации File_sequence удобно использовать стандартный класс “очередь” (queue).

private:
  std::queue<std::string> filenames; // очередь имён файлов, ожидающих открытия.
  std::ifstream current_file;        // текущий файл.

В случае, если текущий файл кончился, или ещё ни один файл не был открыт, нужно открыть некоторый “следующий” файл, либо определить, что больше доступных файлов нет.

Определим функцию open_next_file, которая будет пытаться открыть файл, начиная с текущего имени и до конца, извлекая имена файлов из очереди filenames.

private:
  // Открыть следующий файл из очереди.
  void open_next_file()
  {
    current_file.close();
    while (!filenames.empty() && !current_file.is_open())
    {
      current_file.open(filenames.front());
      filenames.pop();
    }
  }

Теперь можно определить функции открытого интерфейса.

Конструкторы:

public:
  /// Конструктор по умолчанию.
  File_sequence() {}

  /// Задать набор файлов диапазоном имён.
  template <class InIt>
  File_sequence(InIt from, InIt to)
    : filenames(std::deque<std::string>(from, to)) {}
  // std::queue по умолчанию работает поверх объекта std::deque,
  // который можно передать конструктору queue.
  // Мы используем конструктор deque для создания объекта deque из диапазона элементов.

По умолчанию объект класса File_sequence нельзя скопировать, потому что std::ifstream не имеет копирующих конструктора и оператора присваивания. Впрочем, если требуется, можно реализовать нужные операции копирования “руками”, потому что к одному файлу можно привязать несколько объектов ifstream (поток не владеет файлом).

Добавление нового имени файла в конец списка:

  /// Добавить в конец набора файлов заданный файл.
  void push(const std::string &filename)
  {
    filenames.push(filename);
  }

Обращение к вложенному объекту потока выполняет проверку состояния текущего файлового потока и пытается открыть следующий файл в случае необходимости:

  /// Обратиться к текущему файлу.
  std::istream& access_once()
  {
    if (!current_file.is_open() || current_file.eof())
      open_next_file();
    return current_file;
  }

  /// Проверить состояние текущего файла.
  /// Возвращает false либо из-за ошибки чтения (access_once().fail()), либо из-за того, что очередь закончилась.
  operator bool() { return access_once().good(); }


Итак, теперь можно написать код решения исходной задачи:

int main(int argc, const char *argv[])
{
  using namespace std;
  File_sequence lines(argv + 1, argv + argc);
  if (argc < 2)
  {
    // Прочитать имена файлов с потока ввода.
    for (string filename; getline(cin, filename);)
      lines.push(filename);
    cin.clear();
  }

  // Do the job.
  string prev;
  if (getline(lines, prev))
  {
    cout << prev << '\n';
    for (string line; getline(lines, line); )
      if (prev != line)
        cout << (prev = line) << '\n';
  }

  return EXIT_SUCCESS;
}

C++, HTML


Пример 2

Для конкатенации набора текстовых файлов (как последовательностей строк) вычислить минимальное, максимальное и среднее количество непробельных символов в строке.

Решение

Позаимствуем из примера 1 часть решения, в частности, класс File_sequence и каркас консольной программы.

Определим функцию count_nonspace, вычисляющую количество непробельных символов в заданной строке:

std::size_t count_nonspace(const std::string &line, const std::locale &gl)
{
  std::size_t result = 0;
  for (auto ch : line)
    result += !std::isspace(ch, gl);
  return result;
}

Для классификации символов используются средства локализации из Стандартной библиотеки C++.

Для подсчёта минимума, максимума и среднего определим класс “статистики”, который будет функтором, принимающим числа (т.е. замеры, статистику по которым отслеживает объект этого класса) и отслеживающим текущие минимум, максимум, подсчитывающим количество замеров и их сумму.

В C++ объекты, определяющие оператор () и позволяющие синтаксически обращаться к себе как к функции, называются функторы functors (термин специфичен для C++).

Функторы широко используется и являются аналогом “замыканийclosures, популярных в функциональном программировании. В отличие от обычной функции, объект-функтор имеет собственное локальное состояние, сохраняемое между вызовами.

/// Функтор, сохраняющий "статистику" переданных ему значений
/// (минимум, максимум, общее количество).
template <class Num>
class Statistics
{
  Num minv, maxv, sumv; // минимум, максимум, сумма
  std::size_t n; // количество замеров
  using Limits = std::numeric_limits<Num>;

public:
  Statistics()
    : minv(Limits::max()), maxv(Limits::lowest()), sumv(), n(0) {}

  /// Обработать следующий замер.
  Statistics& operator()(Num num)
  {
    ++n;
    sumv += num;
    if (num < minv)
      minv = num;
    else if (maxv < num)
      maxv = num;
    return *this;
  }

  /// Получить минимум из всех замеров.
  Num min() const { return minv; }
  /// Получить максимум из всех замеров.
  Num max() const { return maxv; }
  /// Получить накопленную сумму.
  Num sum() const { return sumv; }
  /// Получить количество всех замеров.
  std::size_t count() const { return n; }
};

Наконец, само консольное приложение:

int main(int argc, const char *argv[])
{
  using namespace std;
  File_sequence lines(argv + 1, argv + argc);
  if (argc < 2)
  {
    // Прочитать имена файлов с потока ввода.
    for (string filename; getline(cin, filename);)
      lines.push(filename);
    cin.clear();
  }

  // NEW
  locale gl; // глобальная локаль по умолчанию.
  Statistics<size_t> stats;
  for (string line; getline(lines, line); )
    stats(count_nonspace(line, gl));

  // Вывести результат
  cout << "Count: " << stats.count();
  cout << "\nMin: " << stats.min();
  cout << "\nMax: " << stats.max();
  cout << "\nAvg: " << double(stats.sum()) / stats.count();
  cout << endl;

  return EXIT_SUCCESS;
}

Дополнительные изменения: добавлен вывод сообщений о невозможности открыть файл (см. File_sequence::open_next_file).

C++, HTML


Варианты

1

Для заданных файлов вывести строки, встречающихся в каждом из исходных файлов. Иными словами, построить пересечение заданных файлов как множеств строк. Алгоритм должен быть не хуже квадратичного по числу строк.

2

Для заданных файлов вывести строки, встречающихся только в одном из исходных файлов (хотя, возможно, и неоднократно в этом одном файле). Алгоритм должен быть не хуже квадратичного по числу строк.

3

Для заданных файлов вывести строки, встречающихся строго один раз. Алгоритм должен быть не хуже квадратичного по числу строк.

4

Для заданных файлов вывести строки, встречающихся по крайней мере в двух из исходных файлов. Повторения строки внутри одного файла не должны влиять на результат работы. Алгоритм должен быть не хуже квадратичного по числу строк.

5

Для последовательности файлов, каждая строка которых представляет собой запись вектора произвольной размерности (не указанной явно), вычислить среднее геометрическое евклидовых длин всех векторов.

Координаты векторов записаны как числа с плавающей запятой и отделены друг от друга пробельными символами (кроме перевода строки, который отделяет друг от друга векторы).

6

Для конкатенации последовательности файлов определить частоты (количества) всех встречающихся в нём пар идущих друг за другом букв без учёта регистра. Пары с нулевыми частотами не выводить.

7

Для конкатенации последовательности файлов определить частоты (количества) всех встречающихся в нём троек идущих друг за другом букв без учёта регистра. Тройки с нулевыми частотами не выводить.

8

Для конкатенации последовательности файлов и заданной ширины колонки (табуляции) в символах заменить каждый символ табуляции на пробелы, выравнивающие положение следующего символа для попадания на следующую колонку (их может быть от нуля до заданной ширины). Остальные символы оставить неизменными.

Пример

Символы табуляции обозначены стрелкой . Исходный файл содержит:

template
→<
→→class String→→= std::string,
→→class ErrorPolicy→= Formatter_error_policy_ignore,
→→class Traits→→= String_traits<String>
→>

Пусть ширина колонки равна 4, тогда результирующий файл будет иметь вид:

template
    <
        class String       = std::string,
        class ErrorPolicy  = Formatter_error_policy_ignore,
        class Traits       = String_traits<String>
    >

9

Для конкатенации последовательности файлов сформировать набор новых файлов, извлекая из него блоки вида:

--(new-file-name
contents
contents
...
contents
--new-file-name)

где new-file-name — любое “слово”, состоящее из непробельных символов. Это слово следует использовать в качестве имени нового файла. В этот новый файл должны попасть строки старого файла, находящиеся между строкой

--(new-file-name

и строкой

--new-file-name)

Таких блоков может быть произвольное число с произвольными именами (об ошибочных именах выводить сообщение в стандартный поток сообщений об ошибках), но блоки не могут быть вложенными, т.е. даже если аналогичные метки встречаются внутри блока, они сохраняются в новый файл вместе с прочим содержимым блока.

10

Макроподстановка. В отдельном файле заданы определения подстановок в виде последовательности строк

макрос = слово

где макрос и слово — последовательности символов, не включающих пробельные символы и знак $.

Данные пары определяют текстовые подстановки, которые надо осуществить в конкатенации файлов, заменяя последовательности $макрос$ на соответствующие слово. Данную замену можно выполнять построчно (так как перевод строки не может содержаться в макрос).

Пример

Дан файл макроподстановок:

d = <div class=
. = >
/d = </div>
s = <span class=
/s = </span>

Дан исходный текст (например, фрагмент конкатенации файлов):

$d$"def"$.$
Алгоритм $s$"eng"$.$algorithm$/s$ --- точное предписание, задающее последовательность действий.
$/d$

Результат:

<div class="def">
Алгоритм <span class="eng">algorithm</span> --- точное предписание, задающее последовательность действий.
</div>

11

Для последовательности файлов, каждая строка которых представляет собой запись координат точки произвольной размерности (не указанной явно), вычислить наименьший параллелотоп со сторонами, параллельными координатным плоскостям, вмещающий все перечисленные точки. То есть ни что иное, как последовательность минимальных значений координат и последовательность максимальных последовательностей координат (две строчки).

Если точки имеют разную размерность, то точки малой размерности не влияют на на результат в дополнительных измерениях.

Пример

Дана последовательность точек:

 1   2   3
-1   4   6  -5
 3  -1.5

Результат (выравнивание по колонкам желательно, но не обязательно):

-1  -1.5 3  -5
 3   4   6  -5

12

Для последовательности файлов исходного кода определить максимальное число отступов. Строки, содержащие только пробельные символы, не учитывать. Один символ табуляции — один отступ. Ширину отступа в пробелах задавать именованной константой.


Общее оглавление

Кувшинов Д.Р. © 2016