C++. Сборник рецептов - Д. Стефенс
Шрифт:
Интервал:
Закладка:
Если вы не знакомы с map, то вам стоит узнать про него, map — это шаблон класса контейнера, который является частью STL. Он хранит пары ключ-значение в порядке, определяемом std::less или вашей собственной функцией сравнения. Типы ключей и значений, которые можно хранить в нем, зависят только от вашего воображения. В этом примере мы просто сохраняем string и int.
В строке 6 я для упрощения читаемости кода использовал typedef.
typedef map<string, int> StrIntMap;
Таким образом, StrIntMap — это map, который хранит пары string/int. Каждая string — это уникальное слово именно по этой причине я использую ее как ключ, — которое было прочитано из входного потока, а связанное с ней int — это число раз, которое это слово встретилось. Все, что осталось, — это прочитать все слова по одному, добавить их в map, если их там еще нет, и увеличить значение счетчика, если они там уже есть.
Это делает countWords. Основная логика кратка.
while (in >> s) {
++words[s];
}
operator>> читает из левого операнда (istream) непрерывные отрезки, не содержащие пробелов, и помещает их в правый операнд (string). После прочтения слова все, что требуется сделать, — это обновить статистику в map, и это делается в следующей строке.
++words[s];
map определяет operator[], позволяющий получить значение данного ключа (на самом деле он возвращает ссылку на само значение), так что для его инкремента просто инкрементируется значение, индексируемое с помощью заданного ключа. Но здесь могут возникнуть небольшие осложнения. Что, если ключа в map еще нет? Разве мы не попытаемся увеличить несуществующий элемент, и не обрушится ли программа, как в случае с обычным массивом? Нет, map определяет operator[] не так, как другие контейнеры STL или обычные массивы.
В map operator[] делает две вещи: если ключ еще не существует, он создает значение, используя конструктор типа значения по умолчанию, и добавляет в map эту новую пару ключ/значение, а если ключ уже существует, то никаких изменений не вносится. В обоих случаях возвращается ссылка на значение, определяемое ключом, даже если это значение было только что создано конструктором по умолчанию. Это удобная возможность (если вы знаете о ее существовании), так как он устраняет необходимость проверки в клиентском коде существования ключа перед его добавлением.
Теперь посмотрите на строки 32 и 33. Итератор указывает на члены, которые называются first и second — что это такое? map обманывает вас, используя для хранения пар имя/значение другой шаблон класса: шаблон класса pair, определенный в <utility> (уже включенный в <map>). При переборе элементов, хранящихся в map, вы получите ссылки на объекты pair. Работа с pair проста. Первый элемент пары хранится в элементе first, а второй хранится, естественно, в second.
В примере 4.27 я для чтения из входного потока непрерывных фрагментов текста использую operator>>, что отличается от некоторых других примеров. Я делаю это для демонстрации того, как это делается, но вам почти наверняка потребуется изменить его поведение в зависимости от определения «слова» текстового файла. Например, рассмотрим фрагмент вывода, генерируемого примером 4.27.
with присутствует 5 раз.
work присутствует 3 раз.
workers присутствует 3 раз.
workers, присутствует 1 раз.
years присутствует 2 раз.
years, присутствует 1 раз.
Обратите внимание, что точки в конце слов рассматриваются как части слов. Скорее всего, вам потребуется с помощью функций проверки символов из <cctype> и <cwctype> изменить определение слова так, чтобы оно означало только буквенно-цифровые символы, как это сделано в рецепте 4.17.
Смотри такжеРецепт 4.17 и табл. 4.3.
4.19. Добавление полей в текстовый файл
ПроблемаИмеется текстовый файл, и в нем требуется сделать поля. Другими словами, требуется сдвинуть обе стороны каждой строки, содержащей какие-либо символы, так, чтобы длина всех строк стала одинаковой.
РешениеПример 4.28 показывает, как добавить в файл поля с помощью потоков, string и шаблона функции getline.
Пример 4.28. Добавление полей в текстовый файл
#include <iostream>
#include <fstream>
#include <string>
#include <cstdlib>
using namespace std;
const static char PAD_CHAR = '.';
// addMargins принимает два потока и два числа. Потоки используются для
// ввода и вывода. Первое из двух чисел представляет
// ширину левого поля (т.е. число пробелов, вставляемых в
// начале каждой строки файла). Второе число представляет
// общую ширину строки.
void addMargins(istream& in, ostream& out,
int left, int right) {
string tmp;
while (!in.eof()) {
getline(in, tmp, 'n'); // getline определена
// в <string>
tmp.insert(tmp.begin(), left, PAD_CHAR);
rpad(tmp, right, PAD_CHAR); // rpad из рецепта
// 4.2
out << tmp << 'n';
}
}
int main(int argc, char** argv) {
if (argc < 3)
return(EXIT_FAILURE);
ifstream in(argv[1]);
ofstream out(argv[2]);
if (!in || !out)
return(EXIT_FAILURE);
int left = 8;
int right = 72;
if (argc == 5) {
left = atoi(argv[3]);
right = atoi(argv[4]);
}
addMargins(in, out, left, right);
out.close();
if (out)
return(EXIT_SUCCESS);
else
return(EXIT_FAILURE);
}
Этот пример делает несколько предположений о формате входного текста, так что внимательно прочтите следующий раздел.
ОбсуждениеaddMargins предполагает, что ввод выглядит примерно так.
The data is still inconclusive. But the weakness
in job creation and the apparent weakness in
high-paying jobs may be opposite sides of a coin.
Companies still seem cautious, relying on
temporary workers and anxious about rising health
care costs associated with full-time workers
Этот текст содержит переносы в позиции 50 символов (см. рецепт 4.16) и выровнен по левому краю (см. рецепт 4.20). addMargins также предполагает, что требуется, чтобы вывод выглядел подобно следующему, который использует для обозначения полей вместо пробелов точки.
.......The data is still inconclusive. But the weakness..............
.......in job creation and the apparent weakness in..................
.......high-paying jobs may be opposite sides of a coin..............
.......Companies still seem cautious, relying on.....................
.......temporary workers and anxious about rising health.............
.......care costs associated with full-time workers..................
По умолчанию левое поле содержит восемь символов, а общая длина строки составляет 72 символа. Конечно, если известно, что входной текст будет всегда выровнен по левому или правому краю, то можно просто дополнить оба конца каждой строки таким количеством символов, которое требуется. В любом случае логика очень проста. Многие методики, используемые в этом рецепте, уже описывались (потоки, дополнение string), так что я не буду здесь на них останавливаться. Единственная новая функция здесь — это getline.
Если требуется прочитать сразу целую строку текста или, более точно, прочитать текст до определенного разделителя, используйте шаблон функции getline, определенный в <string>, как это сделано в примере 4.28.
getline(in, tmp, 'n');
getline читает символы из входного потока и добавляет их в tmp до тех пор, пока не встретится разделитель 'n', который в tmp не добавляется. basic_istream содержит метод с таким же именем, но с другим поведением. Он сохраняет свой вывод в символьном буфере, а не в string. В данном случае я решил использовать преимущества метода из string, так как мне не хотелось читать строку в символьный буфер, а затем копировать ее в string. Таким образом, я использовал getline в версии string.
Смотри такжеРецепты 4.16 и 4.20.
4.20. Выравнивание текста в текстовом файле
ПроблемаТребуется выровнять текст по правому или левому краю.
РешениеИспользуйте потоки и стандартные флаги форматирования потоков right и left, являющиеся частью ios_base, определенного в <ios>. Пример 4.29 показывает, как они работают.
Пример 4.29. Выравнивание текста
#include <iostream>
#include <fstream>
#include <string>
#include <cstdlib>
using namespace std;