Формат v3: уроки D1 (теория+разбор), задачи ступенями, карточки 101 с уровнями и пояснениями
This commit is contained in:
@@ -1,6 +1,26 @@
|
||||
# Задача 03 — кольцевой буфер (C++)
|
||||
|
||||
Кольцевой буфер фиксированной ёмкости — базовая структура для embedded и сетевого кода.
|
||||
> Разминка уровня «основная»: структура маленькая, но встречается везде. Ориентир — 40–60 минут.
|
||||
|
||||
## Глава I. Общая информация
|
||||
|
||||
- **Цель:** научиться писать структуру с фиксированной памятью и без сдвигов элементов.
|
||||
- **Почему это в Eltex:** приём/передача пакетов, буферы DMA, очереди между потоками —
|
||||
всё это кольцевые буферы. Понимание wrap-around и «полный/пустой» — прямой вопрос на
|
||||
собеседовании.
|
||||
- **Что сдаётся:** `solution.cpp`, проходящий `python3 grade.py 03`.
|
||||
|
||||
## Глава II. Что нужно знать до старта
|
||||
|
||||
Идея: массив фиксированного размера + два индекса. `head` — куда писать, `tail` — откуда
|
||||
читать. Когда индекс доходит до конца, он возвращается в начало: `idx = (idx + 1) % capacity`.
|
||||
Сдвигать элементы не нужно никогда — в этом весь смысл.
|
||||
|
||||
Ловушка, из-за которой задача попадает в собеседования: **как отличить пустой буфер от
|
||||
полного, если оба индекса совпали?** Варианты: хранить счётчик `size`, либо оставлять одну
|
||||
ячейку свободной. В нашем интерфейсе есть `size()`, поэтому проще хранить счётчик.
|
||||
|
||||
## Глава III. Задание
|
||||
|
||||
```c++
|
||||
class RingBuffer {
|
||||
@@ -17,9 +37,68 @@ public:
|
||||
```
|
||||
|
||||
Требования:
|
||||
- O(1) на push/pop, никаких сдвигов элементов;
|
||||
- память выделяется один раз в конструкторе, освобождается в деструкторе;
|
||||
- поведение после заворачивания индексов (wrap-around) корректное;
|
||||
- копирование запрещать не обязательно, но двойного освобождения быть не должно.
|
||||
|
||||
Проверка: `python3 grade.py 03`. Критерий: все `ok`, ASAN/UBSAN чистые.
|
||||
- O(1) на `push`/`pop`, никаких сдвигов элементов;
|
||||
- память выделяется один раз в конструкторе и освобождается в деструкторе;
|
||||
- корректное поведение после заворачивания индексов (записали до конца, продолжили с начала);
|
||||
- двойного освобождения быть не должно.
|
||||
|
||||
## Глава IV. Ступени
|
||||
|
||||
**Ступень 1 (10 минут).** Поля: указатель на массив, `capacity_`, `size_`, `head_`, `tail_`.
|
||||
Конструктор выделяет массив и обнуляет поля. Деструктор освобождает. Если хочешь короче —
|
||||
`std::vector<int>` вместо ручного `new[]`, тогда деструктор не нужен вовсе.
|
||||
|
||||
**Ступень 2 (10 минут).** `empty()` — это `size_ == 0`, `full()` — `size_ == capacity_`.
|
||||
Проверь на буфере ёмкости 1: после одного `push` он полон, после одного `pop` пуст.
|
||||
|
||||
**Ступень 3 (15 минут).** `push`: если полон — вернуть `false` и ничего не менять; иначе
|
||||
записать в `head_`, сдвинуть `head_ = (head_ + 1) % capacity_`, увеличить `size_`, вернуть `true`.
|
||||
|
||||
**Ступень 4 (15 минут).** `pop`: если пуст — `false`; иначе отдать `buffer_[tail_]`, сдвинуть
|
||||
`tail_`, уменьшить `size_`, вернуть `true`.
|
||||
|
||||
**Ступень 5.** Проверь руками wrap-around: ёмкость 4, положи 4 элемента, сними 2, положи
|
||||
ещё 3 — все значения должны идти в правильном порядке. Затем `python3 grade.py 03`.
|
||||
|
||||
## Глава V. Критерии приёмки
|
||||
|
||||
`PASS` от `grade.py 03`: сборка без предупреждений, ASAN/UBSAN чистые, все проверки `ok`,
|
||||
включая порядок FIFO и корректный wrap-around.
|
||||
|
||||
## Глава VI. Подсказки (после первой попытки)
|
||||
|
||||
<details>
|
||||
<summary>Значения после заворота идут не в том порядке</summary>
|
||||
|
||||
Проверь, что `pop` читает именно `tail_`, а `push` пишет именно в `head_`, и что оба индекса
|
||||
инкрементируются **после** операции. Классическая ошибка — сдвинуть `head_` до записи.
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Перезапись при полном буфере</summary>
|
||||
|
||||
Ты не проверяешь `full()` в `push`. По условию перезапись запрещена: полный буфер возвращает
|
||||
`false` и не портит старые данные.
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>ASAN: утечка или двойное освобождение</summary>
|
||||
|
||||
Если память выделена через `new[]`, освобождать нужно `delete[]` (не `delete`). Проще взять
|
||||
`std::vector<int>` — тогда вопрос исчезает.
|
||||
</details>
|
||||
|
||||
## Глава VII. Частые ошибки
|
||||
|
||||
- Сдвиг элементов вместо индексов (теряется весь смысл O(1)).
|
||||
- Путаница «пусто/полно» при совпавших индексах.
|
||||
- `%` на каждом шаге там, где можно было обойтись условием — не ошибка, но на собеседовании
|
||||
спросят «а быстрее можно?» (ответ: `if (++idx == cap) idx = 0;`).
|
||||
- Копирование объекта без правила трёх → двойное освобождение. Если сдаёшь с ручным `new[]`,
|
||||
запрети копирование (`= delete`).
|
||||
|
||||
## После сдачи
|
||||
|
||||
Разбор: как устроены буферы в драйверах (ring buffer с head/tail для DMA), почему в
|
||||
многопоточном варианте нужны атомарные индексы и `memory_order`.
|
||||
|
||||
Reference in New Issue
Block a user