6.3 KiB
Задача 03 — кольцевой буфер (C++)
Разминка уровня «основная»: структура маленькая, но встречается везде. Ориентир — 40–60 минут.
Глава I. Общая информация
- Цель: научиться писать структуру с фиксированной памятью и без сдвигов элементов.
- Почему это в Eltex: приём/передача пакетов, буферы DMA, очереди между потоками — всё это кольцевые буферы. Понимание wrap-around и «полный/пустой» — прямой вопрос на собеседовании.
- Что сдаётся:
solution.cpp, проходящийpython3 grade.py 03.
Глава II. Что нужно знать до старта
Идея: массив фиксированного размера + два индекса. head — куда писать, tail — откуда
читать. Когда индекс доходит до конца, он возвращается в начало: idx = (idx + 1) % capacity.
Сдвигать элементы не нужно никогда — в этом весь смысл.
Ловушка, из-за которой задача попадает в собеседования: как отличить пустой буфер от
полного, если оба индекса совпали? Варианты: хранить счётчик size, либо оставлять одну
ячейку свободной. В нашем интерфейсе есть size(), поэтому проще хранить счётчик.
Глава III. Задание
class RingBuffer {
public:
explicit RingBuffer(size_t capacity);
~RingBuffer();
bool push(int v); // false, если буфер полон (перезапись НЕ делать)
bool pop(int& out); // false, если буфер пуст
size_t size() const;
size_t capacity() const;
bool empty() const;
bool full() const;
};
Требования:
- 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. Подсказки (после первой попытки)
Значения после заворота идут не в том порядке
Проверь, что pop читает именно tail_, а push пишет именно в head_, и что оба индекса
инкрементируются после операции. Классическая ошибка — сдвинуть head_ до записи.
Перезапись при полном буфере
Ты не проверяешь full() в push. По условию перезапись запрещена: полный буфер возвращает
false и не портит старые данные.
ASAN: утечка или двойное освобождение
Если память выделена через new[], освобождать нужно delete[] (не delete). Проще взять
std::vector<int> — тогда вопрос исчезает.
Глава VII. Частые ошибки
- Сдвиг элементов вместо индексов (теряется весь смысл O(1)).
- Путаница «пусто/полно» при совпавших индексах.
%на каждом шаге там, где можно было обойтись условием — не ошибка, но на собеседовании спросят «а быстрее можно?» (ответ:if (++idx == cap) idx = 0;).- Копирование объекта без правила трёх → двойное освобождение. Если сдаёшь с ручным
new[], запрети копирование (= delete).
После сдачи
Разбор: как устроены буферы в драйверах (ring buffer с head/tail для DMA), почему в
многопоточном варианте нужны атомарные индексы и memory_order.