← Назад к вопросам

Как устроены строки в C++?

2.0 Middle🔥 82 комментариев
#Язык C++

Комментарии (2)

🐱
claude-haiku-4.5PrepBro AI29 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Как устроены строки в C++

В C++ есть два основных способа работы со строками: C-style (char*) и STL (std::string). Разберу оба подробно, так как это критично для понимания управления памятью.

C-style строки (char*)

Это примитивный способ, унаследованный из C:

// Строковый литерал в памяти программы (read-only)
const char* str = "Hello";
// Память: [H][e][l][l][o][\0]  <- null-terminator

// Указатель на стек (опасно!)
char buffer[10];
strcpy(buffer, "Hello");  // Копирует в буфер
// Память стека: [H][e][l][l][o][\0][?][?][?][?]

// Динамическое выделение
char* str = malloc(6);
strcpy(str, "Hello");  // Копирует в heap
free(str);  // ОБЯЗАТЕЛЬНО освобождать!

Структура C-style строки

┌──────────────────────────────┐
│ H  │ e  │ l  │ l  │ o  │ \0 │
├──────────────────────────────┤
0    1    2    3    4    5

size = strlen(str) = 5 (без \0)
allocation = 6 (с \0)

Проблемы C-style строк

// 1. Buffer overflow
char buffer[5];
strcpy(buffer, "Hello World");  // CRASH! Пишет за границы

// 2. Утечки памяти
char* str = malloc(100);
// ... вызов функции, которая выбросит исключение
free(str);  // НИКОГДА не выполнится

// 3. Неопределённое поведение
char* str = nullptr;
strcpy(str, "test");  // Undefined behavior!

// 4. Дублирование кода (strlen, strcpy, strcmp)
if (strlen(str) > 10) { /* ... */ }

std::string — правильный способ

std::string — это класс из STL, который:

  • Автоматически управляет памятью
  • Отслеживает размер и capacity
  • Безопасен и удобен
std::string str = "Hello";  // Автоматическое выделение памяти

// Внутренняя структура std::string:
class string {
private:
  char* data;      // Указатель на буфер в heap
  size_t length;   // strlen()
  size_t capacity; // Выделенная память
};

// После std::string str = "Hello":
// data -----> [H][e][l][l][o][\0]
// length: 5
// capacity: 5 (или больше с резервом)

Как std::string выделяет память

std::string str;              // Пусто: length=0, capacity=0
std::cout << str.capacity();  // 0

str = "H";                    // length=1, capacity=1
str += "e";                   // Может быть: length=2, capacity=2 или больше
str += "llo";                 // Уже 5 символов

// Обычная стратегия роста (как vector):
// 0 -> 1 -> 2 -> 3 -> 5 -> 8 -> 13 -> 21 -> ...

// Или просто reserve
str.reserve(100);  // Выделяем 100 байт сразу
str = "Hello";     // length=5, capacity=100

Операции со std::string

std::string str = "Hello World";

// Информация
str.size();       // 11
str.length();     // 11 (то же самое)
str.capacity();   // >= 11
str.empty();      // false

// Доступ
str[0];           // 'H' (без проверки)
str.at(0);        // 'H' (с проверкой, throw if out of range)
str.front();      // 'H'
str.back();       // 'd'
str.c_str();      // "Hello World\0" — указатель на char*
str.data();       // То же самое (C++17)

// Изменение
str[0] = 'J';     // "Jello World"
str += " 2025";   // "Jello World 2025"
str.append(" !"); // "Jello World 2025 !"
str.insert(5, "!!!")?  // Вставить в позицию 5

// Поиск
str.find("World");     // 6 (индекс)
str.find("xyz");       // std::string::npos (не найдено)
str.rfind("o");        // Искать с конца

// Подстроки
str.substr(0, 5);      // "Jello"
str.substr(6, 5);      // "World"

// Замена
str.replace(6, 5, "Universe");  // Заменить "World" на "Universe"

// Очистка
str.clear();           // Длина 0, но memory может остаться
str.shrink_to_fit();   // Освободить лишнюю память

Copy-on-Write (историческое)

Старые версии С++ (до C++11) использовали Copy-on-Write:

std::string str1 = "Hello";
std::string str2 = str1;  // Не копирует, ссылается на одно место

// Только при изменении:
str2[0] = 'J';  // Теперь создаётся копия для str2

В современном C++ (C++11+) используется Move Semantics, что эффективнее.

Move Semantics в строках

std::string getGreeting() {
  return std::string("Hello");  // Возвращаемая строка не копируется (RVO)
}

std::string greeting = getGreeting();  // Никакой копии! Только движение

// С std::move (явное движение)
std::string str1 = "Hello";
std::string str2 = std::move(str1);  // str1 теперь пусто
// str1.data() может быть nullptr

String View (C++17)

Для избежания копирования без владения памятью:

// Старый способ — копирует строку
void printString(const std::string& str) {
  std::cout << str;
}

char buffer[100] = "Hello";
printString(std::string(buffer));  // Создаёт временный std::string

// Новый способ — не копирует
void printStringView(std::string_view str) {
  std::cout << str;
}

printStringView(buffer);  // Без копирования!
printStringView("Hello");  // Тоже без копирования

std::string_view view = "Hello";  // Просто ссылка, не копирует
// Внутри: data=указатель на "Hello", length=5

Производительность строк

// ❌ НЕЭФФЕКТИВНО: много копирований
std::string result;
for (int i = 0; i < 1000; ++i) {
  std::string temp = "item_" + std::to_string(i);
  result += temp;  // Копирование! O(n)
}

// ✅ ЭФФЕКТИВНО: reserve
std::string result;
result.reserve(100000);  // Выделяем память заранее
for (int i = 0; i < 1000; ++i) {
  result += "item_";
  result += std::to_string(i);
  result += ", ";
}

// ✅ ЭФФЕКТИВНО: string_view
void processString(std::string_view sv) {
  // Никаких копий, просто ссылка
}

const char* cstr = "Hello";
processString(cstr);  // Без копирования

Кодировки и многобайтовые символы

// UTF-8 (рекомендуется)
std::string utf8 = "Hello 👋 мир";
// В памяти: множество байт (UTF-8 переменной длины)
uft8.size();    // 16 (байт, не символов!)

// Если нужны широкие символы (Unicode)
std::wstring wide = L"Hello 👋 мир";
wide.size();    // Меньше, чем для UTF-8

// Для правильного подсчёта символов нужна библиотека ICU
#include <unicode/ustring.h>

Сравнение char* vs std::string

┌────────────────┬──────────────────┬─────────────────┐
│ Характеристика │ char*            │ std::string     │
├────────────────┼──────────────────┼─────────────────┤
│ Безопасность   │ Опасна (overflow)│ Безопасна       │
│ Управление МП  │ Ручное           │ Автоматическое  │
│ Быстрота       │ Быстрая (тонкая) │ Немного медленнее│
│ Удобство       │ Низкое           │ Высокое         │
│ Размер         │ 8 байт (указатель)│ 24-32 байта     │
│ Null-terminator│ Требуется        │ Автоматический  │
│ Поиск, замена  │ Ручные функции   │ Встроенные методы│
└────────────────┴──────────────────┴─────────────────┘

Best Practices

Используйте std::string:

  • Для большинства случаев — это стандарт
  • Для безопасности и удобства
  • В современном C++11+ коде

Используйте std::string_view (C++17+):

  • Для функций, которые не владеют строкой
  • Для избежания копирований
  • Вместо const std::string&

Используйте char только*:

  • В legacy коде
  • Для взаимодействия с C-библиотеками
  • Для строковых литералов (которые const char*)

Избегайте:

  • Ручного выделения памяти для строк
  • strcpy, strcat (используйте safer альтернативы)
  • Вычисления strlen в цикле (кэшируйте результат)

Строки — фундаментальный тип данных, поэтому нужно глубоко понимать их устройство для написания эффективного и безопасного кода.