Модифікація std::string: присвоювання, додавання, вставка, видалення та заміна
Модифікація std::string
Рядок — не масив, він вміє «самостійно» змінюватись
Подивіться на цей код і спробуйте передбачити вивід:
#include <iostream>
#include <string>
using namespace std;
int main()
{
string s = "Hello, World!";
s.replace(7, 5, "C++");
s.insert(0, ">>> ");
s.erase(s.length() - 1, 1);
cout << s << "\n";
return 0;
}
Три виклики методів — і рядок повністю перетворено. У цій статті ми розберемо весь арсенал методів модифікації std::string: від простого присвоювання до точкової заміни підрядка.
Присвоювання: assign
Оператор = — найпростіший спосіб замінити вміст рядка. Метод .assign() пропонує ті ж операції, але з більшими можливостями:
#include <iostream>
#include <string>
using namespace std;
int main()
{
string s;
// Присвоїти C-рядок
s.assign("Hello");
cout << s << "\n"; // Hello
// Присвоїти n копій символу
s.assign(5, '*');
cout << s << "\n"; // *****
// Присвоїти підрядок іншого рядка: assign(str, pos, count)
string src = "Hello, World!";
s.assign(src, 7, 5); // з позиції 7, 5 символів
cout << s << "\n"; // World
// Присвоїти повністю інший рядок
s.assign(src);
cout << s << "\n"; // Hello, World!
return 0;
}
.assign() повертає std::string& — посилання на сам рядок. Це дозволяє ланцюжок викликів: s.assign("X").append("Y"). Усі методи модифікації, що повертають std::string&, підтримують таке «fluent»-з'єднання.Додавання в кінець: append та push_back
Оператор += і метод .append() — синоніми для більшості випадків. Різниця: .append() надає ширший набір форм виклику.
#include <iostream>
#include <string>
using namespace std;
int main()
{
string s = "Hello";
// Один символ — push_back або += char
s.push_back('!');
cout << s << "\n"; // Hello!
// C-рядок
s.append(" World");
cout << s << "\n"; // Hello! World
// n копій символу
s.append(3, '.');
cout << s << "\n"; // Hello! World...
// Підрядок іншого рядка: append(str, pos, count)
string extra = "---[END]---";
s.append(extra, 3, 5); // "[END]"
cout << s << "\n"; // Hello! World...[END]
// += — той самий результат, але коротше
s += " OK";
cout << s << "\n"; // Hello! World...[END] OK
return 0;
}
.pop_back() — зворотня операція до push_back(): видаляє останній символ. Пара push_back / pop_back перетворює std::string на простий стек символів. UB на порожньому рядку.Вставка: insert
.insert() додає символи у довільну позицію рядка. Всі символи після точки вставки зсуваються вправо:
#include <iostream>
#include <string>
using namespace std;
int main()
{
string s = "HelloWorld";
// Вставити рядок за індексом
s.insert(5, ", ");
cout << s << "\n"; // Hello, World
// Вставити n копій символу за індексом
s.insert(0, 3, '>');
cout << s << "\n"; // >>>Hello, World
// Вставити підрядок: insert(pos, str, from, count)
string tag = "[IMPORTANT]";
s.insert(3, tag, 0, 11);
cout << s << "\n"; // >>>[IMPORTANT]Hello, World
return 0;
}
n і вставки у позицію p — складність O(n − p). Якщо потрібно часто вставляти на початок або в середину, розгляньте std::deque<char> або збір шматків у вектор рядків з подальшим з'єднанням.Видалення: erase
.erase() видаляє символи з довільної позиції:
#include <iostream>
#include <string>
using namespace std;
int main()
{
string s = ">>>Hello, World<<<";
// erase(pos, count) — видалити count символів з позиції pos
s.erase(0, 3); // видалити ">>>" на початку
cout << s << "\n"; // Hello, World<<<
s.erase(s.length() - 3, 3); // видалити "<<<" в кінці
cout << s << "\n"; // Hello, World
// erase(pos) — видалити від pos до кінця
s.erase(5);
cout << s << "\n"; // Hello
// erase() без аргументів — очистити весь рядок
s.erase();
cout << "'" << s << "'" << "\n"; // ''
cout << "empty: " << boolalpha << s.empty() << "\n"; // true
return 0;
}
.clear() — скорочений синонім .erase(). Обидва знуляють довжину, але не обов'язково звільняють виділену пам'ять (ємність лишається). Якщо потрібно також звільнити пам'ять — виклик s.clear() + s.shrink_to_fit().Заміна підрядка: replace
.replace(pos, count, ...) видаляє count символів з позиції pos і на їх місце вставляє новий текст:
#include <iostream>
#include <string>
using namespace std;
int main()
{
string s = "I love cats and cats are great";
// replace(pos, count, newStr) — замінити один фрагмент
s.replace(7, 4, "dogs");
cout << s << "\n"; // I love dogs and cats are great
// Замінити всі входження — через цикл з find()
string from = "cats";
string to = "dogs";
size_t pos = s.find(from);
while (pos != string::npos)
{
s.replace(pos, from.length(), to);
pos = s.find(from, pos + to.length()); // шукаємо далі
}
cout << s << "\n"; // I love dogs and dogs are great
// replace з n копіями символу: replace(pos, count, n, ch)
s.replace(0, 1, 3, '!');
cout << s << "\n"; // !!! love dogs and dogs are great
return 0;
}
Зміна розміру: resize
.resize(n) змінює довжину рядка до n символів:
- якщо
n < length()— рядок обрізається; - якщо
n > length()— рядок розширюється, нові символи заповнюються'\0'(або вказаним символом).
#include <iostream>
#include <string>
using namespace std;
int main()
{
string s = "Hello";
// Обрізання
s.resize(3);
cout << s << "\n"; // Hel
// Розширення з заповнювачем за замовчуванням ('\0')
s.resize(7);
cout << s.length() << "\n"; // 7 (але є '\0' всередині!)
// Розширення з власним символом-заповнювачем
s.assign("Hello");
s.resize(8, '!');
cout << s << "\n"; // Hello!!!
return 0;
}
resize(n) з n > length() рядок містить нульові байти '\0' між старим вмістом і кінцем. Такий рядок матиме length() == n, але при виводі через cout символи після першого '\0' не відображатимуться — тому що потік зупиняється на нулі. Якщо потрібні бінарні дані з нулями — передавайте s.data() і s.length() явно.Витяг підрядка: substr
.substr(pos, count) повертає нову копію рядка, що містить count символів починаючи з позиції pos:
#include <iostream>
#include <string>
using namespace std;
int main()
{
string s = "Hello, World!";
// substr(pos, count)
string hello = s.substr(0, 5); // "Hello"
string world = s.substr(7, 5); // "World"
cout << hello << "\n"; // Hello
cout << world << "\n"; // World
// substr(pos) — від pos до кінця
string tail = s.substr(7);
cout << tail << "\n"; // World!
// Типовий патерн: витягнути розширення файлу
string filename = "report.2026.pdf";
size_t dot = filename.rfind('.'); // остання крапка
string ext = filename.substr(dot + 1);
cout << ext << "\n"; // pdf
return 0;
}
.substr() завжди копіює символи — це O(n) за часом і пам'яттю. Якщо копія не потрібна — передайте std::string_view(s.data() + pos, count) (C++17). string_view — лише «вікно» у існуючий буфер без копіювання.Порівняння рядків
Оператори ==, !=, <, >, <=, >=
std::string підтримує всі оператори порівняння. Порівняння лексикографічне — за кодами символів зліва направо:
#include <iostream>
#include <string>
using namespace std;
int main()
{
string a = "apple";
string b = "banana";
string c = "apple";
cout << boolalpha;
cout << (a == c) << "\n"; // true
cout << (a != b) << "\n"; // true
cout << (a < b) << "\n"; // true ('a' < 'b')
cout << (b > a) << "\n"; // true
// Порівняння зі C-рядком — теж працює
cout << (a == "apple") << "\n"; // true
// Увага: порівняння регістрозалежне
string upper = "Apple";
cout << (a == upper) << "\n"; // false ('a' != 'A')
return 0;
}
Метод .compare() — тричастинне порівняння
.compare() повертає int: негативне якщо *this < other, нуль якщо рівні, позитивне якщо *this > other. Основна перевага — можливість порівнювати підрядки:
#include <iostream>
#include <string>
using namespace std;
int main()
{
string s = "Hello, World!";
// Порівняти підрядок з окремим рядком
// s.compare(pos, count, other)
int r1 = s.compare(7, 5, "World"); // "World" == "World" → 0
int r2 = s.compare(0, 5, "World"); // "Hello" vs "World" → від'ємне
int r3 = s.compare(7, 5, "Alpha"); // "World" vs "Alpha" → позитивне
cout << r1 << "\n"; // 0
cout << (r2 < 0 ? "менше" : "не менше") << "\n"; // менше
cout << (r3 > 0 ? "більше" : "не більше") << "\n"; // більше
// Порівняти підрядок з підрядком іншого рядка
string other = "Hello, Planet!";
int r4 = s.compare(0, 7, other, 0, 7); // "Hello, " == "Hello, " → 0
cout << r4 << "\n"; // 0
return 0;
}
==/</> зручніші для загальних порівнянь. Метод .compare() потрібний коли необхідно порівняти частину рядка без виклику .substr() (а отже — без копіювання).Порівняння без урахування регістру
У стандартній бібліотеці C++ немає готового compareIgnoreCase. Найпростіше рішення — привести обидва рядки до одного регістру перед порівнянням, або використати std::equal з функцією-предикатом:
#include <iostream>
#include <string>
#include <algorithm>
#include <cctype>
using namespace std;
bool equalsIgnoreCase(const string& a, const string& b)
{
if (a.length() != b.length()) return false;
return equal(a.begin(), a.end(), b.begin(),
[](unsigned char ca, unsigned char cb)
{
return tolower(ca) == tolower(cb);
});
}
int main()
{
cout << boolalpha;
cout << equalsIgnoreCase("Hello", "hello") << "\n"; // true
cout << equalsIgnoreCase("Hello", "HELLO") << "\n"; // true
cout << equalsIgnoreCase("Hello", "World") << "\n"; // false
return 0;
}
Зведена таблиця методів модифікації
assign(str), assign(n, ch), assign(str, pos, count).append(str), append(n, ch), append(str, pos, count). Еквівалент +=.pop_back() — видалити останній.pos. Форми: insert(pos, str), insert(pos, n, ch), insert(pos, str, from, count).count символів з позиції pos. Без аргументів — очистити весь рядок.count символів з позиції pos на str. Розміри можуть відрізнятись.n. Скорочення обрізає, розширення заповнює '\0' (або вказаним символом).count за замовчуванням — до кінця рядка.Практика
Рівень 1 — Форматування імені
Напишіть програму, що зчитує прізвище та ім'я користувача (двома рядками) і виводить їх у форматі "Прізвище І." (скорочення імені до першої літери з крапкою).
#include <iostream>
#include <string>
using namespace std;
int main()
{
string lastName, firstName;
cout << "Прізвище: ";
getline(cin, lastName);
cout << "Ім'я: ";
getline(cin, firstName);
if (lastName.empty() || firstName.empty())
{
cout << "Порожній ввід.\n";
return 1;
}
// Перша літера великою, решта малими
lastName[0] = static_cast<char>(
toupper(static_cast<unsigned char>(lastName[0])));
for (size_t i = 1; i < lastName.length(); ++i)
lastName[i] = static_cast<char>(
tolower(static_cast<unsigned char>(lastName[i])));
string result = lastName + " " + firstName[0] + ".";
cout << result << "\n";
return 0;
}
Рівень 2 — Видалення зайвих пробілів
Напишіть функцію normalize(std::string s), що повертає рядок, у якому: всі послідовності пробілів замінено одним пробілом, а пробіли на початку та в кінці видалено.
#include <iostream>
#include <string>
using namespace std;
string normalize(string s)
{
// Видалити ведучі пробіли
size_t start = s.find_first_not_of(' ');
if (start == string::npos)
return ""; // весь рядок — пробіли
s.erase(0, start);
// Видалити хвостові пробіли
size_t end = s.find_last_not_of(' ');
s.erase(end + 1);
// Замінити кратні пробіли одним
size_t pos = 0;
while ((pos = s.find(" ", pos)) != string::npos)
{
// Знайшли два пробіли підряд — видаляємо один
s.erase(pos, 1);
}
return s;
}
int main()
{
cout << "'" << normalize(" Hello World ") << "'\n";
cout << "'" << normalize(" ") << "'\n";
cout << "'" << normalize("no extra spaces") << "'\n";
return 0;
}
Рівень 3 — Просте шаблонне заповнення
Напишіть функцію fillTemplate(std::string tmpl, const std::string& name, const std::string& subject), що замінює всі входження {name} і {subject} у шаблоні tmpl відповідними значеннями і повертає готовий рядок.
#include <iostream>
#include <string>
using namespace std;
void replaceAll(string& s,
const string& from,
const string& to)
{
size_t pos = 0;
while ((pos = s.find(from, pos)) != string::npos)
{
s.replace(pos, from.length(), to);
pos += to.length(); // перестрибнути замінений текст
}
}
string fillTemplate(string tmpl,
const string& name,
const string& subject)
{
replaceAll(tmpl, "{name}", name);
replaceAll(tmpl, "{subject}", subject);
return tmpl;
}
int main()
{
string tmpl =
"Шановний(а) {name}!\n"
"Ваш іспит із предмету '{subject}' заплановано на завтра.\n"
"Бажаємо успіху, {name}!";
cout << fillTemplate(tmpl, "Олена", "Програмування на C++") << "\n";
return 0;
}
Резюме
assign та append
assign() замінює весь вміст; append() і += додають у кінець. push_back(ch) / pop_back() — стекоподібний доступ до кінця рядка. Всі форми: рядок, n символів, підрядок іншого рядка.insert та erase
insert(pos, ...) вставляє у довільну позицію (O(n)). erase(pos, count) видаляє зазначений фрагмент. clear() скидає довжину до 0 без звільнення ємності.replace та substr
replace(pos, count, newStr) видаляє count символів і вставляє новий текст на їх місце. substr(pos, count) повертає нову копію підрядка. Для читання без копіювання — std::string_view.resize
resize(n) змінює довжину. Обрізає при n < length(), розширює (заповнює '\0' або вказаним символом) при n > length(). Не плутати з reserve() — той змінює ємність, не довжину.Порівняння
==, !=, <, > — лексикографічне порівняння. .compare(pos, count, other) — для порівняння підрядків без копіювання. Без регістру — std::equal з лямбдою або std::tolower.Ланцюжок викликів
assign, append, insert, erase, replace повертають std::string&. Це дозволяє ланцюжок: s.assign("X").append("Y").insert(0, ">>> "). Читабельно, без зайвих копій.Що далі? Наступна стаття — пошук у рядку: методи find, rfind, find_first_of, find_last_of, find_first_not_of, find_last_not_of, а також знайомство з std::string_view — легковагим «вікном» у рядок без копіювання.
Довжина, ємність та доступ до символів std::string
Різниця між length() та capacity(), механізм подвоєння буфера при перевиділенні памяті, Small String Optimization, методи reserve() та shrink_to_fit() для оптимізації, доступ до символів через [], at(), front(), back() та ітерація рядком.
Пошук у std::string: find, npos та практичні патерни
Методи пошуку в std::string: find(), rfind(), find_first_of(), find_last_of(), find_first_not_of(), find_last_not_of(). Поняття std::string::npos. Практичні патерни: знайти всі входження, split, trim, парсинг key=value.