Глава 4 Строки и текст

4.0. Введение

Эта глава содержит рецепты работы со строками и текстовыми файлами. Большая часть программ на C++ независимо от сферы их применения в той или иной степени работает со строками и текстовыми файлами. Однако, несмотря на различия в сферах применения, требования к ним часто одни и те же: для строк — обрезка, дополнение, поиск, разбиение и т.п.; для текстовых файлов — перенос строк, переформатирование, чтение файлов с разделителями и др. Следующие рецепты предоставляют решения многих из часто встречающихся задач, не имеющих готовых решений в стандартной библиотеке С++.

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

basic_string
(для строк),
basic_istream
и
basic_ostream
(для входных и выходных текстовых потоков). Почти все методики, описанные в этой главе, используют или расширяют эти шаблоны классов. В тех случаях, когда они не делают того, что требуется, я использую другую часть стандартной библиотеки, содержащей общие готовые решения: алгоритмы и контейнеры.

Строки используют все, так что если что-то, что вам требуется, отсутствует в стандартной библиотеке, то велика вероятность, что это уже написано кем-то другим. Библиотека Boost String Algorithms (алгоритмы для работы со строками), написанная Паволом Дробой (Pavol Droba), заполняет большинство пробелов стандартной библиотеки, реализуя большую часть алгоритмов, которые могут понадобиться в различных ситуациях, и делает это переносимым и эффективным способом. Для получения дополнительной информации и документации по библиотеке String Algorithms обратитесь к проекту Boost по адресу www.boost.org. Библиотека String Algorithms и решения, приводимые в этой главе, в некоторых частях дублируют друг друга. В большинстве случаев я привожу примеры или, по крайней мере, упоминаю алгоритмы Boost связанные с приводимым решением.

Для большинства примеров приводится как версия с использованием шаблонов, так и без них. Это сделано по двум причинам. Во-первых, большая часть областей стандартной библиотеки, использующих символьные данные, — это шаблоны классов, параметризованных по типу символов — узкие (

char
) или широкие (
wchar_t
). Следуя этой модели, можно повысить совместимость программного обеспечения со стандартной библиотекой. Во-вторых, работаете ли вы со стандартной библиотекой или нет, шаблоны классов и функций предоставляют прекрасную возможность написания программного обеспечения, не привязанного к конкретной ситуации. Однако, если шаблоны не нужны, используйте нешаблонные версии, хотя, если вы новичок в шаблонах, я рекомендую вам поэкспериментировать с ними.

Стандартная библиотека очень активно использует шаблоны, а для того, чтобы оградить программистов от многословного синтаксиса шаблонов, использует

typedef
. В результате термины
basic_string
,
string
и
wstring
используются как взаимозаменяемые, поскольку то, что верно для одного из них, обычно верно и для двух других,
string
и
wstring
являются
typedef
для
basic_string
и
basic_string
.

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

4.1. Дополнение строк

Проблема

Требуется «дополнить» — или заполнить - строку некоторым количеством символов до определенной длины. Например, может потребоваться дополнить строку

"Chapter 1"
точками до 20 символов в длину так, чтобы она выглядела как
"Chapter 1..........."
.

Решение

Для дополнения строк начальными или концевыми символами используйте функции-члены (методы)

insert
и
append
класса
string
. Например, чтобы дополнить конец строки 20 символами
X
:

std::string s = "foo";

s.append(20 - s.length(), 'X');

Чтобы дополнить начало строки:

s.insert(s.begin(), 20 - s.length(), 'X');

Обсуждение

Разница в использовании двух функций заключается в первом параметре

insert
. Это итератор, который указывает на символ, справа от которого требуется вставить новые символы. Метод
begin
возвращает итератор, указывающий на первый элемент строки, так что в этом примере последовательность символов добавляется слева от него. Параметры, общие для всех функций, — это количество раз, которое требуется повторить символ, и сам символ.

insert
и
append
— это методы шаблона класса
basic_string
, описанного в заголовочном файле
(
string
— это
typedef
для
basic_string
, a
wstring
— это
typedef
для
basic_string
), так что они работают как для строк из узких, так и широких символов. Их использование по мере необходимости, как в предыдущем примере, прекрасно работает, но при использовании методов
basic_string
в собственных вспомогательных функциях общего назначения эти функции следует создавать, используя общий существующий дизайн стандартной библиотеки и шаблоны функций. Рассмотрим код примера 4.1, который определяет общий шаблон функции pad, который работает для строк типа basic_string.

Пример 4.1. Общий шаблон функции pad

#include 

#include 


using namespace std;


// Общий подход

template

void pad(basic_string& s,

 typename basic_string::size_type n, T c) {

 if (n > s.length())

  s.append(n - s.length(), c);

}


int main() {

 string s = "Appendix A";

 wstring ws = L"Acknowledgments"; // "L" указывает, что

                  // этот литерал состоит из

 pad(s, 20. "*");          // широких символов

 pad(ws, 20, L'*');

 // cout << s << std::endl; // He следует пытаться выполнить это

 wcout << ws << std::endl; // одновременно

}

pad
в примере 4.1 дополняет данную строку
s
до длины n, используя символ
c
. Так как шаблон функции использует параметризованный тип элементов строки (
T
), он будет работать для
basic_string
из любых символов:
char
,
wchar_t
или любых других, определенных пользователем.

4.2. Обрезка строк

Проблема

Требуется обрезать несколько символов в конце или начале строки, обычно пробелов.

Решение

Для определения позиции строки, которую требуется удалить, используйте итераторы, а для ее удаления — метод

erase
. Пример 4.2 показывает функцию
rtrim
, которая удаляет символ в конце строки.

Пример 4.2. Обрезка символов строки

#include 

#include 


// Подход для строк из узких символов

void rtrim(std::string& s, char с) {

 if (s.empty()) return;

 std::string::iterator p;

 for (p = s.end(); p != s.begin() && *--p == c;);

 if (*p != c) p++;

 s.erase(p, s.end());

}


int main() {

 std::string s = "zoo";

 rtrim(s, 'o');

 std::cout << s << '\n';

}

Обсуждение

Пример 4.2 выполняет все необходимое для строк длины

char
, но работает только для них. Аналогично тому, что показано в примере 4.1, можно использовать общий дизайн
basic_string
и шаблон функции. Пример 4.3 использует для удаления символов в конце строки любого типа шаблон функции.

Пример 4.3. Обобщенная версия rtrim

#include 

#include 


using namespace std;


// Общий подход к обрезке отдельных

// символов строки

template

void rtrim(basic_string& s, T с) {

 if (s.empty()) return;

 typename basic_string::iterator p;

 for (p = s.end(); p != s.begin() && *--p == c;);

 if (*p != c) p++;

 s.erase(p, s.end());

}


int main() {

 string s = "Great!!!!";

 wstring ws = L"Super!!!!";

 rtrim(s, '!');

 rtrim(ws, L'!');

 cout << s << '\n';

 wcout << ws << L'\n';

}

Эта функция работает точно так же, как и предыдущая, необобщенная версия из примера 4.2, но так как она параметризована по типу символов, она будет работать для

basic_string
любого типа.

Примеры 4.2 и 4.3 удаляют из строки последовательность одного символа. Однако обрезка пробелов выглядит по-другому, так как пробельный символ может быть представлен одним из нескольких символов. Для удобства стандартная библиотека предоставляет простейший способ справиться с этим: функцию

isspace
из заголовочного файла
(и ее
wchar_t
-эквивалент
iswspace
из
). Пример 4.4 определяет общую функцию, которая обрезает концевые пробелы.

Пример 4.4. Удаление концевых пробелов

#include 

#include 

#include 

#include 


using namespace std;


template

void rtrimws(basic_string& s, F f) {

 if (s.empty()) return;

 typename basic_string::iterator p;

 for (p = s.end(); p ! = s.begin() && f(*--p););

 if (!f(*p))

  p++;

 s.erase(p, s.end());

}


// Перегрузка для облегчения вызовов в клиентском коде

void rtrimws(string& s) {

 rtrimws(s, isspace);

}


void rtrimws(wstring& ws) {

 rtrimws(ws, iswspace);

}


int main() {

 string s = "zing ";

 wstring ws = L"zong ";

 rtrimws(s) rtrimws(ws);

 cout << s << "|\n";

 wcout << ws << L"|\n";

}

Шаблон функции

rtrimws
в примере 4 4 — это шаблон обобщённой функции, аналогичной предыдущим примерам, которая принимает
basic_string
и удаляет пробелы в ее конце. Но в отличие от других примеров, она для проверки элемента строки и определения того, должен ли он быть удален, принимает не символ, а объект функции.

Перегружать

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

Но, увы, это решение требует, чтобы вы писали код сами. Если же вы предпочитаете использовать библиотеку — и именно это и следует делать, — то библиотека Boost String Algorithms предоставляет огромное количество функций для обрезки строки, и в ней на верняка есть то, что вам надо. На самом деле, в библиотеке String Algorithms имеется огромное количество удобных функций обрезки, и при возможности использования Boost на них следует посмотреть. Таблица 4.1 приводит шаблоны функций этой библиотеки, используемые для обрезки строк, включая некоторые вспомогательные функции. Так как это шаблоны функций, они имеют параметры шаблонов, представляющие различные используемые типы. Вот что они означают.

Seq

Это тип, удовлетворяющий требованиям к последовательностям стандарта C++.

Coll

Это тип, удовлетворяющий менее строгим требованиям, чем стандартная последовательность. Для того чтобы узнать, каким требованиям удовлетворяет коллекция, обратитесь к определениям Boost String Algorithms.

Pred

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

OutIt

Это тип, который удовлетворяет требованиям выходного итератора, как определено в стандарте С++. В частности, он должен поддерживать инкрементирование и присвоение нового положения для добавления элементов в конец последовательности, на которую он указывает.


Табл. 4.1. Шаблоны функций обрезки строк Boost

Объявление Описание
template void trim(Seq& s, const locale& loc = locale());
Обрезает пробелы с обоих концов строки, используя для классификации пробельных символов функцию классификации локали
template void trim_if(Seq& s, Pred p);
Обрезает с обоих концов последовательности s элементы для которых
p(*it)
равно
true
, где
it
— это итератор, указывающий на элемент последовательности. Обрезка прекращается, когда
p(*it) = false
template Seq trim_copy(const Seq& s, const locale& loc = locale());
Делает то же самое, что и
trim
, но вместо изменения
s
возвращает новую последовательность, содержащую обрезанные результаты
template Seq trim_copy_if(const Seq& s, Pred p);
Делает то же самое, что и
trim_if
, но вместо изменения
s
возвращает новую последовательность, содержащую обрезанные результаты
template OutIt trim_copy_if(OutIt out, const Coll& c, Pred p);
Делает то же, что и предыдущая версия
trim_copy_if
, но с некоторыми отличиями. Во-первых, она дает гарантию строгой безопасности исключений. Во-вторых, она в качестве первого аргумента принимает выходной итератор и возвращает выходной итератор, указывающий на одну позицию после конца результирующей последовательности. Наконец, она принимает тип коллекции, а не последовательности. За дополнительной информацией обратитесь к списку перед этой таблицей
trim_left trim_right
Работает как
trim
, но только для левого или правого конца строки
trim_left_if trim_right_if
Работает как
trim_if
, но только для левого или правого конца строки
trim_left_copy trim_right_copy
Работает как
trim_сору
, но только для левого или правого конца строки
trim_left_copy_if trim_right_copy_if
Работает как
 trim_copy_if
, но только для левого или правого конца строки. Обе функции имеют две версии — одна работает с последовательностью, а другая — с коллекцией

Первые четыре шаблона функции, описанные в табл. 4.1, — это базовая функциональность функций обрезки библиотеки String Algorithms. Остальные являются вариациями на их тему. Чтобы увидеть некоторые из них в действии, посмотрите на пример 4.5. Он показывает некоторые преимущества от использования этих функций перед методами

string
.

Пример 4.5. Использование функций обрезки строк Boost

#include 

#include 

#include 


using namespace std;

using namespace boost;


int main() {

 string s1 = " ведущие пробелы?";

 trim_left(s1); // Обрезка оригинальной строки

 string s2 = trim_left_copy(s1); // Обрезка, но оригинал остается без изменений

 cout << "s1 = " << s1 << endl;

 cout << "s2 = " << s2 << endl;


 s1 = "YYYYboostXXX"; 

 s2 = trim_copy_if(s1, is_any_of("XY")); // Используется предикат

 trim_if(s1, is_any_of("XY")); 

 cout << "s1 = " << s1 << endl;

 cout << "s2 = " << s2 << endl;


 s1 = "1234 числа 9876";

 s2 = trim_copy_if(s1, is_digit());

 cout << "s1 = " << s1 << endl;

 cout << "s2 = " << s2 << endl;


 // Вложенные вызовы функций обрезки

 s1 = " ****Обрезка!*** ";

 s2 = trim_copy_if(trim_copy(s1), is_any_of("*"));

 cout << "s1 = " << s1 << endl;

 cout << "s2 = " << s2 << endl;

}

Пример 4.5 демонстрирует, как использовать функции обрезки строк Boost. Обычно способ их использования понятен из их названия, так что я не буду вдаваться в описания более подробные, чем даны в табл. 4.1. Единственная функция, имеющаяся в этом примере и отсутствующая в таблице, — это

is_any_of
. Это шаблон функции, который возвращает объект функции-предиката, используемый функциями серии
trim_if
. Она используется, когда требуется обрезать набор символов. Также есть аналогичная функция классификации, которая называется
is_from_range
и принимает два аргумента и возвращает унарный предикат, который возвращает истину, когда символ находится в заданном диапазоне. Например, чтобы обрезать в строке символы с
а
до
d
, требуется сделать что-то, похожее на следующее.

s1 = "abcdXXXabcd";

trim_if(s1, is_from_range('a', 'd'));

cout << "s1 = " << s1 << endl; // Теперь s1 = XXX

Заметьте, что эта конструкция чувствительна к регистру, так как диапазон от

а
до
d
не включает заглавных версий этих букв.

4.3. Хранение строк в последовательности

Проблема

Требуется сохранить набор строк в виде последовательности, которая ведет себя как массив.

Решение

Для хранения строк в виде массива используйте

vector
. Пример 4.6 показывает простой образец.

Пример 4 6. Хранение строк в векторе

#include 

#include 

#include 


using namespace std;


int main() {

 vector v;

 string s = "one";

 v.push_back(s);

 s = "two";

 v.push_back(s);

 s = "three";

 v.push_back(s);

 for (int i = 0; i < v.size(); ++i) {

  cout << v[i] << "\n";

 }

}

vector
использует для произвольного доступа семантику массива (а также делает много другого), так что он прост и понятен в использовании. Однако
vector
— это только одна из многих последовательностей стандартной библиотеки. Чтобы узнать об этом побольше, читайте дальше.

Обсуждение

vector
— это динамическая последовательность объектов, которая предоставляет произвольный доступ с помощью оператора в стиле массивов
operator[]
. Метод
push_back
при помощи копирующего конструктора копирует свой аргумент, добавляет копию в последний элемент вектора и увеличивает его размер на единицу.
pop_back
выполняет обратную операцию, удаляя последний элемент. Вставка и удаление элементов в конце вектора занимает постоянное время, а время вставки и удаления элементов в середине вектора линейно зависит от его размера. Это основы векторов. Кроме этого, они умеют еще много чего.

В большинстве случаев

vector
должен быть первым выбором вместо массива в стиле С. Во-первых, их размеры изменяются динамически, что означает, что эти размеры увеличиваются по мере необходимости. Не требуется проводить каких-либо исследований для выбора оптимального размера статического массива, как в случае с массивами С, — vector растет по мере надобности, а при необходимости может быть увеличен или уменьшен вручную. Во-вторых,
vector
при использовании метода
at
(но не при использовании
operator[]
) предлагает проверку границ, так что при ссылке на несуществующий индекс программа не обрушится и не продолжит выполнение с неверными данными. Посмотрите на пример 4.7, Он показывает, как работать с индексами, выходящими за границы массива.

Пример 4.7. Проверка границ для векторов

#include 

#include 

#include 


using namespace std;


int main() {

 char carr[] = {'a', 'b', 'c', 'd', 'e'};

 cout << carr[100000] << '\n'; // Оп, кто знает, что дальше

                // произойдет

 vector v;

 v.push_back('a');

 v.push_back('b');

 v.push_back('c');

 v.push_back('d');

 v push_back('e');

 try {

  cout << v.at(10000) << "\n"; // at проверяет границы и выбрасывает

 } catch(out_of_range& е) {   // out_of_range, если произошел выход за них

  cerr << e.what() << '\n';

 }

}

Перехват

out_of_range
, определенного в
, позволяет грамотно справиться с неправильными индексами. А также можно вызвать метод
what
, позволяющий в зависимости от используемой реализации получить осмысленное сообщение об ошибке, как возвращаемая в коде примера 4.7:

invalid vector subscript

Однако

vector
не является единственной возможностью. В C++ имеется большое количество способов хранить последовательности. Кроме
vector
имеются
list
,
set
и двунаправленные очереди (
deque
— double-ended queue). Все они поддерживают множество одинаковых операций, и каждый поддерживает свои собственные. Кроме того, каждый имеет различную алгоритмическую сложность, требования по хранению и семантику. Так что имеется богатый выбор.

Посмотрите внимательно на пример 4.6. Вы, вероятно, обратите внимание, что я изменяю значение строки

s
до того, как добавляю ее в конец контейнера с помощью
push_back
. Логично ожидать такого вывода этого примера

three

three

three

Я поместил в вектор одну и ту же строку три раза, так что каждый раз, когда я переприсваиваю строку, разве не должны все элементы вектора указывать на одну и ту же строку? Нет. Это важный момент, касающийся контейнеров STL.

Контейнеры STL сохраняют копии объектов, помещаемых в них, а не сами объекты. Так что после помещения в контейнер всех трех строк в памяти остается четыре строки: три копии, созданные и хранящиеся в контейнере, и одна копия, которой присваиваются значения.

Ну и что? Было создано несколько новых копий: большое дело. Но это действительно большое дело, так как если используется большое количество строк, за каждую копию приходится платить процессорным временем, памятью или и тем и другим. Копирование элементов в контейнерах — это намеренное поведение STL, и все контейнеры организованы именно так.

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

delete
указатели при его уничтожении. Память для указателей выделяет ваш код, так что он и должен ее очищать. Это относится и к ситуации, когда происходит полное удаление контейнера и когда удаляется только один его элемент.

В целях создания альтернативного решения давайте рассмотрим еще одну возможность. Рассмотрим шаблон класса

list
, определенный в
, который является двусвязным списком (doubly linked list). Если планируется большое количество вставок и удалений элементов в середине последовательности или если требуется гарантировать, что итераторы, указывающие на элементы последовательности, не станут недействительными при ее изменении, используйте
list
. Пример 4.8 вместо
vector
для хранения нескольких строк типа
string
использует
list
. Также он для перебора этих строк и печати вместо оператора индекса, как это делается в случае с простыми массивами, использует
for_each
.

Пример 4.8. Хранение строк в списке

#include 

#include 

#include 

#include 


using namespace std;


void write(const string& s) {

 cout << s << '\n';

}


int main() {

 list lst;

 string s = "нож";

 lst.push_front(s);

 s = "вилка";

 lst.push_back(s);

 s = "ложка";

 lst.push_back(s);

 // У списка нет произвольного доступа, так что

 // требуется использовать for_each()

 for_each(lst.begin(), lst.end(), write);

}

Целью этого отступления от первоначальной проблемы (хранения строк в виде последовательностей) является краткое введение в последовательности STL. Здесь невозможно дать полноценное описание этого вопроса. За обзором STL обратитесь к главе 10 книги C++ in a Nutshell Рэя Лишнера (Ray Lischner) (O'Reilly).

4.4. Получение длины строки

Проблема

Требуется узнать длину строки.

Решение

Используйте метод

length
класса
string
.

std::string s = "Raising Arizona";

int i = s.length();

Обсуждение

Получение длины строки — это тривиальная задача, но она является хорошей возможностью обсудить схему размещения

string
(как узких, так и широких символов).
string
, в отличие от массивов строк, завершаемых нулем, в С являются динамическими и увеличиваются по мере надобности. Большая часть реализаций стандартной библиотеки начинают с относительно низкой емкости и увеличивают ее в два раза каждый раз, когда достигается предел. Знание того, как анализировать этот рост, если и не точного алгоритма, помогает диагностировать проблемы производительности, связанные со строками.

Символы в

basic_string
хранятся в буфере, который является единым фрагментом памяти статического размера. Этот буфер, используемый строкой, изначально имеет некий размер, и по мере добавления в строку символов он заполняется до тех пор, пока не будет достигнут предел его емкости. Когда это происходит, буфер увеличивается. В частности, выделяется новый буфер большего размера, символы копируются из старого буфера в новый, и старый буфер удаляется.

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

capacity
. Если требуется вручную установить емкость и избежать ненужных копирований буфера, используйте метод reserve и передайте ему числовой аргумент, указывающий требуемый размер буфера. Также имеется максимально возможный размер буфера, получить который можно с помощью вызова
max_size
. Это все можно использовать, чтобы посмотреть на расходование памяти в данной реализации стандартной библиотеки. Посмотрите на пример 4.9, показывающий, как это сделать.

Пример 4.9. Длина строки и ее емкость

#include 

#include 


using namespace std;


int main() {

 string s = "";

 string sr = "";

 sr.reserve(9000);

 cout << "s.length = " << s.length( ) << '\n';

 cout << "s.capacity = " << s.capacity( ) << '\n';

 cout << "s.max.size = " << s.max_size() << '\n';

 cout << "sr.length = " << sr.length() << '\n';

 cout << "sr.capacity = " << sr.capacity() << '\n';

 cout << "sr.max_size = " << sr.max_size() << '\n';

 for (int i = 0; i < 10000; ++i) {

  if (s.length() == s.capacity()) {

  cout << "s достигла емкости " << s.length() << увеличение... \n";

  }

  if (sr.length() == sr.capacity()) {

  cout << "sr достигла емкости " << sr.length() << ", увеличение...\n";

  }

  s += 'x';

  sr += 'x';

 }

}

При использовании Visual C++ 7.1 вывод выглядит так.

s.length = 0

s.capacity = 15

s.max_size = 4294967294

sr.length = 0

sr.capacity = 9007

sr.max_size = 4294967294

s достигла емкости 15, увеличение...

s достигла емкости 31, увеличение...

s достигла емкости 47, увеличение...

s достигла емкости 70, увеличение...

s достигла емкости 105, увеличение...

s достигла емкости 157, увеличение...

s достигла емкости 235, увеличение...

s достигла емкости 352, увеличение...

s достигла емкости 528, увеличение...

s достигла емкости 792, увеличение...

s достигла емкости 1188, увеличение...

s достигла емкости 1782, увеличение...

s достигла емкости 2673, увеличение...

s достигла емкости 4009, увеличение...

s достигла емкости 6013, увеличение...

sr достигла емкости 9007, увеличение...

s достигла емкости 9019, увеличение...

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

s
начинает заполняться с емкости 15 (зависит от компилятора), а затем увеличивается каждый раз примерно на 50%.

Если ожидается значительное увеличение строки или имеется большое количество строк, которые будут увеличиваться хотя бы немного, для минимизации числа перераспределений буфера используйте

reserve
. Также следует провести эксперименты с имеющейся реализацией стандартной библиотеки и посмотреть, как она выполняет увеличение строк.

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

empty
. Это метод, который возвращает истину, если длина строки равна нулю.

4.5. Обращение строк

Проблема

Требуется обратить (реверсировать) строку.

Решение

Чтобы обратить строку «на месте», не используя временной строки, используйте шаблон функции reverse из заголовочного файла

:

std::reverse(s.begin(), s.end());

Обсуждение

reverse
работает очень просто: она изменяет диапазон, переданный ей, так, что его порядок меняется на обратный оригинальному. Время, необходимое для этого, линейно зависит от длины диапазона.

В случае, если требуется скопировать строку в другую строку, но в обратном порядке символов, используйте реверсивные итераторы, как здесь:

std::string s = "Los Angeles";

std::string rs;

rs.assign(s.rbegin(), s.rend());

rbegin
и
rend
возвращают реверсивные итераторы. Реверсивные итераторы ведут себя так, как будто они просматривают последовательность в обратном порядке.
rbegin
возвращает итератор, который указывает на последний элемент, a
rend
возвращает итератор, указывающий на позицию перед первым элементом. Это в точности обратно тому, что делают
begin
и
end
.

Но должны ли вы обращать строку? С помощью

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

4.6. Разделение строки

Проблема

Требуется разделить строку с разделителями на несколько строк. Например, может потребоваться разделить строку

"Name|Address|Phone"
на три отдельных строки —
"Name"
,
"Address"
и
"Phone"
, удалив при этом разделитель.

Решение

Для перехода от одного вхождения разделителя к следующему используйте метод

find
класса
basic_string
, а для копирования каждой подстроки используйте
substr
. Для хранения результатов используйте любую стандартную последовательность. Пример 4.10 использует
vector
.

Пример 4.10. Разделение строки с разделителями

#include 

#include 

#include 

#include 


using namespace std;


void split(const string& s, char c, vector& v) {

 string::size_type i = 0;

 string::size_type j = s.find(c);

 while (j != string::npos) {

  v.push_back(s.substr(i, j-i));

  i = ++j;

  j = s.find(c, j);

  if (j == string::npos)

  v.push_back(s.substr(i, s.length()));

 }

}


int main() {

 vector v;

 string s = "Account Name|Address 1|Address 2 |City";

 split(s, '|', v);

 for (int i = 0; i < v.size(); ++i) {

  cout << v[i] << '\n';

 }

}

Обсуждение

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

string
на
basic_string
.

template

void split(const basic_string& s, T c,

 vector >& v) {

 basic_string::size_type i = 0;

 basic_string::size_type j = s.find(c);

 while (j != basic_string::npos) {

  v.push_back(s.substr(i, j-i));

  i = ++j;

  j = s.find(c, j);

  if (j == basic_string::npos)

  v.push back(s.substr(i, s.length()));

 }

}

Логика при этом не меняется.

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

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

string
пример использует метод
find
, а для копирования символов диапазона в новую
string
, помещаемую в
vector
, — метод
substr
. Это тот же самый принцип, который используется в функциях разбиения строк большинства скриптовых языков и является специальным случаем разделения строки текста на лексемы (tokenizing), описываемого в рецепте 4.7.

Разделение строки, использующей единственный символ-разделитель, является очень распространенной задачей, и неудивительно, что ее решение есть в библиотеке Boost String Algorithms. Оно просто в использовании. Чтобы увидеть, как разделить строку с помощью функции

split
из Boost, посмотрите на пример 4.11.

Пример 4.11. Разделение строки с помощью Boost

#include 

#include 

#include 

#include 


using namespace std;

using namespace boost;


int main() {

 string s = "one,two,three,four";

 list results;

 split(results, s, is_any_of(",")); // Обратите внимание - это boost::split

 for (list::const_iterator p = results.begin();

  p != results.end(); ++p) {

  cout << *p << endl;

 }

}

split
— это шаблон функции, принимающий три аргумента. Он объявлен вот так.

template

Seq& split(Seq& s, Coll& c, Pred p,

 token_compress_mode_type e = token_compress_off);

Seq
,
Coll
и
Pred
представляют типы результирующей последовательности, входной коллекции и предиката, используемого для определения, является ли очередной объект разделителем. Аргумент последовательности — это последовательность, определенная по стандарту C++, содержащая нечто, что может хранить части того, что находится во входной коллекции. Так, например, в примере 4.11 был использован
list
, но вместо него можно было бы использовать и
vector
. Аргумент коллекции — это тип входной последовательности. Коллекция — это нестандартная концепция, которая похожа на последовательность, но с несколько меньшими требованиями (за подробностями обратитесь к документации по Boost по адресу www.boost.org). Аргумент предиката — это объект унарной функции или указатель на функцию, которая возвращает
bool
, указывающий, является ли ее аргумент разделителем или нет. Она вызывается для каждого элемента последовательности в виде
f(*it)
, где
it
— это итератор, указывающий на элемент последовательности.

is_any_of
— это удобный шаблон функции, поставляющийся в составе String Algorithms, которая облегчает жизнь при использовании нескольких разделителей. Он конструирует объект унарной функции, которая возвращает
true
, если переданный ей аргумент является членом набора. Другими словами:

bool b = is_any_of("abc")('a'); // b = true

Это облегчает проверку нескольких разделителей, не требуя самостоятельного написания объекта функции.

4.7. Разбиение строки на лексемы

Проблема

Требуется разбить строку на части, используя набор разделителей.

Решение

Для перебора элементов строки и поиска места нахождения следующих лексем и не-лексем используйте методы

find_first_of
и
first_first_not_of
. Пример 4.12 представляет простой класс
StringTokenizer
, выполняющий эту задачу.

Пример 4.12. Разбиение строки на лексемы

#include 

#include 


using namespace std;


// Класс, разбивающий строку на лексемы.

class StringTokenizer {

public:

 StringTokenizer(const string& s, const char* delim = NULL) :

  str_(s), count(-1), begin_(0), end_(0) {

  if (!delim)

  delim_ = " \f\n\r\t\v"; //по умолчанию пробельные символы

  else

  delim_ = delim;

  // Указывает на первую лексему

  begin_ = str_.find_first_not_of(delim);

  end_ = str.find_first_of(delim_, begin_);

 }


 size_t countTokens() {

  if (count_ >= 0) // если уже посчитали, то выход

  return(count_);

  string::size_type n = 0;

  string::size_type i = 0;

  for (;;) {

  // переход на первую лексему

  if ((i = str_.find_first_not_of(delim_, i)) == string::npos)

   break;

  // переход на следующий разделитель

  i = str_.find_first_of(delim_, i+1);

  n++;

  if (i == string::npos) break;

  }

  return (count_ = n);

 }


 bool hasMoreTokens() { return(begin_ != end_); }


 void nextToken(string& s) {

  if (begin_ != string::npos && end_ != string::npos) {

  s = str_.substr(begin_, end_-begin_);

  begin_ = str_.find_first_not_of(delim_, end_);

  end_ = str_.find_first_of(delim_, begin_);

  } else if (begin_ != string::npos && end_ == string::npos) {

  s = str_.substr(begin_, str_.length()-begin_);

  begin_ = str_.find_first_not_of(delim_, end_);

  }

 }


private:

 StringTokenizer() {}

 string delim_;

 string str_;

 int count_;

 int begin_;

 int end_;

};


int main() {

 string s = " razzle dazzle giddyup ";

 string tmp;

 StringTokenizer st(s);

 cout << "Здесь содержится" << st.countTokens() << " лексемы.\n";

 while (st.hasMoreTokens()) {

 st.nextToken(tmp);

 cout << "token = " << trap << '\n';

 }

}

Обсуждение

Разбиение строки с четко определенной структурой, как в примере 4.10, конечно, хорошо, но не все так просто. Предположим, что, вместо того чтобы просто разделить строку на основе единственного разделителя, требуется разбить строку на лексемы. Наиболее частым вариантом этой задачи является разделение на лексемы с игнорированием пробелов. Пример 4.12 дает реализацию класса

StringTokenizer
(аналогичного стандартному классу Java™ с таким же именем) для C++, который принимает символы-разделители, но по умолчанию использует пробелы.

Наиболее важные строки в

StringTokenizer
используют методы
find_first_of
и
find_first_not_of
шаблона класса
basic_string
. Их описание и примеры использования даны в рецепте 4.9. Пример 4.12 дает такой вывод.

Здесь содержится 3 лексемы.

token = razzle

token = dazzle

token = giddyu
p

StringTokenizer
— это более гибкая форма функции
split
из примера 4.10. Он поддерживает свое состояние, так что можно просто последовательно переходить с одной лексемы на другую, не разбивая вначале всю строку на части. Также есть возможность подсчитать число лексем.

В

StringTokenizer
можно внести пару усовершенствований. Во-первых, для простоты
StringTokenizer
написан так, что он работает только с простыми строками — другими словами, строками из узких символов. Если требуется, чтобы один и тот же класс работал как с узкими, так и с широкими символами, параметризуйте тип символов, как это сделано в предыдущих рецептах. Другим улучшением является расширение
StringTokenizer
так, чтобы он обеспечивал более дружественное взаимодействие с последовательностями и был более гибок. Вы всегда можете сделать это сами, а можете использовать имеющийся класс разбиения на лексемы. Проект Boost содержит класс
tokenizer
, делающий все это. За подробностями обратитесь к www.boost.org.

Смотри также

Рецепт 4.24.

4.8. Объединение нескольких строк

Проблема

Имея последовательность строк, такую как вывод примера 4.10, вам требуется объединить их в одну длинную строку, возможно, с разделителями.

Решение

В цикле переберите всю последовательность строк и добавьте каждую из них в выходную строку. В качестве входа можно обрабатывать любую стандартную последовательность. Пример 4.13 использует

vector
из элементов типа
string
.

Пример 4.13. Объединение последовательности строк

#include 

#include 

#include 


using namespace std;


void join(const vector& v, char c, string& s) {

 s.clear();

 for (vector::const_iterator p = v.begin();

  p ! = v.end(); ++p) {

  s += *p;

  if (p != v.end() - 1) s += c;

 }

}


int main() {

 vector v;

 vector v2;

 string s;

 v.push_back(string("fее"));

 v.push_back(string("fi"));

 v.push_back(string("foe"));

 v.push_back(string("fum"));

 join(v, '/', s);

 cout << s << '\n';

}

Обсуждение

Пример 4.13 содержит одну методику, которая несколько отличается от предыдущие примеров. Посмотрите на эту строку.

for (vector::const_iterator p = v.begin();

Предыдущие примеры работы со строками использовали

iterator
'ы без части «const», но здесь без этого не обойтись, так как
v
объявлен как ссылка на объект
const
. Если имеется объект контейнера
const
, то для доступа к его элементам можно использовать только
const_iterator
. Это так потому, что простой
iterator
позволяет записывать в объект, на который он указывает, что, конечно, нельзя делать в случае с объектами контейнера типа
const
.

v
объявлен как
const
по двум причинам. Во-первых, я знаю, что я не собираюсь изменять его содержимое, так что я хочу, чтобы компилятор выдал сообщение об ошибке, если это произойдет. Компилятор гораздо лучше меня в деле поиска таких вещей, особенно когда к такому присвоению приводит тонкая семантическая или синтаксическая ошибка. Во-вторых, я хочу показать пользователям этой функции, что я ничего не делаю с их контейнером, и
const
— это великолепный способ сделать это. Теперь я просто должен создать обобщенную версию, которая работает с различными типами символов.

Как и в рецепте 4.6, превращение

join
в общий шаблон функции очень просто. Все, что требуется сделать, — это изменить заголовок, параметризовав тип символов, как здесь:

template

void join(const std::vector >& v, T c,

 std::basic_string& s)

Но

vector
может оказаться не единственным возможным входом функции. Вам может потребоваться объединить строки в стиле С. Класс
string
C++ предпочтительнее строк в стиле С, так что если возникает такая задача, объединяйте их в C++
string
. После этого всегда можно получить версию С, вызвав метод
string c_str
, который возвращает указатель
const
на завершающийся нулем массив символов.

Пример 4.14 предлагает общую версию

join
, которая объединяет массив символов в
string
. Так как новая общая версия параметризована по типу символов, она будет работать как для массивов узких, так и для массивов широких символов.

Пример 4.14 Объединение строк в стиле C

#include 

#include 


const static int MAGIC_NUMBER = 4;


template

void join(T* arr[], size_t n, T c, std::basic_string& s) {

 s.clear();

 for (int i = 0; i < n; ++i) {

  if (arr[i] != NULL)

  s += arr[i];

  if (i < n-1) s += c;

 }

}


int main() {

 std::wstring ws;

 wchar_t* arr[MAGIC_NUMBER];

 arr[0] = L"you";

 arr[1] = L"ate";

 arr[2] = L"my";

 arr[3] = L"breakfast";

 join(arr, MAGIC_NUMBER, L'/', ws);

}

4.9. Поиск в строках

Проблема

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

Решение

Используйте один из методов «find» из

basic_string
. Почти все методы поиска начинаются со слова «find», и их имена говорят достаточно о том, что они делают. Пример 4.15 показывает, как работают некоторые из этих методов поиска.

Пример 4.15. Поиск строк

#include 

#include 


int main() {

 std::string s = "Charles Darwin";

 std::cout << s.find("ar") << '\n'; // Поиск от

                   // начала

 std::cout << s.rfind("ar") << "\n"; // Поиск с конца

 std::cout << s.find_first_of("swi") // Найти первое вхождение одного

  << '\n'; // из этих символов

 std::cout << s.find_first_not_of("Charles") // Найти первое,

  << '\n';                   // что не входит в этот

                       // набор

 std::cout << s.find_last_of("abg") << '\n'; // Найти первое вхождение любого

                       // из этих символов,

                       // начиная с конца

 std::cout << s.find_last_not_of("aDinrw") // Найти первое,

 << '\n';                  // что не входит в этот

                      // набор, начиная с конца

}

Все эти методы поиска обсуждаются более подробно в разделе «Обсуждение».

Обсуждение

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

basic_string
, либо
charT*
(
charT
— это символьный тип). Каждый имеет параметр
pos
типа
basic_string::size_type
, который позволяет указать индекс, с которого следует начать поиск, и есть перегрузка с параметром
n
типа
size_type
, который позволяет выполнить поиск только n символов из набора.

Запомнить все эти методы довольно сложно, так что в табл. 4.2 дается краткая справка по каждому из них и их параметрам.


Табл. 4.2. Методы для поиска строк

Метод Описание
size_type find(const basic_string& str, size_type pos = 0) const;
Возвращает индекс первого вхождения символа или подстроки начиная с начала или индекса, указанного в параметре
pos
.
size_type find (const charT* s, size_type pos, size_type n) const; size_type find (const charT* s, size_type pos = 0) const; size_type find(charT c, size_type pos = 0) const;
Если указан
n
, то при поиске используются первые
n
символов целевой строки
size_type rfind(...)
Находит первое вхождение символа или подстроки, начиная с конца строки и двигаясь к ее началу. Другими словами делает то же, что и
find
, но начинает поиск с конца строки
size_type find_first_of(...)
Находит первое вхождение любого символа из набора, переданного как
basic_string
или указатель на символы. Если указан
n
, то ищутся только первые
n
символов используемого набора
size_type find_last_of(...)
Находит последнее вхождение любого символа из набора, переданного как
basic_string
или указатель на символы. Если указан
n
, то ищутся только первые
n
символов используемого набора
size_type find_first_not_of(...)
Находит первое вхождение любого символа, не входящего в набор, переданный как
basic_string
или указатель на символы. Если указан
n
, то принимаются во внимание только первые n символов используемого набора
size_type find_last_not_of(...)
Находит последнее вхождение любого символа, не входящего в набор, переданный как
basic_string
или указатель на символы. Если указан
n
, то принимаются во внимание только первые
n
символов используемого набора

Все эти методы возвращают индекс вхождения искомого элемента, который имеет тип

basic_string::size_type
. Если поиск заканчивается неудачей, возвращается
basic_string::npos
, которое является специальным значением (обычно -1), указывающим, что поиск был неудачен. Даже хотя обычно это значение -1, сравнивать возвращаемое значение следует именно с
npos
, что обеспечит переносимость. Также это сделает код более понятным, так как сравнение с
npos
является явной проверкой, не содержащей магических чисел.

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

basic_string
не предоставляет то, что требуется, то перед написанием своего кода посмотрите на
. Стандартные алгоритмы работают с последовательностями, используя итераторы и почти также часто — объекты функций. Для удобства и простоты переноса
basic_string
предоставляет итераторы, так что подключение итераторов
string
к стандартным алгоритмам является тривиальным. Скажем, вам требуется найти первое вхождение двух одинаковых символов подряд. Для поиска двух одинаковых расположенных рядом («расположенных рядом» означает, что их позиции отличаются на один шаг итератора, т.е.
*iter == *(iter + 1))
символов в строке используйте шаблон функции
adjacent_find
.

std::string s = "There was a group named Kiss in the 70s";

std::string::iterator p =

 std::adjacent_find(s.begin(), s.end());

Результатом будет итератор, указывающий на первый из двух смежных элементов.

Если вам требуется написать собственный алгоритм работы со строками, не используйте

basic_string
так, как это делается со строками в стиле С, используя для доступа к элементам
operator[]
. Используйте существующие методы. Каждая функция поиска принимает параметр
size_type
, указывающий индекс, с которого должен начаться поиск. Последовательно используя функции поиска, можно пройти по всей строке. Рассмотрим пример 4.16, который подсчитывает число уникальных символов в строке.

Пример 4.16. Подсчет уникальных символов

#include 

#include 


template

int countUnique(const std::basic_string& s) {

 using std::basic_string;

 basic_string chars;

 for (typename basic_string::const_iterator p = s.begin();

  p != s.end(); ++p) {

  if (chars.find(*p) == basic.string::npos)

  chars += *p;

 }

 return(chars.length());

}


int main() {

 std: :string s = "Abracadabra'";

 std::cout << countUnique(s) << '\n';

}

Функции поиска очень часто оказываются полезными. Когда требуется найти что- либо в строке типа

string
, они должны быть первым, что следует использовать.

4.10. Поиск n-го вхождения подстроки

Проблема

Имея источник

source
и шаблон
pattern
типа
string
, требуется найти
n
-е вхождение
pattern
в
source
.

Решение

Для поиска последовательных вхождений искомой подстроки используйте метод

find
. Пример 4.17 содержит простую функцию
nthSubstr
.

Пример 4.17. Поиск n-го вхождения подстроки

#include 

#include 


using namespace std;


int nthSubstr(int n, const strings s,

 const strings p) {

 string::size_type i = s.find(p); // Найти первое вхождение

 int j;

 for (j = 1; j < n && i != string::npos; ++j)

  i = s.find(p, i+1); // Найти следующее вхождение

 if (j == n) return(i);

 else return(-1);

}


int main() (

 string s = "the wind, the sea, the sky, the trees";

 string p = "the";

 cout << nthSubstr(1, s, p) << '\n';

 cout << nthSubstr(2, s, p) << '\n';

 cout << nthSubstr(5, s, p) << '\n';

}

Обсуждение

В функцию

nthSubstr
, имеющую вид, показанный в примере 4.17, можно внести пару улучшений. Во-первых, ее можно сделать общей, сделав из нее вместо обычной функции шаблон функции. Во-вторых, можно добавить параметр, позволяющий учитывать подстроки, которые перекрываются друг с другом. Под перекрывающимися подстроками я понимаю такие, у которых начало строки соответствует части конца такой же строки, как в строке «abracadabra», где последние четыре символа такие же, как и первые четыре. Это демонстрируется в примере 4.18.

Пример 4.18. Улучшенная версия nthSubstr

#include 

#include 


using namespace std;


template

int nthSubstrg(int n, const basic_string& s,

 const basic_string& p, bool repeats = false) {

 string::size_type i = s.find(p);

 string::size_type adv = (repeats) ? 1 : p.length();

 int j;

 for (j = 1; j < n && i != basic_string::npos; ++j)

  i = s.find(p, i+adv);

 if (j == n)

  return(i);

 else

  return(-1);

}


int main() {

 string s = AGATGCCATATATATACGATATCCTTA";

 string p = "ATAT";

 cout << p << " без повторений встречается в позиции "

  << nthSubstrg(3, s, p) << '\n';

 cout << p << " с повторениями встречается в позиции "

  << nthSubstrg(3, s, p, true) << '\n';

}

Вывод для строк, использованных в примере 4.18, выглядит так.

ATAT без повторений встречается в позиции 18

ATAT с повторениями встречается в позиции 11

Смотри также

Рецепт 4.9.

4.11. Удаление подстроки из строки

Проблема

Требуется удалить из строки подстроку.

Решение

Используйте методы

basic_string find
,
erase
и
length
:

std::string t = "Banana Republic";

std::string s = "nana";

std::string::size_type i = t.find(s);

if (i != std::string::npos) t.erase(i, s.length());

Этот код удаляет

s.length()
элементов, начиная с индекса, по которому
find
находит первое вхождение подстроки.

Обсуждение

На практике встречается огромное количество вариаций на тему поиска и удаления подстрок. Например, может потребоваться удалить все вхождения подстроки, а не одно из них. Или только последнее. Или седьмое. Каждый раз действия будут одни и те же: найдите индекс начала шаблона, который требуется удалить, затем вызовите

erase
для этого индекса и n последующих символов, где n — это длина строки шаблона. За описанием различных методов поиска подстрок обратитесь к рецепту 4.9.

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

Пример 4.19. Удаление всех подстрок из строки (обобщенная версия)

#include 

#include 


using namespace std;


template

void removeSubstrs(basic_string& s,

 const basic_string& p) {

 basic_string::size_type n = p.length();

 for (basic_string::size_type i = s.find(p);

  i != basic_string::npos; i = s.find(p))

  s.erase(i, n);

}


int main() {

 string s = "One fish, two fish, red fish, blue fish";

 string p = "fish";

 removeSubstrs(s, p);

 cout << s << '\n';

}

Здесь всю важную работу выполняет метод

erase basic_string
. В
он перегружен три раза. Использованная в примере 4.19 версия принимает индекс, с которого требуется начать удаление, и число удаляемых символов. Другая версия принимает в качестве аргументов начальный и конечный итераторы, а также есть версия, которая принимает единственный итератор и удаляет элемент, на который он указывает. Чтобы обеспечить оптимальную производительность при планировании удаления нескольких последовательных элементов, используйте первые две версии и не вызывайте
s.erase(iter)
несколько раз для удаления каждого из идущих подряд элементов. Другими словами, используйте методы, работающие с диапазонами, а не с одним элементом, особенно в случае тех методов, которые изменяют содержимое строки (или последовательности). В этом случае вы избежите дополнительных вызовов функции
erase
для каждого элемента последовательности и позволите реализации
string
более грамотно управлять ее содержимым.

4.12. Преобразование строки к нижнему или верхнему регистру

Проблема

Имеется строка, которую требуется преобразовать к нижнему или верхнему регистру.

Решение

Для преобразования символов к нижнему или верхнему регистру используйте функции

toupper
и
tolower
из заголовочного файла
. Пример 4.20 показывает, как использовать эти функции. Смотри также обсуждение альтернативных методик.

Пример 4.20. Преобразование регистра строки

#include 

#include 

#include 

#include 

#include 


using namespace std;


void toUpper(basic_string& s) {

 for (basic_string::iterator p = s.begin();

 p != s.end(); ++p) {

  *p = toupper(*p); // toupper is for char

 }

}


void toUpper& s) {

 for (basic_string::iterator p = s.begin();

  p != s.end(); ++p) {

  *p = towupper(*p); // towupper is for wchar_t

 }

}


void toLower(basic_string& s) {

 for (basic_string::iterator p = s.begin();

  p != s.end(); ++p) {

  *p = tolower(*p);

 }

}


void toLower(basic_string& s) {

 for (basic_string::iterator p = s.begin();

  p != s.end(); ++p) {

  *p = towlower(*p);

}


int main() {

 string s = "shazam";

 wstring ws = L"wham";

 toUpper(s); toUpper(ws);

 cout << "s = " << s << endl;

 wcout << "ws = " << ws << endl;

 toLower(s);

 toLower(ws);

 cout << "s = " << s << endl;

 wcout << "ws = " << ws << endl;

}

Этот код производит следующий вывод.

s = SHAZAM

ws = WHAM

s = shazam

ws = wham

Обсуждение

Кто-то может подумать, что стандартный класс

string
содержит метод, преобразующий всю строку к верхнему или нижнему регистру, но на самом деле это не так. Если требуется преобразовать строку символов к верхнему или нижнему регистру, это требуется делать самостоятельно.

Неудивительно, что имеется несколько способов преобразования регистра строки (и когда я говорю «строки», то имею в виду последовательность символов как узких, так и широких). Простейшим способом сделать это является использование одной из четырех функций преобразования символов

toupper
,
towupper
,
tolower
и
towlower
. Первая форма этих функций работает с узкими символами, а вторая форма (с дополнительной буквой
w
) является ее эквивалентом для широких символов.

Каждая из этих функций преобразует регистр символа, используя текущие правила локали для преобразования регистра. Верхний и нижний регистры зависят от символов, используемых в текущей локали. Некоторые символы не имеют верхнего или нижнего регистра, и в этом случае указанные функции возвращают переданный им символ. За дополнительной информацией о локалях обратитесь к главе 13. Возможности C++ по работе с различными локалями довольно сложны, и я не могут уделить им сейчас достаточно места.

Выполнение собственно преобразования символов просто. Рассмотрим функцию

toUpper
из примера 4.20.

void toUpper(basic_string& s) {

 for (basic_string::iterator p = s.begin();

 p != s.end(); ++p) {

  *p = toupper(*p);

 }

}

Строка, выделенная жирным, выполняет всю работу. Версия для широких символов почти идентична.

void toUpper(basic_string& s) {

 for (basic_string::iterator p = s.begin();

  p != s.end(); ++p) {

  *p = towupper(*p);

 }

}

Я перегрузил

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

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

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

template

void toUpper2(basic_string& s, const locale& loc = locale()) {

 typename basic_string::iterator p;

 for (p = s.begin(); p ! = s.end(); ++p) {

  *p = use_facet >(loc).toupper(*p);

 }

}


template

void tolower2(basic_string& s, const locale& loc = locale()) {

 typename basic_string::iterator p;

 for (p = s.begin(), p ! = s.end(++p) {

  *p = use_facet >(loc).tolower(*p);

 }

}

Строки, выделенные жирным, выполняют всю работу. Функционально они работают точно так же, как и функции для верхнего и нижнего регистров, использованные в примере 4.20, за исключением того, что они используют для этого возможности интернационализации из заголовочного файла

. За более подробным обсуждением локалей и возможностей интернационализации обратитесь к главе 13.

4.13. Выполнение сравнения строк без учета регистра

Проблема

Имеются две строки и требуется узнать, не равны ли они, не учитывая регистр их символов. Например, «cat» не равно «dog», но «Cat» должна быть равна «cat», «CAT» или «caT».

Решение

Сравните строки, используя стандартный алгоритм

equal
(определенный в
), и создайте свою собственную функцию сравнения, которая использует для сравнения версий с верхним регистром символов функцию
toupper
из
(или
towupper
из
для широких символов). Пример 4.21 показывает обобщенное решение. Также он демонстрирует использование и гибкость STL. За полным объяснением обратитесь к обсуждению ниже.

Пример 4.21. Сравнение строк без учета регистра

1  #include 

2  #include 

3  #include 

4  #include 

5  #include 

6

7  using namespace std;

8

9  inline bool caseInsCharCompareN(char a, char b) {

10  return(toupper(a) == toupper(b));

11 }

12

13 inline bool caseInsCharCompareW(wchar_t a, wchar_t b) {

14  return(towupper(a) == towupper(b));

15 }

16 

17 bool caseInsCompare(const string& s1, const string& s2) {

18  return((s1.size() == s2.size()) &&

19  equal(s1.begin(), s1.end(), s2.begin(), caseInsCharCompareN));

20 }

21

22 bool caseInsCompare(const wstring& s1, const wstring& s2) {

23  return((s1.size() == s2.size())

24  equal(s1.begin(), s1.end(), s2.begin(), caseInsCharCompareW));

25 }

26

27 int main() {

28  string s1 = "In the BEGINNING...";

29  string s2 = "In the beginning...";

30  wstring ws1 = L"The END";

31  wstring ws2 = L"the end";

32

33  if (caseInsCompare(s1, s2))

34  cout << "Equal!\n";

35

36  if (caseInsCompare(ws1, ws2))

37  cout << "Equal!\n";

38 }

Обсуждение

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

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

Строки 9-15 примера 4.21 определяют функции, которые выполняют сравнение —

caseInsCharCompareN
и
caseInsCharCompareW
. Они для преобразования символов к верхнему регистру используют
toupper
и
towupper
, а затем сообщают, равны ли они.

После написания этих функций сравнения настает время использовать стандартный алгоритм, выполняющий применение этих функций сравнения к произвольной последовательности символов. Именно это делают функции

caseInsCompare
, определенные в строках 17-25 и использующие
equal
. Здесь сделано две перегрузки — по одной для каждого типа интересующих нас символов. Они обе делают одно и то же, но каждая использует для своего типа символов соответствующую функцию сравнения. Для этого примера я перегрузил две обычные функции, но этот же эффект может быть достигнут и с помощью шаблонов. Для пояснений обратитесь к врезке «Следует ли использовать шаблон?».

equal
сравнивает две последовательности на равенство. Имеется две версии: одна использует
operator==
, а другая использует переданный ей функциональный объект двоичного предиката (т.е. такой, который принимает два аргумента и возвращает
bool
). В примере 4.21
caseInsCharCompareN
и
W
— это функции двоичного предиката.

Но это не всё, что требуется сделать; также требуется сравнить размеры. Рассмотрим объявление

equal
.

template

 typename BinaryPredicate>

bool equal(InputIterator1 first, InputIterator1 last1,

 InputIterator2 first2, BinaryPredicate pred);

Пусть n — это расстояние между

first1
и
last1
, или, другими словами, длина первого диапазона.
equal
возвращает
true
, если первые
n
элементов обеих последовательностей равны. Это означает, что если есть две последовательности, где первые
n
элементов равны, но вторая содержит больше чем
n
элементов, то
equal
вернет
true
. Чтобы избежать такой ошибки требуется проверять размер.

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

if (caseInsCompare(s1, s2)) { // они равны, делаем что-нибудь

чем такое:

if ((s1.size() == s2.size()) &&

std::equal(s1.begin(), s1.end(s2.begin(), caseInsCharCompare)) {

 // они равны, делаем что-нибудь

когда требуется выполнить сравнение строк без учета регистра.

4.14. Выполнение поиска строк без учета регистра

Проблема

Требуется найти в строке подстроку, не учитывая разницу в регистре.

Решение

Используйте стандартные алгоритмы

transform
и
search
, определенные в
, а также свои собственные функции сравнения символов, аналогичные уже показанным. Пример 4.22 показывает, как это делается.

Пример 4.22. Поиск строк без учета регистра

#include 

#include 

#include 

#include 

#include 


using namespace std;


inline bool caseInsCharCompSingle(char a. char b) {

 return(toupper(a) == b);

}


string::const_iterator caseInsFind(string& s, const string& p) {

 string tmp;

 transform(p.begin( ), p.end(), // Преобразуем шаблон

  back_inserter(tmp),       // к верхнему регистру

  toupper);

 return(search(s.begin(), s.end(), // Возвращаем итератор.

  tmp.begin(), tmp.end(),      // возвращаемый из

  caseInsCharCompSingle));      // search

}


int main() {

 string s = "row, row, row, your boat";

 string p = "YOUR";

 string::const_iterator ir = caseInsFind(s, p);

 if (it != s.end()) {

  cout << "Нашли!\n;

 }

}

Возвращая итератор, который указывает на элемент целевой строки, где начинается шаблонная строка, мы обеспечиваем совместимость с другими стандартными алгоритмами, так как большинство из них принимают в качестве аргумента итератор.

Обсуждение

Пример 4.22 демонстрирует обычный ход действий при работе со стандартными алгоритмами. Создайте функцию, которая выполняет работу, а затем подключите ее как объект функции к наиболее подходящему алгоритму. Здесь работу выполняет функция

charInsCharCompSingle
, но в отличие от примера 4.21 эта функция сравнения символов переводит к верхнему регистру только первый аргумент. Это сделано потому, что немного далее в
caseInsFind
я перед использованием строки шаблона в поиске преобразую ее к верхнему регистру полностью и тем самым избегаю многократного преобразования символов строки шаблона к верхнему регистру.

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

transform
и
search
.
transform
используется для преобразования к верхнему регистру всего шаблона (но не целевой строки). После этого используйте для поиска места вхождения подстроки search совместно с функцией сравнения.

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

bool
, указывающий, дало ли сравнение истину или ложь.

В примере 4.22 есть одна вещь, которая выглядит странно. Вы видите, что

caseInsCompare
возвращает
const_iterator
, как в

string::const_iterator caseInsFind(const string& s,

 const string& p)

Что, если требуется изменить элемент, на который указывает возвращенный итератор? Тому есть причина. Она состоит в том, что константный итератор используется потому, что строки, которые передаются в

caseInsFind
, также передаются как
const
, и, следовательно, невозможно получить не-
const
итератор на
const
-строку. Если требуется итератор, который можно использовать для изменения строки, удалите
const
из параметров и измените объявление функции так, чтобы она возвращала
string::iterator
.

4.15. Преобразование между табуляциями и пробелами в текстовых файлах

Проблема

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

Решение

Независимо от того, производится ли замена табуляций на пробелы или пробелов на табуляции, используйте классы

ifstream
и
ofstream
из
. В первом (более простом) случае прочтите данные по одному символу с помощью входного потока, изучите их и, если очередной символ — это табуляция, запишите в выходной поток некоторое количество пробелов. Пример 4.23 демонстрирует, как это делается.

Пример 4.23. Замена табуляций на пробелы

#include 

#include 

#include 


using namespace std;


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);

 char c;

 while (in.get(c)) {

  if (c == '\t')

  out << " "; // 3 пробела

  else

  out << c;

 }

 out.close();

 if (out)

  return(EXIT_SUCCESS);

 else

  return(EXIT_FAILURE);

}

Если же требуется заменить пробелы на табуляции, обратитесь к примеру 4.24. Он содержит функцию

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

Пример 4.24. Замена пробелов на табуляции

#include 

#include 

#include 

#include 

#include 


using namespace std;


void spacesToTabs(istream& in, ostream& out, int spaceLimit) {

 int consecSpaces = 0;

 char c;

 while (in.get(c)) {

  if (c != ' ') {

  if (consecSpaces > 0) {

   for (int i = 0; i < consecSpaces; i++) {

   out.put(' ');

   }

   consecSpaces = 0;

  }

  out.put(c);

  } else {

  if (++consecSpaces == spaceLimit) {

   out.put('\t');

   consecSpaces = 0;

  }

  }

 }

}


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);

 spacesToTabs(in, out, 3);

 out.сlose();

 if (out)

  return(EXIT_SUCCESS);

 else

  return(EXIT_FAILURE);

}

Обсуждение

Механизм обоих этих решений один и тот же, отличаются только алгоритмы. Символы читаются из входного потока с помощью

get
, а в выходной поток помещаются с помощью
put
. Логика, выполняющая преобразования, помещается между этими двумя функциями.

Вы, вероятно, заметили в примере 4.24, что в функции

main in
и
out
объявлены как переменные типов
ifstream
и
ofstream
соответственно и что параметры
spacesToTabs
— это
istream
и
ostream
. Это сделано для того, чтобы позволить
spacesToTabs
работать с любыми типами входных и выходных потоков (ну, не любыми типами потоков, а теми, которые наследуются от
basic_istream
или
basic_ostream
), а не только с файловыми потоками. Например, текст, который требуется переформатировать, может находиться в строковом потоке (
istringstream
и
ostringstream
из
). В этом случае сделайте что-то похожее на следующее.

istringstream istr;

ostringstream ostr;

// заполняем istr текстом...

spacesToTabs(istr, ostr);

Как и в случае со строками, потоки — это на самом деле шаблоны классов, параметризованные по типу символов, с которыми работает поток. Например,

ifstream
— это
typedef
для
basic_ifstream
, a
wifstream
— это
typedef
для
basic_ifstream
. Таким образом, если требуется, чтобы
spacesToTabs
из примеров 4.23 или 4.24 работала с потоками любых символов, то вместо
typedef
используйте эти шаблоны классов.

template

void spacesToTabs(std::basic_istream& in,

 std::basic_ostream& out, int spaceLimit) { //...

4.16. Перенос строк в текстовом файле

Проблема

Требуется сделать перенос текста файла после определенного количества символов. Например, если требуется сделать перенос текста после 72 символов, то после каждого 72 символа файла требуется вставить символ новой строки. Если файл содержит текст, читаемый человеком, то, вероятно, потребуется избежать разделения слов.

Решение

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

istream::get(char)
, выполняет какие-либо действия и записывает символы с помощью
ostream::put(char)
. Пример 4.25 показывает, как это делается с файлом, который содержит обычный текст, с учетом сохранения целостности слов.

Пример 4.25. Перенос текста

#include 

#include 

#include 

#include 

#include 

#include 


using namespace std;


void textWrap(istream& in, ostream& out, size_t width) {

 string tmp;

 char cur = '\0';

 char last = '\0';

 size_t i = 0;

 while (in.get(cur)) {

  if (++i == width) {

  ltrimws(tmp);    // ltrim как в рецепте

  out << '\n' << tmp; // 4.1

  i = tmp.length();

  tmp.clear();

  } else if (isspace(cur) && // Это конец

  !isspace(last)) {     // слова

  out << tmp;

  tmp.clear();

  }

  tmp += cur;

  last = cur;

 }

}


int main(int argc, char** argv) {

 if (argc < 3)

  return(EXIT_FAILURE);

 int w = 72;

 ifstream in(argv[1]);

 ofstream out(argv[2]);

 if (!in || !out)

  return(EXIT_FAILURE);

 if (argc == 4) w = atoi(argv[3]);

 textWrap(in, out, w);

 out.close();

 if (out)

  return(EXIT_SUCCESS);

 else

  return(EXIT_FAILURE);

}

Обсуждение

textWrap
читает по одному символы из входного потока. Каждый символ добавляется к временной строке
tmp
до тех пор, пока не будет достигнут конец слов или максимальная длина строки. Если достигнут конец слова, а максимальная длина строки еще не достигнута, то временная строка записывается в выходной поток. В противном случае, если максимальная длина строки была превышена, в выходной поток записывается новая строка, пробел в начале временной строки удаляется, и строка записывается в выходной поток. Таким образом,
textWrap
записывает в выходной поток столько, сколько можно, но не превышая максимальной длины строки. Вместо разделения слов она переносит все слово на новую строку.

Пример 4.25 использует потоки почти так же, как и рецепт 4.15. За дополнительной информацией о потоках и их использовании обратитесь к этому рецепту.

Смотри также

Рецепт 4.15.

4.17. Подсчет числа символов, слов и строк в текстовом файле

Проблема

Требуется подсчитать число символов, слов и строк — или каких-либо других элементов текста — в текстовом файле.

Решение

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

countStuff
, которая именно это и делает.

Пример 4.26. Подсчет статистики по текстовому файлу

#include 

#include 

#include 

#include 


using namespace std;


void countStuff(istream& in,

 int& chars, int& words, int& lines) {

 char cur = '\0';

 char last = '\0';

 chars = words = lines = 0;

 while (in.get(cur)) {

  if (cur == '\n' ||

  (cur == '\f' && last == '\r'))

  lines++;

  else chars++;

  if (!std::isalnum(cur) && // Это конец

  std::isalnum(last))    // слова

  words++;

 last = cur;

 }

 if (chars > 0) {      // Изменить значения слов

  if (std::isalnum(last)) // и строк для специального

  words++;         // случая

  lines++;

 }

}


int main(int argc, char** argv) {

 if (argc < 2)

  return(EXIT _FAILURE);

 ifstream in(argv[1]);

 if (!in)

  exit(EXIT_FAILURE);

 int c, w, l;

 countStuff(in, c, w, l);

 cout << "символов: " << c << '\n';

 cout << "слов: " << w << '\n';

 cout << "строк: " << l << '\n';

}

Обсуждение

Этот алгоритм очень прост. С символами все просто: увеличивайте счетчик символов при каждом вызове

get
для входного потока. Со строками все не намного сложнее, так как способ представления концов строк зависит от операционной системы. К счастью, обычно это либо символ новой строки (
\n
), либо последовательность из символов возврата каретки и перевода строки (
\r\n
). Отслеживая текущий и предыдущий символы, можно легко обнаружить вхождения этой последовательности. Со словами все проще или сложнее, в зависимости от определения того, что такое «слово».

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

isalnum
из
. Но это еще не все — с помощью аналогичных функций можно проверять символы на целый ряд других качеств. Функции, которые предназначены для проверки характеристик символов, приведены в табл. 4.3. Для широких символов используйте функции с такими же именами, но с буквой «w» после «is», например
iswSpace
. Версии для широких символов объявлены в заголовочном файле
.


Табл. 4.3. Функции для проверки символов из и

Функция Описание
isalpha iswalpha
Буквенные символы: a-z, A-Z (верхний или нижний регистр)
isupper iswupper
Буквенные символы верхнего регистра: A-Z
islower iswlower
Буквенные символы нижнего регистра: a-z
isdigit iswdigit
Числовые символы: 0-9
isxdigit iswxdigit
Шестнадцатеричные числовые символы: 0-9, a-f, A-F
isspace iswspace
Пробельные символы. ' ', \n, \t, \v, \r, \l
iscntrl iswcntrl
Управляющие символы: ASCII 0-31 и 127
ispunct iswpunct
Символы пунктуации, не принадлежащие предыдущим группам
isalnum iswalnum
isalpha
или
isdigit
равны true
isprint iswprint
Печатаемые символы ASCII
isgraph iswgraph
isalpha
,
isdigit
или
ispunct
равны true

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

Методика использования потоков в примере 4.26 почти идентична той, которая описана в рецептах 4.14 и 4.15, но несколько проще, так как он только исследует файл, не внося никаких изменений.

Смотри также

Рецепты 4.14 и 4.15.

4.18. Подсчет вхождений каждого слова в текстовом файле

Проблема

Требуется подсчитать количество вхождений в текстовом файле каждого слова.

Решение

Для чтения из текстового файла непрерывных фрагментов текста используйте

operator>>
, определенный в
, а для сохранения каждого слова и его частоты в файле используйте
map
, определенный в
. Пример 4.27 демонстрирует, как это делается.

Пример 4.27. Подсчет частоты слов

1  #include 

2  #include 

3  #include 

4  #include 

5

6  typedef std::map StrIntMap;

7

8  void countWords(std::istream& in, StrIntMap& words) {

9

10  std::string s;

11

12  while (in >> s) {

13  ++words[s];

14  }

15 }

16 

17 int main(int argc, char** argv) {

18

19  if (argc < 2)

20  return(EXIT_FAILURE);

21

22  std::ifstream in(argv[1]);

23

24  if (!in)

25  exit(EXIT_FAILURE);

26

27  StrIntMap w;

28  countWords(in, w);

29 

30  for (StrIntMap::iterator p = w.begin();

31  p != w.end(); ++p) {

32  std::cout << p->first << " присутствует "

33   << p->second << " раз.\n";

34  }

35 }

Обсуждение

Пример 4.27 кажется вполне простым, но в нем делается больше, чем кажется. Большая часть тонкостей связана с

map
, так что вначале давайте обсудим его.

Если вы не знакомы с

map
, то вам стоит узнать про него,
map
— это шаблон класса контейнера, который является частью STL. Он хранит пары ключ-значение в порядке, определяемом
std::less
или вашей собственной функцией сравнения. Типы ключей и значений, которые можно хранить в нем, зависят только от вашего воображения. В этом примере мы просто сохраняем
string
и
int
.

В строке 6 я для упрощения читаемости кода использовал

typedef
.

typedef map 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
, определенный в
(уже включенный в
). При переборе элементов, хранящихся в
map
, вы получите ссылки на объекты
pair
. Работа с
pair
проста. Первый элемент пары хранится в элементе
first
, а второй хранится, естественно, в
second
.

В примере 4.27 я для чтения из входного потока непрерывных фрагментов текста использую

operator>>
, что отличается от некоторых других примеров. Я делаю это для демонстрации того, как это делается, но вам почти наверняка потребуется изменить его поведение в зависимости от определения «слова» текстового файла. Например, рассмотрим фрагмент вывода, генерируемого примером 4.27.

with присутствует 5 раз.

work присутствует 3 раз.

workers присутствует 3 раз.

workers, присутствует 1 раз.

years присутствует 2 раз.

years, присутствует 1 раз.

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

и
изменить определение слова так, чтобы оно означало только буквенно-цифровые символы, как это сделано в рецепте 4.17.

Смотри также

Рецепт 4.17 и табл. 4.3.

4.19. Добавление полей в текстовый файл

Проблема

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

Решение

Пример 4.28 показывает, как добавить в файл поля с помощью потоков,

string
и шаблона функции
getline
.

Пример 4.28. Добавление полей в текстовый файл

#include 

#include 

#include 

#include 


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 определена

              // в 

  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
, определенный в
, как это сделано в примере 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
, определенного в
. Пример 4.29 показывает, как они работают.

Пример 4.29. Выравнивание текста

#include 

#include 

#include 

#include 


using namespace std;


int main(int argc, char** argv) {

 if (argc < 3)

  return(EXIT_FAILURE);

 ifstream in(argv[1]);

 ofstream out(argv[2]);

 int w = 72;

 if (argc == 4)

  w = atoi(argv[3]);

 string tmp;

 out.setf(ios_base::right); // Указать потоку на

               // выравнивание по правому краю

 while (!in.eof()) {

  out.width(w);       // Сбросить ширину после

  getline(in, tmp, "\n"); // каждой записи

  out << tmp << '\n';

 }

 out.close();

}

Этот пример принимает три аргумента: входной файл, выходной файл и ширину выровненного по правому краю текста. Входной файл может иметь следующий вид.

With automatic download of Microsoft's (Nasdaq:

MSFT) enormous SP2 security patch to the Windows

XP operating system set to begin the industry

still waits to understand its ramifications. Home

users that have their preferences set to receive

operating system updates as they are made

available by Microsoft may be surprised to learn

that some of the software they already run on

their systems could be disabled by SP2 or may run

very differently.

Вывод будет иметь следующий вид.

  With automatic download of Microsoft's (Nasdaq:

  MSFT) enormous SP2 security patch to the Windows

   XP operating system set to begin the industry

 still waits to understand its ramifications. Home

  users that have their preferences set to receive

     operating system updates as they are made

  available by Microsoft may be surprised to learn

   that some of the software they already run on

 their systems could be disabled by SP2 or may run

                 very differently.

Второй пример текста выровнен по правому краю и имеет в ширину 50 символов.

Обсуждение

Шаблон класса

ios_base
содержит большое количество флагов форматирования числовых и текстовых данных, читаемых из потоков или записываемых в них. Два флага, управляющих выравниванием текста, — это
right
и
left
. Они являются
static const
-членами
ios_base
и имеют тип
fmtflags
(который зависит от реализации). Все это хозяйство определено в
.

Чтобы установить флаги форматирования, используйте

ios_base::setf
. Она объединяет переданные в нее и уже установленные ранее флаги потока с помощью операции OR (ИЛИ). Например, эта строка включает выравнивание по правому краю:

out.setf(std::ios_base::right);

Но выравнивание по правому краю не имеет смысла без установки правого поля, по которому требуется выравнивать. Чтобы установить это поле, используйте

ios_base::width
, как здесь.

out.width(w);

Этот код устанавливает ширину выходного поля для передаваемого значения, что означает, что при выравнивании текста по правому краю начало строки будет дополняться пробелами так, чтобы ее правый край достиг правого поля. Заметьте, что ширину я задаю в цикле, в то время как флаг

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

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

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

string
, изменяет
string
, устанавливает флаги форматирования и записывает
string
в поток, то могут возникнуть нежелательные побочные эффекты. После того как клиентский код вызывает эту функцию журналирования, его поток содержит другой набор флагов форматирования. Решением является копирование старых флагов и восстановление их по окончании работы.

Например, функция журналирования ошибок может выглядеть вот так.

using namespace std;


void logFrror(ostream& out, const string& s) {

 string tmp(s);

 tmp.insert(0, "ERROR: ");

 ios_base::fmtflags figs =  // setf возвращает

 out.setf(ios_base::left); // флаги, которые уже

               // были установлены

 out.width(72);

 out << tmp << '\n';

 out.flags(flgs); // вернуть оригинальные

}

Метод

flags
работает аналогично
setf
, но не объединяет с помощью OR переданные ему флаги с уже установленными, а заменяет их. Таким образом, при вызове
flags
и передаче ему оригинальных флагов форматирования можно быть уверенным, что флаги будут восстановлены.

4.21. Замена в текстовом файле последовательностей пробелов на один пробел

Проблема

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

Решение

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

operator>>
, определенный в
. Затем используйте его двойника
operator<<
, который записывает каждую из этих последовательностей в выходной поток, и после каждой из них добавьте по одному пробелу. Пример 4.30 дает краткий пример этой методики.

Пример 4 30. Замена последовательностей пробелов на один пробел

#include 

#include 

#include 


using namespace std;


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);

 string tmp;

 in >> tmp;  // Прочитать первое слове

 out << tmp; // Записать его в выходной поток

 while (in >> tmp) { // operator>> игнорирует пробелы, так что все, что

  out << ' ';    // я должен сделать, - это записать пробел и каждую

  out << tmp;    // последовательность «непробелов»

 }

 out.close();

}

Обсуждение

Это просто сделать, если использовать потоки и строки. Даже если требуется реализовать другой вариант этого — например, чтобы сохранить переходы на новую строку, — эта методика будет работать. Если требуется добавить переходы на новые строки, для их расстановки в нужных местах используйте решение, представленное в рецепте 4.16.

Смотри также

Рецепты 4.15 и 4.16.

4.22. Автозамена текста при изменении буфера

Проблема

Имеется класс, который представляет некий тип текстового поля или документа, и по мере добавления в него текста требуется автоматически корректировать неправильно написанные слова, как это делает функция Autocorrect (Автозамена) в Microsoft Word.

Решение

Это можно реализовать в относительно небольшом коде, если использовать

map
, который определен в
,
string
и различные возможности стандартной библиотеки. Пример 4.31 показывает, как это делается.

Пример 4.31. Автозамена текста

#include 

#include 

#include 

#include 


using namespace std;


typedef map StrStrMap;


// Класс для хранения текстовых полей

class TextAutoField {

public:

 TextAutoField(StrStrMap* const p) : pDict_(p) {}

 ~TextAutoField() {}

 void append(char c);

 void getText(string& s) {s = buf_;}

private:

 TextAutoField();

 string buf_;

 StrStrMap* const pDict ;

};


// Добавление с автозаменой

void TextAutoField::append(char c) {

 if ((isspace(c) || ispunct(c)) &&   // Выполнять автоза-

  buf_.length() > 0 &&          // мену, только когда вводятся

  !isspace(buf_[buf_.length() - 1])) { // ws или punct

  string::size_type i = buf_.find_last_of(" \f\n\r\t\v");

  i = (i == string::npos) ? 0 : ++i;

  string tmp = buf_.substr(i, buf_.length() - i);

  StrStrMap::const_iterator p = DDict_->find(tmp);

  if (p != pDict_->end()) {     // Нашли, так что стираем

  buf_.erase(i, buf_.length() - i); // и заменяем

  buf_ += p->second;

  }

 }

 buf_ += с;

}


int main() {

 // Создаем map

 StrStrMap dict;

 TextAutoField txt(&dict);

 dict["taht"] = "that";

 dict["right"] = "wrong";

 dict["bug"] = "feature";

 string tmp = "He's right, taht's a bug.";

 cout << "Оригинальная версия: " << tmp << '\n';

 for (string::iterator p = tmp.begin(); p != tmp.end(); ++p) {

  txt.append(*p);

 }

 txt.getText(tmp);

 cout << "Исправленная версия. " << tmp << '\n';

}

Вывод примера 3.2 таков.

Оригинальная версия: He's right, taht's a bug.

Исправленная версия: He's wrong, that's a feature.

Обсуждение

string
и
map
удобны в ситуациях, когда требуется отслеживать ассоциации
string
.
TextAutoField
— это простой текстовый буфер, использующий
string
для хранения данных. Интересной
TextAutoField
делает ее метод
append
, который «слушает» пробелы или знаки пунктуации и при их появлении выполняет обработку.

Чтобы сделать автозамену работающей, требуется две вещи. Во-первых, требуется некий словарь, который содержит неправильно написанные варианты слов и связанные с ними правильные написания, map хранит пары ключ/значение, где ключ и значение могут быть любого типа, так что он является идеальным кандидатом на эту роль. В начале примера 4.31 имеется

typedef
для пар
string
:

typedef map StrStrMap;

За более подробным описанием map обратитесь к рецепту 4.18.

TextAutoField
хранит указатель на
map
, так как, вероятнее всего, для всех полей потребуется только один общий словарь.

Предполагая, что клиентский код помещает в

map
что-то осмысленное,
append
просто должен периодически проверять
trap
. В примере 4.31
append
ждет появления пробела или знака пунктуации. Для проверки на пробел можно использовать
isspace
, а для поиска знаков пунктуации можно использовать ispunct. Обе эти функции для узких символов определены в
(см. табл. 4.3).

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

string tmp
содержит последний фрагмент текста, который был добавлен в
TextAutoField
. Чтобы увидеть, был ли он написан с ошибками, поищите его в словаре вот так.

StrStrMap::iterator p = pDict->find(tmp);

if (p != pDict_->end()) {

Здесь важно то, что

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

buf_.erase(i, buf_.length() - i);

buf_ += p->second;

Добавьте символ, который инициировал весь процесс (либо пробел, либо знак пунктуации), и все.

Смотри также

Рецепты 4.17, 4.18 и табл. 4.3.

4.23. Чтение текстового файла с разделителями-запятыми

Проблема

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

Smith, Bill, 5/1/2002, Active

Stanford, John, 4/5/1999, Inactive

Такие файлы обычно временно хранят наборы данных, экспортируемые из электронных таблиц, баз данных или других форматов файлов.

Решение

Пример 4.32 демонстрирует, как это делается. Если читать текст в

string
непрерывными кусками с помощью
getline
(шаблон функции определен в
), то для анализа текста и создания структуры данных можно использовать функцию
split
, которая была представлена в рецепте 4.6.

Пример 4.32. Чтение файла с разделителями

#include 

#include 

#include 

#include 


using namespace std;


void split(const string& s, char c, vector& v) {

 int i = 0;

 int j = s.find(c);

 while (j >= 0) {

  v.push_back(s.substr(i, j-i));

  i = ++j;

  j = s.find(c, j);

  if (j < 0) {

  v.push_back(s.substr(i, s.length()));

  }

 }

}


void loadCSV(istream& in, vector*>& data) {

 vector* p = NULL;

 string tmp;

 while (!in.eof()) {

  getline(in, tmp, '\n'); // Получить следующую строку

  p = new vector();

  split(tmp, '.', *p); // Использовать split из

            // Рецепта 4.7

  data.push_back(p);

  cout << tmp << '\n';

  tmp.clear();

 }

}


int main(int argc, char** argv) {

 if (argc < 2)

  return(EXIT_FAILURE);

 ifstream in(argv[1]);

 if (!in)

  return(EXIT_FAILURE);

 vector*> data;

 loadCSV(in, data);

 // Выполнить с данными какие-либо действия...

 for (vector*>::iterator p = data.begin();

  p != data end(); ++p) {

  delete *p; // Убедитесь, что p

 }      // разыменован!

}

Обсуждение

В примере 4.32 почти нет ничего, что еще не было бы описано,

getline
обсуждается в рецепте 4.19, a
vector
— в рецепте 4.3. Единственный фрагмент, заслуживающий упоминания, — это выделение памяти.

loadCSV
создает новый
vector
для каждой прочитанной строки данных и сохраняет его в другом vector, состоящем из указателей на
vector
. Так как память для каждого из этих векторов выделяется из кучи, кто-то должен удалить ее, и этот кто-то — это вы (а не реализация
vector
).

vector
ничего не знает о том, содержит ли он значение или указатель на значение или что-либо еще. Все, что он знает, — это то, что при его удалении он должен вызвать деструктор для каждого содержащегося в нем элемента. Если
vector
хранит объекты, то все нормально, объект будет удален правильно. Но если
vector
содержит указатели, то удалены будут указатели, а не объекты, на которые они указывают.

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

for (vector*>::iterator p = data.begin();

 p != data.end(); ++p) {

 delete *p;

}

Либо можно использовать указатель со счетчиком ссылок, такой как

smart_ptr
из проекта Boost, который станет частью будущего стандарта C++0x. Но реализация этого нетривиальна, так что я рекомендую почитать, что такое
smart_ptr
и как он работает. Для получения дополнительной информации по Boost посетите его домашнюю страницу по адресу www.boost.org.

4.24. Использование регулярных выражений для разделения строки

Проблема

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

Решение

Используйте шаблон класса

regex
Boost.
regex
позволяет использовать для строк и текстовых данных регулярные выражения. Пример 4.33 показывает, как использовать
regex
для разделения строк.

Пример 4.33. Использование регулярных выражений Boost

#include 

#include 

#include 


int main() {

 std::string s = "who,lives-in-a,pineapple under the sea?";

 boost::regex re(',|:|-|\\s+"); // Создаем регулярное выражение

 boost::sregex_token_iterator  // Создаем итератор, используя

 p(s.begin(), s.end(), re, -1), // последовательность и это выражение

  boost::sregex_token_iterator end; // Создаем маркер

                   // «конец-рег-выражения»

 while (p != end)

  std::cout << *p++ << '\n';

}

Обсуждение

Пример 4.33 показывает, как использовать

regex
для перебора соответствий регулярному выражению. Следующая строка создает регулярное выражение.

boost::regex re(' ,|:| -|\\s+");

Она гласит, что каждое соответствие регулярному выражению — это либо запятая, либо двоеточие, либо тире, либо один или несколько пробелов. Символ канала — это логический оператор OR, используемый для объединения разделителей. Следующие две строки создают итератор.

boost::sregex_token_iterator

p(s.begin(), s.end(), re, -1);

boost::sregex_token_iterator end;

Итератор

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

Загрузка...