JavaRush /Курси /C++ SELF /Модифікація std::string: erase, replace, append

Модифікація std::string: erase, replace, append

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

1. append(...): додаємо текст

Коли ви щойно починаєте вивчати рядки, може здаватися, що вони потрібні приблизно для трьох речей: запитати імʼя користувача, вивести «Привіт!» і, у кращому разі, склеїти firstName + " " + lastName. Але реальне життя швидко підкидає складніші тексти: із зайвими символами, коментарями, дивними роздільниками або у форматі «ключ=значення».

Гарна новина: std::string — змінюваний обʼєкт. Це не «камʼяна табличка». У вас є операції, які роблять саме те, що й обіцяють: додають, видаляють і замінюють. Погана новина теж проста: як і будь-який інструмент, вони можуть «відпиляти зайве», якщо переплутати індекси або довжини. Тому сьогодні діятимемо як хірурги: спочатку шукаємо, потім ріжемо.

У цій лекції ми почнемо робити маленький TextPrep — навчальний фрагмент застосунку, який приймає рядок команди або конфігурації й приводить його до зручнішого вигляду. Повний розбір на токени буде пізніше. А зараз завдання простіше: «знайти → виправити».

Мінімальний приклад: «зібрати повідомлення» через append

Коли ви тільки починаєте писати код, рука часто тягнеться до +: "Hello, " + name + "!". Це нормально, але в завданнях з обробки тексту часто зручніше збирати рядок поступово, частинами. Для цього добре підходить append(...) і його близький родич +=.

#include <iostream>
#include <string>

int main() {
    std::string msg;
    msg.append("Hello, ");
    msg.append("Bob");
    msg.append("!\n");

    std::cout << msg; // Hello, Bob!
}

Зверніть увагу: append змінює рядок на місці. Він не створює новий рядок і не змушує вас гадати, як саме компілятор обʼєднає вираз.

append vs +=

Зазвичай s += "abc" читається коротше, а s.append("abc") — трохи «офіційніше» й помітніше в коді. Корисно вміти користуватися обома варіантами. Сьогодні в прикладах ми частіше використовуватимемо append, оскільки він явно підкреслює дію «додати в кінець».

#include <iostream>
#include <string>

int main() {
    std::string s = "file";
    s += "_2026";
    s.append(".txt");

    std::cout << s << '\n'; // file_2026.txt
}

Патерн «будуємо результат окремо»

Іноді логіка така: «я не хочу псувати вихідний рядок, а хочу побудувати новий». Тоді ми створюємо std::string out і додаємо туди лише те, що потрібно. Це особливо стане в пригоді в наступних лекціях, де ми почнемо нормалізувати пробіли й токенізувати текст. Але основу — як збирати рядок — закладаємо вже зараз.

#include <iostream>
#include <string>

int main() {
    std::string raw = "milk";
    std::string out;

    out.append("[item=");
    out.append(raw);
    out.append("]");

    std::cout << out << '\n'; // [item=milk]
}

2. erase(...): видаляємо фрагменти рядка

Якщо append — це «наростити текст», то erase — це «прибрати зайве». У роботі з рядками erase трапляється постійно: вирізати коментар наприкінці рядка, видалити зайвий роздільник, прибрати ".tmp", викинути випадкову кому або стерти префікс на кшталт "cmd:".

Ми використовуватимемо дві найзрозуміліші форми:

  • erase(pos) видаляє все від позиції pos до кінця.
  • erase(pos, count) видаляє рівно count символів, починаючи з pos.

Видаляємо хвіст за маркером: «усе після # — коментар»

Це типовий сценарій: рядок на кшталт "value = 42 # comment". Ми шукаємо '#' через find, і якщо знаходимо, відрізаємо хвіст.

#include <iostream>
#include <string>

int main() {
    std::string line = "value = 42 # comment";
    std::size_t hash = line.find('#');

    if (hash != std::string::npos) {
        line.erase(hash); // видаляємо від '#' до кінця
    }

    std::cout << line << '\n'; // value = 42 
}

Зверніть увагу на пробіл наприкінці: він залишився, тому що ми видалили лише коментар, але не нормалізували пробіли. Це нормально. Пробілами займемося пізніше, а сьогодні тренуємо саме «відрізати хвіст».

Видаляємо фіксований фрагмент: прибираємо один символ-роздільник

Іноді потрібно видалити рівно один символ, наприклад двокрапку в "user:alice" (а замінити її на "=" — це вже replace, див. далі). Для видалення одного символу зручно використовувати erase(pos, 1).

#include <iostream>
#include <string>

int main() {
    std::string s = "user:alice";
    std::size_t colon = s.find(':');

    if (colon != std::string::npos) {
        s.erase(colon, 1); // видалили ':'
    }

    std::cout << s << '\n'; // useralice
}

Так, слова «злиплися». Саме тому замість erase часто хочеться використати replace. Але цей приклад добре показує сам механізм.

Важливий нюанс: після erase позиції «зʼїжджають»

Якщо ви знайшли позицію pos, а потім видалили якийсь фрагмент, то все, що було праворуч, зсунеться вліво. Старі індекси стають ненадійними. Тому в серії правок часто надійніше працювати так: «знайшов → виправив → знайшов заново».

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

3. replace(pos, count, text): замінюємо фрагменти

Коли ви робите текстові перетворення, найчастіше не хочеться спочатку щось видаляти, а потім окремо вставляти нове. Простіше сказати по-людськи: «ось цю ділянку замінити на ось цю». Для цього й існує replace.

Форма, якою ми користуватимемося сьогодні:

replace(pos, count, text) — замінити ділянку довжини count, починаючи з pos, на рядок text.

Тут дуже важливо не переплутати другий аргумент: це довжина замінюваної ділянки, а не «індекс кінця».

Замінюємо один символ: ':''='

Повернімося до рядка "user:alice", але тепер зробімо це акуратніше: замінимо ':' на '='.

#include <iostream>
#include <string>

int main() {
    std::string s = "user:alice";
    std::size_t colon = s.find(':');

    if (colon != std::string::npos) {
        s.replace(colon, 1, "="); // замінили один символ
    }

    std::cout << s << '\n'; // user=alice
}

Це один із найзручніших прикладів replace: заміна «один символ на один символ». Водночас ви можете замінити його і на рядок іншої довжини.

Замінюємо слово на слово: "on""off"

#include <iostream>
#include <string>

int main() {
    std::string flags = "feature=on";
    std::size_t pos = flags.find("on");

    if (pos != std::string::npos) {
        flags.replace(pos, 2, "off"); // "on" довжини 2
    }

    std::cout << flags << '\n'; // feature=off
}

Якби після цього ми захотіли замінити ще щось праворуч, то знову виконали б find, оскільки рядок змінив довжину.

Заміна «відрізка»: обережно з межами

replace(pos, count, ...) не вимагає, щоб count «потрапив точно в слово». Він просто замінює count символів. Тому, якщо ви помилитеся на 1 символ, можете зʼїсти зайве або, навпаки, щось залишити. Це класична помилка початківця, і виправляється вона просто: завжди вголос проговорюйте, що саме означає count.

4. Патерн «знайти → змінити»

Тепер зберемо в одну конструкцію все, що вже вміємо: find, перевірку на std::string::npos, а потім erase, replace або append. Цей патерн настільки поширений, що його зручно сприймати як невелику блок-схему.

Ось схема:

flowchart TD
    A[Маємо рядок s] --> B[Знаходимо маркер або фрагмент через find]
    B --> C{pos != npos?}
    C -- ні --> D[Нічого не робимо або обираємо інший сценарій]
    C -- так --> E[Змінюємо рядок: erase / replace / append]
    E --> F[За потреби шукаємо заново, бо довжина змінилася]

Сценарій: прибрати коментар і замінити роздільник

Уявімо, що ми приймаємо рядки мініконфігурації формату: "user:alice # main user". Нам хочеться отримати: "user=alice". Зробімо це у два кроки: спочатку видалимо коментар, потім замінимо ':' на '='.

#include <iostream>
#include <string>

int main() {
    std::string line = "user:alice # main user";

    std::size_t hash = line.find('#');
    if (hash != std::string::npos) {
        line.erase(hash);
    }

    std::size_t colon = line.find(':');
    if (colon != std::string::npos) {
        line.replace(colon, 1, "=");
    }

    std::cout << line << '\n'; // user=alice 
}

Так, пробіл у кінці залишився, бо перед # зазвичай стоїть пробіл. Це нормально: пробіли — тема окремої наступної лекції про нормалізацію.

«Замінити лише перше входження»

replace замінює саме ту ділянку, позицію якої ви йому передали. Тому за замовчуванням, якщо ви знайшли перше входження через find, ви зміните тільки перше.

#include <iostream>
#include <string>

int main() {
    std::string s = "a=b=c";
    std::size_t eq = s.find('=');

    if (eq != std::string::npos) {
        s.replace(eq, 1, ":");
    }

    std::cout << s << '\n'; // a:b=c
}

Якщо хочеться замінити всі, тоді вже потрібен цикл «знайти → замінити → шукати далі». Але тут одразу постає запитання: куди зсувати позицію після заміни?

Мініцикл: замінити всі крапки на підкреслення

Зробімо акуратний цикл із максимально прозорою логікою: після заміни рухаємося на 1 символ уперед. Тут це безпечно, тому що ми замінюємо 1 символ на 1 символ.

#include <iostream>
#include <string>

int main() {
    std::string name = "report.2026.final.txt";
    std::size_t pos = 0;

    while ((pos = name.find('.', pos)) != std::string::npos) {
        name.replace(pos, 1, "_");
        pos += 1; // рухаємося далі
    }

    std::cout << name << '\n'; // report_2026_final_txt
}

Цей приклад вдалий саме тому, що він «чесний»: заміна має однакову довжину, отже позиція після неї передбачувана.

5. Два підходи: правити на місці чи будувати новий рядок

Саме тут багато початківців починають сперечатися самі з собою: «А що краще — змінювати вихідний рядок чи збирати новий?» Відповідь нудна, але практична: усе залежить від завдання. Якщо ви робите точкову операцію на кшталт «відрізати коментар», простіше правити на місці (erase). Якщо ви фільтруєте символи, перебудовуєте формат, щось пропускаєте й щось додаєте, то часто простіше побудувати новий рядок через append.

Є й психологічний чинник: підхід «будую новий рядок» зазвичай дає менше помилок з індексами, тому що ви просто рухаєтеся зліва направо й додаєте те, що потрібно.

Побудова результату: прибираємо всі # як символи

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

#include <iostream>
#include <string>

int main() {
    std::string s = "a#b##c";
    std::string out;

    for (std::size_t i = 0; i < s.size(); ++i) {
        if (s[i] != '#') out.append(1, s[i]); // додати 1 символ
    }

    std::cout << out << '\n'; // abc
}

Тут є невелика «магія»: append(1, s[i]) означає «додай один символ один раз». Це коректний спосіб додати char через append, хоча часто для цього використовують push_back. Але сьогодні тримаємо фокус саме на родині методів append.

Порівняння підходів

Завдання Зазвичай простіше Чому
«Відрізати хвіст після маркера»
erase(pos)
Одна операція, мінімум логіки
«Замінити один роздільник»
replace(pos, 1, "...")
Не треба вручну видаляти й вставляти
«Зібрати форматований рядок»
append(...)
у новий рядок
Менше ризику помилитися з індексами, структура зрозуміліша

6. Міні-інструмент «TextPrep»: функція prepareLine

Тепер зробимо фрагмент, який уже справді схожий на частину застосунку. Ми напишемо функцію prepareLine, яка приймає рядок і повертає зручніший варіант.

На сьогодні правила такі (без пробілів і токенізації — це буде далі):

  1. Спочатку видаляємо коментар від # до кінця.
  2. Потім замінюємо : на = (якщо такий символ є).
  3. Потім додаємо префікс "OK: ", просто щоб побачити, як append допомагає зібрати результат.

Функція prepareLine

#include <string>

std::string prepareLine(std::string line) {
    std::size_t hash = line.find('#');
    if (hash != std::string::npos) line.erase(hash);

    std::size_t colon = line.find(':');
    if (colon != std::string::npos) line.replace(colon, 1, "=");

    std::string out;
    out.append("OK: ");
    out.append(line);
    return out;
}

Зверніть увагу на важливий момент проєктування: ми приймаємо line за значенням, тобто копіюємо рядок, змінюємо його й повертаємо новий результат. Для початківця це часто простіше: зовні функція виглядає безпечною — «передали рядок, отримали результат». Про тонкощі вартості копіювання поговоримо значно пізніше. Зараз головне — ясність.

Мініперевірка в main

#include <iostream>
#include <string>

std::string prepareLine(std::string line);

int main() {
    std::string raw = "user:alice # main user";
    std::string ready = prepareLine(raw);

    std::cout << ready << '\n'; // OK: user=alice 
}

Якщо ви зараз дивитеся на пробіл наприкінці й думаєте: «ну все, світ несправедливий», — це цілком нормальна інженерна реакція. Ми справді виправимо пробіли в наступних лекціях, тому що це окрема тема.

7. Типові помилки

Помилка № 1: використовувати erase/replace з позицією std::string::npos.
std::string::npos — це спеціальне значення «не знайдено», і воно не є коректним індексом. Якщо зробити line.erase(npos) або line.replace(npos, ...), ви отримаєте поведінку, яка в найкращому разі завершиться винятком або помилкою виконання, а в найгіршому — загадковою поломкою логіки. Рішення просте: спочатку завжди пишемо if (pos != std::string::npos), а вже потім виконуємо будь-які операції.

Помилка № 2: плутати «довжину» і «кінцевий індекс» у другому параметрі replace та erase.
У replace(pos, count, text) і erase(pos, count) параметр count — це саме кількість символів, а не «позиція кінця». Якщо ви мислите так: «заміню з 3 по 7», то в коді потрібно написати replace(3, 7 - 3, ...). Ця помилка особливо підступна тим, що код компілюється й навіть часто «майже працює», але час від часу зʼїдає зайвий символ.

Помилка № 3: робити кілька правок підряд, використовуючи старі позиції.
Щойно ви виконали erase або replace, рядок міг змінити довжину. Це означає, що індекс, знайдений «раніше», може вже вказувати не туди. Початківці часто роблять так: знайшли pos1, знайшли pos2, потім видалили фрагмент на початку — і раптом pos2 став неправильним. У ситуаціях, де рядок змінюється, безпечніше шукати заново після кожної суттєвої правки, особливо поки ви ще не впевнено орієнтуєтеся в індексах.

Помилка № 4: очікувати, що append/erase/replace повертають «новий рядок», а не змінюють поточний.
Ці методи насамперед модифікують обʼєкт. Так, багато з них повертають посилання на рядок, що дозволяє будувати ланцюжки викликів, але сенс саме в тому, що ви змінюєте поточний рядок. Якщо ви написали line.erase(pos); і чекаєте, що «десь зʼявиться новий рядок», — він не зʼявиться. Або працюємо з тим самим обʼєктом, або свідомо будуємо out і повертаємо його.

Помилка № 5: збирати рядки через + у довгих виразах і втрачати читабельність.
Іноді вираз на кшталт a + b + c + d + e перетворюється на «рядковий суп», у якому вже складно зрозуміти, що саме будується. У таких місцях append або покрокове збирання результату зазвичай читається краще: видно, що є префіксом, що — даними, а що — завершенням формату. І це знижує ризик випадково забути пробіл, двокрапку або переведення рядка.

Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ