Когда мы разрабатываем под embedded, нам приходится сталкиваться с такими флагами компиляции как -nostdlib -fno-exceptions -fno-rtti.
Во многих средах нет malloc/free (new/delete) и вообще нет встроенного выделения памяти.
Использование «больших» стандартных контейнеров C++ (например, std::vector) нередко исключено
В результате приходится решать задачу «ручного» управления памятью. Ниже рассмотрим два подхода:
-
Буфер + переопределение оператора new (placement new)
-
Собственная куча (Heap)
Почему динамической аллокации часто нет
-
Ограниченные ресурсы
Память (RAM/Flash) в контроллерах может исчисляться десятками или сотнями килобайт, поэтому каждый байт на счету.
-
Отсутствие стандартных библиотек
При флагах
-nostdlibи-fno-exceptionsмы исключаем всю поддержку C/C++ runtime:-
Нет
new/delete(кроме placement-new, если его оставить). -
malloc/freeнередко не реализованы или ведут к непредсказуемым последствиям (heap overflow, фрагментация).
-
-
Простота сертификации и детерминированность
Встраиваемые приложения часто требуют детерминированного поведения. Фрагментация кучи и непредсказуемые задержки неприемлемы.
Поэтому разработчики вынуждены либо заранее выделять «сырой» буфер и вызывать placement new, либо писать собственный аллокатор (кучу) на фиксированном участке памяти.
Способ 1. Буфер + placement new
Идея
-
Заранее выделить в статической памяти фиксированный буфер (массив байт), подходящий по размеру и выравниванию под нужный объект.
-
При необходимости «конструировать» объект поверх этого буфера с помощью placement new.
-
При этом не использовать глобальный operator new(size_t), а локально в коде писать new (&buffer) Type(args…).
Пример
Допустим, у нас есть класс
Ppo::timer, и мы хотим разместить ровно один его экземпляр в статическом участке памяти.
-
// cgf.h #pragma once namespace PpoDbTimer { enum PpoDbTimerEnum : int{ timer1, TotalCount }; }
// ram.cpp #include "cfg.h" namespace PpoDbRam { int timers [ PpoDbTimer::TotalCount ] { 0 }; }
// rom.cpp #include <cstddef> #include "cfg.h" namespace PpoDbRam { extern int timers[]; } namespace Ppo { class timer{ public: explicit timer(int *_timer) noexcept :time(_timer) {} void tick() noexcept { if (time) { ++(*time); } } private: int * const time; /// ... }; } /* Переопределение оператора new */ inline void *operator new( size_t, void *ptr ) { return ptr; } namespace { template <size_t Len, size_t Align> struct aligned_storage{ struct type{ alignas(Align) unsigned char data[Len]; }; }; aligned_storage< sizeof( Ppo::timer ), alignof( Ppo::timer ) >::type timer1Buf; } void init(){ auto & timer1 = *new (&timer1Buf) Ppo::timer(&PpoDbRam::timers[PpoDbTimer::timer1]); timer1.tick(); } int main(){ init(); return 0; }
Что важно
-
Статический буфер
timer1Bufхранится в области.bssили.data(в зависимости от инициализации). -
Выравнивание: используется
std::aligned_storage_t<…>, чтобы гарантироватьalignof(Ppo::timer). -
Placement new:
new (raw) Ppo::timer(timerAddr)не вызывает глобальныйoperator new(size_t), а просто конструирует объект в уже выделенной памяти. -
Отсутствие
delete: поскольку у нас нет поддержки освобождения, объект живёт «вечно» до конца программы. -
UB при обращении до
init(): если где-то код попытается прочитатьtimer1Ptr->…до вызоваinit(), будет UB.
Способ 2. Собственная куча (Heap)
Идея
-
Создать класс
MyHeap, который внутри держит:-
Статический массив-буфер (например, 2 МБ)
-
Текущий сдвиг (offset) — сколько байт уже занято.
-
-
Реализовать метод
allocate(size_t size, size_t alignment):-
Выравнивять текущий указатель вверх под
alignment. -
Проверить, что
offset + size ≤ BUFFER_SIZE. -
При успехе вернуть
void*на выровненный адрес и обновитьoffset. -
При нехватке памяти вернуть
nullptr.
-
-
(Опционально) Переопределить глобальный
operator new(size_t)так, чтобы он делалMyHeap::allocate(...). И тогда в коде можно писатьnew T(args...), и вся память будет «брать» из нашего буфера. Но чаще держатcreate<T>(...)внутриMyHeap, чтобы не мешать стандартнымnewв других модулях.
Пример
//heap.h #pragma once #include <cstddef> class MyHeap { public: // Пытается выдать size байт с выравниванием alignment. Возвращает nullptr, если кончилась память. static void* allocate(size_t size, size_t alignment = alignof(size_t)) noexcept { size_t cur = reinterpret_cast<size_t>(buffer) + offset; size_t aligned = (cur + alignment - 1) & ~(alignment - 1); size_t newOffset = static_cast<size_t>(aligned - reinterpret_cast<size_t>(buffer)) + size; if (newOffset > BUFFER_SIZE) return nullptr; void* ptr = reinterpret_cast<void*>(aligned); offset = newOffset; return ptr; } template <typename T, typename... Args> static T* create(Args&&... args) noexcept { void* mem = allocate(sizeof(T), alignof(T)); if (!mem) return nullptr; return new (mem) T(static_cast<Args&&>(args)...); } private: static constexpr size_t BUFFER_SIZE = 2 * 1024 * 1024; alignas(alignof(size_t)) static char buffer[]; static size_t offset; };
// rom.cpp #include "heap.h" #include "cfg.h" #include <cstddef> namespace PpoDbRam { extern int timers[]; } alignas(alignof(std::max_align_t)) char MyHeap::buffer[ MyHeap::BUFFER_SIZE ]; std::size_t MyHeap::offset = 0; // Оставляем стандартный «placement new», чтобы new(mem) T() работал: void* operator new(size_t, void* ptr) noexcept { return ptr; } // Глобальный operator new(size) будет выделять через MyHeap::allocate. void* operator new(size_t size) noexcept { return MyHeap::allocate(size, alignof(max_align_t)); } // Пустой operator delete (никакого освобождения) void operator delete(void*) noexcept { } void operator delete(void*, size_t) noexcept { } namespace Ppo { class timer{ public: explicit timer(int *_timer) noexcept :time(_timer) {} void tick() noexcept { if (time) { ++(*time); } } private: int * const time; /// ... }; } void init(){ auto & timer1 = *new Ppo::timer(&PpoDbRam::timers[ PpoDbTimer::timer1 ]); timer1.tick(); } int main(){ init(); return 0; }
Сравнение двух подходов
|
Критерий |
Способ 1 (буфер + placement new) |
Способ 2 (своя куча) |
|---|---|---|
|
Простота реализации |
Очень просто: для каждого объекта — свой буфер и |
Нужно написать класс-кучу, методы allocate/offset, переопределить |
|
Гибкость |
Подходит, когда заранее известен тип и кол-во объектов. |
Позволяет произвольно вызывать |
|
Утилизация памяти |
«Жестко» закреплён за конкретным объектом. |
Можно выделять разные размеры, кучу «жирнее» (несколько объектов в одном буфере, при одинаковом выравнивании). |
|
Разрез «тайминг» |
Инициализация только в том месте, где объявлен буфер. |
Выделение памяти в любом месте программы, пока осталось место. |
|
Кодовая избыточность |
При N разных типов/объектов — N буферов. |
Один класс-куча для всех типов. |
|
Риск UB и ошибок |
Мало кода — меньше шансов допустить ошибку. |
Больше кода/логики — может быть ошибка в вычислении выравнивания, offset и т. д. |
Рекомендации
-
Если вам нужно одно-два статических объекта фиксированных типов, проще использовать Способ 1:
static std::aligned_storage_t<sizeof(MyType), alignof(MyType)> myObjBuf; MyType& myObj = *new (&myObjBuf) MyType(arg…);
Это гарантирует, что объект хранится в статической памяти, а вы не задумываетесь о второй куче.
-
Если вы пишете более сложное приложение, где создаются десятки (или тысячи как в моем проекте) объектов разных типов, выбирайте Способ 2 и реализуйте свою кучу. Так вы сможете писать обычные
new T(), а все они будут «упаковываться» в предопределённый буфер. Особенно если Вам надо саллоцировать что-то такое unordered_map<int,array<int>>.
Заключение
В embedded-мире почти всегда отсутствует стандартный allocator и вся стихийная флеш-рамка стандартного C++ отключена флагами -nostdlib -fno-exceptions -fno-rtti. Если задача требует динамического (пусть и внутри жестко ограниченного) выделения памяти, у вас есть два основных пути:
-
Буфер+placement new — быстрый и надёжный способ, когда заранее известны объёмы и типы.
-
Собственная куча — гибкий и масштабируемый способ, когда объём и количество объектов заранее не фиксированы.
ссылка на оригинал статьи https://habr.com/ru/articles/918122/
Добавить комментарий