JavaRush /Курси /C++ SELF /Екранування в рядках і лапки під час введення

Екранування в рядках і лапки під час введення

C++ SELF
Рівень 9 , Лекція 5
Відкрита

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++ Навіщо це потрібно
Перенесення рядка
"\n"
Робити багаторядковий вивід, форматувати текст
Табуляція
"\t"
Просте вирівнювання колонок
Лапка "
"\""
Друкувати цитати, JSON-подібний текст, підказки
Зворотний слеш \
"\\"
Шляхи Windows, регулярні вирази (пізніше), спеціальні формати
Апостроф '
'\''
Потрібно рідше, але в текстах трапляється
Нульовий байт
"\0"
У навчальних задачах майже не потрібен, але знати корисно

Є й екзотичніші варіанти — шістнадцяткові, 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 читає одне слово (токен): послідовність символів до першого пробілу, табуляції або перенесення рядка. Тобто для нього пробіл — це роздільник.

Лапки " у цьому режимі не мають жодної магії. Це просто символ.

Якщо користувач вводить "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>> читає до пробільного символу]
    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().

1
Опитування
Рядки детальніше та розбір тексту, рівень 9, лекція 5
Недоступний
Рядки детальніше та розбір тексту
Рядки детальніше та розбір тексту
Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ