1. Неожиданно сложная часть программы
Когда вы только начинаете программировать, кажется логичным: «пользователь ввёл строку — ну и я её прочитаю». А потом внезапно getline возвращает пустую строку, команда " add milk 2 " не распознаётся, а ввод после числа почему-то пропускается. Это не магия и не проклятие компилятора — это просто особенности того, как работает поток ввода.
Представьте, что ввод — это конвейер (буфер). Пользователь «напечатает» символы, а std::cin их забирает. Команда operator>> забирает токен (кусок до пробела), а std::getline забирает всю строку до '\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" не сработает. Нормализация должна быть стадийной: сначала края, потом внутренности.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ