В современном мире информационных технологий, где данные часто представлены в текстовом формате, эффективность и безопасность работы со строками становятся краеугольным камнем разработки надежного и высокопроизводительного программного обеспечения. От парсинга логов до обработки пользовательского ввода, от сетевых протоколов до анализа больших текстовых массивов — манипуляции строками лежат в основе бесчисленного множества приложений. Недооценка сложности этой области или применение устаревших подходов может привести к серьезным уязвимостям, таким как переполнение буфера, утечки памяти или катастрофическое снижение производительности.
Целью данной курсовой работы является разработка методологической и теоретической базы для создания программного обеспечения, предназначенного для эффективной и безопасной манипуляции строками на языке C/C++. В рамках работы будет проведен глубокий аналитический обзор ключевых строковых структур и алгоритмов, исследованы современные стандарты языка C++ (включая C++17 и C++20), а также предложены архитектурные решения для построения модульного, отказоустойчивого и кроссплатформенного строкового модуля. Структура документа охватывает теоретические основы, алгоритмическую часть, вопросы обработки исключений и особенности файлового ввода-вывода, завершаясь практическими рекомендациями по реализации и документированию.
Глава 1. Теоретические основы структур данных для строк
Строки в C++ представляют собой одну из наиболее часто используемых и, одновременно, одну из самых сложных для эффективного и безопасного управления структур данных. Исторически сложилось так, что в языке сосуществуют несколько подходов к их представлению, каждый из которых имеет свои преимущества и недостатки. Понимание этих различий критически важно для разработчика, стремящегося создавать надежное и производительное ПО, поскольку от выбора представления напрямую зависит стабильность и скорость работы программы.
C-строки (char*
): Управление памятью и риски
Истоки работы со строками в C++ лежат в его предшественнике, языке C. C-строка, или строка в стиле C, по своей сути является обычным массивом символов типа char
, который имеет одно важное отличие: она всегда завершается нулевым символом (\0
). Этот нулевой терминатор служит маркером конца строки, позволяя функциям работать с массивом символов произвольной длины без необходимости передавать эту длину явно.
Для манипуляции C-строками используется набор функций из стандартной библиотеки языка C, доступных через заголовочный файл <cstring>
(или его старый вариант <string.h>
):
strlen()
: Вычисляет длину строки (до нулевого символа).strcpy()
/strncpy()
: Копирует строку.strcat()
/strncat()
: Конкатенирует (объединяет) строки.strcmp()
: Сравнивает строки.
Управление памятью C-строк представляет собой одну из главных сложностей и потенциальных источников ошибок. Когда мы объявляем C-строку как массив фиксированного размера, например, char buffer[256];
, ее размер определяется во время компиляции и не может быть изменен в процессе выполнения. Если же требуется строка динамической длины, разработчик обязан вручную выделять память в куче с помощью операторов new char[...]
(для C++) или функций malloc()
(для C). Соответственно, после использования эту память необходимо освободить, используя delete[]
или free()
.
Анализ ключевых рисков:
- Переполнение буфера (Buffer Overflow): Это наиболее серьезная и распространенная уязвимость. Если функция
strcpy()
илиstrcat()
пытается записать в буфер больше символов, чем он может вместить, данные будут записаны за пределами выделенного блока памяти. Это может привести к повреждению соседних данных, ошибкам сегментации (Segmentation Fault) или, что еще хуже, к выполнению вредоносного кода. Пример:char buffer[10]; // Попытка скопировать строку "Очень длинная строка" в буфер на 10 символов strcpy(buffer, "Очень длинная строка"); // Вызовет переполнение буфера!
- Утечки памяти (Memory Leaks): Если память, выделенная с помощью
new
илиmalloc
, не будет освобождена с помощьюdelete
илиfree
после использования, она останется занятой до завершения программы, что приведет к постепенному исчерпанию доступной оперативной памяти. - Неинициализированные указатели (Dangling Pointers): Если указатель продолжает использоваться после того, как память, на которую он указывал, была освобождена, это может привести к непредсказуемому поведению программы.
- Отсутствие нулевого терминатора: Если строка, созданная или модифицированная вручную, не будет корректно завершена нулевым символом, функции
strlen()
,strcpy()
и другие будут читать память за пределами строки, пока не встретят случайный\0
, что также может вызвать ошибки сегментации или некорректные результаты.
Работа с C-строками требует от программиста высокой дисциплины и внимательности к деталям управления памятью, поскольку малейшая невнимательность может обернуться критическими уязвимостями, подвергающими риску целостность данных и безопасность всей системы.
Класс std::string
: Автоматизация и безопасность
Для устранения многочисленных проблем, связанных с ручным управлением памятью и безопасностью C-строк, в Стандартной Библиотеке Шаблонов (STL) языка C++ был введен класс std::string
(объявленный в заголовочном файле <string>
). Этот класс представляет собой мощную и безопасную обертку над C-строками, которая инкапсулирует механизмы управления памятью и предоставляет удобный объектно-ориентированный интерфейс для манипуляции строками.
Основное преимущество std::string
заключается в автоматическом управлении памятью. Разработчику не нужно вручную выделять или освобождать память: std::string
делает это самостоятельно. При изменении размера строки (например, при конкатенации или добавлении символов), объект std::string
автоматически перераспределяет память по мере необходимости, увеличивая или уменьшая ее объем. Это исключает такие распространенные ошибки, как переполнение буфера и утечки памяти, связанные с char*
.
Сравнение с char*
с точки зрения объектных накладных расходов:
Сам по себе объект std::string
не хранит все символы строки напрямую. Вместо этого он обычно содержит несколько метаданных:
- Указатель на фактический массив символов, который хранится в динамической памяти (куче).
- Текущий размер (длину) строки.
- Текущую емкость (выделенный объем памяти), которая может быть больше размера для оптимизации будущих операций.
На 64-битных платформах, размер самого объекта std::string
(без учета данных строки, хранящихся в куче) обычно составляет от 24 до 32 байт. Например:
- В компиляторах Clang с библиотекой libc++ размер объекта
std::string
составляет 24 байта (8 байт на указатель, 8 на размер, 8 на емкость). - В GCC и MSVC он может составлять 32 байта.
Эти "накладные расходы" являются минимальной платой за автоматизацию и безопасность. Для большинства приложений преимущества std::string
значительно перевешивают эти небольшие издержки.
КРИТИЧЕСКИЙ АНАЛИЗ ПРОИЗВОДИТЕЛЬНОСТИ: Оптимизация коротких строк (SSO) и std::string_view
Понимание того, как std::string
управляет памятью, является ключом к оптимизации производительности. Однако современные реализации STL идут еще дальше, применяя умные стратегии для минимизации накладных расходов, особенно для коротких строк.
Детальное описание механизма SSO и его влияния на производительность:
Оптимизация коротких строк (Short String Optimization, SSO) — это техника, используемая в большинстве современных реализаций std::string
, при которой для строк небольшой длины данные хранятся непосредственно внутри самого объекта std::string
(на стеке), а не в динамически выделяемой памяти (куче). Это означает, что:
- Избегается выделение памяти в куче: Кучевые аллокации (heap allocations) являются относительно дорогостоящими операциями, так как они включают взаимодействие с операционной системой и могут вызывать фрагментацию памяти. SSO полностью исключает эти накладные расходы для коротких строк.
- Улучшается локальность данных: Строковые данные располагаются рядом с объектом
std::string
на стеке, что приводит к лучшему кэшированию и, как следствие, к более быстрому доступу. - Снижается риск исключений: Отсутствие выделения памяти в куче означает отсутствие исключений
std::bad_alloc
при нехватке памяти для коротких строк.
Конкретные лимиты SSO для ведущих компиляторов:
Фактическая емкость SSO (максимальная длина строки, которая может быть сохранена на стеке) зависит от реализации стандартной библиотеки, которая поставляется с конкретным компилятором. Это критически важная деталь, которая часто упускается в поверхностных обзорах:
- В компиляторах GCC и MSVC на 64-битных системах емкость SSO обычно составляет 15 символов (плюс завершающий
\0
). Это означает, что строка длиной до 15 символов будет храниться непосредственно в объектеstd::string
, избегая кучевого выделения. - В компиляторах Clang с библиотекой libc++ емкость SSO может достигать 22 или 23 символов (плюс завершающий
\0
).
Понимание этих лимитов позволяет разработчикам принимать более обоснованные решения при проектировании структур данных и оценке производительности, особенно в системах с интенсивной работой с короткими строками. Что из этого следует? Для максимальной эффективности при работе с короткими строками (менее 15-23 символов) следует избегать излишних операций, которые могли бы спровоцировать кучевое выделение памяти, например, частых конкатенаций, приводящих к превышению лимита SSO, и по возможности использовать строковые литералы напрямую.
std::string_view
(C++17): Избегание дорогостоящего копирования:
Даже с SSO, передача объекта std::string
в функцию по значению (т.е. void func(std::string s)
) может быть крайне неэффективной. Такая передача влечет за собой выделение памяти и копирование всех N символов строки, что имеет временную сложность O(N). Если строка длинная, это копирование может стать узким местом производительности.
std::string_view
, введенный в стандарте C++17, является революционным решением этой проблемы. Это невладеющий (non-owning) ссылочный тип, который представляет собой "вид" на существующую последовательность символов. Он не владеет памятью, на которую указывает, а лишь хранит указатель на начало строки и ее длину.
Как std::string_view
предотвращает дорогостоящее копирование:
- Когда
std::string
или C-строка передается в функцию какstd::string_view
, происходит копирование только двух компонентов: указателя на начало строки и ее длины. Эта операция имеет постоянную временную сложность O(1), независимо от длины строки. - Это критически важно для производительности в функциях, которые только читают (просматривают) строку и не модифицируют ее. Использование
std::string_view
значительно снижает накладные расходы на копирование и выделение памяти, особенно в системах с большим количеством вызовов функций, работающих со строками.
Пример:
void process_string(std::string_view sv) {
// Работа со строкой, но без ее модификации
std::cout << "Обрабатываемая строка: " << sv << std::endl;
}
std::string my_long_string = "Это очень, очень длинная строка, которую мы не хотим копировать.";
process_string(my_long_string); // Передается std::string_view, копирования строки не происходит
process_string("Просто C-строка"); // Также передается как std::string_view
Таким образом, комбинация std::string
для владеющих строковых данных и std::string_view
для невладеющих просмотров обеспечивает гибкость, безопасность и максимальную производительность при работе со строками в современном C++.
Глава 2. Фундаментальные строковые алгоритмы и анализ вычислительной сложности
Эффективная манипуляция строками немыслима без понимания алгоритмов, лежащих в ее основе. Одним из наиболее частых и ресурсоемких видов строковых операций является поиск подстроки в тексте. Выбор правильного алгоритма может кардинально повлиять на производительность программного обеспечения, особенно при работе с большими объемами текстовых данных.
Введение в анализ сложности (O-нотация)
Для количественной оценки эффективности алгоритмов в Computer Science используется концепция асимптотической сложности, выражаемая с помощью О-нотации (Big O notation). Эта нотация позволяет описать, как время выполнения или объем памяти, требуемый алгоритму, изменяется в зависимости от размера входных данных, когда размер входных данных стремится к бесконечности.
Определение и правила использования О-нотации:
О-нотация фокусируется на высшем порядке роста функции сложности, игнорируя константные множители и члены низшего порядка. Например, если алгоритм выполняет 3n2 + 5n + 10
операций, его асимптотическая сложность будет O(n2), поскольку при больших n
член 3n2
доминирует.
Виды оценки сложности:
- Худший случай (Worst-Case Complexity): Наибольшее время выполнения для любого входного набора данных заданного размера. Это наиболее часто используемая оценка, поскольку она дает гарантию верхней границы производительности.
- Лучший случай (Best-Case Complexity): Наименьшее время выполнения для любого входного набора данных заданного размера. Часто используется для демонстрации потенциальной эффективности, но редко является практической гарантией.
- Средний случай (Average-Case Complexity): Ожидаемое время выполнения для случайного входного набора данных. Требует знания распределения входных данных.
О-нотация является фундаментальным инструментом для сравнения алгоритмов и выбора наиболее подходящего для конкретной задачи, предоставляя разработчику возможность предсказывать поведение программы при масштабировании данных, а также является незаменимым средством для проектной документации.
Наивный алгоритм поиска
Простейший подход к поиску подстроки-образца (паттерна) в тексте называется наивным или прямым алгоритмом поиска. Его логика интуитивно понятна: последовательно сравнивать образец с каждой возможной позицией в тексте.
Описание простого алгоритма:
Пусть у нас есть текст T
длины n
и образец P
длины m
.
Алгоритм работает следующим образом:
- Начать с первого символа текста.
- Сравнить образец
P
с подстрокой текстаT[i...i+m-1]
. - Если все
m
символов совпадают, образец найден. - Если есть несовпадение или образец не найден, сдвинуть образец на одну позицию вправо в тексте и повторить сравнение.
- Продолжать до тех пор, пока образец не будет найден или не будет исчерпан текст.
Демонстрация его худшей временной сложности O(n · m) на патологических примерах:
Хотя на первый взгляд алгоритм может показаться эффективным, в худшем случае его производительность значительно снижается. Это происходит, когда большинство символов совпадают при каждом сдвиге, но в итоге всегда обнаруживается несовпадение.
Рассмотрим патологический пример:
- Текст (T):
"AAAAAAAAAB"
(длинаn = 10
) - Образец (P):
"AAB"
(длинаm = 3
)
Последовательность сравнений:
T[0..2]
("AAA"
) vsP
("AAB"
):AA
совпадает,A
vsB
не совпадает.- Сдвиг на 1 позицию.
T[1..3]
("AAA"
) vsP
("AAB"
):AA
совпадает,A
vsB
не совпадает. - … (повторяется 7 раз) …
- Сдвиг.
T[7..9]
("AAB"
) vsP
("AAB"
): Полное совпадение.
В каждом из n - m + 1
возможных сдвигов алгоритм выполняет до m
сравнений символов. В худшем случае, когда образец почти совпадает, но в итоге не совпадает (как в примере выше), общее количество операций приближается к (n - m + 1) * m
. Если m
сравнимо с n
, это приводит к квадратичной временной сложности: O(n · m). Это делает наивный алгоритм неприемлемым для больших текстов и/или длинных образцов.
Линейные алгоритмы: Кнута-Морриса-Пратта (КМП)
Наивный алгоритм неэффективен, потому что он "забывает" информацию о частичных совпадениях, обнаруженных ранее. Алгоритм Кнута-Морриса-Пратта (КМП) решает эту проблему, достигая линейной временной сложности за счет "интеллектуальных" сдвигов.
Описание логики работы КМП, использование префикс-функции:
КМП алгоритм, разработанный Кнутом, Моррисом и Праттом в 1970-х годах, основан на идее, что при несовпадении не обязательно сдвигать образец всего на один символ. Вместо этого можно использовать информацию о структуре самого образца (о его префиксах и суффиксах), чтобы определить, насколько далеко можно безопасно сдвинуть образец, не пропуская при этом потенциальные совпадения.
Ключевым элементом КМП является префикс-функция (также известная как pi-функция
или failure function
), которая для каждого префикса образца P[1…k] вычисляет длину самого длинного собственного префикса, который также является суффиксом этого префикса. Проще говоря, она отвечает на вопрос: "Какой самый длинный префикс образца P[1…k] совпадает с самым длинным суффиксом образца P[1…k]?"
Как работает алгоритм:
- Предварительная обработка образца: Вычисляется префикс-функция для образца
P
. Это занимает O(m) времени. - Поиск: При поиске в тексте
T
, когда происходит несовпадение междуT[i]
иP[j]
, вместо сдвига образца на один символ, алгоритм использует значение префикс-функции π[j-1], чтобы определить новый индексj
. Это позволяет избежать повторных сравнений уже известных частей образца.
Доказательство линейной сложности O(n + m):
- Построение таблицы префикс-функции: Занимает O(m) времени, где
m
— длина образца. Каждый символ образца обрабатывается не более двух раз (увеличениеj
и уменьшениеj
через префикс-функцию). - Поиск в тексте: Занимает O(n) времени, где
n
— длина текста. В каждом шаге либо указатель на текстi
увеличивается, либо указатель на образецj
уменьшается (но никогда не становится отрицательным и не превышаетm
). Общее количество увеличенийi
равноn
, общее количество уменьшенийj
не может превышать количество увеличений.
Следовательно, общая временная сложность КМП алгоритма составляет O(n + m). Пространственная сложность составляет O(m) для хранения таблицы префикс-функции.
Блок-схема алгоритма КМП:
graph TD
A[Начало] --> B{Построить префикс-функцию для P};
B --> C{Инициализировать: i=0 (текст), j=0 (образец)};
C --> D{Пока i < n};
D -- Да --> E{Пока j > 0 И T[i] != P[j]};
E -- Да --> F{j = pi[j-1]};
E -- Нет --> G{Если T[i] == P[j]};
G -- Да --> H{j++};
G -- Нет --> H_no{Продолжить};
H --> I{Если j == m};
I -- Да --> J{Образец найден по индексу i - m + 1};
J --> K{j = pi[j-1]};
K --> L{i++};
L --> D;
I -- Нет --> L;
H_no --> L;
F --> G;
D -- Нет --> M[Конец: Образец не найден];
Оптимизированные алгоритмы: Бойера-Мура (БМ)
Алгоритм Бойера-Мура (БМ) является одним из наиболее эффективных алгоритмов поиска подстроки общего назначения. Его отличительной особенностью является то, что он сравнивает образец с текстом справа налево, что позволяет делать более значительные сдвиги, чем КМП, и часто оказывается быстрее на практике.
Описание принципа работы БМ (сравнение справа налево):
В отличие от наивного алгоритма и КМП, которые начинают сравнение с начала образца, Бойер-Мур начинает с его конца. Если происходит несовпадение, алгоритм использует информацию о несовпавшем символе (или о совпавшем суффиксе) для определения максимального безопасного сдвига.
Объяснение эвристик "плохого символа" и "хорошего суффикса":
БМ использует две основные эвристики для определения величины сдвига:
- Правило плохого символа (Bad Character Heuristic):
- Когда происходит несовпадение между символом
T[i]
в тексте иP[j]
в образце (при сравнении справа налево), алгоритм смотрит на символT[i]
. - Если этот символ
T[i]
не встречается в образцеP
вообще, или встречается только левее текущей позицииj
, то можно безопасно сдвинуть образец наm
символов (или на расстояние до первого появленияT[i]
в образце, если оно есть), минуяT[i]
. - Если
T[i]
встречается в образце правее позицииj
, образец сдвигается так, чтобыT[i]
совпал с ближайшим вхождением этого символа в образце. - Для этого правила требуется предварительно построить таблицу (или массив), которая для каждого символа алфавита хранит его последнее вхождение в образце.
- Когда происходит несовпадение между символом
- Правило хорошего суффикса (Good Suffix Heuristic):
- Когда происходит несовпадение, но часть образца (суффикс) уже совпала с текстом, правило хорошего суффикса пытается найти в образце другую подстроку, которая является префиксом этого совпавшего суффикса.
- Это позволяет сдвинуть образец так, чтобы этот "хороший суффикс" снова совпал, минимизируя повторные сравнения.
- Это правило является более сложным для реализации и требует предварительного построения таблицы, аналогичной префикс-функции КМП.
Алгоритм Бойера-Мура всегда выбирает максимальный сдвиг, предложенный обеими эвристиками, что позволяет ему "прыгать" через большие участки текста.
Анализ лучшего случая O(n/m) и уточнение общей линейной сложности O(n + m):
- Лучший случай O(n/m): БМ демонстрирует свою максимальную эффективность, когда в тексте встречаются символы, которых нет в образце, или когда несовпадение происходит в первой же позиции при сравнении справа налево. В таких случаях алгоритм может сдвигать образец сразу на
m
позиций за одно сравнение. Например, если образецP = "XYZ"
и текстT = "ABCABCABC..."
, при каждом сравненииZ
не совпадает, и образец сдвигается на 3 позиции. Это приводит к сложности O(n/m). - Уточнение общей линейной сложности O(n + m): Изначально для некоторых упрощенных или устаревших вариаций БМ (например, алгоритм Хорспула, который является упрощением БМ) в худшем случае могла быть продемонстрирована квадратичная сложность O(n · m) на специально подобранных "патологических" данных. Однако для полной, корректно реализованной версии алгоритма Бойера-Мура (а также его модификаций, таких как "Turbo-BM"), худшая временная сложность является линейной и составляет O(n + m). Этот результат был доказан Робертом Седжвиком и другими исследователями. Пространственная сложность БМ составляет O(|Σ| + m), где
|Σ|
— размер алфавита, для хранения таблиц эвристик.
Таким образом, Бойер-Мур на практике часто оказывается быстрее КМП для больших текстов и реальных данных благодаря своим эвристикам, позволяющим пропускать значительные части текста.
Глава 3. Разработка архитектуры программного обеспечения и обработка ошибок
Разработка надежного программного обеспечения для работы со строками требует не только знания эффективных алгоритмов, но и продуманной архитектуры, а также комплексного подхода к обработке ошибок. Модульность, инкапсуляция и корректное управление исключениями являются краеугольными камнями качественного кода.
Модульная архитектура для работы со строками
Для обеспечения повторного использования кода, его легкости в модификации и тестировании, а также для инкапсуляции логики работы со строками, крайне важно применять принципы модульного проектирования.
Предложение структуры модуля для обеспечения повторного использования кода и инкапсуляции:
Оптимальным решением является создание специализированного класса или набора функций, которые будут отвечать за все операции со строками.
- Класс
StringProcessor
(илиTextAnalyzer
):- Назначение: Инкапсулирует сложные операции со строками, такие как поиск, замена, парсинг, валидация.
- Преимущества:
- Инкапсуляция: Вся логика работы со строками скрыта внутри класса, предоставляя пользователю простой и понятный интерфейс.
- Состояние: Класс может хранить внутреннее состояние (например, кешированные результаты, таблицы для алгоритмов), что позволяет оптимизировать последовательные операции.
- Повторное использование: Один раз разработанный и отлаженный класс может быть использован в различных частях проекта или в других проектах.
- Обработка ошибок: Методы класса могут быть спроектированы таким образом, чтобы генерировать специфические исключения, отражающие внутренние ошибки работы со строками.
- Примерная структура класса:
class StringProcessor { public: // Конструктор, возможно, принимающий строку для инициализации StringProcessor(std::string_view initial_text = ""); // Методы для поиска: // Возвращает позицию первого вхождения, или std::string::npos size_t findFirst(std::string_view pattern, size_t start_pos = 0) const; // Возвращает вектор всех позиций std::vector<size_t> findAll(std::string_view pattern) const; // Методы для замены: std::string replaceFirst(std::string_view old_sub, std::string_view new_sub); std::string replaceAll(std::string_view old_sub, std::string_view new_sub); // Методы для валидации с использованием регулярных выражений: bool isValidEmail(std::string_view email) const; bool matchPattern(std::string_view text, const std::string& regex_pattern) const; // Другие полезные функции (split, join, trim и т.д.) std::vector<std::string> split(char delimiter) const; // Метод для установки текста, с которым будет работать процессор void setText(std::string_view new_text); private: std::string current_text_; // Строка, с которой работает процессор // Возможно, дополнительные поля для оптимизации или кеширования // Например, std::regex email_regex_; };
- Набор независимых функций (
Utils
):- Назначение: Для более простых, атомарных операций, которые не требуют хранения состояния.
- Преимущества: Легковесность, отсутствие необходимости в создании объекта.
- Пример:
trim(std::string_view)
(удаление пробелов по краям),toLower(std::string_view)
(перевод в нижний регистр). Эти функции могут быть статическими методами в вспомогательном классе или обычными функциями в отдельном пространстве имен.
Модульный подход позволяет разделить ответственность, облегчает тестирование отдельных компонентов и повышает общую поддерживаемость кодовой базы.
Современные методы манипуляции: Регулярные выражения
Когда дело доходит до сложного поиска, замены или проверки строк по шаблону, традиционные алгоритмы поиска подстрок (как КМП или Бойер-Мур) становятся недостаточными. Здесь на помощь приходят регулярные выражения.
Описание использования библиотеки <regex>
(std::regex
, std::regex_match
, std::regex_search
) для сложных операций:
В C++11 была добавлена стандартная библиотека <regex>
, которая предоставляет мощные инструменты для работы с регулярными выражениями. Она основана на классе std::regex
, который компилирует шаблон регулярного выражения.
Основные компоненты:
std::regex
: Объект, который хранит скомпилированное регулярное выражение.std::smatch
(дляstd::string
) илиstd::cmatch
(для C-строк): Объект для хранения результатов совпадения, включая всю найденную подстроку и захваченные группы.
Основные функции:
std::regex_match(str, regex)
: Проверяет, полностью ли строкаstr
соответствует регулярному выражениюregex
. Возвращаетtrue
илиfalse
.std::string text = "пример@email.com"; std::regex email_pattern(R"(\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,4}\b)"); if (std::regex_match(text, email_pattern)) { std::cout << "Строка является корректным email." << std::endl; }
std::regex_search(str, match_result, regex)
: Ищет первое вхождение подстроки, соответствующей регулярному выражениюregex
, в строкеstr
. Результаты сохраняются вmatch_result
. Возвращаетtrue
илиfalse
.std::string text = "Дата: 07.10.2025. Встреча запланирована."; std::regex date_pattern(R"(\d{2}\.\d{2}\.\d{4})"); std::smatch matches; if (std::regex_search(text, matches, date_pattern)) { std::cout << "Найдена дата: " << matches[0] << std::endl; // matches[0] - полное совпадение }
std::regex_replace(str, regex, fmt)
: Заменяет все вхождения шаблонаregex
в строкеstr
на форматированную строкуfmt
.std::string text = "Телефон: 123-45-67, Домашний: 890-12-34"; std::regex phone_pattern(R"(\d{3}-\d{2}-\d{2})"); std::string replaced_text = std::regex_replace(text, phone_pattern, "***-**-**"); std::cout << "Замененный текст: " << replaced_text << std::endl; // Вывод: Замененный текст: Телефон: ***-**-**, Домашний: ***-**-**
Регулярные выражения предоставляют чрезвычайно гибкий и мощный механизм для обработки текстовых данных, значительно упрощая сложные задачи парсинга и валидации.
Иерархия исключений STL и безопасная обработка
Корректная обработка ошибок является критически важным аспектом разработки надежного программного обеспечения. В C++ для этого используется механизм исключений. Понимание иерархии стандартных исключений и правил их обработки позволяет создавать отказоустойчивые приложения.
Обзор иерархии исключений в C++:
Все стандартные исключения в C++ наследуются от базового класса std::exception
, определенного в заголовочном файле <exception>
. Этот базовый класс предоставляет виртуальный метод what()
, который возвращает C-строку с описанием ошибки, что позволяет полиморфно обрабатывать любые стандартные исключения.
Иерархия выглядит следующим образом:
std::exception
├── std::logic_error (ошибки, которые можно было предотвратить до выполнения)
│ ├── std::invalid_argument (недопустимый аргумент функции)
│ ├── std::domain_error
│ ├── std::length_error (попытка превысить максимальную длину объекта)
│ └── std::out_of_range (доступ к элементу вне допустимого диапазона)
└── std::runtime_error (ошибки, возникающие во время выполнения)
├── std::overflow_error
├── std::underflow_error
├── std::range_error
├── std::system_error
└── std::regex_error (ошибки регулярных выражений)
Детальное описание специфических исключений для строк:
std::out_of_range
: Это исключение генерируется, когда происходит попытка доступа к элементу или подстроке, находящейся вне допустимого диапазона для строкового объекта.- Примеры возникновения:
- При вызове метода
std::string::at(pos)
с индексомpos
, который выходит за пределы строки. (Оператор[]
не генерирует исключений, а приводит к неопределенному поведению). - При вызове
std::string::substr(pos, len)
с недопустимой начальной позициейpos
(еслиpos > string.size()
).
- При вызове метода
- Пример кода:
std::string s = "Hello"; try { char c = s.at(10); // Индекс 10 выходит за пределы строки (длина 5) } catch (const std::out_of_range& e) { std::cerr << "Ошибка диапазона: " << e.what() << std::endl; }
- Примеры возникновения:
std::length_error
: Это исключение выбрасывается, когда операция со строкой приводит к попытке создать объект, длина которого превышает максимальный предел, поддерживаемый реализацией (обычноstd::string::max_size()
).- Примеры возникновения:
- При попытке создать
std::string
с длиной, превышающейmax_size()
. - При конкатенации строк, если результат превышает
max_size()
. - При вызове
std::string::reserve()
с аргументом, превышающимmax_size()
.
- При попытке создать
- Пример кода:
try { // Теоретический пример: попытка создать строку, которая слишком длинная // В реальных условиях max_size() очень велико, поэтому трудно воспроизвести std::string very_long_string(std::string::max_size() + 1, 'a'); } catch (const std::length_error& e) { std::cerr << "Ошибка длины: " << e.what() << std::endl; }
- Примеры возникновения:
std::regex_error
: Это исключение генерируется библиотекой<regex>
при возникновении ошибок, связанных с неверным синтаксисом или использованием регулярных выражений.- Коды ошибок
std::regex_error
:- Исключение
std::regex_error
содержит методcode()
, который возвращает конкретный тип ошибки, определенный в перечисленииstd::regex_constants::error_type
. - Примеры кодов ошибок:
std::regex_constants::error_brack
: Несогласованные квадратные скобки[]
.std::regex_constants::error_paren
: Несогласованные круглые скобки()
.std::regex_constants::error_badrepeat
: Неверное использование повторителя (например,*
или+
в начале шаблона).std::regex_constants::error_complexity
: Слишком сложный шаблон, приводящий к чрезмерной рекурсии или времени вычисления.
- Исключение
- Пример кода:
try { std::regex bad_pattern("[a-z"); // Несогласованная скобка } catch (const std::regex_error& e) { std::cerr << "Ошибка регулярного выражения: " << e.what() << " (Код: " << e.code() << ")" << std::endl; }
- Коды ошибок
Правила обработки исключений:
- Генерировать исключения по значению (
throw
): Когда возникает исключительная ситуация, следует создавать объект исключения и выбрасывать его по значению. Например,throw std::runtime_error("Описание ошибки");
. - Перехватывать исключения по константной ссылке (
catch
): Для обработки исключений всегда используйтеcatch (const std::exception& e)
. Это позволяет:- Сохранить полиморфный тип: Вы сможете обрабатывать исключения различных типов через их базовый класс
std::exception
. - Избежать срезания (slicing): Если перехватить по значению, объект исключения будет скопирован, и его тип может быть "урезан" до типа
std::exception
, теряя специфическую информацию о производном типе. - Предотвратить лишнее копирование: Передача по ссылке более эффективна.
- Сохранить полиморфный тип: Вы сможете обрабатывать исключения различных типов через их базовый класс
Пример общей обработки исключений:
try {
// Код, который может генерировать исключения
StringProcessor processor("initial text");
processor.setText("another string");
// ...
} catch (const std::out_of_range& e) {
std::cerr << "Ошибка в StringProcessor: выход за диапазон: " << e.what() << std::endl;
// Специфичная обработка для out_of_range
} catch (const std::regex_error& e) {
std::cerr << "Ошибка в StringProcessor: регулярное выражение: " << e.what() << std::endl;
// Специфичная обработка для regex_error
} catch (const std::exception& e) {
std::cerr << "Общая ошибка в StringProcessor: " << e.what() << std::endl;
// Обработка всех остальных стандартных исключений
} catch (...) {
std::cerr << "Неизвестная ошибка в StringProcessor!" << std::endl;
// Обработка любых других исключений (нестандартных, C-исключений)
}
Такой многоуровневый подход к обработке исключений гарантирует, что программа сможет адекватно реагировать на различные сбои, повышая ее надежность и устойчивость.
Глава 4. Ввод-вывод и кроссплатформенная работа с кодировками
Работа со строками тесно связана с операциями ввода-вывода, особенно с чтением и записью данных в файлы. При этом возникает ряд специфических проблем, связанных с кодировками символов, особенно в контексте многоязычных приложений и кроссплатформенной разработки.
Безопасный файловый ввод-вывод
Для работы с файлами в C++ используется стандартная библиотека <fstream>
, предоставляющая объектно-ориентированный интерфейс к файловым потокам.
Использование классов <fstream>
:
std::ifstream
: Поток для ввода из файла (чтения).std::ofstream
: Поток для вывода в файл (записи).std::fstream
: Универсальный поток для чтения и записи.
Пример открытия файла для чтения:
#include <fstream>
#include <iostream>
#include <string>
// ...
std::ifstream fin("input.txt");
if (!fin.is_open()) {
std::cerr << "Ошибка открытия файла input.txt" << std::endl;
// Обработка ошибки, выход из программы или другое действие
return;
}
// Файл успешно открыт, можно читать
Демонстрация корректного цикла чтения строк с помощью while (std::getline(fin, s))
:
Одной из наиболее распространенных задач является построчное чтение текстового файла. Для этого используется функция std::getline()
, которая считывает строку до символа новой строки (или другого заданного разделителя) и сохраняет ее в объект std::string
.
Наиболее надежным и идиоматическим способом организации цикла чтения является использование самого потокового объекта в условии цикла:
std::string line;
while (std::getline(fin, line)) {
// В этом блоке 'line' содержит успешно прочитанную строку
std::cout << "Прочитано: " << line << std::endl;
}
Почему это корректно: Оператор >>
и функция std::getline()
возвращают ссылку на потоковый объект, который неявно преобразуется в bool
. Это преобразование возвращает true
, если операция ввода прошла успешно, и false
, если произошла ошибка (например, конец файла, ошибка чтения, неверный формат). Таким образом, цикл автоматически завершается при достижении конца файла или возникновении ошибки.
Объяснение ошибки "последней строки" при некорректном использовании fin.eof()
:
Часто начинающие программисты используют fin.eof()
(End Of File) в условии цикла:
// НЕПРАВИЛЬНЫЙ СПОСОБ:
while (!fin.eof()) {
std::string line;
std::getline(fin, line);
if (!line.empty()) { // Дополнительная проверка, чтобы избежать пустых строк в конце
std::cout << "Прочитано (некорректно): " << line << std::endl;
}
}
Этот подход неправилен и часто приводит к ошибке, при которой последняя строка считывается некорректно или дважды. Причина в том, что флаг eof
устанавливается после попытки чтения, которая наткнулась на конец файла. То есть, после того как была прочитана последняя осмысленная строка, следующая операция std::getline()
потерпит неудачу (потому что больше нет данных), но флаг eof
будет установлен только после этой неуспешной операции. В результате, тело цикла может быть выполнено еще раз с пустой или некорректной строкой.
Корректное использование std::getline(fin, line)
в условии цикла гарантирует, что блок кода будет выполнен только тогда, когда чтение прошло успешно.
Проблема Unicode и устаревшие/современные решения
В мире, где текст не ограничивается латиницей, поддержка многоязычных символов (Unicode) становится не просто желательной, а обязательной. Однако в C++ работа с Unicode исторически была сопряжена со сложностями.
Анализ проблемы кроссплатформенности std::wstring
из-за зависимости размера wchar_t
от ОС:
Для работы с широкими символами в C++ был введен тип wchar_t
(wide character) и соответствующий ему строковый класс std::wstring
. Идея заключалась в том, что wchar_t
должен быть достаточно большим, чтобы вместить любой символ локально используемой кодировки.
Однако ключевая проблема wchar_t
и std::wstring
заключается в их некроссплатформенности и платформозависимости размера:
- В операционных системах Windows,
wchar_t
обычно имеет размер 2 байта и используется для хранения символов в кодировке UTF-16. - В операционных системах Linux/macOS,
wchar_t
обычно имеет размер 4 байта и используется для хранения символов в кодировке UTF-32.
Это означает, что одна и та же программа, использующая std::wstring
, будет по-разному интерпретировать символы на разных платформах, если они ожидают разную кодировку. Например, файл, записанный в UTF-16 на Windows, может быть некорректно прочитан как UTF-32 на Linux, приводя к "кракозябрам". Из-за этой платформозависимости std::wstring
не рекомендуется для кроссплатформенной работы с Unicode.
Критическое замечание: Устаревание <codecvt>
в C++17 и удаление в C++26:
Для преобразования между различными кодировками (например, char
и wchar_t
, или UTF-8 и UTF-16) в C++11 и C++14 предлагались классы std::codecvt
(как грань локали) и std::wstring_convert
.
Однако, эти механизмы были официально объявлены устаревшими (deprecated) в C++17 и запланированы к удалению из стандартной библиотеки в C++26. Причина этого решения — их сложность в корректном использовании и неспособность адекватно обрабатывать все аспекты Unicode (например, суррогатные пары в UTF-16, нормализацию Unicode). Они были спроектированы в эпоху, когда основным стандартом для широких символов был UCS-2, а не полноценный UTF-16/32.
Таким образом, использование
std::codecvt
иstd::wstring_convert
в новых проектах строго не рекомендуется, так как это приведет к использованию устаревшего кода, который будет удален из будущих стандартов. Какой важный нюанс здесь упускается? Разработчики должны понимать, что, несмотря на наличие этих классов в более старых стандартах, их применение в современных проектах создает технический долг и риски несовместимости с будущими версиями компиляторов и библиотек.
Введение в char8_t
(C++20) как типобезопасный способ работы со строками UTF-8 и соответствующие строковые литералы u8"..."
:
С появлением C++20 ситуация с Unicode значительно улучшилась благодаря введению типа char8_t
.
char8_t
(C++20): Это новый фундаментальный тип символов, специально предназначенный для хранения кодовых единиц UTF-8. Его ключевое преимущество в том, что он типобезопасен и делает кодировку явной. В отличие отchar
(который может быть как знаковым, так и беззнаковым, и часто используется для байтов, а не для текстовых символов),char8_t
всегда беззнаковый и однозначно ассоциируется с UTF-8. Это исключает двусмысленность и потенциальные ошибки при работе с кодировками.- Соответствующие строковые типы:
std::u8string
(на основеchar8_t
)std::u16string
(на основеchar16_t
для UTF-16)std::u32string
(на основеchar32_t
для UTF-32)std::string
по умолчанию продолжает использоваться для локальных или UTF-8 строк (но без типобезопасностиchar8_t
).
- Строковые литералы
u8"..."
: Для создания строковых литералов типаchar8_t
используется префиксu8
.#include <string> #include <iostream> int main() { std::u8string utf8_str = u8"Привет, мир! 😊"; std::cout << "UTF-8 строка: "; // Для вывода u8string на консоль может потребоваться настройка локали или преобразование for (char8_t c : utf8_str) { std::cout << static_cast<char>(c); // Временное решение для вывода на std::cout } std::cout << std::endl; return 0; }
Использование char8_t
и std::u8string
в C++20 представляет собой современный, типобезопасный и кроссплатформенный подход к работе с UTF-8, которая является де-факто стандартом кодирования текста в интернете и многих операционных системах. Для преобразования между кодировками рекомендуется использовать специализированные сторонние библиотеки (например, ICU — International Components for Unicode) или низкоуровневые C-функции (mbstowcs
/wcstombs
), тщательно контролируя локали, поскольку стандартная библиотека C++ до сих пор не предлагает полноценного кроссплатформенного решения для конвертации Unicode.
Глава 5. Практическая реализация и документация
Завершающий этап работы над курсовым проектом — это не только написание кода, но и его корректное оформление в виде технического отчета, а также создание исчерпывающей пользовательской документации. Эти компоненты демонстрируют понимание полного жизненного цикла разработки ПО и являются обязательными для академической работы.
Проектная документация и листинг кода
Качественная проектная документация является неотъемлемой частью любого программного проекта. Она служит мостом между идеей, реализацией и будущим развитием, обеспечивая понимание системы как для автора, так и для других разработчиков.
Обзор структуры финального технического отчета (включая архитектуру, блок-схемы, тестовые примеры):
Финальный технический отчет должен представлять собой всесторонний документ, который охватывает все аспекты разработанного программного модуля. Его структура может включать следующие разделы:
- Введение:
- Актуальность проблемы.
- Цели и задачи курсовой работы.
- Обзор предметной области.
- Теоретические основы:
- Подробное описание строковых типов (C-строки,
std::string
,std::string_view
,char8_t
). - Сравнительный анализ и обоснование выбора используемых типов.
- Введение в O-нотацию и основы анализа алгоритмов.
- Подробное описание строковых типов (C-строки,
- Алгоритмическая часть:
- Детальное описание выбранных алгоритмов поиска и манипуляции строками (например, КМП, Бойера-Мура).
- Блок-схемы для каждого ключевого алгоритма, наглядно иллюстрирующие их логику.
- Анализ временной и пространственной сложности, включая лучшие, худшие и средние случаи (где применимо).
- Архитектура программного обеспечения:
- Описание модульной структуры разработанного ПО (например, класс
StringProcessor
и вспомогательные функции). - Обоснование архитектурных решений (инкапсуляция, повторное использование).
- Диаграммы классов (UML), иллюстрирующие взаимосвязи между компонентами.
- Обзор используемых современных методов (регулярные выражения, C++20 Unicode).
- Стратегия обработки исключений, включая иерархию и правила использования
try/catch
.
- Описание модульной структуры разработанного ПО (например, класс
- Детали реализации:
- Краткое описание ключевых фрагментов кода.
- Используемые структуры данных (если не описаны в теоретической части).
- Особенности работы с файловым вводом-выводом и кодировками.
- Тестирование:
- Описание методологии тестирования (например, модульное, интеграционное).
- Тестовые примеры с ожидаемыми результатами для проверки корректности работы алгоритмов и функционала.
- Анализ результатов тестирования.
- Руководство пользователя: (Подробно описано в следующем разделе)
- Заключение:
- Суммирование выполненных работ и достигнутых целей.
- Выводы по эффективности и безопасности разработанного модуля.
- Возможные направления дальнейшего развития.
- Список использованных источников:
- Ссылки на учебники, научные статьи, стандарты ISO C++.
- Приложения:
- Полный листинг кода программы.
Указание на необходимость предоставления полного листинга кода с комментариями:
Весь исходный код проекта должен быть приложен к курсовой работе в качестве приложения. Каждый файл, класс и функция должны быть снабжены подробными комментариями, объясняющими их назначение, параметры, возвращаемые значения, а также особенности реализации и потенциальные исключения. Хорошо прокомментированный код значительно упрощает его понимание и верификацию.
Руководство пользователя
Руководство пользователя является ключевым элементом документации, который позволяет целевой аудитории (в данном случае — пользователю программного модуля) эффективно взаимодействовать с разработанным ПО, не углубляясь в его внутреннюю реализацию.
Описание, как оформляется раздел "Руководство пользователя":
Раздел "Руководство пользователя" должен быть написан простым и понятным языком, избегая излишней технической терминологии, которая уже раскрыта в основной части отчета. Он должен содержать следующую информацию:
- Назначение программы: Краткое и ясное описание того, для чего предназначена программа и какие задачи она решает (например, "Программа предназначена для выполнения различных операций со строками, включая поиск подстрок, замену, и валидацию по регулярным выражениям").
- Системные требования:
- Операционная система (Windows, Linux, macOS).
- Минимальные требования к процессору и оперативной памяти.
- Необходимые компиляторы и библиотеки (например, "C++17-совместимый компилятор (GCC 7+, Clang 5+, MSVC 15+)").
- Установка:
- Подробные инструкции по компиляции исходного кода (если это применимо) или установке исполняемого файла.
- Пример команд для сборки проекта (например, с использованием
g++
илиCMake
). - Указание на необходимые зависимости.
- Запуск программы:
- Команды для запуска программы из командной строки.
- Описание аргументов командной строки (если они используются), их синтаксис и назначение.
- Пример:
my_string_processor.exe -i input.txt -o output.txt --pattern "search_term"
- Описание входных данных:
- Формат входных файлов (если программа работает с файлами).
- Требования к кодировке текста (например, "Входные файлы должны быть в кодировке UTF-8").
- Примеры входных данных.
- Описание выходных данных:
- Формат и структура выходных файлов или информации, выводимой на консоль.
- Примеры выходных данных.
- Примеры использования:
- Пошаговые примеры использования программы для решения типичных задач.
- Каждый пример должен включать:
- Краткое описание задачи.
- Команду для запуска.
- Ожидаемый результат (вывод на консоль или содержимое выходного файла).
- Обработка ошибок и сообщения:
- Описание возможных ошибок, которые могут возникнуть при работе программы (например, "файл не найден", "неверный формат входных данных").
- Примеры сообщений об ошибках и рекомендации по их устранению.
- Контактная информация: (Необязательно для академической работы)
Тщательно составленное руководство пользователя не только повышает ценность курсовой работы, но и демонстрирует способность автора к ясному и структурированному изложению технической информации для различных аудиторий.
Заключение
В рамках данной методологической разработки была построена всеобъемлющая теоретическая и практическая база для создания программного модуля по манипуляции строками на языке C++. Мы последовательно рассмотрели эволюцию строковых структур, начиная с низкоуровневых C-строк (char*
) с их присущими рисками переполнения буфера и утечек памяти, и дошли до современных и безопасных решений, таких как std::string
.
Особое внимание было уделено критически важным аспектам производительности: детально проанализирована оптимизация коротких строк (SSO) с приведением конкретных лимитов для ведущих компиляторов (15 символов для MSVC/GCC, 22-23 для Clang/libc++), а также раскрыта роль std::string_view
(C++17) как эффективного средства для предотвращения дорогостоящего копирования строк O(N) в функциях, тем самым значительно повышая общую эффективность кода.
В разделе, посвященном алгоритмам, мы исследовали фундаментальные подходы к поиску подстрок, начиная с наивного алгоритма и его квадратичной сложности O(n · m), и переходя к высокоэффективным линейным решениям: Кнута-Морриса-Пратта (КМП) с его доказанной сложностью O(n + m) и Бойера-Мура (БМ), который, благодаря своим эвристикам, в лучшем случае достигает O(n/m) и обладает общей линейной сложностью O(n + m) на практике.
Мы также спроектировали модульную архитектуру для строкового процессора, обеспечивающую инкапсуляцию и повторное использование, и подробно остановились на современных методах обработки строк, включая мощные возможности регулярных выражений (std::regex
). Ключевой акцент был сделан на безопасной иерархии исключений STL, с детальным описанием std::out_of_range
, std::length_error
и std::regex_error
, а также лучших практик их обработки.
Наконец, мы рассмотрели нюансы файлового ввода-вывода, подчеркнув важность корректного цикла чтения while (std::getline(fin, s))
и проблему устаревших подходов. Была особо отмечена проблема кроссплатформенности std::wstring
и устаревание <codecvt>
в C++17 и его удаление в C++26. В качестве современного, типобезопасного и кроссплатформенного решения для работы с UTF-8 представлен char8_t
из C++20.
Таким образом, разработанный методологический подход обеспечивает прочную основу для создания программного модуля, который соответствует самым высоким требованиям к безопасности, эффективности, модульности и поддержке современных стандартов C++. Он готов стать надежным фундаментом для дальнейшей практической реализации и академического отчета.
Список использованной литературы
- Дейтел, Х., Дейтел, П. Как программировать на С++ [Электронный ресурс].
- Липпман, Дж. Основы программирования на С++. Санкт-Петербург, 2002.
- Либерти, Дж. Освой самостоятельно C++ за 21 день [Электронный ресурс].
- Страуструп, Б. Язык программирования С++ [Электронный ресурс].
- Конспект лекций по курсу «Основы алгоритмизации и программирования».
- Введение в регулярные выражения в современном C++ // habr.com : [сайт]. URL: https://habr.com/ru/articles/532328/ (дата обращения: 07.10.2025).
- Работа со строками в C++: основы, методы и применение // kedu.ru : [сайт]. URL: https://kedu.ru/press-center/pro-c/string_cpp/ (дата обращения: 07.10.2025).
- Чем обусловлены различия в работе со строками и другими массивами? // habr.com : [сайт]. URL: https://habr.com/ru/qa/62499/ (дата обращения: 07.10.2025).
- Сильно ли string ресурсозатратнее char[]? // stackoverflow.com : [сайт]. URL: https://ru.stackoverflow.com/questions/827618/сильно-ли-string-ресурсозатратнее-char (дата обращения: 07.10.2025).
- Алгоритм Кнута-Морриса-Пратта // neerc.ifmo.ru : [сайт]. URL: http://neerc.ifmo.ru/wiki/index.php?title=Алгоритм_Кнута-Морриса-Пратта&oldid=85136 (дата обращения: 07.10.2025).
- C++ | Строки с поддержкой Unicode // metanit.com : [сайт]. URL: https://metanit.com/cpp/tutorial/6.15.php (дата обращения: 07.10.2025).
- Регулярные выражения (C++) // microsoft.com : [сайт]. URL: https://learn.microsoft.com/ru-ru/cpp/standard-library/regular-expressions-cpp?view=msvc-170 (дата обращения: 07.10.2025).
- Исключения в C++: типы, синтаксис и обработка // tproger.ru : [сайт]. URL: https://tproger.ru/articles/exceptions-in-c-types-syntax-and-handling/ (дата обращения: 07.10.2025).
- Какие основные различия между массивами char и строками в C++? // ya.ru : [сайт]. URL: https://ya.ru/znatoki/question/kakie-osnovnye-razlichiya-mezhdu-massivami-char-i-057d195c-373e-4340-93a8-a37a6b9a9578 (дата обращения: 07.10.2025).
- Строковые алгоритмы на практике. Часть 2 — Алгоритм Бойера — Мура // habr.com : [сайт]. URL: https://habr.com/ru/articles/656113/ (дата обращения: 07.10.2025).
- Алгоритм Бойера-Мура // neerc.ifmo.ru : [сайт]. URL: http://neerc.ifmo.ru/wiki/index.php?title=Алгоритм_Бойера-Мура&oldid=84985 (дата обращения: 07.10.2025).
- Как алгоритмы KMP и Boyer-Moore улучшают поисковые системы // habr.com : [сайт]. URL: https://habr.com/ru/articles/750404/ (дата обращения: 07.10.2025).
- Временная сложность алгоритма // wikipedia.org : [сайт]. URL: https://ru.wikipedia.org/wiki/Временная_сложность_алгоритма (дата обращения: 07.10.2025).
- Типы исключений — C++ // metanit.com : [сайт]. URL: https://metanit.com/cpp/tutorial/7.4.php (дата обращения: 07.10.2025).
- Обработка исключений — C++ // metanit.com : [сайт]. URL: https://metanit.com/cpp/tutorial/7.1.php (дата обращения: 07.10.2025).
- Операторы try, throw и catch (C++) // microsoft.com : [сайт]. URL: https://learn.microsoft.com/ru-ru/cpp/cpp/try-throw-and-catch-statements-cpp?view=msvc-170 (дата обращения: 07.10.2025).
- Файловый ввод-вывод в C++ // 179.ru : [сайт]. URL: https://www.179.ru/~mgu/files/cpp/files.htm (дата обращения: 07.10.2025).
- Теоретический материал: файловый ввод-вывод (C++) : Файлы // informatics.msk.ru : [сайт]. URL: https://informatics.msk.ru/course/view.php?id=305&chapter=1025 (дата обращения: 07.10.2025).
- Файловый ввод, сделанный по-человечески // habr.com : [сайт]. URL: https://habr.com/ru/articles/796688/ (дата обращения: 07.10.2025).