JavaRush /Курсы /C++ SELF /Надёжное чтение строк

Надёжное чтение строк

C++ SELF
9 уровень , 4 лекция
Открыта

1. Неожиданно сложная часть программы

Когда вы только начинаете программировать, кажется логичным: «пользователь ввёл строку — ну и я её прочитаю». А потом внезапно getline возвращает пустую строку, команда " add milk 2 " не распознаётся, а ввод после числа почему-то пропускается. Это не магия и не проклятие компилятора — это просто особенности того, как работает поток ввода.

Представьте, что ввод — это конвейер (буфер). Пользователь «напечатает» символы, а std::cin их забирает. Команда operator>> забирает токен (кусок до пробела), а std::getline забирает всю строку до '\n'. И вот эти два подхода живут рядом, но «едят» символы по-разному.

Сделаем мини-таблицу, чтобы зафиксировать разницу:

Что читаем Как читаем Где останавливаемся Пример, что получим из Hello world
токен
std::cin >> s
на пробеле/переводе строки
Hello
строку
std::getline(std::cin, s)
на
'\n'
Hello world

Ключевой вывод на сегодня: когда вы строите консольное приложение с командами, почти всегда выгодно читать строку целиком, а потом уже разбирать её. Это как сначала взять письмо целиком, а потом решить, где там адрес, где подпись, а где «я забыл прикрепить файл» (классика корпоративной культуры).

2. std::getline: читаем строку целиком и не пугаемся пустых строк

std::getline — это ваш главный инструмент для «честного» чтения пользовательского ввода, где пробелы могут быть частью текста. Но у него есть характер: он возвращает пустую строку, если пользователь просто нажал Enter. И это нормально. Пустой ввод — тоже ввод, просто… без текста. Как сообщение «ок» без «ок».

Минимальный пример чтения одной строки:

#include <iostream>
#include <string>

int main() {
    std::string line;
    std::getline(std::cin, line);

    std::cout << "line=[" << line << "]\n"; // например: line=[hello world]
}

Теперь добавим правильную реакцию на пустую строку. В реальной программе пустая строка часто означает «ничего не делаем, спрашиваем снова»:

#include <iostream>
#include <string>

int main() {
    std::string line;
    std::getline(std::cin, line);

    if (line.empty()) {
        std::cout << "empty input\n"; // empty input
    } else {
        std::cout << "len=" << line.size() << "\n"; // len=11 (например)
    }
}

Важная привычка: прежде чем обращаться к line[0], всегда проверяйте line.empty(). Индекс 0 в пустой строке — это как попытка открыть дверь, которой нет: вроде жест правильный, но результат неприятный.

3. Ловушка >> + getline: почему я получил пустую строку

Сейчас будет классический фокус, который ломал психику поколениям студентов (и пару преподавателей тоже). Когда вы читаете число через std::cin >> x, ввод останавливается перед пробелом или переводом строки, но сам '\n' часто остаётся в буфере. Следующий getline тут же читает «остаток строки» — то есть пустоту до '\n'.

Посмотрим на проблему:

#include <iostream>
#include <string>

int main() {
    int n = 0;
    std::cin >> n;

    std::string line;
    std::getline(std::cin, line);

    std::cout << "n=" << n << "\n";
    std::cout << "line=[" << line << "]\n"; // часто: line=[]
}

Решение: std::ws — пропусти пробелы перед getline

Самый компактный и популярный трюк: читать так:

std::getline(std::cin >> std::ws, line);

Манипулятор std::ws говорит: «сначала пропусти ведущие пробельные символы (включая '\n'), потом читай строку».

Пример:

#include <iostream>
#include <string>

int main() {
    int age = 0;
    std::cin >> age;

    std::string comment;
    std::getline(std::cin >> std::ws, comment);

    std::cout << "age=" << age << ", comment=[" << comment << "]\n";
    // age=20, comment=[hello]
}

Здесь логика «устойчивая»: после числа мы действительно читаем следующую осмысленную строку.

Решение: ignore(...) — выкини всё до конца строки

Иногда вам нужно более «явное» поведение: после чтения токена/числа вы хотите выкинуть всё до конца текущей строки. Для этого есть ignore.

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

#include <iostream>
#include <string>
#include <limits>

int main() {
    int n = 0;
    std::cin >> n;

    std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n');

    std::string line;
    std::getline(std::cin, line);

    std::cout << "line=[" << line << "]\n";
}

Смысл: «игнорируй символы, пока не встретишь '\n' (или пока лимит не закончится)».

Небольшой забавный факт: даже в мире стандартизации C++ обсуждали, как сделать std::istream::ignore менее «сюрпризным». Так что вы не одиноки, если он кажется вам немного странным.

Как выбрать: ws или ignore

Если вы просто хотите «после >> нормально прочитать следующую строку» — std::ws обычно проще и короче.

Если вы хотите строго «добить текущую строку и начать с чистого листа» (например, у вас ввод формата: число, потом на следующей строке текст) — ignore читается как более явный контракт.

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

4. Нормализация ввода: пустые строки и пробелы

Когда вы делаете приложение, которое ждёт команды, вы быстро поймёте: пользователь вводит не только «идеальные» строки. Он может ввести пустую строку, может поставить десять пробелов, может случайно добавить пробел в конец. И всё это должно обрабатываться спокойно — без истерики, без падений, без «команда не найдена» на очевидной команде.

Здесь полезно разделять три похожих, но разных случая:

  • пустая строка: ""
  • строка из пробелов: " "
  • строка с текстом и лишними пробелами: " add milk 2 "

Для программы с командами первые два случая чаще всего означают «ничего не делать». А третий — «привести к нормальной форме и разбирать».

Чтобы это работало, нам нужна нормализация: мы берём строку пользователя и приводим её к виду, с которым удобно работать. В рамках этой лекции сделаем две простые операции: уберём пробелы по краям (trim) и превратим серии пробелов внутри строки в один пробел (схлопывание). Мы сознательно считаем пробелом только символ ' ', без усложнений табами и экзотикой.

Trim: убираем пробелы слева и справа

Сделаем функцию trim_spaces, которая возвращает новую строку (так проще объяснять; оптимизация нам сейчас не нужна):

#include <string>

std::string trim_spaces(const std::string& s) {
    std::size_t begin = 0;
    while (begin < s.size() && s[begin] == ' ') ++begin;

    std::size_t end = s.size();
    while (end > begin && s[end - 1] == ' ') --end;

    return s.substr(begin, end - begin);
}

Обратите внимание на аккуратность с индексами: справа мы проверяем s[end - 1], потому что end — это позиция после последнего символа в диапазоне. Это типичная модель в C++: [begin, end).

Мини-проверка в main:

#include <iostream>
#include <string>

int main() {
    std::string s = "   hello   ";
    std::cout << "[" << trim_spaces(s) << "]\n"; // [hello]
}

Схлопывание пробелов: "a b""a b"

Теперь пройдём по строке и будем добавлять пробел только если предыдущий символ не был пробелом:

#include <string>

std::string collapse_spaces(const std::string& s) {
    std::string out;
    bool prev_space = false;

    for (std::size_t i = 0; i < s.size(); ++i) {
        char c = s[i];
        if (c == ' ') {
            if (!prev_space) out += ' ';
            prev_space = true;
        } else {
            out += c;
            prev_space = false;
        }
    }
    return out;
}

И короткая демонстрация:

#include <iostream>
#include <string>

int main() {
    std::string s = "a   b    c";
    std::cout << collapse_spaces(s) << "\n"; // a b c
}

Собираем нормализацию в одну функцию

Теперь объединим: сначала trim (удаление пробелов), потом схлопывание. Порядок важен: если не сделать trim, у вас может остаться пробел в начале/конце (а это иногда мешает распознаванию команд).

#include <string>

std::string normalize_spaces(const std::string& s) {
    std::string trimmed = trim_spaces(s);
    return collapse_spaces(trimmed);
}

5. Строим устойчивое чтение команд для мини-приложения

Сейчас мы соберём всё в маленький, но цельный кирпичик: функцию read_command_line(), которая читает строку, нормализует пробелы и возвращает результат. Это заготовка, которую вы потом будете использовать почти в любом консольном проекте с командами: калькулятор, заметки, мини-игра, «умный список покупок» — что угодно.

Сделаем простую версию: читаем строку через getline, а если чтение не удалось, вернём пустую строку. Мы не углубляемся в состояния cin (это будет позже), но базовая проверка нам не повредит.

#include <iostream>
#include <string>

std::string read_command_line() {
    std::string line;
    if (!std::getline(std::cin, line)) return ""; // EOF или ошибка

    return normalize_spaces(line);
}

Теперь минимальный цикл «командного режима»:

#include <iostream>
#include <string>

int main() {
    while (true) {
        std::cout << "> ";
        std::string cmd = read_command_line();

        if (cmd.empty()) continue;
        if (cmd == "exit") break;

        std::cout << "cmd=[" << cmd << "]\n";
    }
}

Если пользователь вводит " exit ", после нормализации это станет "exit", и команда сработает. Это именно то, что мы и называем «стабильный ввод»: программа не зависит от того, насколько аккуратно пользователь попал по пробелу.

Мини-схема: что происходит с вводом до разбора

Полезно один раз увидеть весь пайплайн глазами:

flowchart TD
    A["getline: читаем строку как есть"] --> B["normalize: trim + collapse"]
    B --> C{"пусто?"}
    C -->|да| D["ничего не делаем / ждём следующую строку"]
    C -->|нет| E["дальше: разбор команды (find/substr или токенизация)"]

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

6. Типичные ошибки

Ошибка №1: смешали >> и getline, получили «пустую строку» и решили, что getline сломан.
На самом деле сломан не getline, а ожидания: после >> в буфере часто остаётся '\n', и getline честно читает «до конца строки», то есть пустоту. Лечится дисциплиной: либо всегда читаем строки через getline, либо делаем переход через std::ws или через ignore.

Ошибка №2: не отличают пустую строку от строки из пробелов.
getline может вернуть " " — она не empty(), но полезного текста там нет. Если вы строите ввод команд, то такая строка должна превращаться в пустую после нормализации. Поэтому проверка empty() становится надёжной именно после trim (или полного normalize).

Ошибка №3: делают line[0], чтобы проверить первую букву, не проверив empty().
Это классическая ошибка границ: в пустой строке нет символа с индексом 0. Сначала empty(), потом индексирование. Всегда.

Ошибка №4: пишут substr(begin, end) вместо substr(begin, end - begin).
Это одна из самых частых путаниц: второй параметр substr — не «конечный индекс», а длина. Если вы передали туда end, вы почти гарантированно получите не то, что ожидали (а иногда ещё и выход за границы, если end больше size()).

Ошибка №5: схлопывают пробелы, но забывают сделать trim.
В итоге строка " add milk" превращается в " add milk" — вроде стало лучше, но первая команда всё равно начинается с пробела, и сравнение cmd == "add" не сработает. Нормализация должна быть стадийной: сначала края, потом внутренности.

1
Задача
C++ SELF, 9 уровень, 4 лекция
Недоступна
Пустой отзыв
Пустой отзыв
1
Задача
C++ SELF, 9 уровень, 4 лекция
Недоступна
Число и заметка
Число и заметка
1
Задача
C++ SELF, 9 уровень, 4 лекция
Недоступна
Обрезать края
Обрезать края
1
Задача
C++ SELF, 9 уровень, 4 лекция
Недоступна
Очистка команд
Очистка команд
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ