C++

Вступ до std::string

Навіщо std::string замінив C-style рядки, як клас basic_string організований в стандартній бібліотеці, способи створення та ініціалізації рядків, конвертація в C-style, ввід/вивід та типова пастка з getline — повний фундамент для роботи з текстом у C++.

Вступ до std::string

Знайомий незнайомець

Ви вже зустрічали рядки на самому початку навчання — ще у першій програмі:

StringHello.cpp
#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;
}

Правий варіант коротший, виразніший і безпечніший. Немає ручних буферів, немає strcpy, немає ризику переповнення. Клас std::string сам керує своєю пам'яттю, самостійно росте за потреби і надає всі звичні оператори (=, +, ==, <) у їх природному сенсі.


Навіщо потрібен std::string

Проблеми C-style рядків

Перш ніж рухатися далі, корисно зрозуміти, чому C-style рядки настільки незручні. Причина корениться в їхній природі: char[] — це просто масив байтів. Мова нічого не знає про те, що цей масив «є рядком». Звідси й усі наслідки:

Ручне управління пам'яттю. Щоб зберегти рядок "Hello!", потрібно самостійно виділити буфер потрібного розміру, не забути про нуль-термінатор, а після роботи — звільнити пам'ять:

ManualMemory.cpp
#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:

ShallowCopy.cpp
#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* перевіряє, чи вказують вони на одну й ту саму адресу пам'яті, а не чи однаковий їх текст. Це типова помилка початківців:

WrongCompare.cpp
#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;
}
./WrongCompare
$ ./WrongCompare
same address
same content
Execution finished with exit code 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)
}
Loading diagram...
@startuml
skinparam style plain
skinparam defaultFontName "JetBrains Mono"
skinparam backgroundColor #f8fafc
skinparam defaultFontSize 13

title Ієрархія рядкових класів у std

rectangle "basic_string<CharT, Traits, Allocator>" as base #3b82f6 {
  rectangle "Весь функціонал: конструктори, оператори,\nметоди пошуку, модифікації, порівняння..." as impl #2563eb
}

rectangle "string\n= basic_string<char>" as str #22c55e
rectangle "wstring\n= basic_string<wchar_t>" as wstr #f59e0b
rectangle "u8string\n= basic_string<char8_t>\n(C++20)" as u8str #64748b
rectangle "u16string\n= basic_string<char16_t>\n(C++11)" as u16str #64748b
rectangle "u32string\n= basic_string<char32_t>\n(C++11)" as u32str #64748b

base -down-> str  : "char = 1 байт\nASCII / UTF-8"
base -down-> wstr : "wchar_t = 2/4 байти\nUTF-16 / UTF-32"
base -down-> u8str
base -down-> u16str
base -down-> u32str

note bottom of str
  Основний тип для більшості завдань.
  На курсі ми використовуємо саме його.
end note

note bottom of wstr
  Windows API, Qt, Java-bridge.
  wchar_t — 2 байти на Windows,
  4 байти на Linux/macOS
end note

@enduml

Який тип використовувати

У переважній більшості програм використовується std::string — рядок із символів типу char. Він зберігає байти, які на практиці є або ASCII (якщо рядок лише з латиниці та цифр), або UTF-8 (якщо рядок містить символи поза ASCII — кирилицю, емодзі, тощо).

std::wstring використовується переважно при роботі з Windows API (функції CreateFileW, MessageBoxW тощо) та деякими GUI-бібліотеками.

Весь функціонал — методи, оператори, алгоритми — реалізований у базовому шаблоні basic_string<> і однаково доступний у std::string, std::wstring та інших. Вивчаєте std::string — автоматично знаєте API і для решти.

Створення та ініціалізація

Конструктор за замовчуванням — порожній рядок

Найпростіший спосіб створити рядок — оголосити його без ініціалізатора. Результат — порожній рядок:

DefaultConstruct.cpp
#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;
}
./DefaultConstruct
$ ./DefaultConstruct
Value: ''
Length: 0
Empty: true
Execution finished with exit code 0.

Ініціалізація рядковим літералом

Найчастіший спосіб — передати рядковий літерал або const char* у конструктор (або через =):

InitFromLiteral.cpp
#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;
}
./InitFromLiteral
$ ./InitFromLiteral
Hello, World!
Hello, World!
true
Execution finished with exit code 0.

Запис std::string s2 = "Hello" виглядає як присвоювання, але насправді це виклик конструктора. Компілятор автоматично конвертує const char* у тимчасовий std::string і потім (з C++17 — без копіювання через NRVO/RVO) ініціалізує s2.

Конструктор копіювання

Якщо потрібна незалежна копія існуючого рядка — передайте його у конструктор. На відміну від char*, тут справжнє глибоке копіювання:

CopyConstruct.cpp
#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;
}
./CopyConstruct
$ ./CopyConstruct
Hello
Jello
Execution finished with exit code 0.

Підрядок при конструюванні

Можна відразу взяти частину іншого рядка — вказавши стартову позицію та кількість символів:

SubstringConstruct.cpp
#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;
}
./SubstringConstruct
$ ./SubstringConstruct
World
World!
Execution finished with exit code 0.

Рядок із повторюваного символу

Конструктор (n, char) створює рядок із n однакових символів:

RepeatChar.cpp
#include <iostream>
#include <string>

using namespace std;

int main()
{
    string dashes(20, '-');
    string stars(5, '*');

    cout << dashes << "\n"; // --------------------
    cout << stars  << "\n"; // *****

    return 0;
}
./RepeatChar
$ ./RepeatChar
--------------------
*****
Execution finished with exit code 0.

Конвертація числа в рядок: std::to_string

std::string не має конструктора для чисел безпосередньо — std::string s(42) викличе конструктор «повторення символу» і дасть 42 нуль-байти, що не те, що очікується. Правильний шлях — функція std::to_string (C++11):

NumberToString.cpp
#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;
}
./NumberToString
$ ./NumberToString
42
3.140000
1000000
Знайдено 42 елементів
Execution finished with exit code 0.
std::to_string для double завжди дає 6 знаків після коми. Для форматування дійсних чисел з довільною точністю використовуйте std::ostringstream із маніпулятором std::setprecision, або (C++20) std::format("{:.2f}", price).

Конвертація рядка в число

Зворотна операція — стандартні функції з <string>, доступні з C++11:

StringToNumber.cpp
#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;
}
./StringToNumber
$ ./StringToNumber
42
3.14
255
Execution finished with exit code 0.
std::stoi(s)
int
Перетворює рядок на int. Другий параметр — покажчик на позицію після числа, третій — основа числення (за замовчуванням 10).
std::stol(s)
long
Перетворює рядок на long.
std::stoll(s)
long long
Перетворює рядок на long long.
std::stof(s)
float
Перетворює рядок на float.
std::stod(s)
double
Перетворює рядок на double.
std::stoul(s)
unsigned long
Перетворює рядок на 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() повертає вказівник на внутрішній буфер рядка — нуль-термінований:

PrintfInterop.cpp
#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;
}
./PrintfInterop
$ ./CStr
Opening: data.txt
data.txt
Execution finished with exit code 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 рядка:

FromCString.cpp
#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;
}
./FromCString
$ ./FromCString
Hello, World!
Hello
Execution finished with exit code 0.
Конструктор std::string(const char*, size_t n) копіює рівно n байтів — він не зупиняється на нуль-байті всередині рядка. Це відрізняється від поведінки конструктора std::string(const char*), де зупинкою є '\0'.

Ввід та вивід рядків

Вивід через std::cout

Клас std::string перевантажує оператор << для std::ostream — вивід виглядає так само, як для int або double:

PrintString.cpp
#include <iostream>
#include <string>

using namespace std;

int main()
{
    string name    = "Олена";
    string surname = "Коваль";

    cout << name << " " << surname << "\n";
    cout << "Довжина: " << name.length() << "\n";

    return 0;
}
./PrintString
$ ./Output
Олена Коваль
Довжина: 10
Execution finished with exit code 0.
Довжина "Олена" — 10 байтів, хоча символів 5. Причина — кирилиця в UTF-8 займає по 2 байти на символ. Метод .length() повертає кількість байтів, а не символів. Про це детально написано в статті про Unicode.

Ввід одного «слова» через std::cin >>

Оператор >> зчитує рядок до першого пробілу (або символу-розділювача):

InputWord.cpp
#include <iostream>
#include <string>

using namespace std;

int main()
{
    cout << "Введіть ім'я: ";
    string name;
    cin >> name; // зупиняється на пробілі

    cout << "Привіт, " << name << "!\n";
    return 0;
}
./InputWord
$ ./InputWord
Введіть ім'я: Іван Петров
Привіт, Іван!
Execution finished with exit code 0.

Якщо користувач ввів Іван Петров, name отримає лише "Іван". Рядок " Петров" залишиться в буфері вводу і буде зчитаний наступним оператором >>.

Ввід цілого рядка через std::getline

Щоб зчитати рядок разом із пробілами, використовується std::getline(stream, string) — зчитує до символу '\n' (або до вказаного розділювача):

Getline.cpp
#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;
}
./Getline
$ ./Getline
Введіть повне ім'я: Іван Петров
Введіть вік: 25
Ім'я: Іван Петров
Вік: 25
Execution finished with exit code 0.

Пастка: std::cin >> + std::getline

Це одна з найпоширеніших помилок у C++. Що станеться, якщо спочатку зчитати число через >>, а потім рядок через getline?

CinTrap.cpp
#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;
}
./CinTrap — ПРОБЛЕМА ⚠️
$ ./CinTrap
Оберіть варіант (1 або 2): 2
Введіть назву: (програма не чекає вводу!)
Вибір: 2
Назва: ''
Execution finished with exit code 0.

Чому так? Оператор >> зчитує 2, але символ нового рядка '\n' (що натиснула клавіша Enter) залишається у вхідному буфері. Коли наступним йде getline, він бачить '\n' і думає, що введено порожній рядок — і негайно повертається.

Рішення — видалити залишковий '\n' із буфера за допомогою std::cin.ignore():

CinFixed.cpp
#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;
}
./CinFixed — ВИПРАВЛЕНО ✅
$ ./CinFixed
Оберіть варіант (1 або 2): 2
Введіть назву: Мій проєкт
Вибір: 2
Назва: 'Мій проєкт'
Execution finished with exit code 0.
std::numeric_limits<std::streamsize>::max() — максимально можливий розмір. Така форма ignore гарантовано видалить усе до '\n', навіть якщо перед ним є кілька символів. Це надійніший варіант, ніж довільне число на кшталт 32767.
Loading diagram...
@startuml
skinparam style plain
skinparam defaultFontName "JetBrains Mono"
skinparam backgroundColor #f8fafc
skinparam defaultFontSize 13

title Пастка cin >> + getline: що відбувається у буфері

rectangle "Буфер вводу після cin >> choice" as b1 #475569 {
  rectangle "Символ '\\n' залишився!" as n1 #ef4444
}

rectangle "getline без ignore" as bad #ef4444 {
  rectangle "Бачить '\\n' → повертає порожній рядок" as r1 #dc2626
}

rectangle "std::cin.ignore(..., '\\n')" as fix #22c55e {
  rectangle "Поглинає '\\n' → буфер чистий" as r2 #16a34a
}

rectangle "getline після ignore" as good #22c55e {
  rectangle "Чекає нового вводу → коректний результат" as r3 #15803d
}

b1 -right-> bad  : "без ignore"
b1 -down->  fix
fix -down-> good

note bottom of bad
  Типова помилка:
  name = "" (порожній рядок)
end note

note bottom of good
  Правильна поведінка:
  name = "Мій проєкт"
end note

@enduml

Конкатенація та довжина

Оператор + і +=

std::string перевантажує + для з'єднання двох рядків і += для додавання до існуючого. Це інтуїтивно і безпечно — клас сам розширить буфер при потребі:

Concat.cpp
#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;
}
./Concat
$ ./Concat
Hello World!
Hello there!
Знайдено 5 файлів
Execution finished with exit code 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() перевіряє, чи рядок порожній:

Length.cpp
#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;
}
./Length
$ ./Length
5
12
0
false
true
Execution finished with exit code 0.
Для перевірки «рядок порожній» завжди використовуйте .empty(), а не s.length() == 0. Причина: .empty() гарантовано є операцією O(1) і краще виражає намір.

Продуктивність конкатенації у циклі

Оператор + кожного разу створює новий тимчасовий рядок. У циклі це може призвести до багатьох виділень пам'яті. Правильний підхід — використовувати += або .append():

ConcatPerf.cpp
#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++. Ресурс (динамічна пам'ять для зберігання символів) захоплюється при конструюванні об'єкта і автоматично звільняється при його знищенні — коли об'єкт виходить за межі своєї области видимості.

StringOwnership.cpp
#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;
}
./StringOwnership
$ ./RAII
Hello
WORLD
Execution finished with exit code 0.

Порівняйте з C-style аналогом, де все управління пам'яттю лежить на програмісті:

void process()
{
    string s = "Hello";
    // ... робота ...
} // автоматичне звільнення
Loading diagram...
@startuml
skinparam style plain
skinparam defaultFontName "JetBrains Mono"
skinparam backgroundColor #f8fafc
skinparam defaultFontSize 13

title RAII у std::string — час життя об'єкта і пам'яті

skinparam sequence {
  ArrowColor #94a3b8
  LifeLineBorderColor #475569
  ParticipantBorderColor #334155
  ParticipantBackgroundColor #1e293b
  ParticipantFontColor #e2e8f0
}

participant "Стек" as stack #334155
participant "std::string s" as obj #3b82f6
participant "Купа (heap)" as heap #22c55e

stack -> obj : "std::string s = \"Hello\";\n(конструктор)"
obj -> heap  : "new char[6]\nзберігає \"Hello\\0\""
note right of heap : пам'ять виділена

stack -> obj : "s += \" World\";\n(append)"
obj -> heap  : "realloc/new char[12]\nзберігає \"Hello World\\0\""
note right of heap : буфер розширено

stack -> obj : "}\n(кінець scope →\nдеструктор)"
obj -> heap  : "delete[] buffer"
note right of heap : пам'ять звільнена

@enduml

Практика

Рівень 1 — Знайомство

Напишіть програму, що:

  1. Запитує ім'я та прізвище через std::getline (два окремих виклики)
  2. Об'єднує їх у повне ім'я через +
  3. Виводить повне ім'я та його довжину в байтах

Рівень 2 — Конвертація та змішаний ввід

Напишіть програму, що:

  1. Запитує кількість товарів (через std::cin >>) та назву кожного (через getline)
  2. Формує рядок-звіт виду "Товар 1: Яблука\nТовар 2: Груші\n..."

Ключовий момент: після cin >> count потрібно очистити буфер перед першим getline.

Рівень 3 — Функція repeat

Напишіть функцію std::string repeat(const std::string& s, int n, const std::string& sep), яка повторює рядок s рівно n разів, розділяючи копії рядком sep. При n <= 0 повертає порожній рядок.


Резюме

У цій статті ми заклали фундамент для роботи з рядками в C++:

Навіщо std::string

C-style рядки вимагають ручного управління пам'яттю, не мають безпечних операторів = і ==, ризикують переповненням буфера. std::string вирішує всі ці проблеми через RAII та перевантаження операторів.

Ієрархія класів

std::string — це basic_string<char>. Той самий шаблон лежить в основі wstring, u8string, u16string, u32string. Весь функціонал однаковий для всіх.

Створення рядків

6 способів: порожній, з літерала, копіюванням, з підрядка іншого рядка, з повторюваного символу, з числа через 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().

Copyright © 2026