Строки в C++: частые ошибки новичка и как их избежать
std::string, const char*, string_view — три способа работать с текстом в C++, и легко перепутать, кто владеет памятью. Разбираем ловушки, из-за которых программа падает или тихо портит данные.

Вы написали функцию, которая возвращает std::string_view на локальную строку, программа скомпилировалась без единого предупреждения, а при запуске выдала мусор или упала. Это классическая ловушка строк в C++: язык даёт три разных инструмента для работы с текстом, и все три легко перепутать. Ниже — что реально стоит держать в голове, если вы только перешли на C++ с языка, где строка была одним понятным типом.
Материал для C++17 и новее — именно там появился std::string_view, который меняет привычки. Если вы компилируете под C++11/14, часть примеров с string_view не соберётся, об этом ниже.
Три типа строк, которые вы встретите
В коде на C++ текст живёт в трёх основных видах, и путаница между ними — источник большинства багов у новичков.
const char*— сишная строка: указатель на массив символов, завершённый нулевым байтом'\0'. Не знает своей длины, не владеет памятью, приходит из литералов ("hello") и старого C-API.std::string— полноценный объект: владеет своей памятью, знает длину, умеет расти, сам освобождает буфер в деструкторе. Это ваш выбор по умолчанию.std::string_view(C++17) — окно в чужую строку: указатель плюс длина, но без владения. Не копирует данные и не продлевает им жизнь. Дешёвый для передачи, опасный для хранения.
Правило, которое стоит запомнить с первого дня:std::stringвладеет памятью,string_viewиconst char*— нет. Всё, что не владеет, может протухнуть в любой момент.
Почему это важно на практике
Тип, который не владеет памятью, живёт ровно столько, сколько живёт объект, на который он смотрит. Если исходная строка уничтожена или изменена так, что переехал буфер, ваш string_view или указатель показывает в никуда. Компилятор об этом не предупредит — код формально корректен.
Ошибка первая: возврат view на временный объект
Самая частая беда с string_view. Функция возвращает окно на строку, которая перестанет существовать сразу после возврата.
#include <string>
#include <string_view>
// ОПАСНО: view смотрит на локальную строку,
// которая уничтожится при выходе из функции
std::string_view make_greeting_bad(const std::string& name) {
std::string result = "Привет, " + name;
return result; // result умирает здесь, view повисает
}
// Правильно: возвращаем владеющий объект
std::string make_greeting_ok(const std::string& name) {
return "Привет, " + name; // string владеет своими данными
}Второй вариант вернёт настоящую строку с собственной памятью. Первый скомпилируется, но при использовании результата вы получите неопределённое поведение — от мусора до падения.
Ошибка вторая: временная строка под const char*
Когда вы берёте c_str() у временного std::string, указатель живёт до конца полного выражения, а дальше — нет.
#include <string>
#include <cstdio>
std::string build_path() {
return "/tmp/data.txt";
}
int main() {
// ОПАСНО: временная строка уничтожается сразу,
// ptr смотрит на освобождённую память
const char* ptr = build_path().c_str();
std::printf("%s\n", ptr); // мусор или падение
// Правильно: сначала сохраняем строку в переменную
std::string path = build_path();
std::printf("%s\n", path.c_str()); // path живёт до конца main
}Разница в одной строке, а поведение отличается кардинально. В первом случае к моменту printf буфер уже освобождён.
Ошибка третья: сравнение через ==, которое сравнивает не то
Здесь std::string ведёт себя интуитивно, а const char* — нет.
#include <string>
#include <cassert>
int main() {
std::string a = "test";
std::string b = "test";
assert(a == b); // сравнивает содержимое, всё честно
const char* x = "test";
const char* y = "test";
// x == y сравнивает АДРЕСА указателей, а не текст.
// Может оказаться true из-за объединения литералов,
// но полагаться на это нельзя.
// Для сравнения содержимого нужен strcmp:
// std::strcmp(x, y) == 0
}Если хотя бы один операнд — std::string, оператор == сравнивает содержимое. Если оба — const char*, вы сравниваете адреса. Это тихая ошибка: тесты иногда проходят из-за оптимизации литералов, а на реальных данных ломаются.
Ошибка четвёртая: string_view не завершён нулём
std::string_view хранит указатель и длину, но не гарантирует нулевой байт в конце. Передавать .data() в функцию, ожидающую C-строку, нельзя.
#include <string_view>
#include <cstdio>
void print_c_api(std::string_view sv) {
// ОПАСНО: sv.data() может не заканчиваться '\0',
// printf прочитает лишнее за границей
// std::printf("%s\n", sv.data());
// Безопасно: копируем в string, у неё нуль гарантирован
std::string tmp(sv);
std::printf("%s\n", tmp.c_str());
}Особенно коварно, когда string_view получен как подстрока (substr) большой строки: за его концом идут другие символы, а не ноль.
Когда какой тип брать
Простое правило для параметров и хранения:
| Ситуация | Что использовать | Почему |
|---|---|---|
| Хранить строку как поле класса, элемент контейнера | std::string | Нужно владение — данные должны пережить вызов |
| Параметр функции только для чтения, вы не храните его | std::string_view | Не копирует, принимает и string, и литерал |
| Параметр, который вы скопируете внутрь себя | std::string по значению | Вызывающий сам решает: move или копия |
| Совместимость со старым C-API | const char* через .c_str() | Только так примут функции из C |
Модификация во время итерации
Ещё одна ловушка: методы вроде push_back или += могут перевыделить буфер строки. Все итераторы, указатели и string_view, взятые до этого, становятся недействительными.
#include <string>
int main() {
std::string s = "abc";
const char* p = s.data();
s += "defghijklmnop"; // возможно перевыделение буфера
// p теперь может указывать на старую освобождённую память
// брать s.data() нужно ПОСЛЕ последней модификации
}Что делать дальше
Сначала научитесь по умолчанию писать std::string и осознанно переходить на string_view только для параметров, которые вы точно не храните. Дальше — включите санитайзеры при отладке: -fsanitize=address в GCC и Clang ловит именно те висячие указатели и обращения к освобождённой памяти, о которых шла речь выше. Прогоните под ним свои тесты — большинство описанных ошибок всплывёт сразу.
Работа с юникодом и многобайтовыми кодировками — отдельная большая тема: std::string хранит байты, а не символы, и .size() для строки с кириллицей в UTF-8 вернёт число байт, а не букв. Здесь мы это не разбирали.
Транскрибатор встреч: Запись созвона или диктофона — в текст с разделением по спикерам, протоколом и списком задач.
Спросить за 25 ₽Источники: std::string_view — cppreference.com, std::string — cppreference.com
Частые вопросы
А если у меня C++11 или C++14, там нет string_view?
Почему мой код с висячим указателем работает, а на другой машине падает?
Стоит ли всегда передавать строки по const std::string&, чтобы не копировать?
Чем .data() отличается от .c_str() у std::string?
Как правильно собрать строку из кусков в цикле, чтобы не тормозило?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.