Строки в C++ — это последовательности символов, но хранить их можно двумя принципиально разными способами: низкоуровневая C-строка (массив char[], который заканчивается нулевым байтом '\0') и высокоуровневый класс std::string из заголовка <string>, который сам управляет памятью и хранит длину явно, отдельным числом. В статье разберем, чем они отличаются на практике, как создавать и менять строки через std::string, и на какой типичной ошибке с длиной чаще всего спотыкаются новички.
Содержание
- Два вида строк: C-строка и std::string
- Создание строк: конструкторы std::string
- Длина и доступ к символам: size() против strlen()
- Беззнаковая длина: типичная ошибка в цикле
- Ввод и вывод строк
- Сравнение и конкатенация: операторы
- Изменение строки: resize, clear, empty, push_back, append
- Совместимость с C: метод c_str()
- Выводы
- Где применяется / связь с практикой
- FAQ
Два вида строк: C-строка и std::string
C-строка пришла в C++ по наследству от языка C: область памяти типа char, где символы идут подряд, а конец помечен байтом '\0' (нуль-терминатор). Длину такой строки каждый раз вычисляет функция strlen() из <cstring>, идя по байтам до '\0'.
std::string устроен иначе: класс хранит указатель на буфер И текущую длину отдельным числом, поэтому size() не пересчитывает строку заново. Память под буфер std::string выделяет и освобождает сам (RAII) — вручную new/delete вызывать не нужно.
| Термин | Что это | Как узнается длина | Управление памятью |
|---|---|---|---|
| C-строка | char[] или char*, обязателен '\0' в конце |
strlen(), поиск до первого '\0' |
вручную (стек или ручной new/delete) |
| std::string | класс из <string>, буфер + сохраненная длина |
size()/length(), мгновенно |
автоматическое (RAII) |
Строка с нулевым байтом внутри данных, а не только в конце, ведет себя по-разному в двух мирах — ниже это видно на конкретном примере.
Создание строк: конструкторы std::string
У std::string несколько полезных конструкторов — пустая строка, строка из литерала, строка из n одинаковых символов, строка как часть другой строки.
#include <iostream>
#include <string>
int main() {
std::string s1; // пустая строка, s1.size() == 0
std::string s2 = "hello"; // из строкового литерала (C-строки), 5 символов
std::string s3(10, 'z'); // явный вызов конструктора: 10 символов 'z'
std::string s4(s2, 1, 3); // копия части s2: с индекса 1, длиной 3 символа
std::cout << "s1='" << s1 << "' size=" << s1.size() << "\n";
std::cout << "s2='" << s2 << "' size=" << s2.size() << "\n";
std::cout << "s3='" << s3 << "'\n";
std::cout << "s4='" << s4 << "'\n";
}
Вывод:
s1='' size=0
s2='hello' size=5
s3='zzzzzzzzzz'
s4='ell'
s4 — это часть s2 («hello»): начиная с индекса 1 (буква ‘e’) и длиной 3 символа получаем «ell».
Длина и доступ к символам: size() против strlen()
К отдельному символу строки можно обратиться по индексу, как в массиве: s[0] — первый символ (нумерация с нуля), s[s.size() - 1] — последний. У std::string есть и безопасный вариант s.at(i), который бросает исключение std::out_of_range при выходе за границы, тогда как s[i] при неверном индексе — неопределенное поведение (UB), программа может не упасть сразу, а тихо прочитать чужую память.
Разница между size() и strlen() не только в скорости, но и в смысле — покажем это на строке со встроенным нулевым байтом:
#include <cstring>
#include <iostream>
#include <string>
int main() {
const char* c_str = "AB\0CD"; // C-строка: strlen остановится на первом '\0'
std::string cpp_str("AB\0CD", 5); // std::string: явно хранит 5 байт, включая '\0' внутри
std::cout << "strlen(c_str) = " << std::strlen(c_str) << "\n";
std::cout << "cpp_str.size() = " << cpp_str.size() << "\n";
}
Вывод:
strlen(c_str) = 2
cpp_str.size() = 5
Для C-строки нулевой байт внутри данных — конец строки, все после него strlen() не видит («AB» — 2 символа). Для std::string нулевой байт — просто один из символов, длина задана явно и равна 5. Вывод: если в данных возможны нулевые байты (например, бинарные данные), std::string подходит, а обычная C-строка — нет.
Беззнаковая длина: типичная ошибка в цикле
size() возвращает значение беззнакового типа std::size_t (как и все методы, возвращающие длину или индекс). Это создает ловушку при вычитании из пустой или короткой строки.
#include <iostream>
#include <string>
int main() {
std::string s; // пустая строка, s.size() == 0
std::size_t bad = s.size() - 1; // беззнаковое вычитание: 0 - 1 не "-1", а переполнение
std::cout << "s.size() = " << s.size() << "\n";
std::cout << "s.size() - 1 = " << bad << "\n";
}
Вывод (для 64-битного std::size_t, платформозависимо):
s.size() = 0
s.size() - 1 = 18446744073709551615
В цикле for (std::size_t i = 0; i < s.size() - 1; ++i) для пустой строки условие не станет ложным сразу: цикл попытается пройти почти по всему адресному пространству и обратится к памяти за пределами строки (UB). Исправление — не вычитать из размера, а прибавлять к индексу: for (std::size_t i = 0; i + 1 < s.size(); ++i) — для пустой строки такое условие сразу ложно.
Ввод и вывод строк
Вывод строки в поток работает как для чисел, через <<. Для ввода есть два инструмента с разным поведением: >> читает одно «слово» до пробела, а getline() — всю строку целиком, включая пробелы внутри.
#include <iostream>
#include <string>
int main() {
std::string word;
std::string line;
std::cin >> word; // читает до первого пробельного символа
std::cin.ignore(); // пропускаем один оставшийся разделитель
std::getline(std::cin, line); // читает всю оставшуюся строку до конца строки
std::cout << "word='" << word << "'\n";
std::cout << "line='" << line << "'\n";
}
Для ввода Hello world from Otus (одна строка, Enter в конце) программа выведет:
word='Hello'
line='world from Otus'
cin >> word останавливается на пробеле, cin.ignore() убирает этот пробел, а getline() дочитывает остаток целиком. Связка нужна, когда после слова или числа нужен остаток строки без потери пробелов.
Сравнение и конкатенация: операторы
std::string перегружает привычные операторы, поэтому со строками можно работать почти как с числами.
| Оператор | Что делает |
|---|---|
= |
присваивание значения |
+= |
добавление в конец символа, C-строки или другой std::string |
+ |
конкатенация (склеивание) двух строк или строки и символа |
==, != |
посимвольное сравнение на равенство |
<, >, <=, >= |
лексикографическое сравнение (по кодам символов, как в словаре) |
Например, для std::string a = "abc" и b = "abd": a == b дает false, a < b дает true (строки совпадают до третьего символа, а 'c' идет раньше 'd'), а a + b дает "abcabd" — склеивание, а не арифметическое сложение.
Изменение строки: resize, clear, empty, push_back, append
std::string можно менять на месте, без пересоздания.
| Метод | Что делает |
|---|---|
resize(n) |
меняет длину на n: обрезает лишнее или дополняет символами по умолчанию |
resize(n, c) |
то же, но новые позиции при увеличении заполняются символом c |
clear() |
делает строку пустой |
empty() |
true, если строка пустая |
push_back(c) |
добавляет один символ char в конец |
append(n, c) |
добавляет n одинаковых символов c |
append(t) |
добавляет содержимое строки t (C-строки или std::string) |
append(t, pos, count) |
добавляет часть t с индекса pos, длиной count символов |
#include <iostream>
#include <string>
int main() {
std::string s = "cpp";
s.push_back('!'); // один символ в конец
s.append(3, '?'); // три одинаковых символа в конец
std::cout << "1) " << s << " (size=" << s.size() << ")\n";
s.resize(4); // обрезали строку до 4 символов
std::cout << "2) " << s << "\n";
s.resize(7, '_'); // увеличили строку, новые символы '_'
std::cout << "3) " << s << "\n";
std::cout << std::boolalpha;
std::cout << "4) empty() = " << s.empty() << "\n";
s.clear();
std::cout << "5) empty() = " << s.empty() << "\n";
}
Вывод:
1) cpp!??? (size=7)
2) cpp!
3) cpp!___
4) empty() = false
5) empty() = true
Совместимость с C: метод c_str()
Многие функции C ждут указатель const char*, а не объект std::string. Для этого есть метод c_str(), отдающий указатель на внутренний буфер с гарантированным '\0' в конце.
#include <cstring>
#include <iostream>
#include <string>
void print_c_string(const char* text) { // старая C-функция, ждет char*
std::cout << "C-функция получила: " << text << " (strlen=" << std::strlen(text) << ")\n";
}
int main() {
std::string s = "Otus";
print_c_string(s.c_str()); // c_str() отдает буфер, заканчивающийся '\0'
}
Вывод:
C-функция получила: Otus (strlen=4)
Граница: указатель от c_str() действителен только до следующего изменения строки (resize, append и подобные методы могут переместить буфер). Сохранять его «про запас» и использовать после изменения строки — ошибка.
Выводы
- C-строка (
char[], заканчивается'\0') и std::string (буфер + явная длина) — разные сущности, особенно при нулевых байтах внутри данных. size()/length()возвращают сохраненную длину мгновенно,strlen()у C-строки каждый раз идет по байтам до'\0'.- Длина строки — беззнаковый тип; вычитание из
size()при короткой строке может дать переполнение вместо отрицательного числа. cin >> sчитает слово до пробела,getline()— строку целиком;c_str()дает совместимость с C, но указатель живет только до следующего изменения строки.
Где применяется / связь с практикой
Освойте тему на практике
Работа со строками — база почти любой программы на C++: разбор ввода, форматирование вывода, обработка файлов и логов. На курсе C++ Developer эти темы разбирают на практике — от конструкторов std::string до ошибок с памятью и границами из примеров выше.
Освойте тему на практике
Если пока не готовы к полному курсу, можно начать с открытых уроков Otus — там разбирают практические задачи по C++.
FAQ
Можно ли сравнивать std::string и C-строку (char*) напрямую?
Да. У std::string операторы ==, < и другие перегружены и для const char*: C-строка автоматически участвует в сравнении по содержимому, отдельно приводить типы не нужно.
Что быстрее — std::string или char* (C-строка)?
Однозначно не ответить: современные реализации std::string для коротких строк используют small string optimization и не обращаются к куче, поэтому разница в реальных программах часто мала. Точный выигрыш зависит от компилятора и сценария — решать профилированием, а не общим правилом.
Как убрать пробелы в начале и конце строки?
Готового метода «обрезать пробелы» у std::string в стандартной библиотеке C++ до C++23 нет. Обычно ищут первый и последний непробельный символ через find_first_not_of/find_last_not_of и вырезают часть методом substr().



