嵌入式持久化配置:访问者模式下如何规避动态内存分配?
嵌入式持久化配置模块的无动态内存分配方案
我正在为嵌入式项目开发持久化配置模块,计划采用访问者模式实现序列化,希望通过PersistentConfig类注册各结构体来统一执行读写操作(Flash需要整页擦除,批量写入更高效)。但嵌入式项目要避免动态内存分配,当前实现注册时需要在堆上创建Entry对象,想请教:有没有无需动态分配Entry对象的方案?或者应该用固定内存池来安全分配?
示例序列化结构体
struct softwareInfo { uint32_t version; std::array<uint8_t, 40> commitHash; uint32_t buildDate; template <class T> void pack(T& archive) { archive.process(version); archive.process(commitHash); archive.process(buildDate); } };
结构体注册方式
PersistentConfig conf{}; conf.register(&softwareInfo, "swinfo"); conf.register(&foo, "foo");
相关核心定义
struct persistentEntry { EntryBase* object; std::string_view name; uint32_t size; uint32_t datacrc32; }; std::array<persistentEntry, maxEntries> entries; class EntryBase { public: virtual ~EntryBase() {} virtual bool pack(serializer::Packer& packer) = 0; virtual bool pack(serializer::Unpacker& unpacker) = 0; }; template <typename T> class Entry : public EntryBase { public: explicit Entry(T* item) : item(item) { } bool pack(serializer::Packer& packer) override { return item->pack(packer); } bool pack(serializer::Unpacker& unpacker) override { return item->pack(unpacker); } T* const item; };
当前注册函数(含需规避的动态分配)
template <class PackableObject> bool registerEntry(PackableObject* obj, const std::string_view& name) { entries.at(numberOfEntries).object = new Entry<PackableObject>(obj); //需避免的动态分配 entries.at(numberOfEntries).name = name; entries.at(numberOfEntries).hashName = Checksum::crc32(name.data(), name.size()); return ++numberOfEntries > maxEntries; }
可行解决方案
方案一:静态预分配Entry对象,完全规避动态内存
核心思路是提前为每个要注册的结构体创建静态存储区的Entry实例,注册时直接引用这些对象,彻底摆脱堆分配。
修改注册函数:
template <class PackableObject> bool registerEntry(Entry<PackableObject>* entry, const std::string_view& name) { if (numberOfEntries >= maxEntries) return false; entries[numberOfEntries].object = entry; entries[numberOfEntries].name = name; entries[numberOfEntries].hashName = Checksum::crc32(name.data(), name.size()); numberOfEntries++; return true; }
用户注册时的写法:
// 静态创建Entry实例,存储在全局/局部静态区,无需堆内存 static Entry<softwareInfo> swInfoEntry(&softwareInfo); static Entry<Foo> fooEntry(&foo); PersistentConfig conf{}; conf.registerEntry(&swInfoEntry, "swinfo"); conf.registerEntry(&fooEntry, "foo");
这种方式完全符合嵌入式无动态内存的要求,所有Entry对象的生命周期与程序一致,无需手动管理。
方案二:用std::variant替代多态,消除基类指针依赖
如果项目支持C++17及以上特性,可以用std::variant存储不同类型的Entry,不需要基类和虚函数,利用变体的类型安全特性处理序列化,全程无动态分配。
修改核心结构定义:
// 预定义所有可能的Entry类型(编译期确定) using EntryVariant = std::variant<Entry<softwareInfo>, Entry<Foo>, /* 其他结构体的Entry类型 */>; struct persistentEntry { EntryVariant entry; std::string_view name; uint32_t size; uint32_t datacrc32; }; std::array<persistentEntry, maxEntries> entries;
注册函数直接构造Entry存入变体:
template <class PackableObject> bool registerEntry(PackableObject* obj, const std::string_view& name) { if (numberOfEntries >= maxEntries) return false; entries[numberOfEntries].entry = Entry<PackableObject>(obj); entries[numberOfEntries].name = name; entries[numberOfEntries].hashName = Checksum::crc32(name.data(), name.size()); numberOfEntries++; return true; }
序列化时用std::visit调用对应pack方法:
struct PackVisitor { serializer::Packer& packer; bool operator()(auto& entry) { return entry.pack(packer); } }; // 批量打包逻辑 for (auto& e : entries) { std::visit(PackVisitor{packer}, e.entry); }
这种方案没有虚函数开销,类型安全,适合编译期就能确定所有可注册结构体的场景。
方案三:固定内存池分配(仅适用于必须动态创建的场景)
如果确实需要在运行时动态创建Entry(比如注册数量不确定但不超过maxEntries),可以提前分配固定大小的内存池,用原地构造替代new,避免堆碎片。
实现简单的内存池:
// 取所有Entry类型中最大的尺寸作为单块内存大小 constexpr size_t MAX_ENTRY_SIZE = sizeof(Entry<softwareInfo>); // 内存池对齐到最大对齐要求,确保对象构造合法 alignas(alignof(std::max_align_t)) uint8_t entryPool[MAX_ENTRY_SIZE * maxEntries]; size_t poolOffset = 0; template <class PackableObject> Entry<PackableObject>* allocateEntry(PackableObject* obj) { static_assert(sizeof(Entry<PackableObject>) <= MAX_ENTRY_SIZE, "Entry尺寸超过内存池限制"); if (poolOffset + sizeof(Entry<PackableObject>) > sizeof(entryPool)) { return nullptr; } // 原地构造Entry对象,不调用堆分配 auto entry = new (entryPool + poolOffset) Entry<PackableObject>(obj); poolOffset += sizeof(Entry<PackableObject>); return entry; }
修改注册函数:
template <class PackableObject> bool registerEntry(PackableObject* obj, const std::string_view& name) { if (numberOfEntries >= maxEntries) return false; auto entry = allocateEntry(obj); if (!entry) return false; entries[numberOfEntries].object = entry; entries[numberOfEntries].name = name; entries[numberOfEntries].hashName = Checksum::crc32(name.data(), name.size()); numberOfEntries++; return true; }
注意:嵌入式场景下通常不需要销毁这些对象,内存池全程不需要释放,避免了堆管理的风险。
内容的提问来源于stack exchange,提问作者klonyyy
相关产品推荐
相关产品推荐

