C++ geek
3.61K subscribers
277 photos
5 videos
28 links
Учим C/C++ на примерах
Download Telegram
🏗 Что на самом деле происходит, когда std::vector «лопается»?

Мы все любим push_back. Это удобно: кидаешь данные в вектор, а он сам разбирается с памятью. Но что происходит, когда вы добавляете элемент, а capacity (вместимость) вектора закончилась?

Происходит Реаллокация (Reallocation). И это гораздо дороже, чем кажется.

⚙️ Алгоритм катастрофы:

1. Поиск новой земли: Вектор понимает, что места нет. Он обращается к оперативной памяти и просит выделить новый блок памяти. Обычно он в 1.5 или 2 раза больше текущего.

2. Великое переселение: Все элементы из старого блока памяти копируются (или перемещаются, если есть noexcept move-конструктор) в новый блок.
⚫️Если у вас там 1,000,000 тяжелых объектов - удачи процессору. 😅


3. Уничтожение: Для всех объектов в старом блоке вызываются деструкторы.

4. Снос: Старый блок памяти возвращается системе.

🚨 Почему это проблема?

1. Удар по производительности:
Обычно push_back работает за O(1) (мгновенно). Но в момент реаллокации сложность подскакивает до O(N). Это создает непредсказуемые лаги (latency spikes). В системах реального времени (gamedev, high-load) это недопустимо.

2. Инвалидация итераторов и ссылок (ОПАСНО):
Это источник багов №1.

std::vector<int> vec = {1, 2, 3};
int& ref = vec[0]; // Ссылка на первый элемент

// ... добавляем много элементов ...
for(int i=0; i < 100; ++i) vec.push_back(i);

// 💥 Вектор переехал в новую память.
// Старая память удалена. ref теперь указывает в мусор.
std::cout << ref; // Undefined Behavior (Crash)




🛡 Как лечить?

Если вы хотя бы примерно знаете, сколько элементов будет в векторе, всегда используйте reserve().


std::vector<User> users;
users.reserve(1000); // Сразу выделяем память

// Теперь первые 1000 push_back будут дешевыми
// и не вызовут реаллокации.



💡 Итог: std::vector это отличный инструмент, но за его «магию» расширения платит процессор. Помогайте ему через reserve().

#cpp #stdvector #performance #memory #coding #tips

➡️ @cpp_geek
Please open Telegram to view this post
VIEW IN TELEGRAM
👍84👀4💯1
🏗 Анатомия std::vector::push_back: Когда память заканчивается

Мы все любим push_back. Это удобно: просто кидаешь данные в вектор, а он сам разбирается с памятью. Но что происходит, когда вы добавляете элемент, а место (capacity) закончилось?

Происходит Реаллокация (Reallocation). И это дорогая операция.

⚙️ Что происходит «под капотом»?

1. Поиск новой земли: Вектор понимает, что текущий буфер полон. Он просит у операционной системы выделить новый блок памяти. Обычно он в 1.5 или 2 раза больше предыдущего (геометрический рост).

2. Великое переселение: Все элементы из старого блока копируются (или перемещаются, если есть noexcept move-конструктор) в новый блок.
⚫️Представьте, что вы перевозите 10,000 коробок в новый дом только ради того, чтобы поставить еще одну.

3. Зачистка: Для всех объектов в старом блоке вызываются деструкторы.

4. Снос: Старая память возвращается системе.

🚨 Почему это проблема?

1. Удар по производительности:
Обычно push_back работает за амортизированное O(1) (мгновенно). Но в момент реаллокации сложность подскакивает до O(N). Это вызывает непредсказуемые лаги (latency spikes).

2. Инвалидация итераторов и ссылок (ОПАСНО):
Это источник багов №1. После реаллокации старая память удалена. Все указатели, ссылки и итераторы, которые смотрели на элементы вектора, становятся недействительными.


std::vector<int> vec = {1, 2, 3};
int& ref = vec[0]; // Ссылка на первый элемент

// ... добавляем много элементов, вызывая реаллокацию ...
for(int i=0; i < 100; ++i) vec.push_back(i);

// 💥 Вектор переехал. Старая память удалена.
// ref теперь указывает в мусор.
std::cout << ref; // Undefined Behavior (Crash или мусор)





🛡 Как лечить?

Если вы хотя бы примерно знаете, сколько элементов будет в векторе, всегда используйте reserve().


std::vector<User> users;
users.reserve(1000); // Сразу выделяем память под 1000 мест

// Теперь первые 1000 push_back будут дешевыми
// и гарантированно не вызовут реаллокации.



💡 Итог: std::vector это мощный инструмент, но за его автоматическое расширение платит процессор. Помогайте ему через reserve(), чтобы код был быстрым и безопасным.

#cpp #stdvector #performance #memory #coding #tips

➡️ @cpp_geek
Please open Telegram to view this post
VIEW IN TELEGRAM
👍72
🏗 Анатомия std::vector: Что происходит, когда место заканчивается?

std::vector - самый популярный контейнер в C++. Мы просто пишем push_back, и магия работает. Но что происходит «под капотом», когда вы пытаетесь добавить элемент, а свободное место (capacity) закончилось?

Происходит Реаллокация. И это гораздо дороже, чем просто добавление числа.

⚙️ Сценарий катастрофы (пошагово):

Допустим, у вектора было место под 4 элемента, и оно занято. Вы добавляете 5-й.

1. Поиск новой земли: Вектор понимает, что текущий буфер полон. Он просит у операционной системы выделить новый блок памяти (обычно в 1.5 или 2 раза больше старого).

2. Великое переселение: Все элементы из старого блока копируются (или перемещаются) в новый.
- Представьте: чтобы поставить на полку одну новую книгу, вам приходится переезжать в новую квартиру и перетаскивать туда всю библиотеку.

3. Зачистка: Старые объекты разрушаются (вызываются деструкторы), а старая память возвращается системе.

4. Вставка: И только теперь новый элемент добавляется в хвост.

🚨 Почему это проблема?

1. Удар по производительности
Операция push_back обычно мгновенна (). Но при реаллокации она превращается в тяжелую операцию . Если вектор огромный, программа может «подвиснуть» в самый неподходящий момент.

2. Инвалидация ссылок (Источник багов №1)
Это самое опасное. Как только произошла реаллокация, старая память удаляется. Все указатели, ссылки и итераторы, которые смотрели на элементы вектора, становятся невалидными.


std::vector<int> data = {1, 2, 3, 4};
int& ref = data[0]; // Ссылка на первый элемент

// Добавляем элемент -> места нет -> реаллокация!
data.push_back(5);

// ☠️ ОШИБКА: ref ссылается на очищенную память.
// Получим мусор или краш программы.
std::cout << ref;



🛡 Как лечить?

Если вы знаете (хотя бы примерно), сколько элементов будет в векторе - используйте reserve().


std::vector<int> data;
data.reserve(1000); // Сразу выделяем память

// Теперь реаллокации точно не будет,
// пока мы не превысим 1000 элементов.



💡 Итог: Помогайте вектору с помощью reserve(). Это спасает и от тормозов, и от сложнейших багов с памятью.

#cpp #stdvector #memory #performance #coding #tips

➡️ @cpp_geek
👍122
🏗 Тетрис в памяти: Почему порядок полей в классе важен?

Вы создали простую структуру: bool, int и еще один bool.
Математика проста: 1 байт + 4 байта + 1 байт = 6 байт.

Вы проверяете через sizeof и видите... 12 байт. 🤯
Куда делись еще 6 байт? Вы только что потеряли 50% памяти на "воздух".

Это называется Padding (Выравнивание).

⚙️ Как это работает?
Процессор не любит читать данные по произвольным адресам. Ему удобно читать кусками по 4 или 8 байт (слова). Чтобы int (4 байта) не "разломился" посередине двух слов, компилятор вставляет пустые байты-заглушки.

Плохой пример (Bad Layout):


struct Bad {
bool a; // 1 байт
// ... 3 байта PADDING (воздух) ...
int b; // 4 байта (должен начинаться с кратного 4 адреса)
bool c; // 1 байт
// ... 3 байта PADDING (чтобы выровнять общий размер) ...
};
// Итог: 12 байт



Хороший пример (Good Layout):

Просто меняем порядок полей. Правило: "От больших к маленьким".


struct Good {
int b; // 4 байта
bool a; // 1 байт
bool c; // 1 байт
// ... 2 байта PADDING (добиваем до кратности 4) ...
};
// Итог: 8 байт



📉 Почему это важно?
Кажется, что 4 байта ерунда. Но если у вас std::vector<Bad> на 1,000,000 элементов:

⚫️Bad: ~12 MB памяти.
⚫️Good: ~8 MB памяти.

Вы экономите 4 мегабайта просто переставив строчки местами! Плюс, более плотные данные лучше ложатся в кэш процессора (CPU Cache), что ускоряет обработку.

💡 Совет:
Объявляйте поля в порядке убывания их размера:

1. Указатели и double (8 байт)
2. int, float (4 байта)
3. short (2 байта)
4. bool, char (1 байт)

#cpp #optimization #memory #alignment #coding #tips

➡️ @cpp_geek
Please open Telegram to view this post
VIEW IN TELEGRAM
👍144💯2
🪄 Магия std::string: Почему короткие строки работают быстрее? (SSO)

Многие думают, что std::string - это всегда:

1. Выделение памяти в куче (new / malloc).

2. Копирование данных туда.

3. Освобождение памяти (delete) в деструкторе.

Это медленно. Но если вы создадите строку "Hello", никаких аллокаций не произойдет. Почему?

Благодаря Small String Optimization (SSO).

⚙️ Как это работает?

Стандартная строка (на 64-битной системе) обычно занимает 24 или 32 байта (размер самой структуры sizeof(std::string)). В ней хранятся указатель на данные, размер и вместимость (capacity).

Разработчики STL подумали:
"Зачем нам тратить эти байты на указатели, если строка очень короткая? Давайте хранить текст прямо внутри объекта!"

Внутри std::string используется union:

⚫️Вариант А (Длинная строка): Хранит указатель на кучу (Heap), размер и вместимость.
⚫️Вариант Б (Короткая строка): Использует те же байты памяти как буфер для хранения символов.

📏 Где граница?

Это зависит от компилятора:

⚫️MSVC (Windows): ~15 символов.
⚫️GCC (Linux): ~15 символов.
⚫️Clang (libc++): ~22 символа (благодаря хитрому сжатию битов).

Пример:


void Benchmark() {
// 🚀 БЫСТРО (SSO):
// Память не выделяется. Строка лежит на стеке, как char[16].
std::string shortStr = "Hello World";

// 🐢 МЕДЛЕННО (Heap Allocation):
// Текст не влезает в буфер SSO.
// Вызывается malloc/new, данные летят в кучу.
std::string longStr = "Hello World is a remarkably long phrase";
}



📉 Почему это важно для производительности?

1. Нет аллокаций: new и delete - это системные вызовы, они дорогие. SSO их исключает.

2. Cache Locality: Данные лежат на стеке, рядом с другими локальными переменными. Процессор обожает линейный доступ к памяти (L1 Cache), а прыжки в кучу (Heap) - ненавидит.

💡 Совет: Если вы оптимизируете структуру данных и у вас много коротких ID или имен (до 15 символов), обычный std::string будет работать великолепно без всяких хитростей. Не нужно менять его на char[] "для скорости" без замеров.

#cpp #optimization #sso #memory #stdstring #coding #tips

➡️ @cpp_geek
Please open Telegram to view this post
VIEW IN TELEGRAM
👍9🔥63
😱 std::vector<bool>: Великий обман C++

Вы думаете, что std::vector<bool> это просто вектор, который хранит булевы значения? Нет. Это совершенно уникальный монстр, который нарушает правила стандартной библиотеки.

📉 В чем подвох? Обычный bool занимает 1 байт (минимум адресуемой памяти). Но создатели C++ решили сэкономить память. std::vector<bool> - это специализация. Внутри него каждый bool занимает всего 1 бит.

В одном байте хранится сразу 8 значений true/false. Экономия памяти в 8 раз! Круто?

🛑 Проблема: Вы не можете взять адрес элемента

В C++ нельзя создать указатель или ссылку на отдельный бит. Память адресуется байтами.


std::vector<int> nums = {1, 2};
int* p = &nums[0]; // ОК. Указатель на первый int.

std::vector<bool> flags = {true, false};
bool* b = &flags[0]; // ОШИБКА КОМПИЛЯЦИИ!
// Мы не можем получить адрес бита.



🤖 Проблема: Прокси-объекты

Когда вы пишете flags[0], вектор возвращает не bool& (ссылку), а специальный временный объект - Proxy Class (std::vector<bool>::reference).

Этот объект "притворяется" ссылкой. Когда вы присваиваете ему значение, он делает побитовые сдвиги и маски (&, |, <<), чтобы изменить нужный бит внутри байта.

Это медленно.

⚠️ Ловушка с auto


std::vector<bool> vec = {true, false};

// Вы думаете, что val — это bool.
// На самом деле val — это 'std::vector<bool>::reference'.
auto val = vec[0];

vec.push_back(true); // Реаллокация памяти!

// 💥 Если val — это прокси, он может ссылаться на
// старую, уже удаленную память вектора.
val = false; // Undefined Behavior / Crash



💡 Что делать?

1. Если вам важна память: Используйте std::vector<bool> (или std::bitset для фиксированного размера).

2. Если вам важна скорость: Используйте std::vector<char> или std::vector<uint8_t>. Это займет в 8 раз больше памяти, но будет работать мгновенно, и вы получите нормальные ссылки.

3. Осторожно с auto: Всегда пишите тип явно: bool val = vec[0];, чтобы заставить прокси превратиться в значение.

#cpp #stl #vector #gotchas #memory #coding #tips

➡️ @cpp_geek
👍84
🌉 Забудьте про передачу указателей и размеров! (std::span)

Помните, мы обсуждали std::string_view - легковесное «окно» для строк? В C++20 у него появился старший брат для массивов и векторов - std::span.

До C++20 у нас была классическая проблема. Допустим, вы пишете функцию, которая должна обработать список чисел.

🐢 Как мы писали раньше:

Вариант 1: Принимать const std::vector<int>&.
Минус: Функция теперь намертво привязана к std::vector. Если у вас данные лежат в std::array или обычном си-массиве int arr[10], придется копировать их в вектор. Аллокации, тормоза.

Вариант 2: Си-стайл (Указатель + размер).
Минус: Легко ошибиться с размером, потерять контекст, код выглядит грязно.


void ProcessOld(const int* data, size_t size) { /* ... */ }



🚀 Как мы пишем теперь (C++20):


#include <span>

// Принимаем любой непрерывный кусок памяти!
void ProcessNew(std::span<const int> data) {
for (int val : data) {
std::cout << val << " ";
}
}



👀 Что такое std::span?
Как и string_view, это просто указатель на начало данных и их длина (обычно 16 байт). Он не владеет памятью, он только на нее смотрит.

Магия в том, что std::span умеет автоматически создаваться из чего угодно:


std::vector<int> vec = {1, 2, 3};
std::array<int, 3> arr = {4, 5, 6};
int raw[3] = {7, 8, 9};

// Одна функция работает со всеми типами контейнеров! Без копирования!
ProcessNew(vec);
ProcessNew(arr);
ProcessNew(raw);



✂️ Суперсила: Subspan (Подмассивы)
Вам нужно передать в функцию только часть вектора, например, со 2-го по 5-й элемент? Никаких итераторов и копирования:


// Передаем кусок вектора за O(1)
ProcessNew( std::span{vec}.subspan(1, 4) );



⚠️ Важный нюанс:
std::span не умеет изменять размер данных (никаких push_back). Но он может изменять сами элементы, если вы передадите std::span<int> (без const).

💡 Итог: Если ваша функция принимает набор данных только для чтения или изменения элементов на месте, всегда используйте std::span. Это золотой стандарт современного C++.

#cpp #cpp20 #stdspan #optimization #memory #coding #tips

➡️ @cpp_geek
👍10🔥32
🧬 Двойная цена std::shared_ptr: Почему профи всегда пишут make_shared?

Мы все используем умные указатели. Но то, как вы их создаете, кардинально меняет работу с памятью под капотом.

Встречали такой код?

// 🐢 ПЛОХО: Классический подход
std::shared_ptr<User> user(new User());


Кажется, всё логично: выделили память через new, передали в shared_ptr. Но на деле вы заставляете программу сделать две аллокации (выделения памяти) вместо одной.

⚙️ Анатомия shared_ptr

std::shared_ptr состоит из двух частей:

1. Сам объект (ваши данные User).

2. Контрольный блок (Control Block) - служебная структура, где лежат счетчики ссылок (reference count) и счетчики weak_ptr.

Когда вы пишете std::shared_ptr<User>(new User()), происходит следующее:

1. Отрабатывает new User() - программа идет к ОС и просит кусок памяти.

2. Конструктор shared_ptr видит сырой указатель, понимает, что ему нужен Контрольный блок, и еще раз идет к ОС за вторым куском памяти.

Два системных вызова. Фрагментация кучи (heap). Промахи кэша процессора, потому что объект и счетчик лежат в разных концах памяти.



🚀 Решение: std::make_shared


// 🚀 ХОРОШО: Единый блок памяти
auto user = std::make_shared<User>();


Что делает make_shared? Он считает размер вашего объекта User + размер Контрольного блока, и просит у операционной системы один большой кусок памяти за один раз.

Плюсы:
В 2 раза меньше аллокаций. Код работает быстрее.

Cache Locality. Объект и счетчик ссылок лежат в памяти впритык друг к другу. Процессор это обожает.

Безопасность. До C++17 старый подход с new мог привести к утечке памяти, если функция принимала несколько аргументов и один из них бросал исключение. С make_shared это исключено.

🦇 Темная сторона (О чем не пишут в туториалах)

Есть ровно один случай, когда make_shared может навредить. Это связано со слабыми указателями (std::weak_ptr).

Если вы удалили все shared_ptr, вызывается деструктор объекта User. Но если остался хотя бы один weak_ptr, Контрольный блок обязан жить!
А так как make_shared склеил Контрольный блок и объект в один кусок памяти, оперативная память из-под объекта User не вернется системе, пока жив weak_ptr (даже если сам объект уже "мертв" и деструктор отработал).
Если ваш объект весит 500 Мегабайт - вы получите «фантомную» утечку памяти.

💡В 99% случаев используйте std::make_shared. Используйте new std::shared_ptr только если у вас гигантские объекты, на которые подолгу смотрят «зависшие» weak_ptr, или если вам нужен кастомный удалитель (custom deleter).

#cpp #memory #pointers #optimization #sharedptr #coding #tips

➡️ @cpp_geek
👍51
♻️ Идеальное преступление: Как создать утечку памяти с помощью умных указателей?

С появлением std::shared_ptr в C++ многие выдохнули: "Наконец-то счетчик ссылок всё сделает за нас, больше никаких утечек!". Но умные указатели не обладают интеллектом. И их очень легко обмануть.

Самая частая причина «фантомных» утечек памяти в современном C++ это Циклическая зависимость (Circular Dependency).

🪤 Ловушка: Змея, кусающая себя за хвост

Представьте игру. У нас есть Игрок (Player) и Гильдия (Guild).
• Игрок должен знать, в какой Гильдии он состоит.
• Гильдия должна знать, кто её лидер (Игрок).

Вы пишете такой код:


struct Player; // Предварительное объявление

struct Guild {
std::shared_ptr<Player> leader;
~Guild() { std::cout << "Guild deleted\n"; }
};

struct Player {
std::shared_ptr<Guild> myGuild;
~Player() { std::cout << "Player deleted\n"; }
};

void Play() {
auto p = std::make_shared<Player>(); // ref_count(Player) = 1
auto g = std::make_shared<Guild>(); // ref_count(Guild) = 1

p->myGuild = g; // ref_count(Guild) = 2
g->leader = p; // ref_count(Player) = 2
}
// Конец функции. Локальные p и g уничтожаются.
// ref_count(Player) падает до 1.
// ref_count(Guild) падает до 1.


Итог: Функция завершилась. Объекты больше никому в программе не нужны. Но их деструкторы никогда не вызовутся. Они держат друг друга в заложниках, потому что счетчик не упал до нуля. Вы потеряли память.

⚔️ Спаситель: std::weak_ptr

std::weak_ptr - это умный указатель-наблюдатель. Он умеет смотреть на объект, которым владеет shared_ptr, но не увеличивает его счетчик ссылок.

Чтобы разорвать цикл, мы должны определить, кто кем владеет (кто важнее), а кто просто ссылается. Допустим, Гильдия существует независимо от лидера, поэтому она будет просто "наблюдать" за ним.

Правильный код:


struct Guild {
// Слабая ссылка! Не влияет на время жизни Player.
std::weak_ptr<Player> leader;
};


Теперь при выходе из функции счетчик Player спокойно упадет до нуля. Player удалится. Его деструктор удалит shared_ptr на Гильдию. Счетчик Гильдии упадет до нуля, и она тоже удалится. Чистая победа!

👀 Как пользоваться weak_ptr?

Так как weak_ptr не гарантирует, что объект еще жив (ведь он его не держит), из него нельзя просто так прочитать данные. Вы обязаны превратить его в shared_ptr с помощью метода .lock().


// Если Игрок еще жив, lock() вернет валидный shared_ptr.
// Если Игрок удален, lock() вернет nullptr.
if (std::shared_ptr<Player> ptr = g->leader.lock()) {
std::cout << "Лидер на месте: " << ptr->name;
} else {
std::cout << "Лидер покинул нас...";
}


💡 Золотое архитектурное правило:
Стройте связи в виде дерева.
Сверху вниз - владение (shared_ptr или unique_ptr).
Снизу вверх - наблюдение (weak_ptr или обычный *, если время жизни жестко гарантировано). Никогда не делайте цикл из shared_ptr.

#cpp #memory #smartpointers #leaks #oop #coding #tips

➡️ @cpp_geek
👍94