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

Функция принимает const std::string&, а вызывают её с литералом "config.json" — и на каждый вызов компилятор молча создаёт временный std::string с аллокацией в куче. На горячем пути логирования это десятки тысяч ненужных malloc в секунду. В C++ три базовых способа держать строку — сырой char*, владеющий std::string и невладеющий std::string_view — и путаница между ними стоит либо копий, либо use-after-free. Разберём, чем платит каждый и когда что выбирать. Всё написано для C++17 и новее: string_view появился именно в C++17.
Три инструмента и один вопрос: кто владеет памятью
Разница между ними сводится к одному: кто отвечает за буфер с символами и когда он освобождается.
char*/const char*— просто указатель. Он ничем не владеет и ничего не знает о длине: конец строки определяется нулевым байтом'\0'. Всё управление памятью — на вас.std::string— владеющий контейнер. Он держит свой буфер, копирует данные при копировании, освобождает их в деструкторе. Знает длину за O(1), умеет расти.std::string_view— пара «указатель + длина» без владения. Смотрит на чужой буфер, ничего не копирует и не освобождает. Живёт ровно столько, сколько живут данные, на которые он смотрит.
Нулевой байт ('\0') — символ с кодом 0, которым в C-строках отмечается конец данных. У std::string и string_view длина хранится отдельно, поэтому внутри них могут быть нулевые байты — но string_view не гарантирует, что после его данных стоит '\0'.
char*: когда без него не обойтись
Сырой указатель нужен ровно на границах: системные вызовы, POSIX API, C-библиотеки, аргументы main. Функция open(), getenv(), драйвер СУБД на C — все они говорят на const char*. Внутри своего кода на C++ держать голый char* как способ хранения строк — почти всегда ошибка: ручное управление длиной и буфером даёт переполнения и утечки.
Ключевой момент: получить const char* из std::string легко через .c_str() — он гарантированно завершён нулём. А вот из string_view напрямую c_str() не получить, потому что представление не обязано заканчиваться нулевым байтом.
#include <cstdio>
#include <string>
void open_config(const std::string& path) {
// .c_str() отдаёт буфер, завершённый '\0', — то, что ждёт fopen
FILE* file = std::fopen(path.c_str(), "r");
if (file) {
std::fclose(file);
}
}
std::string: владение и цена копии
Берите std::string, когда объекту нужно хранить строку дольше вызова: поле класса, элемент контейнера, результат, который переживёт исходные данные. Он владеет буфером, поэтому висячих ссылок с ним не бывает по построению.
Плата — аллокации и копии. Каждое копирование std::string с длинной строкой — это malloc плюс memcpy. Здесь работает SSO — small string optimization: короткие строки (обычно до 15–22 байт, зависит от реализации стандартной библиотеки) хранятся прямо внутри объекта, без кучи. Поэтому для коротких ключей копия дешёвая, а для длинных путей и JSON — уже нет.
Правило большого пальца: если строку нужно пережить владельца —std::string. Если только прочитать в пределах вызова —string_view. Промежуточных «на всякий случай скопирую» быть не должно.
SSO (small string optimization) — приём, при котором короткая строка лежит в самом объекте std::string, а не в отдельном буфере кучи. Порог не стандартизирован и различается между libstdc++, libc++ и MSVC.
string_view: чтение без копий и главная ловушка
Меняйте параметр const std::string& на std::string_view в функциях, которые только читают строку и не хранят её. string_view одинаково дёшево принимает и std::string, и литерал, и подстроку — без единой аллокации. Именно так убирается тот самый временный std::string из первого абзаца.
#include <string_view>
#include <iostream>
// принимает и std::string, и литерал, и подстроку — без копий
bool has_prefix(std::string_view text, std::string_view prefix) {
return text.size() >= prefix.size()
&& text.substr(0, prefix.size()) == prefix;
}
int main() {
std::string url = "https://example.com";
std::cout << has_prefix(url, "https://") << '\n'; // 1
// substr на string_view не аллоцирует — просто сдвигает границы
}
Ловушка одна, но серьёзная: string_view не продлевает жизнь данным. Классический баг — вернуть view на временный объект:
#include <string>
#include <string_view>
std::string_view get_name() {
std::string local = "Anna";
return local; // ОШИБКА: view смотрит на буфер, который уничтожится
} // здесь local умирает — use-after-free
То же случается с string_view на результат функции, возвращающей std::string по значению: std::string_view sv = build_path(); — временный объект уничтожается в конце выражения, view повисает. Поэтому string_view хранят как поле класса только тогда, когда точно понятно, что буфер переживёт объект — иначе там должен быть std::string.
Сравнение по сути
| Критерий | const char* | std::string | std::string_view |
|---|---|---|---|
| Владеет памятью | нет | да | нет |
| Знает длину за O(1) | нет (нужен strlen) | да | да |
| Завершён нулём | да | да (через c_str) | не гарантировано |
| Аллокации при приёме литерала | нет | да (временный) | нет |
| Может пережить исходные данные | нет | да | нет |
| Риск use-after-free | высокий | нет | высокий при хранении |
Когда что выбирать
- Параметр функции, которую только читают и не хранят —
std::string_view. Годится для парсинга, сравнения, поиска префиксов. - Поле класса, элемент вектора, возвращаемое значение с владением —
std::string. Владелец, который живёт своей жизнью. - Граница с C-кодом и системными вызовами —
const char*, получаемый через.c_str()изstd::string. - Функция строит новую строку и отдаёт её наружу — возвращайте
std::stringпо значению, неstring_view. Compiler справится с move, лишней копии не будет.
Что ломается на практике
string_viewна временный. Компилятор с флагами-Wdangling(GCC 13+) и-Wdangling-gsl(Clang) ловит часть таких случаев, но не все. AddressSanitizer (-fsanitize=address) находит use-after-free в рантайме — включайте его в тестах.- Передача
string_viewв C-функцию. Забыли, что нет завершающего нуля —fopenчитает за границей данных. Если нуженconst char*, сначала соберитеstd::stringиз view. - Скрытая копия из-за
const std::string&. Параметр поconst&кажется бесплатным, но вызов с литералом создаёт временныйstd::string. На горячем пути замените наstring_view. - Инвалидация после
push_back.string_viewнаstd::stringстановится висячим, если строка выросла и переаллоцировала буфер. То же, что с итераторами вектора.
Что настроить дальше
Пройдитесь по сигнатурам функций проекта: параметры const std::string&, которые только читаются, — кандидаты на std::string_view. Включите -fsanitize=address в CI и добавьте -Wdangling в набор предупреждений, чтобы висячие view вылезали до прода. Если работаете с C++20, посмотрите в сторону std::span для бинарных данных — это тот же принцип невладеющего представления, но для произвольных байтов, а не только строк. Точный набор гарантий по string_view сверяйте с cppreference для вашего стандарта.
Задачи: математика, физика, химия: Разбор задачи по шагам с проверкой и похожей задачей
Спросить за 10 ₽Источники: std::string_view — cppreference.com, std::string — cppreference.com
Частые вопросы
У меня старый компилятор без C++17. Есть ли string_view?
Если я передаю string_view, а внутри мне нужен const char* для C-функции — что делать?
Правда ли, что std::string всегда аллоцирует память в куче?
А если я вернул string_view из функции и не упало — значит всё нормально?
Стоит ли принимать параметр по string_view вместо const std::string& везде?
Чем string_view отличается от span в C++20?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.