Учебные материалы: механизм вместо тезисов, блоки «Факты для карточек», Ловушки (Claude Code) + diag/cards_src.tsv

This commit is contained in:
Kodlo-chan
2026-09-24 13:38:58 +07:00
parent e0ad8e0fee
commit 4259fbce75
17 changed files with 1688 additions and 115 deletions
+33 -10
View File
@@ -7,18 +7,26 @@
- **Цель:** научиться писать структуру с фиксированной памятью и без сдвигов элементов.
- **Почему это в Eltex:** приём/передача пакетов, буферы DMA, очереди между потоками —
всё это кольцевые буферы. Понимание wrap-around и «полный/пустой» — прямой вопрос на
собеседовании.
собеседовании. Механизм тот же, что у сетевой карты: пакеты пишутся в кольцо по мере
прихода, драйвер вычитывает их с другого конца, и обе стороны никогда не двигают уже
записанные данные по памяти — двигаются только индексы `head`/`tail`.
- **Что сдаётся:** `solution.cpp`, проходящий `python3 grade.py 03`.
## Глава II. Что нужно знать до старта
Идея: массив фиксированного размера + два индекса. `head` — куда писать, `tail` — откуда
читать. Когда индекс доходит до конца, он возвращается в начало: `idx = (idx + 1) % capacity`.
Сдвигать элементы не нужно никогда — в этом весь смысл.
Сдвигать элементы не нужно никогда — в этом весь смысл: `push`/`pop` двигают только индекс,
а не байты в памяти, поэтому обе операции остаются O(1) независимо от размера буфера.
Ловушка, из-за которой задача попадает в собеседования: **как отличить пустой буфер от
полного, если оба индекса совпали?** Варианты: хранить счётчик `size`, либо оставлять одну
ячейку свободной. В нашем интерфейсе есть `size()`, поэтому проще хранить счётчик.
полного, если оба индекса совпали?** При `head == tail` возможны оба состояния — буфер мог
быть только что создан (пуст) или заполнен ровно `capacity` раз (полон) — по одним индексам
это не различить. Варианты: хранить счётчик `size`, либо оставлять одну ячейку свободной.
В нашем интерфейсе есть `size()`, поэтому проще хранить счётчик.
Почему дальше: тот же счётчик `size_` вместо пересчёта по индексам — частый приём и в
`std::deque`, и в реализациях lock-free очередей, где индексы вообще нельзя лишний раз читать.
## Глава III. Задание
@@ -89,14 +97,29 @@ public:
`std::vector<int>` — тогда вопрос исчезает.
</details>
## Глава VII. Частые ошибки
## Глава VII. Ловушки
- Сдвиг элементов вместо индексов (теряется весь смысл O(1)).
- Путаница «пусто/полно» при совпавших индексах.
- Сдвиг элементов вместо индексов → цена `push`/`pop` вырастает до O(n) → теряется весь
смысл структуры, видно по сравнению с наивным `std::vector` на бенчмарке.
- Путаница «пусто/полно» при совпавших индексах (`head_ == tail_`) без отдельного `size_` →
`full()` и `empty()` дают одинаковый ответ на разных состояниях → тест на заполненный буфер
ёмкости > 1 падает.
- `%` на каждом шаге там, где можно было обойтись условием — не ошибка, но на собеседовании
спросят «а быстрее можно?» (ответ: `if (++idx == cap) idx = 0;`).
- Копирование объекта без правила трёх → двойное освобождение. Если сдаёшь с ручным `new[]`,
запрети копирование (`= delete`).
спросят «а быстрее можно?» (ответ: `if (++idx == cap) idx = 0;`, деление по модулю дороже
сравнения).
- Копирование объекта без правила трёх/пяти → два объекта владеют одним и тем же `new[]`-буфером
→ двойное освобождение при выходе обоих из области видимости, ловится ASAN. Если сдаёшь с
ручным `new[]`, запрети копирование (`= delete`).
**Факты для карточек**
- base | Сложность `push`/`pop` кольцевого буфера? — O(1)
- core | Как отличить пустой буфер от полного при `head_ == tail_`? — хранить отдельный
счётчик `size_` (или жертвовать одной ячейкой)
- core | Чем `delete[]` отличается от `delete` для массива, выделенного `new[]`? — `delete`
без `[]` на массиве — UB, вызовет деструктор только для первого элемента и испортит подсчёт
размера аллокации
- base | Какое условие делает `push` дешевле, чем `% capacity_` на каждый вызов? —
`if (++idx == cap) idx = 0;`
## После сдачи