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

Что произойдет при копировании std::string?

1.0 Junior🔥 141 комментариев
#Язык C++

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

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

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

Что произойдет при копировании std::string?

Копирование std::string — это важный процесс, требующий понимания того, как работает управление памятью и оптимизации компилятора.

Общее описание

Когда копируешь std::string, создаётся глубокая копия (deep copy) содержимого. Это отличается от простого копирования указателя.

std::string str1 = "Hello";
std::string str2 = str1;  // Глубокая копия!

// Они независимы:
str2[0] = 'J';
std::cout << str1 << std::endl;  // Выведет "Hello", не "Jello"
std::cout << str2 << std::endl;  // Выведет "Jello"

Детали процесса копирования

Внутренняя структура std::string:

// Упрощённая версия реализации
struct string_internal {
    char* data;        // Указатель на буфер
    size_t length;     // Текущая длина
    size_t capacity;   // Выделённая память
};

Процесс копирования шаг за шагом:

// Шаг 1: Создание новой строки
std::string str1 = "Hello";      // Выделяет 5 байт (+ null terminator)

// Шаг 2: Копирование конструктор вызывается
std::string str2 = str1;         // Копирование начинается

// Что происходит внутри:
// 1. Выделяется новый буфер (malloc/new) размером >= 5
// 2. Копируются данные из str1.data в новый буфер (memcpy)
// 3. Обновляются length и capacity для str2
// 4. Оба string указывают на РАЗНЫЕ буферы

Как это выглядит в памяти

До копирования:
str1 {
    data:     -> [H][e][l][l][o][0]  (в heap)
    length:   5
    capacity: 5 (или больше)
}

После копирования:
str1 {
    data:     -> [H][e][l][l][o][0]  (в heap, адрес #1)
    length:   5
    capacity: 5 (или больше)
}

str2 {
    data:     -> [H][e][l][l][o][0]  (в heap, адрес #2, ДРУГОЙ адрес!)
    length:   5
    capacity: 5 (или больше)
}

Performance cost

Копирование дорогое!

// Время O(n) где n = длина строки
std::string str1("Very long string");  // 16 байт
std::string str2 = str1;               // 16 байт копируются

std::string str3(1000000, 'a');        // 1MB строка
std::string str4 = str3;               // 1MB копируется! МЕДЛЕННО!

Измерение:

#include <chrono>

std::string large_str(10000000, 'x');  // 10MB

auto start = std::chrono::high_resolution_clock::now();
std::string copy = large_str;  // Копирование
auto end = std::chrono::high_resolution_clock::now();

auto duration = std::chrono::duration_cast<std::chrono::milliseconds>
    (end - start).count();
    
std::cout << "Копирование заняло: " << duration << " мс\n";
// На современном CPU: ~10-50 мс для 10MB

Оптимизация 1: Small String Optimization (SSO)

Для коротких строк копирование НЕ идёт в heap!

std::string short_str = "Hi";    // Длина 2
std::string copy = short_str;    // Может НЕ выделять heap!

// Внутри:
struct string_with_sso {
    union {
        char* ptr;              // Для больших строк
        char buffer[24];        // Для маленьких (встроенный буфер)
    } data;
    size_t length;
    // флаг или битовая маска для is_small
};

// Для маленькой строки копирование = копирование 32 байта структуры
// Это очень быстро (просто stack operations)

SSO порог:

  • libstdc++ (GCC): 16 байт
  • libc++ (Clang): 24 байта
  • MSVC: различается по версии
std::string small = "12";           // 2 байта < SSO, no heap
std::string copy1 = small;          // Очень быстро (SSO copy)

std::string large = "0123456789abcdefghijklmnop";  // 24+ байта
std::string copy2 = large;          // Медленнее (heap copy)

Оптимизация 2: Move semantics (C++11)

Вместо копирования можешь использовать move:

std::string str1 = "Hello";

// Копирование (дорого)
std::string str2 = str1;  // Copy constructor

// Move (дёшево, просто обмен указателей)
std::string str3 = std::move(str1);  // Move constructor

// После move:
// str3.data указывает на буфер "Hello"
// str1 пусто или в некотором valid state
// Никакого копирования данных!

Как move работает внутри:

std::string&& move(std::string& x) noexcept {
    return static_cast<std::string&&>(x);
}

// Move constructor
string(string&& other) noexcept
    : data(other.data), length(other.length), capacity(other.capacity) {
    // Просто скопировали указатели! (O(1))
    
    // Очищаем other
    other.data = nullptr;
    other.length = 0;
    other.capacity = 0;
}

Результат:

std::string create_string() {
    return std::string("Hello");  // NRVO или move
}

std::string result = create_string();
// Копирование НЕ происходит! Компилятор оптимизирует это

Проблема: Данные остаются после удаления?

Нет, данные удаляются автоматически:

{
    std::string str = "Hello";
    // str выходит из scope
    // Деструктор вызывается автоматически
    // Буфер освобождается (delete)
}
// После блока память освобождена

// Это RAII (Resource Acquisition Is Initialization)

Конкретный пример: Copy vs Move

void process_by_copy(std::string str) {
    // str копируется при передаче
    std::cout << str << std::endl;
}  // str удаляется при выходе

void process_by_reference(const std::string& str) {
    // str НЕ копируется, передаётся ссылка
    std::cout << str << std::endl;
}  // Без удаления

void process_by_move(std::string&& str) {
    // str перемещается, не копируется
    std::cout << str << std::endl;
}  // str удаляется, но уже пуст

// Использование
std::string large = std::string(10000000, 'x');  // 10MB

process_by_copy(large);          // МЕДЛЕННО: копирует 10MB
process_by_reference(large);     // БЫСТРО: просто ссылка
process_by_move(std::move(large)); // БЫСТРО: move 10MB

Таблица: Copy операции в разных сценариях

СценарийОперацияCostПричина
Короткая строкаCopyO(1)SSO встроенный буфер
Длинная строкаCopyO(n)Heap копирование
Любая строкаMoveO(1)Просто обмен указателей
Параметр функцииCopyO(n)Если pass-by-value
Параметр функцииRefO(1)Если const&
Return valueMoveO(1)NRVO или move

Лучшие практики

1. Передавай string_view вместо копирования:

// ПЛОХО: копирует
void print(std::string str) {
    std::cout << str << std::endl;
}

// ХОРОШО: без копирования
void print(std::string_view str) {
    std::cout << str << std::endl;
}

print("Hello");  // Без копирования!

2. Используй move для больших строк:

std::string get_large_string() {
    return std::string(10000000, 'x');  // Move, не copy
}

3. Избегай ненужного копирования:

// ПЛОХО
std::string process(std::string input) {
    std::string copy = input;  // Лишнее копирование
    // ...
    return copy;  // Move, но исходное скопировано зря
}

// ХОРОШО
std::string process(std::string input) {
    // Используй input напрямую
    // ...
    return input;  // Move
}

4. Reserve память если знаешь размер:

std::string str;
str.reserve(10000);  // Pre-allocate

for (int i = 0; i < 10000; ++i) {
    str += "x";  // Не перестраивается, используется reserved память
}

Профилирование копирования

# Используй perf/valgrind для выявления ненужного копирования
valgrind --tool=callgrind ./program
# Смотри на memcpy calls

Итог

При копировании std::string:

  1. Deep copy — создаётся новый независимый буфер
  2. Cost O(n) где n = длина строки
  3. SSO оптимизирует для коротких строк (O(1))
  4. Move оптимизирует для больших строк (O(1))
  5. Compiler оптимизирует NRVO и return value optimization

Правило: Используй std::string_view для чтения, std::move для передачи владения, и избегай ненужного копирования через параметры функций.

Что произойдет при копировании std::string? | PrepBro