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, потом удалили кусок до 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.
Сравнение подходов
| Задача | Обычно проще | Почему |
|---|---|---|
| «Отрезать хвост после маркера» | |
Одна операция, минимум логики |
| «Заменить один разделитель» | |
Не надо руками делать delete+insert |
| «Собрать форматированную строку» | в новую строку |
Меньше рисков с индексами, понятнее структура |
6. Мини-инструмент “TextPrep”: функция prepareLine
Теперь сделаем кусочек, который реально похож на часть приложения. Мы напишем функцию prepareLine, которая принимает строку и возвращает более удобный вариант.
Правила на сегодня (без пробелов и токенизации, это будет дальше):
- Сначала удаляем комментарий от # до конца.
- Потом заменяем : на = (если он есть).
- Потом добавляем префикс "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 или пошаговая сборка результата обычно читается лучше: видно, что является префиксом, что данными, а что — окончанием формата. И это снижает шанс случайно забыть пробел, двоеточие или перевод строки.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ