Практический курс: Основы работы со строками в C++

Этот практический курс научит студентов основам работы со строками в C++, начиная от классических символьных массивов и заканчивая современным классом std::string. Вы освоите корректный ввод и вывод текста, манипуляции с отдельными символами, а также базовые операции через подробные примеры кода и закрепляющие упражнения.

1. Введение в символьные массивы

Введение в символьные массивы: C-строки

Компьютеры на самом низком уровне не понимают текст. Процессоры оперируют исключительно числами. Чтобы научить машину работать с буквами, программисты придумали таблицы кодировок, где каждому символу присвоен уникальный числовой код. В языке C++ базовым типом данных для хранения одного символа является char (от английского character). Он занимает в памяти ровно один байт, что позволяет хранить различных значений.

Однако в реальных задачах нам редко нужен только один символ. Мы работаем со словами, предложениями и целыми текстами. Чтобы сохранить слово, нам нужно расположить несколько символов char в памяти компьютера друг за другом. Такая структура данных называется символьным массивом.

Исторически в языке C (предшественнике C++) не было специального удобного типа данных для строк. Программисты использовали обычные массивы символов. Этот подход перекочевал и в C++, получив название C-строки (C-strings). Понимание того, как они работают, критически важно, так как это фундамент, на котором строятся более современные и удобные инструменты.

Анатомия C-строки и нуль-терминатор

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

С текстом разработчики языка пошли другим путем. Чтобы не таскать за каждой строкой переменную с ее длиной, было принято элегантное решение: строка заканчивается там, где встречается специальный символ-маркер. Этот маркер называется нуль-терминатором и записывается как \0.

Нуль-терминатор — это символ, числовой код которого в таблице ASCII равен . Не путайте его с символом цифры '0' (ее код равен ). Когда программа читает символьный массив, она перебирает букву за буквой, пока не наткнется на \0. Как только маркер найден, чтение прекращается.

!Визуализация C-строки в памяти

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

Объявление и инициализация

В C++ существует несколько способов создать и заполнить символьный массив. Рассмотрим их от самых низкоуровневых к более удобным.

Посимвольная инициализация

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

Если в этом примере забыть написать \0, команда std::cout выведет слово «Hello», а затем продолжит выводить случайные символы из соседних ячеек памяти, пока случайно не наткнется на нулевой байт. Это классическая ошибка, приводящая к чтению чужой памяти.

Инициализация строковым литералом

Посимвольный ввод крайне неудобен. Поэтому компилятор C++ позволяет использовать строковые литералы — текст, заключенный в двойные кавычки.

Обратите внимание на разницу: одинарные кавычки ('A') используются для одного символа типа char, а двойные ("A") — для строки, даже если она состоит из одной буквы. Строка "A" занимает в памяти 2 байта: саму букву 'A' и скрытый \0.

Массивы с запасом памяти

Часто мы не знаем заранее, какое слово введет пользователь. В таких случаях мы создаем массив фиксированного размера «с запасом».

> Практическое задание для самостоятельной работы: > Напишите программу, в которой объявлен символьный массив размером 20 элементов. Инициализируйте его названием вашего любимого фрукта с помощью строкового литерала. Выведите результат на экран.

Ввод и вывод C-строк

Вывод символьных массивов на экран с помощью объекта std::cout работает интуитивно понятно. Объект cout спроектирован так, что при получении указателя на char (каковым является имя массива), он печатает символы до тех пор, пока не встретит \0.

С вводом данных через std::cin ситуация немного сложнее. Рассмотрим типичный пример:

Если вы запустите эту программу и введете Иван, она отработает идеально. Но попробуйте ввести Иван Иванов. Программа выведет только Привет, Иван!.

Почему так происходит? Стандартный оператор ввода >> читает данные до первого пробельного символа (пробела, знака табуляции или переноса строки). Как только cin видит пробел после слова «Иван», он останавливает запись в массив name и автоматически добавляет \0. Фамилия «Иванов» остается в буфере клавиатуры и будет прочитана при следующем вызове cin.

Чтение строк с пробелами: функция getline

Чтобы прочитать целую строку, включая пробелы, необходимо использовать метод getline, который встроен в объект cin.

Метод cin.getline() принимает два обязательных аргумента:

  • Имя массива, куда нужно сохранить текст.
  • Максимальное количество символов, которое можно прочитать (включая место под нуль-терминатор).
  • Использование cin.getline() не только решает проблему с пробелами, но и защищает вашу программу от переполнения буфера (buffer overflow). Если пользователь попытается ввести 150 символов в массив размером 100, cin.getline() прочитает ровно 99 символов, поставит \0 и остановится. Обычный cin >> попытался бы записать все 150 символов, разрушив данные в соседних ячейках памяти, что привело бы к аварийному завершению программы.

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

    Разница между размером массива и длиной строки

    При работе с C-строками важно четко разделять два понятия: физический размер выделенной памяти и логическую длину самой строки.

    Допустим, мы объявили массив: char city[50] = "Moscow";.

    Физический размер массива — это количество байт, которое он занимает в оперативной памяти. В нашем случае это байт. Мы можем узнать это значение с помощью оператора sizeof().

    Логическая длина строки — это количество полезных символов до нуль-терминатора. В слове "Moscow" букв.

    В этом примере цикл while проверяет каждый элемент массива. Если элемент не равен \0, счетчик length увеличивается на . Как только цикл натыкается на нуль-терминатор, он останавливается. Это базовый алгоритм, который лежит в основе стандартной функции вычисления длины строки, с которой мы познакомимся в следующих уроках.

    Понимание того, что строка — это просто массив символов с маркером конца, дает вам полный контроль над текстом. Вы можете обращаться к отдельным буквам по их индексу, например buffer[0] вернет 'D', а buffer[3] вернет 'a'. Вы можете изменять эти буквы, переводить их в верхний регистр или заменять одни символы на другие, просто перезаписывая значения в ячейках массива.

    2. Ввод и вывод строк

    В прошлом уроке мы выяснили, что базовая строка в языке C++ — это массив символов, который обязательно заканчивается специальным маркером, нуль-терминатором \0. Мы научились создавать такие массивы и заполнять их текстом прямо в коде программы. Однако настоящая интерактивность начинается тогда, когда программа умеет общаться с пользователем: запрашивать данные с клавиатуры и корректно выводить результаты на экран.

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

    Базовый вывод строк на экран

    Вывод символьных массивов (C-строк) реализуется максимально просто. Стандартный объект std::cout из библиотеки <iostream> изначально спроектирован так, чтобы понимать символьные массивы.

    Когда вы передаете имя массива greeting в std::cout, программа не печатает адрес памяти (как это было бы с массивом чисел). Вместо этого она начинает последовательно выводить на экран символ за символом: 'Д', 'о', 'б', 'р', 'о'... Этот процесс продолжается ровно до тех пор, пока программа не наткнется на скрытый нуль-терминатор \0. Как только маркер найден, вывод мгновенно прекращается.

    Проблема пробелов при базовом вводе

    Для чтения данных с клавиатуры мы привыкли использовать объект std::cin и оператор извлечения >>. Давайте посмотрим, как он работает с текстом.

    Если вы запустите эту программу и введете Алексей, результат будет ожидаемым. Но если вы введете Алексей Смирнов, программа выведет только Сохраненные данные: Алексей. Фамилия бесследно исчезнет из вывода.

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

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

    !Схема работы буфера ввода и оператора cin

    В нашем примере cin прочитал слово «Алексей», увидел пробел, автоматически добавил нуль-терминатор \0 в массив name и завершил работу. Слово «Смирнов» и символ Enter остались лежать в буфере ввода, ожидая следующей команды чтения.

    > Практическое задание 1: > Напишите программу, которая объявляет два массива: firstName[30] и lastName[30]. Попросите пользователя ввести имя и фамилию через пробел. Используйте команду std::cin >> firstName >> lastName;, чтобы прочитать оба слова за один раз, а затем выведите их на экран.

    Чтение строк с пробелами: функция getline для C-строк

    Чтобы прочитать целую строку текста, включая пробелы, стандартного оператора >> недостаточно. Для работы с символьными массивами в объекте cin предусмотрена специальная встроенная функция — getline.

    Функция cin.getline() принимает два обязательных параметра:

  • Имя массива, в который будут записаны данные.
  • Максимальное количество символов для чтения (размер буфера).
  • Функция cin.getline() решает сразу две проблемы. Во-первых, она читает все символы, включая пробелы, и останавливается только при встрече с символом переноса строки \n (нажатие Enter). Во-вторых, она защищает программу от переполнения буфера. Если пользователь введет 150 символов, функция аккуратно прочитает ровно 99, поставит нуль-терминатор и остановится, не повредив соседние участки оперативной памяти.

    > Практическое задание 2: > Создайте массив quote[150]. Запросите у пользователя его любимую цитату из фильма. Прочитайте ее с помощью cin.getline() и выведите на экран, обрамив кавычками.

    Эволюция: класс std::string

    Работа с символьными массивами (C-строками) требует постоянного контроля за размером памяти. Если вы выделили 50 ячеек, а пользователь ввел 60 символов, часть данных будет потеряна. Чтобы избавить программистов от этой рутины, в C++ был добавлен современный и безопасный инструмент — класс std::string.

    Класс string — это «умная» строка. Она сама управляет своей памятью: автоматически расширяется, если текст не помещается, и сжимается, когда текст удаляется. Для ее использования необходимо подключить библиотеку <string>.

    Оператор >> работает с std::string точно так же, как и с C-строками: он останавливается на первом пробеле. Но как прочитать строку с пробелами в std::string? Метод cin.getline() здесь не сработает, так как он предназначен только для старых символьных массивов.

    Для современных строк используется глобальная функция std::getline().

    Функция std::getline(std::cin, variable) — это золотой стандарт чтения текстовых данных в современном C++. Она безопасна, не требует указания размеров и отлично справляется с любыми объемами текста.

    Ловушка буфера: конфликт cin >> и getline

    Рано или поздно каждый программист на C++ сталкивается с классической ошибкой при совместном использовании обычного ввода чисел и функции getline. Рассмотрим типичный сценарий:

    Если вы запустите этот код, программа спросит возраст, вы введете 20 и нажмете Enter. После этого программа мгновенно завершится, даже не дав вам шанса ввести имя! Результат будет выглядеть так: Возраст: 20, Имя: .

    Почему функция getline была проигнорирована? Вспомним про буфер ввода. Когда вы ввели 20 и нажали Enter, в буфере оказалось: 20\n. Оператор cin >> age забрал число 20, но оставил символ переноса строки \n в буфере.

    Затем управление переходит к std::getline(). Эта функция читает данные до тех пор, пока не встретит \n. Она заглядывает в буфер, видит, что первым же символом там лежит \n, радостно решает, что пользователь ввел пустую строку, забирает этот \n и завершает работу.

    Решение: очистка буфера с помощью cin.ignore()

    Чтобы исправить эту ошибку, нам нужно принудительно удалить оставшийся символ \n из буфера перед вызовом getline. Для этого используется метод cin.ignore().

    Метод cin.ignore() без аргументов просто извлекает и выбрасывает один следующий символ из буфера (в нашем случае это злополучный \n). Теперь буфер чист, и getline будет корректно ждать ввода имени пользователя.

    > Практическое задание 3: > Напишите анкету пользователя. Запросите год рождения (число), затем очистите буфер. После этого запросите название любимой книги (строка с пробелами) с помощью std::getline. Выведите собранные данные в одном предложении.