Вступ до std::string
Вступ до std::string
Знайомий незнайомець
Ви вже зустрічали рядки на самому початку навчання — ще у першій програмі:
#include <iostream>
using namespace std;
int main()
{
cout << "Hello, World!" << endl;
return 0;
}
Той рядковий літерал у лапках — це C-style рядок, масив символів із нуль-термінатором в кінці. Ви детально познайомилися з ними в попередній статті: як вони оголошуються, чому потрібен '\0', які небезпеки несуть.
Але пригадайте всі ті застереження: переповнення буфера, ручне управління пам'яттю, strcpy замість =, strcmp замість ==. Ось типовий код для роботи з C-style рядком — «скопіювати ім'я та додати привітання»:
#include <iostream>
#include <cstring>
using namespace std;
int main()
{
const int BUFFER_SIZE = 50;
char name[BUFFER_SIZE];
// Ввід: ризик переповнення без перевірки розміру
cin.getline(name, BUFFER_SIZE);
// Буфер для результату — розмір треба рахувати вручну
char greeting[BUFFER_SIZE + 7]; // "Hello, " = 7 символів
strcpy(greeting, "Hello, ");
strcat(greeting, name); // ризик переповнення!
cout << greeting << "\n";
cout << "Length: " << strlen(greeting) << "\n";
return 0;
}
#include <iostream>
#include <string>
using namespace std;
int main()
{
string name;
getline(cin, name);
string greeting = "Hello, " + name;
cout << greeting << "\n";
cout << "Length: " << greeting.length() << "\n";
return 0;
}
Правий варіант коротший, виразніший і безпечніший. Немає ручних буферів, немає strcpy, немає ризику переповнення. Клас std::string сам керує своєю пам'яттю, самостійно росте за потреби і надає всі звичні оператори (=, +, ==, <) у їх природному сенсі.
Навіщо потрібен std::string
Проблеми C-style рядків
Перш ніж рухатися далі, корисно зрозуміти, чому C-style рядки настільки незручні. Причина корениться в їхній природі: char[] — це просто масив байтів. Мова нічого не знає про те, що цей масив «є рядком». Звідси й усі наслідки:
Ручне управління пам'яттю. Щоб зберегти рядок "Hello!", потрібно самостійно виділити буфер потрібного розміру, не забути про нуль-термінатор, а після роботи — звільнити пам'ять:
#include <cstring>
using namespace std;
int main()
{
// 7 символів: H-e-l-l-o-!-\0
char* s = new char[7];
strcpy(s, "Hello!");
// ... робота з рядком ...
delete[] s; // не забути! і саме [], не просто delete
return 0;
}
Небезпечне копіювання. Оператор = для char* копіює адресу, а не вміст. Після char* b = a; обидва вказівники дивляться на ту саму пам'ять. Зміна через b змінить і a:
#include <iostream>
#include <cstring>
using namespace std;
int main()
{
char src[] = "Hello";
char* a = src;
char* b = a; // копіюємо адресу, не рядок!
b[0] = 'J'; // змінюємо через b...
cout << a; // ...але бачимо зміну і тут: "Jello"
return 0;
}
Порівняння порівнює адреси. if (a == b) для двох char* перевіряє, чи вказують вони на одну й ту саму адресу пам'яті, а не чи однаковий їх текст. Це типова помилка початківців:
#include <iostream>
#include <cstring>
using namespace std;
int main()
{
const char* s1 = "hello";
const char* s2 = "hello";
// Порівнює АДРЕСИ, а не вміст!
if (s1 == s2)
cout << "same address\n";
// Правильно: порівнює вміст
if (strcmp(s1, s2) == 0)
cout << "same content\n";
return 0;
}
same address у цьому прикладі залежить від компілятора. Більшість компіляторів інтернують рядкові літерали — зберігають ідентичні літерали в одному місці пам'яті. Але це оптимізація, а не гарантія. В загальному випадку s1 == s2 для двох окремих char* повертає false, навіть якщо тексти однакові.Відсутність зручних операторів. Немає + для конкатенації, немає < для лексикографічного порівняння (точніше, є, але порівнює адреси). Потрібно пам'ятати цілий арсенал функцій: strcpy, strcat, strcmp, strncpy, strlen, strncmp, strncat...
Проблеми C-style рядків
- Ручне виділення і звільнення пам'яті (
new[]/delete[]) - Ризик переповнення буфера при
strcpy,strcat - Оператор
=копіює адресу, а не вміст - Оператор
==порівнює адреси, а не текст - Немає природного
+для конкатенації - Потрібно пам'ятати
strlen,strcpy,strcmp,strcat... - Нуль-термінатор завжди «невидимо» присутній і може бути втрачений
Переваги std::string
- Пам'ять керується автоматично (RAII)
- Рядок сам зростає при потребі — без переповнень
- Оператор
=виконує глибоке копіювання - Оператори
==,!=,<,>,<=,>=порівнюють текст - Оператор
+та+=для конкатенації - Зручні методи:
.length(),.find(),.substr(),.replace()... - Інтеграція з алгоритмами STL через ітератори
Рішення: клас з RAII
Ключова ідея std::string — це RAII (Resource Acquisition Is Initialization): клас сам захоплює ресурс (пам'ять для рядка) при створенні і сам звільняє її при знищенні. Програміст не думає про new[] і delete[] — це робить деструктор.
Крім того, клас перевантажує оператори — =, +, ==, < тощо — надаючи їм зрозумілу рядкову семантику. Під капотом це ті самі операції з пам'яттю та байтами, але приховані за зручним інтерфейсом.
Клас std::string: заголовок та ієрархія
Підключення заголовка
Для роботи з std::string потрібно підключити заголовок <string>:
#include <string>
using namespace std;
<iostream> в деяких реалізаціях неявно включає частину <string>, але покладатися на це — погана практика. Завжди підключайте <string> явно, коли використовуєте std::string.Шаблон basic_string
В стандартній бібліотеці рядковий клас реалізований як шаблон basic_string<> — щоб підтримувати рядки з різними типами символів:
namespace std
{
template<
class CharT,
class Traits = char_traits<CharT>,
class Allocator = allocator<CharT>
>
class basic_string;
}
Параметр CharT — тип одного символу. Traits визначає операції над символами (порівняння, копіювання), Allocator — як виділяється пам'ять. Для більшості завдань значення за замовчуванням ідеальні — ви їх не чіпатимете.
На основі basic_string<> визначені конкретні типи:
namespace std
{
using string = basic_string<char>; // ASCII / UTF-8
using wstring = basic_string<wchar_t>; // UTF-16 (Windows) / UTF-32 (Linux/macOS)
using u8string = basic_string<char8_t>; // UTF-8 (явний тип, C++20)
using u16string = basic_string<char16_t>; // UTF-16 (C++11)
using u32string = basic_string<char32_t>; // UTF-32 (C++11)
}
Який тип використовувати
У переважній більшості програм використовується std::string — рядок із символів типу char. Він зберігає байти, які на практиці є або ASCII (якщо рядок лише з латиниці та цифр), або UTF-8 (якщо рядок містить символи поза ASCII — кирилицю, емодзі, тощо).
std::wstring використовується переважно при роботі з Windows API (функції CreateFileW, MessageBoxW тощо) та деякими GUI-бібліотеками.
basic_string<> і однаково доступний у std::string, std::wstring та інших. Вивчаєте std::string — автоматично знаєте API і для решти.Створення та ініціалізація
Конструктор за замовчуванням — порожній рядок
Найпростіший спосіб створити рядок — оголосити його без ініціалізатора. Результат — порожній рядок:
#include <iostream>
#include <string>
using namespace std;
int main()
{
string s; // порожній рядок, length() == 0
cout << "Value: '" << s << "'\n";
cout << "Length: " << s.length() << "\n";
cout << "Empty: " << boolalpha << s.empty() << "\n";
return 0;
}
Ініціалізація рядковим літералом
Найчастіший спосіб — передати рядковий літерал або const char* у конструктор (або через =):
#include <iostream>
#include <string>
using namespace std;
int main()
{
string s1("Hello, World!"); // конструктор з const char*
string s2 = "Hello, World!"; // те саме — через синтаксис копіювання
cout << s1 << "\n";
cout << s2 << "\n";
cout << (s1 == s2) << "\n"; // true
return 0;
}
Запис std::string s2 = "Hello" виглядає як присвоювання, але насправді це виклик конструктора. Компілятор автоматично конвертує const char* у тимчасовий std::string і потім (з C++17 — без копіювання через NRVO/RVO) ініціалізує s2.
Конструктор копіювання
Якщо потрібна незалежна копія існуючого рядка — передайте його у конструктор. На відміну від char*, тут справжнє глибоке копіювання:
#include <iostream>
#include <string>
using namespace std;
int main()
{
string original = "Hello";
string copy(original); // глибока копія
copy[0] = 'J'; // змінюємо копію
cout << original << "\n"; // Hello — не змінився!
cout << copy << "\n"; // Jello
return 0;
}
Підрядок при конструюванні
Можна відразу взяти частину іншого рядка — вказавши стартову позицію та кількість символів:
#include <iostream>
#include <string>
using namespace std;
int main()
{
string source = "Hello, World!";
// від позиції 7, 5 символів
string sub1(source, 7, 5);
// від позиції 7 до кінця (кількість не вказана)
string sub2(source, 7);
cout << sub1 << "\n"; // World
cout << sub2 << "\n"; // World!
return 0;
}
Рядок із повторюваного символу
Конструктор (n, char) створює рядок із n однакових символів:
#include <iostream>
#include <string>
using namespace std;
int main()
{
string dashes(20, '-');
string stars(5, '*');
cout << dashes << "\n"; // --------------------
cout << stars << "\n"; // *****
return 0;
}
Конвертація числа в рядок: std::to_string
std::string не має конструктора для чисел безпосередньо — std::string s(42) викличе конструктор «повторення символу» і дасть 42 нуль-байти, що не те, що очікується. Правильний шлях — функція std::to_string (C++11):
#include <iostream>
#include <string>
using namespace std;
int main()
{
int count = 42;
double price = 3.14;
long big = 1'000'000L;
string s1 = to_string(count);
string s2 = to_string(price);
string s3 = to_string(big);
cout << s1 << "\n"; // 42
cout << s2 << "\n"; // 3.140000
cout << s3 << "\n"; // 1000000
// Конкатенація з числами через to_string
string msg = "Знайдено " + s1 + " елементів";
cout << msg << "\n";
return 0;
}
std::to_string для double завжди дає 6 знаків після коми. Для форматування дійсних чисел з довільною точністю використовуйте std::ostringstream із маніпулятором std::setprecision, або (C++20) std::format("{:.2f}", price).Конвертація рядка в число
Зворотна операція — стандартні функції з <string>, доступні з C++11:
#include <iostream>
#include <string>
using namespace std;
int main()
{
string intStr = "42";
string floatStr = "3.14";
string hexStr = "FF";
int i = stoi(intStr); // string to int
double d = stod(floatStr); // string to double
int h = stoi(hexStr, nullptr, 16); // шістнадцяткова основа
cout << i << "\n"; // 42
cout << d << "\n"; // 3.14
cout << h << "\n"; // 255
return 0;
}
int. Другий параметр — покажчик на позицію після числа, третій — основа числення (за замовчуванням 10).long.long long.float.double.unsigned long. Корисно для шістнадцяткових значень.std::stoi, std::stod тощо кидають виняток std::invalid_argument, якщо рядок не починається з числа, і std::out_of_range, якщо значення виходить за межі типу. У продакшн-коді обертайте виклики у try/catch.Конвертація між std::string та C-style рядком
Отримати const char* з рядка: .c_str()
Багато старих API (функції C, системні виклики POSIX) приймають const char*. Метод .c_str() повертає вказівник на внутрішній буфер рядка — нуль-термінований:
#include <iostream>
#include <string>
#include <cstdio> // printf
using namespace std;
int main()
{
string filename = "data.txt";
// printf вимагає const char*
printf("Opening: %s\n", filename.c_str());
// Або для будь-якої C-функції
const char* raw = filename.c_str();
cout << raw << "\n";
return 0;
}
.data() — варіант без гарантії нуль-термінатора (до C++11)
До C++11 метод .data() повертав const char* без нуль-термінатора в кінці. З C++11 поведінка .data() і .c_str() ідентична. З C++17 також є неконстантне перевантаження char* data(), яке дозволяє пряму запис:
// C++17: неконстантний доступ до буфера
string s = "Hello";
char* p = s.data();
p[0] = 'J'; // безпечно в C++17
.c_str() або .data(), стає невалідним після будь-якої операції, що змінює рядок або перевиділяє його буфер: +=, append, insert, resize, push_back тощо. Ніколи не зберігайте цей вказівник «на потім» — отримуйте його щоразу заново безпосередньо перед використанням.Конструювання std::string з const char*
Зворотна операція тривіальна — вже показана вище через конструктор. Але є тонкість із частиною C-style рядка:
#include <iostream>
#include <string>
using namespace std;
int main()
{
const char* cstr = "Hello, World!";
// Весь рядок
string s1(cstr);
// Перші 5 символів
string s2(cstr, 5);
cout << s1 << "\n"; // Hello, World!
cout << s2 << "\n"; // Hello
return 0;
}
std::string(const char*, size_t n) копіює рівно n байтів — він не зупиняється на нуль-байті всередині рядка. Це відрізняється від поведінки конструктора std::string(const char*), де зупинкою є '\0'.Ввід та вивід рядків
Вивід через std::cout
Клас std::string перевантажує оператор << для std::ostream — вивід виглядає так само, як для int або double:
#include <iostream>
#include <string>
using namespace std;
int main()
{
string name = "Олена";
string surname = "Коваль";
cout << name << " " << surname << "\n";
cout << "Довжина: " << name.length() << "\n";
return 0;
}
"Олена" — 10 байтів, хоча символів 5. Причина — кирилиця в UTF-8 займає по 2 байти на символ. Метод .length() повертає кількість байтів, а не символів. Про це детально написано в статті про Unicode.Ввід одного «слова» через std::cin >>
Оператор >> зчитує рядок до першого пробілу (або символу-розділювача):
#include <iostream>
#include <string>
using namespace std;
int main()
{
cout << "Введіть ім'я: ";
string name;
cin >> name; // зупиняється на пробілі
cout << "Привіт, " << name << "!\n";
return 0;
}
Якщо користувач ввів Іван Петров, name отримає лише "Іван". Рядок " Петров" залишиться в буфері вводу і буде зчитаний наступним оператором >>.
Ввід цілого рядка через std::getline
Щоб зчитати рядок разом із пробілами, використовується std::getline(stream, string) — зчитує до символу '\n' (або до вказаного розділювача):
#include <iostream>
#include <string>
using namespace std;
int main()
{
cout << "Введіть повне ім'я: ";
string fullName;
getline(cin, fullName);
cout << "Введіть вік: ";
string age;
getline(cin, age);
cout << "Ім'я: " << fullName << "\n";
cout << "Вік: " << age << "\n";
return 0;
}
Пастка: std::cin >> + std::getline
Це одна з найпоширеніших помилок у C++. Що станеться, якщо спочатку зчитати число через >>, а потім рядок через getline?
#include <iostream>
#include <string>
using namespace std;
int main()
{
cout << "Оберіть варіант (1 або 2): ";
int choice;
cin >> choice; // зчитує "2", але '\n' залишається в буфері!
cout << "Введіть назву: ";
string name;
getline(cin, name); // отримує '\n' — порожній рядок!
cout << "Вибір: " << choice << "\n";
cout << "Назва: '" << name << "'\n";
return 0;
}
Чому так? Оператор >> зчитує 2, але символ нового рядка '\n' (що натиснула клавіша Enter) залишається у вхідному буфері. Коли наступним йде getline, він бачить '\n' і думає, що введено порожній рядок — і негайно повертається.
Рішення — видалити залишковий '\n' із буфера за допомогою std::cin.ignore():
#include <iostream>
#include <string>
#include <limits> // numeric_limits
using namespace std;
int main()
{
cout << "Оберіть варіант (1 або 2): ";
int choice;
cin >> choice;
// Очищуємо буфер: ігноруємо все до '\n' включно
cin.ignore(numeric_limits<streamsize>::max(), '\n');
cout << "Введіть назву: ";
string name;
getline(cin, name);
cout << "Вибір: " << choice << "\n";
cout << "Назва: '" << name << "'\n";
return 0;
}
std::numeric_limits<std::streamsize>::max() — максимально можливий розмір. Така форма ignore гарантовано видалить усе до '\n', навіть якщо перед ним є кілька символів. Це надійніший варіант, ніж довільне число на кшталт 32767.Конкатенація та довжина
Оператор + і +=
std::string перевантажує + для з'єднання двох рядків і += для додавання до існуючого. Це інтуїтивно і безпечно — клас сам розширить буфер при потребі:
#include <iostream>
#include <string>
using namespace std;
int main()
{
string first = "Hello";
string second = " World";
// Оператор + — новий рядок
string result = first + second + "!";
cout << result << "\n"; // Hello World!
// Оператор += — додає до існуючого
first += " there";
first += '!'; // можна додати і один символ
cout << first << "\n"; // Hello there!
// Конкатенація з числом через to_string
int count = 5;
string msg = "Знайдено " + to_string(count) + " файлів";
cout << msg << "\n";
return 0;
}
+ зі рядковими літералами не завжди працює без std::string поруч. Два літерали не можна з'єднати оператором + безпосередньо — це не std::string, а const char[]:// ПОМИЛКА: обидва операнди — const char[], не string
// string s = "Hello" + " World"; // помилка компіляції
// ПРАВИЛЬНО: хоча б один операнд — string
string s = string("Hello") + " World";
// або:
string s2 = "Hello";
s2 += " World";
Довжина і перевірка на порожнечу
Метод .length() (синонім .size()) повертає кількість байтів у рядку, не включаючи нуль-термінатор. .empty() перевіряє, чи рядок порожній:
#include <iostream>
#include <string>
using namespace std;
int main()
{
string ascii = "Hello";
string cyrillic = "Привіт"; // UTF-8: кожен символ = 2 байти
string empty;
cout << ascii.length() << "\n"; // 5
cout << cyrillic.length() << "\n"; // 12 (6 символів × 2 байти)
cout << empty.length() << "\n"; // 0
cout << boolalpha;
cout << ascii.empty() << "\n"; // false
cout << empty.empty() << "\n"; // true
return 0;
}
.empty(), а не s.length() == 0. Причина: .empty() гарантовано є операцією O(1) і краще виражає намір.Продуктивність конкатенації у циклі
Оператор + кожного разу створює новий тимчасовий рядок. У циклі це може призвести до багатьох виділень пам'яті. Правильний підхід — використовувати += або .append():
#include <iostream>
#include <string>
using namespace std;
int main()
{
// Погано: кожна ітерація — нова копія
string bad;
for (int i = 0; i < 5; ++i)
bad = bad + "x"; // bad + "x" → тимчасовий string
// Добре: додає на місці, без зайвих копій
string good;
good.reserve(5); // підказка: заздалегідь виділити місце для 5 символів
for (int i = 0; i < 5; ++i)
good += 'x'; // += для одного символу — найефективніше
cout << bad << "\n"; // xxxxx
cout << good << "\n"; // xxxxx
return 0;
}
Знищення та час життя: RAII
std::string є прикладом RAII (Resource Acquisition Is Initialization) — одного з фундаментальних принципів C++. Ресурс (динамічна пам'ять для зберігання символів) захоплюється при конструюванні об'єкта і автоматично звільняється при його знищенні — коли об'єкт виходить за межі своєї области видимості.
#include <iostream>
#include <string>
using namespace std;
void processName(const string& name)
{
string upper; // конструктор виділяє пам'ять
for (char c : name)
upper += static_cast<char>(toupper(static_cast<unsigned char>(c)));
cout << upper << "\n";
} // деструктор автоматично звільняє пам'ять upper
int main()
{
{
string s = "Hello"; // пам'ять виділена
cout << s << "\n";
} // s виходить зі scope → деструктор → пам'ять звільнена
processName("world"); // WORLD
// НЕ ПОТРІБНО: жодного delete, жодного free()
return 0;
}
Порівняйте з C-style аналогом, де все управління пам'яттю лежить на програмісті:
void process()
{
string s = "Hello";
// ... робота ...
} // автоматичне звільнення
void process()
{
char* s = new char[6];
strcpy(s, "Hello");
// ... робота ...
delete[] s; // треба пам'ятати!
// Якщо забути або кинути виняток —
// витік пам'яті (memory leak)
}
Практика
Рівень 1 — Знайомство
Напишіть програму, що:
- Запитує ім'я та прізвище через
std::getline(два окремих виклики) - Об'єднує їх у повне ім'я через
+ - Виводить повне ім'я та його довжину в байтах
#include <iostream>
#include <string>
using namespace std;
int main()
{
cout << "Ім'я: ";
string firstName;
getline(cin, firstName);
cout << "Прізвище: ";
string lastName;
getline(cin, lastName);
string fullName = firstName + " " + lastName;
cout << "Повне ім'я: " << fullName << "\n";
cout << "Довжина: " << fullName.length() << " байтів\n";
return 0;
}
Рівень 2 — Конвертація та змішаний ввід
Напишіть програму, що:
- Запитує кількість товарів (через
std::cin >>) та назву кожного (черезgetline) - Формує рядок-звіт виду
"Товар 1: Яблука\nТовар 2: Груші\n..."
Ключовий момент: після cin >> count потрібно очистити буфер перед першим getline.
#include <iostream>
#include <string>
#include <limits>
using namespace std;
int main()
{
cout << "Кількість товарів: ";
int count;
cin >> count;
cin.ignore(numeric_limits<streamsize>::max(), '\n');
string report;
for (int i = 1; i <= count; ++i)
{
cout << "Назва товару " << i << ": ";
string item;
getline(cin, item);
report += "Товар " + to_string(i) + ": " + item + "\n";
}
cout << "\n--- Звіт ---\n";
cout << report;
return 0;
}
Рівень 3 — Функція repeat
Напишіть функцію std::string repeat(const std::string& s, int n, const std::string& sep), яка повторює рядок s рівно n разів, розділяючи копії рядком sep. При n <= 0 повертає порожній рядок.
#include <iostream>
#include <string>
using namespace std;
string repeat(const string& s, int n, const string& sep = "")
{
if (n <= 0) return "";
string result;
result.reserve(s.length() * static_cast<size_t>(n) +
sep.length() * static_cast<size_t>(n - 1));
for (int i = 0; i < n; ++i)
{
if (i > 0) result += sep;
result += s;
}
return result;
}
int main()
{
cout << repeat("ha", 3) << "\n"; // hahaha
cout << repeat("abc", 4, "-") << "\n"; // abc-abc-abc-abc
cout << repeat("*", 5, " ") << "\n"; // * * * * *
cout << repeat("hey", 0) << "\n"; // (порожній)
return 0;
}
Резюме
У цій статті ми заклали фундамент для роботи з рядками в C++:
Навіщо std::string
= і ==, ризикують переповненням буфера. std::string вирішує всі ці проблеми через RAII та перевантаження операторів.Ієрархія класів
std::string — це basic_string<char>. Той самий шаблон лежить в основі wstring, u8string, u16string, u32string. Весь функціонал однаковий для всіх.Створення рядків
std::to_string. Зворотне: std::stoi, std::stod та споріднені функції.Конвертація в C-style
.c_str() — нуль-термінований const char* для C-API. Вказівник стає невалідним після будь-якої модифікації рядка. З C++17 .data() повертає char* для запису.Ввід та вивід
cout << — вивід. cin >> — одне слово. std::getline(cin, s) — цілий рядок. Пастка: cin >> число залишає '\n' у буфері — потрібен cin.ignore(...) перед getline.Конкатенація
+ для нового рядка (копія), += для додавання на місці. У циклі завжди += — + кожного разу виділяє тимчасовий об'єкт. .length() / .size() — кількість байтів.Що далі? У наступній статті ми детально розберемо внутрішню модель пам'яті std::string — різницю між length() та capacity(), механізм подвоєння буфера, Small String Optimization — і навчимося ефективно звертатися до окремих символів через [], at(), front(), back().
C-style рядки
Масив char з нуль-термінатором — фундамент рядкової обробки в C та C++. Детально: оголошення, ініціалізація, char[] vs const char*, бібліотека <cstring>, небезпеки та buffer overflow.
Довжина, ємність та доступ до символів std::string
Різниця між length() та capacity(), механізм подвоєння буфера при перевиділенні памяті, Small String Optimization, методи reserve() та shrink_to_fit() для оптимізації, доступ до символів через [], at(), front(), back() та ітерація рядком.