1. Введение
Когда вы начинаете работать со строками, кажется, что строка — она и в Африке строка: «набор символов». Но в C++ есть принципиальная разница между тем, что написано в исходнике программы, и тем, что пользователь вводит во время выполнения. И если эту разницу не прочувствовать, можно несколько часов спорить с std::cin, а потом обвинить во всём компилятор (он не обидится, он привык).
Строковый литерал — это то, что вы пишете прямо в коде: "Hello\n", "C:\\temp\\file.txt", "He said: \"Hi\"".
Ввод пользователя — это то, что приходит из консоли: например, пользователь набрал He said: "Hi" или C:\temp\file.txt. Для программы это просто символы, которые надо прочитать.
Ключевой момент: экранирование (\n, \", \\) работает только внутри литералов в исходнике, потому что именно компилятор решает, как превратить ваш текст в байты строки. В пользовательском вводе обратный слэш — это просто символ \, у него нет «магических свойств».
2. Экранирование в строковых литералах
Сама идея экранирования звучит как шпионский фильм: «экранируем кавычки, иначе нас вычислят». На самом деле всё проще: некоторым символам тяжело «жить» внутри строкового литерала, потому что они имеют особое значение для синтаксиса языка. Самый очевидный пример — двойная кавычка ": она закрывает строковый литерал, поэтому если вы хотите вывести кавычку как символ, нужно объяснить компилятору: «это не конец строки, это просто кавычка».
В стандарте C++ правила про строковые и символьные литералы описываются на уровне лексики (разделы вида lex.string, lex.ccon и т.п.). То есть это не «фишка iostream», не «особенность Windows» и не «каприз IDE». Это часть языка.
Самые полезные escape-последовательности
Чтобы не превращать жизнь в сериал «угадай, сколько слэшей нужно», полезно иметь маленькую табличку в голове. Сразу договоримся: мы говорим про обычные escape-последовательности, которые нужны вам каждый день.
| Что хотим получить в строке | Как записать в литерале C++ | Зачем это нужно |
|---|---|---|
| Перевод строки | |
Делать многострочный вывод, форматировать текст |
| Табуляция | |
Простенькое выравнивание колонок |
| Кавычка " | |
Печатать цитаты, JSON-подобный текст, подсказки |
| Обратный слэш \ | |
Пути Windows, регулярки (позже), спец-форматы |
| Апостроф ' | |
Реже нужно, но бывает в текстах |
| Нулевой байт | |
В учебных задачах почти не нужен, но знать полезно |
Есть и более экзотические варианты (шестнадцатеричные, Unicode-escape и т.д.), но мы их сегодня не трогаем: нам бы сначала перестать бояться обычных \\.
Мини-пример: почему "\n" работает, а пользовательский \n — нет
Сейчас будет очень показательный пример. Он как лакмусовая бумажка: понимаете ли вы, где «магия компилятора», а где «обычные символы».
#include <iostream>
#include <string>
int main() {
std::string a = "Hello\nWorld";
std::string b = "Hello\\nWorld";
std::cout << a << '\n';
// Hello
// World
std::cout << b << '\n';
// Hello\nWorld
}
Первая строка печатается в две строки, потому что \n внутри литерала превращается в символ перевода строки.
Вторая строка печатается как \n, потому что \\ означает «один обратный слэш», и дальше идёт обычная буква n.
Разница между '\n' и "\n"
На этом месте часто появляется путаница: мы же уже выводили перевод строки как '\n' — зачем нам "\n"?
Разница в типах:
- '\n' — это символ (char)
- "\n" — это строка (строковый литерал)
С практической точки зрения это важно, когда вы склеиваете строки. Символ можно добавить как один знак, строку — как последовательность.
#include <iostream>
#include <string>
int main() {
std::string s = "A";
s += '\n';
s += "B\n";
std::cout << s;
// A
// B
}
Тут мы сначала добавили один символ перевода строки, потом добавили строку "B\n".
Кавычки внутри строкового литерала
Вы хотите вывести подсказку пользователю вроде:
Введите команду: "add milk 2"
Если написать это наивно, компилятор увидит конец строки раньше времени:
// так нельзя:
std::cout << "Введите команду: "add milk 2"\n";
Правильный вариант — экранировать внутренние кавычки:
#include <iostream>
int main() {
std::cout << "Введите команду: \"add milk 2\"\n";
// Введите команду: "add milk 2"
}
Обратите внимание: кавычки внутри — это символы текста, а кавычки снаружи — это синтаксис C++.
Обратный слэш в строке: почему путь Windows «ломается»
В Windows-путях обратный слэш встречается постоянно. Новичок пишет:
std::string path = "C:\temp\file.txt";
И дальше начинаются чудеса (и не те, которые вы просили). Потому что \t — это табуляция, \f — тоже escape, и в итоге строка превращается не в путь, а в набор спецсимволов.
Правильно — удвоить слэш:
#include <iostream>
#include <string>
int main() {
std::string path = "C:\\temp\\file.txt";
std::cout << path << '\n';
// C:\temp\file.txt
}
3. Почему ломается std::cin >>
Вот мы и подошли ко второй половине темы, где боль уже не от слэшей, а от ввода.
Выражение std::cin >> s читает одно слово (токен): последовательность символов до первого пробела (или таба/перевода строки — грубо говоря, до whitespace). То есть для него пробел — это разделитель.
Кавычки " в этом режиме не имеют никакой магии. Это просто символ.
Если пользователь вводит "green tea" и вы делаете:
#include <iostream>
#include <string>
int main() {
std::string x;
std::cin >> x;
std::cout << "x=[" << x << "]\n";
// при вводе: "green tea"
// x=["green]
}
Результат будет "green, потому что чтение остановилось на пробеле. Остаток tea" останется в потоке, и следующий >> прочитает уже его.
Это можно представить так:
flowchart LR
A[Ввод: 'green tea'] --> B[operator>> читает до whitespace]
B --> C[x = 'green']
A --> D[в потоке осталось: 'tea']
Очень человеческая логика: «В терминале я ввёл "hello world", значит это одна фраза». Такая логика работает в некоторых командных оболочках (shell), где кавычки — часть синтаксиса командной строки. Но std::cin >> — не shell. Он не знает правил кавычек, он знает только правило «читай до пробела».
И это важно запомнить как принцип: формат ввода задаёт программа, а не пользователь. Если вы хотите поддержать кавычки как синтаксис — вы должны сами это реализовать.
4. Знакомство с функциями
Хочется тут немного забежать вперед и показать пару примеров с использованием функций. Как писать свои функции мы будет детально разбирать в уровне 14, а тут я расскажу о сути, чтобы вы хорошо понимали последующие примеры.
Раньше весь наш код находился внутри main(). Это нормально для первых шагов, но реальные программы почти никогда не состоят из одной большой функции. Чтобы код было проще читать, понимать и поддерживать, его делят на функции.
Функция — это именованный блок кода, который можно вызвать по имени. Когда программа встречает вызов функции, она временно переходит внутрь неё, выполняет её инструкции, а затем возвращается обратно туда, откуда была вызвана.
Посмотрим на самый простой пример:
#include <iostream>
void say_hello() {
std::cout << "Hello!\n";
}
int main() {
say_hello();
}
Когда выполнение доходит до строки say_hello();, программа переходит в функцию say_hello, печатает текст и возвращается обратно в main. Благодаря этому мы можем давать действиям осмысленные имена и не повторять один и тот же код в разных местах.
Теперь разберём важную часть объявления функции — слово void.
В записи:
void say_hello()
слово void означает, что функция ничего не возвращает. Она выполняет какое-то действие, но не передаёт назад результат в виде значения.
Если функция должна вернуть результат, вместо void указывается тип этого результата. Например:
int add(int a, int b) {
return a + b;
}
Здесь функция возвращает значение типа int, поэтому внутри используется return с выражением. В отличие от этого, у функции с void можно либо вообще не писать return, либо написать его без значения:
void print_line() {
std::cout << "-----\n";
}
Такая функция просто выполняет работу и завершает выполнение. Как видите, ничего сложного.
5. Как читать фразу целиком и обработать кавычки
Когда вы хотите получить строку целиком (с пробелами и кавычками как символами), берите std::getline.
#include <iostream>
#include <string>
int main() {
std::string line;
std::getline(std::cin, line);
std::cout << "line=[" << line << "]\n";
// при вводе: "green tea"
// line=["green tea"]
}
Теперь кавычки и пробелы — просто часть строки.
Если до этого вы читали что-то через std::cin >> (например, число), то не забывайте про устойчивый переход через std::ws:
#include <iostream>
#include <string>
int main() {
int n = 0;
std::cin >> n;
std::string line;
std::getline(std::cin >> std::ws, line);
std::cout << "n=" << n << ", line=[" << line << "]\n";
}
Снять кавычки по краям, если они есть
std::getline не обязан убирать кавычки. Он честно прочитал строку. Если вы хотите, чтобы "green tea" превращалось в green tea, вам нужно сделать ещё один шаг.
Мы не будем строить «полный парсер кавычек». Сегодня сделаем самый понятный и безопасный минимум: если строка начинается с " и заканчивается на ", то убираем эти две кавычки.
#include <string>
std::string strip_quotes(std::string s) {
if (s.size() >= 2 && s.front() == '"' && s.back() == '"') {
return s.substr(1, s.size() - 2);
}
return s;
}
Обратите внимание на проверку s.size() >= 2: иначе строка из одной кавычки — это уже «сломанный формат», и резать там нечего.
Практический пример: мини-консоль команд
Чтобы это было не «в вакууме», продолжим простое консольное приложение “MiniCLI”: оно читает команду целиком строкой, а затем выполняет несколько простых действий. Пока без контейнеров, без списков и без «настоящего shell», но уже похоже на маленькую утилиту.
Идея такая: у нас есть “заголовок” (title). Пользователь может задать его командой:
- title milk
- title "green tea"
Вторая форма позволяет писать пробелы.
Печатаем help с кавычками
Чтобы пользователь понимал формат, печатаем подсказку. И вот тут у нас «сразу два мира»: в коде кавычки нужно экранировать, а на экране они должны выглядеть как обычные кавычки.
#include <iostream>
void print_help() {
std::cout << "Команды:\n";
std::cout << " help - показать справку\n";
std::cout << " title TEXT - задать заголовок\n";
std::cout << " title \"TEXT WITH SPACES\" - заголовок с пробелами\n";
}
Здесь \"TEXT WITH SPACES\" — это литерал, поэтому кавычки экранированы.
Читаем строку команды целиком
Каркас main(), который читает команду строкой:
#include <iostream>
#include <string>
int main() {
std::string line;
while (std::getline(std::cin, line)) {
std::cout << "got=[" << line << "]\n";
}
}
Да, пока это «эхо-режим», но он уже показывает главное: вы получаете строку целиком, без сюрприза «ой, >> съел только первое слово».
Выделяем аргумент после title и снимаем кавычки
Предположим, что нормализацию пробелов (trim) вы уже умеете делать. Здесь оставлен минимальный trim_spaces, чтобы пример был самодостаточным.
#include <string>
std::string trim_spaces(std::string s) {
std::size_t b = 0;
while (b < s.size() && s[b] == ' ') ++b;
std::size_t e = s.size();
while (e > b && s[e - 1] == ' ') --e;
return s.substr(b, e - b);
}
И теперь команда title:
#include <iostream>
#include <string>
int main() {
std::string title = "untitled";
std::string line;
while (std::getline(std::cin, line)) {
if (line.rfind("title ", 0) == 0) { // rfind - поиск справа налево
std::string arg = trim_spaces(line.substr(6));
arg = strip_quotes(arg);
title = arg;
std::cout << "title=[" << title << "]\n";
}
}
}
line.rfind("title ", 0) == 0 — удобная техника «строка начинается с…», если вы пока не хотите использовать starts_with. Мы берём всё после title через substr(6), тримим края, и если есть кавычки по краям — снимаем.
6. Типичные ошибки
Ошибка №1: пытаться вывести кавычки в литерале без экранирования.
Когда вы пишете строку вроде "He said: "Hi"", компилятор считает, что строка закончилась на второй кавычке, а дальше идёт мусор. Правильная запись — "He said: \"Hi\"".
Ошибка №2: забывать, что обратный слэш в литерале — это начало escape-последовательности.
Строка "C:\temp\file.txt" почти наверняка не то, что вы ожидали, потому что \t и другие комбинации имеют специальный смысл. Для Windows-путей используйте "C:\\temp\\file.txt".
Ошибка №3: ожидать, что кавычки во вводе «склеят слова» для std::cin >>.
Если пользователь ввёл "green tea", то std::cin >> s прочитает только "green. Кавычки в этом режиме — обычный символ, а пробел всё равно разделитель.
Ошибка №4: читать «фразу с пробелами» через std::cin >>.
Это фундаментальная ошибка выбора инструмента. Если вам нужна строка целиком, используйте std::getline. Если при этом до неё был ввод через >>, используйте std::getline(std::cin >> std::ws, line), чтобы не поймать пустую строку.
Ошибка №5: «снимать кавычки» без проверки длины и границ.
Попытка сделать s.substr(1, s.size() - 2) для строки длины 0 или 1 приводит к некорректной логике и возможным ошибкам. Минимальная защита — проверка s.size() >= 2 и проверка front()/back().
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ