В современном мире информационных технологий, где данные часто представлены в текстовом формате, эффективность и безопасность работы со строками становятся краеугольным камнем разработки надежного и высокопроизводительного программного обеспечения. От парсинга логов до обработки пользовательского ввода, от сетевых протоколов до анализа больших текстовых массивов — манипуляции строками лежат в основе бесчисленного множества приложений. Недооценка сложности этой области или применение устаревших подходов может привести к серьезным уязвимостям, таким как переполнение буфера, утечки памяти или катастрофическое снижение производительности.
Целью данной курсовой работы является разработка методологической и теоретической базы для создания программного обеспечения, предназначенного для эффективной и безопасной манипуляции строками на языке 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совпадает,AvsBне совпадает.- Сдвиг на 1 позицию.
T[1..3]("AAA") vsP("AAB"):AAсовпадает,AvsBне совпадает. - … (повторяется 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).