Методологическая разработка программного модуля для манипуляции строками на базе современных стандартов C++: Алгоритмы, оптимизация и безопасность

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

Целью данной курсовой работы является разработка методологической и теоретической базы для создания программного обеспечения, предназначенного для эффективной и безопасной манипуляции строками на языке 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().

Анализ ключевых рисков:

  1. Переполнение буфера (Buffer Overflow): Это наиболее серьезная и распространенная уязвимость. Если функция strcpy() или strcat() пытается записать в буфер больше символов, чем он может вместить, данные будут записаны за пределами выделенного блока памяти. Это может привести к повреждению соседних данных, ошибкам сегментации (Segmentation Fault) или, что еще хуже, к выполнению вредоносного кода. Пример:
    char buffer[10];
    // Попытка скопировать строку "Очень длинная строка" в буфер на 10 символов
    strcpy(buffer, "Очень длинная строка"); // Вызовет переполнение буфера!
    
  2. Утечки памяти (Memory Leaks): Если память, выделенная с помощью new или malloc, не будет освобождена с помощью delete или free после использования, она останется занятой до завершения программы, что приведет к постепенному исчерпанию доступной оперативной памяти.
  3. Неинициализированные указатели (Dangling Pointers): Если указатель продолжает использоваться после того, как память, на которую он указывал, была освобождена, это может привести к непредсказуемому поведению программы.
  4. Отсутствие нулевого терминатора: Если строка, созданная или модифицированная вручную, не будет корректно завершена нулевым символом, функции 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.
Алгоритм работает следующим образом:

  1. Начать с первого символа текста.
  2. Сравнить образец P с подстрокой текста T[i...i+m-1].
  3. Если все m символов совпадают, образец найден.
  4. Если есть несовпадение или образец не найден, сдвинуть образец на одну позицию вправо в тексте и повторить сравнение.
  5. Продолжать до тех пор, пока образец не будет найден или не будет исчерпан текст.

Демонстрация его худшей временной сложности O(n · m) на патологических примерах:
Хотя на первый взгляд алгоритм может показаться эффективным, в худшем случае его производительность значительно снижается. Это происходит, когда большинство символов совпадают при каждом сдвиге, но в итоге всегда обнаруживается несовпадение.

Рассмотрим патологический пример:

  • Текст (T): "AAAAAAAAAB" (длина n = 10)
  • Образец (P): "AAB" (длина m = 3)

Последовательность сравнений:

  1. T[0..2] ("AAA") vs P ("AAB"): AA совпадает, A vs B не совпадает.
  2. Сдвиг на 1 позицию. T[1..3] ("AAA") vs P ("AAB"): AA совпадает, A vs B не совпадает.
  3. … (повторяется 7 раз) …
  4. Сдвиг. T[7..9] ("AAB") vs P ("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]?"

Как работает алгоритм:

  1. Предварительная обработка образца: Вычисляется префикс-функция для образца P. Это занимает O(m) времени.
  2. Поиск: При поиске в тексте 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[Конец: Образец не найден];

Оптимизированные алгоритмы: Бойера-Мура (БМ)

Алгоритм Бойера-Мура (БМ) является одним из наиболее эффективных алгоритмов поиска подстроки общего назначения. Его отличительной особенностью является то, что он сравнивает образец с текстом справа налево, что позволяет делать более значительные сдвиги, чем КМП, и часто оказывается быстрее на практике.

Описание принципа работы БМ (сравнение справа налево):
В отличие от наивного алгоритма и КМП, которые начинают сравнение с начала образца, Бойер-Мур начинает с его конца. Если происходит несовпадение, алгоритм использует информацию о несовпавшем символе (или о совпавшем суффиксе) для определения максимального безопасного сдвига.

Объяснение эвристик "плохого символа" и "хорошего суффикса":
БМ использует две основные эвристики для определения величины сдвига:

  1. Правило плохого символа (Bad Character Heuristic):
    • Когда происходит несовпадение между символом T[i] в тексте и P[j] в образце (при сравнении справа налево), алгоритм смотрит на символ T[i].
    • Если этот символ T[i] не встречается в образце P вообще, или встречается только левее текущей позиции j, то можно безопасно сдвинуть образец на m символов (или на расстояние до первого появления T[i] в образце, если оно есть), минуя T[i].
    • Если T[i] встречается в образце правее позиции j, образец сдвигается так, чтобы T[i] совпал с ближайшим вхождением этого символа в образце.
    • Для этого правила требуется предварительно построить таблицу (или массив), которая для каждого символа алфавита хранит его последнее вхождение в образце.
  2. Правило хорошего суффикса (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. Разработка архитектуры программного обеспечения и обработка ошибок

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

Модульная архитектура для работы со строками

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

Предложение структуры модуля для обеспечения повторного использования кода и инкапсуляции:

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

  1. Класс 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_;
              };
              
  2. Набор независимых функций (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-строк): Объект для хранения результатов совпадения, включая всю найденную подстроку и захваченные группы.

Основные функции:

  1. 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;
    }
    
  2. 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] - полное совпадение
    }
    
  3. 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 (ошибки регулярных выражений)

Детальное описание специфических исключений для строк:

  1. 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;
      }
      
  2. 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;
      }
      
  3. 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;
      }
      

Правила обработки исключений:

  1. Генерировать исключения по значению (throw): Когда возникает исключительная ситуация, следует создавать объект исключения и выбрасывать его по значению. Например, throw std::runtime_error("Описание ошибки");.
  2. Перехватывать исключения по константной ссылке (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. Практическая реализация и документация

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

Проектная документация и листинг кода

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

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

  1. Введение:
    • Актуальность проблемы.
    • Цели и задачи курсовой работы.
    • Обзор предметной области.
  2. Теоретические основы:
    • Подробное описание строковых типов (C-строки, std::string, std::string_view, char8_t).
    • Сравнительный анализ и обоснование выбора используемых типов.
    • Введение в O-нотацию и основы анализа алгоритмов.
  3. Алгоритмическая часть:
    • Детальное описание выбранных алгоритмов поиска и манипуляции строками (например, КМП, Бойера-Мура).
    • Блок-схемы для каждого ключевого алгоритма, наглядно иллюстрирующие их логику.
    • Анализ временной и пространственной сложности, включая лучшие, худшие и средние случаи (где применимо).
  4. Архитектура программного обеспечения:
    • Описание модульной структуры разработанного ПО (например, класс StringProcessor и вспомогательные функции).
    • Обоснование архитектурных решений (инкапсуляция, повторное использование).
    • Диаграммы классов (UML), иллюстрирующие взаимосвязи между компонентами.
    • Обзор используемых современных методов (регулярные выражения, C++20 Unicode).
    • Стратегия обработки исключений, включая иерархию и правила использования try/catch.
  5. Детали реализации:
    • Краткое описание ключевых фрагментов кода.
    • Используемые структуры данных (если не описаны в теоретической части).
    • Особенности работы с файловым вводом-выводом и кодировками.
  6. Тестирование:
    • Описание методологии тестирования (например, модульное, интеграционное).
    • Тестовые примеры с ожидаемыми результатами для проверки корректности работы алгоритмов и функционала.
    • Анализ результатов тестирования.
  7. Руководство пользователя: (Подробно описано в следующем разделе)
  8. Заключение:
    • Суммирование выполненных работ и достигнутых целей.
    • Выводы по эффективности и безопасности разработанного модуля.
    • Возможные направления дальнейшего развития.
  9. Список использованных источников:
    • Ссылки на учебники, научные статьи, стандарты ISO C++.
  10. Приложения:
    • Полный листинг кода программы.

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

Руководство пользователя

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

Описание, как оформляется раздел "Руководство пользователя":

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

  1. Назначение программы: Краткое и ясное описание того, для чего предназначена программа и какие задачи она решает (например, "Программа предназначена для выполнения различных операций со строками, включая поиск подстрок, замену, и валидацию по регулярным выражениям").
  2. Системные требования:
    • Операционная система (Windows, Linux, macOS).
    • Минимальные требования к процессору и оперативной памяти.
    • Необходимые компиляторы и библиотеки (например, "C++17-совместимый компилятор (GCC 7+, Clang 5+, MSVC 15+)").
  3. Установка:
    • Подробные инструкции по компиляции исходного кода (если это применимо) или установке исполняемого файла.
    • Пример команд для сборки проекта (например, с использованием g++ или CMake).
    • Указание на необходимые зависимости.
  4. Запуск программы:
    • Команды для запуска программы из командной строки.
    • Описание аргументов командной строки (если они используются), их синтаксис и назначение.
    • Пример: my_string_processor.exe -i input.txt -o output.txt --pattern "search_term"
  5. Описание входных данных:
    • Формат входных файлов (если программа работает с файлами).
    • Требования к кодировке текста (например, "Входные файлы должны быть в кодировке UTF-8").
    • Примеры входных данных.
  6. Описание выходных данных:
    • Формат и структура выходных файлов или информации, выводимой на консоль.
    • Примеры выходных данных.
  7. Примеры использования:
    • Пошаговые примеры использования программы для решения типичных задач.
    • Каждый пример должен включать:
      • Краткое описание задачи.
      • Команду для запуска.
      • Ожидаемый результат (вывод на консоль или содержимое выходного файла).
  8. Обработка ошибок и сообщения:
    • Описание возможных ошибок, которые могут возникнуть при работе программы (например, "файл не найден", "неверный формат входных данных").
    • Примеры сообщений об ошибках и рекомендации по их устранению.
  9. Контактная информация: (Необязательно для академической работы)

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

Заключение

В рамках данной методологической разработки была построена всеобъемлющая теоретическая и практическая база для создания программного модуля по манипуляции строками на языке 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++. Он готов стать надежным фундаментом для дальнейшей практической реализации и академического отчета.

Список использованной литературы

  1. Дейтел, Х., Дейтел, П. Как программировать на С++ [Электронный ресурс].
  2. Липпман, Дж. Основы программирования на С++. Санкт-Петербург, 2002.
  3. Либерти, Дж. Освой самостоятельно C++ за 21 день [Электронный ресурс].
  4. Страуструп, Б. Язык программирования С++ [Электронный ресурс].
  5. Конспект лекций по курсу «Основы алгоритмизации и программирования».
  6. Введение в регулярные выражения в современном C++ // habr.com : [сайт]. URL: https://habr.com/ru/articles/532328/ (дата обращения: 07.10.2025).
  7. Работа со строками в C++: основы, методы и применение // kedu.ru : [сайт]. URL: https://kedu.ru/press-center/pro-c/string_cpp/ (дата обращения: 07.10.2025).
  8. Чем обусловлены различия в работе со строками и другими массивами? // habr.com : [сайт]. URL: https://habr.com/ru/qa/62499/ (дата обращения: 07.10.2025).
  9. Сильно ли string ресурсозатратнее char[]? // stackoverflow.com : [сайт]. URL: https://ru.stackoverflow.com/questions/827618/сильно-ли-string-ресурсозатратнее-char (дата обращения: 07.10.2025).
  10. Алгоритм Кнута-Морриса-Пратта // neerc.ifmo.ru : [сайт]. URL: http://neerc.ifmo.ru/wiki/index.php?title=Алгоритм_Кнута-Морриса-Пратта&oldid=85136 (дата обращения: 07.10.2025).
  11. C++ | Строки с поддержкой Unicode // metanit.com : [сайт]. URL: https://metanit.com/cpp/tutorial/6.15.php (дата обращения: 07.10.2025).
  12. Регулярные выражения (C++) // microsoft.com : [сайт]. URL: https://learn.microsoft.com/ru-ru/cpp/standard-library/regular-expressions-cpp?view=msvc-170 (дата обращения: 07.10.2025).
  13. Исключения в C++: типы, синтаксис и обработка // tproger.ru : [сайт]. URL: https://tproger.ru/articles/exceptions-in-c-types-syntax-and-handling/ (дата обращения: 07.10.2025).
  14. Какие основные различия между массивами 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).
  15. Строковые алгоритмы на практике. Часть 2 — Алгоритм Бойера — Мура // habr.com : [сайт]. URL: https://habr.com/ru/articles/656113/ (дата обращения: 07.10.2025).
  16. Алгоритм Бойера-Мура // neerc.ifmo.ru : [сайт]. URL: http://neerc.ifmo.ru/wiki/index.php?title=Алгоритм_Бойера-Мура&oldid=84985 (дата обращения: 07.10.2025).
  17. Как алгоритмы KMP и Boyer-Moore улучшают поисковые системы // habr.com : [сайт]. URL: https://habr.com/ru/articles/750404/ (дата обращения: 07.10.2025).
  18. Временная сложность алгоритма // wikipedia.org : [сайт]. URL: https://ru.wikipedia.org/wiki/Временная_сложность_алгоритма (дата обращения: 07.10.2025).
  19. Типы исключений — C++ // metanit.com : [сайт]. URL: https://metanit.com/cpp/tutorial/7.4.php (дата обращения: 07.10.2025).
  20. Обработка исключений — C++ // metanit.com : [сайт]. URL: https://metanit.com/cpp/tutorial/7.1.php (дата обращения: 07.10.2025).
  21. Операторы 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).
  22. Файловый ввод-вывод в C++ // 179.ru : [сайт]. URL: https://www.179.ru/~mgu/files/cpp/files.htm (дата обращения: 07.10.2025).
  23. Теоретический материал: файловый ввод-вывод (C++) : Файлы // informatics.msk.ru : [сайт]. URL: https://informatics.msk.ru/course/view.php?id=305&chapter=1025 (дата обращения: 07.10.2025).
  24. Файловый ввод, сделанный по-человечески // habr.com : [сайт]. URL: https://habr.com/ru/articles/796688/ (дата обращения: 07.10.2025).

Похожие записи