C++笔记:现代C++封装内存池

整体架构分层

vbnet 复制代码
┌────────────────────────────┐
│ Public API                 │  allocate<T>(), deallocate
├────────────────────────────┤
│ Pool Interface Layer       │  抽象分配语义
├────────────────────────────┤
│ Slab / Block Management    │  管理内存块
├────────────────────────────┤
│ Raw Memory Provider        │  ::operator new / mmap
└────────────────────────────┘

第一层:Raw Memory Provider(最底层)

职责

  • 向 OS / runtime 申请"大块内存"

  • 不关心对象类型

  • 不关心回收策略

代码实现

cpp 复制代码
#include <cstddef>
#include <memory>
#include <new>
#include <span>
//提供原始内存
class RawBlock {
public:
	RawBlock(std::size_t bytes,std::size_t alignmemt):size_(bytes),alignment_(alignmemt){
		void* p = ::operator new(bytes, std::align_val_t{ alignmemt });//new一块内存
        data_ = static_cast<std::byte*>(p);
	}
	//不允许拷贝
	RawBlock(const RawBlock&) = delete;
	RawBlock& operator=(const RawBlock&) = delete;
	//允许移动
	RawBlock(RawBlock&& other)noexcept :data_(other.data_),size_(other.size_),alignment_(other.alignment_){
		other.data_ = nullptr;
		other.size_ = 0;
	}
	RawBlock& operator=(RawBlock&& other)noexcept {
		if (this != &other) {
			release();
			data_ = other.data_;
			size_ = other.size_;
			alignment_ = other.alignment_;
			other.data_ = nullptr;
			other.size_ = 0;
		}
		return *this;
	}
	~RawBlock() {
		release();
	}
	//返回视图,返回类型是span类型
	std::span<std::byte> span()noexcept {
		return { data_,size_ };
	}
	std::span<const std::byte> span()const noexcept {
		return { data_,size_ };
	}
	std::size_t size()const noexcept { return size_; }
private:
	//释放资源
	void release()noexcept {
		if (data_) {
			::operator delete(data_, std::align_val_t{ alignment_ });
			data_ = nullptr;
		}
	}
	std::byte* data_{ nullptr };//原始字节块
	std::size_t size_{ 0 };//字节数
	std::size_t alignment_{ alignof(std::max_align_t) };//对其数
};

对齐的概念

对齐就是:

一个对象在内存中的起始地址,必须是某个值的整数倍

类型 常见对齐
char 1
int 4
double 8
void* 8(x64)
__m256 32

如果一个 double 的地址不是 8 的倍数,在某些平台上:

  • 要么 性能极差

  • 要么 直接硬件异常

alignof 是什么?

alignof(T)含义是:

类型 T 的最小对齐要求(以字节为单位)

cpp 复制代码
alignof(int)        // 通常 4
alignof(double)     // 通常 8
alignof(void*)      // 通常 8 (x64)

std::max_align_t

这是一个 标准库定义的类型(比如MSVC定义如下)

cpp 复制代码
using max_align_t = double; // most aligned type

标准规定:

std::max_align_t 的对齐要求

所有「基本类型」的对齐要求

也就是说:

cpp 复制代码
alignof(std::max_align_t)

等价于:

当前平台上,malloc / operator new 能"自然"支持的最大对齐

普通 operator new(size) 只保证 alignof(std::max_align_t) 对齐

std::align_val_t(C++17)

这是一个 标签类型(tag type)

它的唯一作用是:

告诉 operator new:我这次要"过对齐"的内存

cpp 复制代码
::operator new(size, std::align_val_t{ alignment });

语义是:

"请给我一块 size 字节的内存,

并且起始地址至少对齐到 alignment"

对应的 delete 必须匹配

cpp 复制代码
::operator delete(ptr, std::align_val_t{ alignment });

普通 new vs 对齐 new

分配方式 对齐保证 代价
operator new(size) alignof(std::max_align_t)
operator new(size, align_val_t) 用户指定 慢一点

第二层:Block / Slab 管理层(核心)

这是内存池真正的"灵魂"。

基本思想:Slab Allocator

  • 管理 N 个等大小 slot

  • O(1) allocate / deallocate

  • 不产生碎片

  • 不做类型构造(只管内存)

我们用 intrusive free list(最经典、最快)

cpp 复制代码
slot:
+------------------+
| next_free (ptr)  |
+------------------+
| user memory ...  |
+------------------+

空闲 slot 的前几个字节存"下一个 free slot 的指针"

代码实现

cpp 复制代码
#include <cassert>
#include <cstddef>
#include <new>
class SlabBlock {
public:
	SlabBlock(std::size_t slot_size,
			std::size_t slot_count,
			std::size_t alignment) :
		slot_size_(slot_size),
		slot_count_(slot_count),
		//构造原始内存
		memory_(slot_size*slot_count,alignment){
		//初始化free list
		auto mem = memory_.span().data();
		free_list_ = mem;
		for (std::size_t i = 0; i < slot_count - 1; i++) {
			auto current = mem + i * slot_size_;
			auto next = mem + (i + 1) * slot_size_;
			//一个内存块的前一部分,存放下一个位置的指针
			*reinterpret_cast<void**>(current) = next;
		}
		//最后一个指向nullptr
		auto last = mem + (slot_count - 1) * slot_size_;
		*reinterpret_cast<void**>(last) = nullptr;
	}
	//分配内存
	void* allocate() {
		//没有内存了
		if (!free_list_) {
			return nullptr;
		}
		void* result = free_list_;
		//拿出之前存的下一个位置的指针,更新当前free list
		free_list_ = *reinterpret_cast<void**>(free_list_);
		used_++;//使用数+1
		return result;
	}
	//归还内存
	void deallocate(void* p) {
		assert(owns(p));
		//p存放当前空位指针
		*reinterpret_cast<void**>(p) = free_list_;
		//free list更新
		free_list_ = p;
		--used_;
	}
	//判断归还的内存是否合法
	bool owns(void* p)const noexcept {
		auto begin = memory_.span().data();
		auto end = begin + memory_.size();
		return p >= begin && p < end;
	}
	bool full() const noexcept {
		return used_ == slot_count_;
	}

	bool empty() const noexcept {
		return used_ == 0;
	}
private:
	RawBlock memory_;//管理的整个内存
	void* free_list_{ nullptr };
	std::size_t used_{ 0 };
	std::size_t slot_size_;
	std::size_t slot_count_;
};

第三层:Pool Interface 接口层

这里我们选择多 size-class Pool Interface 实现,也就是有不同slot大小的Slab管理层的池化接口层,会选择合适大小的Slab里的slot进行分配

代码实现

cpp 复制代码
//实际应用中可以看情况是否选择做成单例
class MemoryPool {
public:
    //size classes
    static constexpr std::array<std::size_t, 8> kSizeClasses = {
        32,64,128,256,512,1024,2048,4096
    };
    //每个Slab槽的个数
    static constexpr std::size_t kSlabSlotCount = 1024;
    explicit MemoryPool(std::size_t alignment = alignof(std::max_align_t)) :alignment_(alignment) {
        //每个SlabBlock的slot大小都不一样
        for (std::size_t i = 0; i < kSizeClasses.size(); i++) {
            slabs_[i] = std::make_unique<SlabBlock>(
                kSizeClasses[i],
                kSlabSlotCount,
                alignment_
            );
        }
    }
    void* allocate(std::size_t size, std::size_t alignment = alignof(std::max_align_t)) {
        //对齐过大Slab无法满足,直接fallback到系统分配
        if (alignment > alignment_) { 
            return system_alloc(size, alignment);
        }
        //找符合大小的
        if (auto idx = pick_slab(size)) {
            if (void* p = slabs_[*idx]->allocate()) {
                return p;
            }
        }
        //slab没有合适的/或者满了
        return system_alloc(size, alignment);
    }
    void  deallocate(void* p, std::size_t  size, std::size_t alignment = alignof(std::max_align_t)) {
        if (!p) return;
        //尝试归还到slab
        if (auto idx = pick_slab(size)) {
            auto& slab = slabs_[*idx];
            if (slab->owns(p)) {
                slab->deallocate(p);
                return;
            }
        }
        // fallback 内存
        system_free(p, alignment);
    }
    // ===== 模板接口 =====
    template<typename T>
    T* allocate() {
        void* mem = allocate(sizeof(T), alignof(T));
        return static_cast<T*>(mem);
    }

    template<typename T>
    void deallocate(T* p) {
        deallocate(p, sizeof(T), alignof(T));
    }
private:
    //选择一个size-class
    static std::optional<std::size_t> pick_slab(std::size_t size) {
        for (std::size_t i = 0; i < kSizeClasses.size(); i++) {
            //选择刚好大一点的
            if (size <= kSizeClasses[i])
                return i;
        }
        //太大了,没有符合的
        return std::nullopt;
    }
    //系统的调用
    static void* system_alloc(std::size_t size, std::size_t alignment) {
        return ::operator new(size, std::align_val_t{ alignment });
    }
    static void system_free(void* p, std::size_t alignment) {
        ::operator delete(p, std::align_val_t{ alignment });
    }
private:
    std::array<std::unique_ptr<SlabBlock>, kSizeClasses.size()> slabs_;
    std::size_t alignment_;
};

使用内存池

裸指针使用

cpp 复制代码
void test_raw() {
    MemoryPool pool;
    struct Foo {
        int a, b;
    };
    Foo* f = pool.allocate<Foo>();//用模板形式申请内存,当然现在的f仅仅是裸内存
    new (f)Foo{ 1,2 };//使用placement new构造对象
    f->~Foo();//析构对象
    pool.deallocate(f);//内存回收
}

当然每次从内存池到释放都要四步还是有点多了,并且还得手动操作,这与现代C++思想也是背道而驰了。

使用智能指针

这里用unique_ptr作为演示

cpp 复制代码
template<typename T>
struct PoolDeleter {
    MemoryPool* pool;
    void operator()(T* p)const noexcept {
        if (!p) return;
        p->~T();// 显式析构对象
        pool->deallocate(p);  // 归还内存到池
    }
};
void test_unique_ptr() {
    MemoryPool pool;
    struct Foo {
        int a, b;
        ~Foo() {
            std::cout << "destroy Foo!\n";
        }
    };
    Foo* raw = pool.allocate<Foo>();
    new (raw) Foo{ 1,2 };
    std::unique_ptr<Foo, PoolDeleter<Foo>> ptr{
        raw,
        PoolDeleter<Foo>{&pool}
    };
}

这样释放就不用自己每次记住了。对于shared_ptr也是一个思路,只不过shared_ptr是传的是函数对象。

智能指针再次封装

当然申请部分也挺费劲,每次都要自己写placement new也挺烦的

cpp 复制代码
template<typename T,typename...Args>
std::unique_ptr<T, PoolDeleter<T>> pool_make_unique(MemoryPool& pool, Args&&...args) {
    T* mem = pool.allocate<T>();
    if (!mem) throw std::bad_alloc{};
    new (mem) T(std::forward<Args>(args)...);
    return std::unique_ptr<T, PoolDeleter<T>>{mem, PoolDeleter<T>{&pool}};
}
void test_unique_ptr_template() {
    MemoryPool pool;
    struct Foo {
        int a, b;
        ~Foo() {
            std::cout << "destroy Foo!\n";
        }
    };
    auto foo_ptr = pool_make_unique<Foo>(pool, 1, 2);
}

总体代码

方便直接copy。当然不保证完全正确,我也没大规模测试

cpp 复制代码
#include<cstddef>
#include <cassert>
#include<memory>
#include<new>
#include<span>
#include <array>
#include<optional>
//提供原始内存
class RawBlock {
public:
	RawBlock(std::size_t bytes,std::size_t alignmemt):size_(bytes),alignment_(alignmemt){
		void* p = ::operator new(bytes, std::align_val_t{ alignmemt });//new一块内存
        data_ = static_cast<std::byte*>(p);
	}
	//不允许拷贝
	RawBlock(const RawBlock&) = delete;
	RawBlock& operator=(const RawBlock&) = delete;
	//允许移动
	RawBlock(RawBlock&& other)noexcept :data_(other.data_),size_(other.size_),alignment_(other.alignment_){
		other.data_ = nullptr;
		other.size_ = 0;
	}
	RawBlock& operator=(RawBlock&& other)noexcept {
		if (this != &other) {
			release();
			data_ = other.data_;
			size_ = other.size_;
			alignment_ = other.alignment_;
			other.data_ = nullptr;
			other.size_ = 0;
		}
		return *this;
	}
	~RawBlock() {
		release();
	}
	//返回视图,返回类型是span类型
	std::span<std::byte> span()noexcept {
		return { data_,size_ };
	}
	std::span<const std::byte> span()const noexcept {
		return { data_,size_ };
	}
	std::size_t size()const noexcept { return size_; }
private:
	//释放资源
	void release()noexcept {
		if (data_) {
			::operator delete(data_, std::align_val_t{ alignment_ });
			data_ = nullptr;
		}
	}
	std::byte* data_{ nullptr };//原始字节块
	std::size_t size_{ 0 };//字节数
	std::size_t alignment_{ alignof(std::max_align_t) };//对其数
};
//
class SlabBlock {
public:
    SlabBlock(std::size_t slot_size,
        std::size_t slot_count,
        std::size_t alignment)
        : slot_count_(slot_count)
        , slot_size_(compute_slot_size(slot_size, alignment))//内部保证slot_size的合理
        , memory_(slot_size_* slot_count_,
            std::max(alignment, alignof(void*)))//这里的alignment原理详见compute_slot_size
    {
        assert(slot_count_ > 0);

        auto mem = memory_.span().data();
        free_list_ = mem;//获取原始地址的指针

        for (std::size_t i = 0; i + 1 < slot_count_; ++i) {
            auto cur = mem + i * slot_size_;
            auto next = mem + (i + 1) * slot_size_;
            //把next的地址信息存入cur里内存里
            std::memcpy(cur, &next, sizeof(next));
        }
        //最后一块地址,指针域存nullptr
        auto last = mem + (slot_count_ - 1) * slot_size_;
        std::byte* nullp = nullptr;
        std::memcpy(last, &nullp, sizeof(nullp));
    }
    //分配内存
    void* allocate() noexcept {
        //没有空闲内存了
        if (!free_list_) return nullptr;
        //取出当前空闲内存
        void* result = free_list_;
        //更新freelist
        free_list_ = load_next(free_list_);
        ++used_;
        return result;
    }
    //归还内存
    void deallocate(void* p) noexcept {
        //要判断一下归还的对不对
        assert(owns(p));
        store_next(p, free_list_);
        //更新freelist
        free_list_ = p;
        --used_;
    }

    bool owns(void* p) const noexcept {
        auto begin = memory_.span().data();
        auto end = begin + memory_.size();
        auto bp = static_cast<std::byte*>(p);
        //如果bp不在原始内存里,肯定不对
        if (bp < begin || bp >= end) return false;
        //而且bp指向的位置也必须是该slot开头,不然也是错误的
        return (bp - begin) % slot_size_ == 0;
    }

private:
    //计算合法的slot_size
    static std::size_t compute_slot_size(std::size_t slot,
        std::size_t alignment) {
        //因为每个slot的头部需要存一个void*,所以对齐也必须至少是sizeof指针
        std::size_t min_align = std::max(alignment, alignof(void*));
        //这也是同理,就算外面传进来slot小,也必须扩大到指针大小
        slot = std::max(slot, sizeof(void*));
        return align_up(slot, min_align);
    }

    static std::size_t align_up(std::size_t n, std::size_t align) {
        //返回align的倍数,保证对齐
        return ((n + align - 1) / align) * align;
    }
    //取当前slot里存的下一个slot的地址
    static void* load_next(void* slot) noexcept {
        void* next;
        std::memcpy(&next, slot, sizeof(next));
        return next;
    }
    //将下一个slot的地址指针next存入当前slot内存里
    static void store_next(void* slot, void* next) noexcept {
        std::memcpy(slot, &next, sizeof(next));
    }

    std::size_t slot_count_{ 0 };//slot个数
    std::size_t slot_size_{ 0 };//每个slot的大小
    RawBlock memory_;//原始内存
    void* free_list_{ nullptr };//当前空闲slot
    std::size_t used_{ 0 };
    
    
};
//实际应用中可以看情况是否选择做成单例
class MemoryPool {
private:
    template<typename T>
    struct PoolDeleter {
        MemoryPool* pool;
        void operator()(T* p)const noexcept {
            if (!p) return;
            p->~T();// 显式析构对象
            pool->deallocate(p);  // 归还内存到池
        }
    };
public:
    //size classes
    static constexpr std::array<std::size_t, 8> kSizeClasses = {
        32,64,128,256,512,1024,2048,4096
    };
    //每个Slab槽的个数
    static constexpr std::size_t kSlabSlotCount = 1024;
    explicit MemoryPool(std::size_t alignment = alignof(std::max_align_t)) :alignment_(alignment) {
        //每个SlabBlock的slot大小都不一样
        for (std::size_t i = 0; i < kSizeClasses.size(); i++) {
            slabs_[i] = std::make_unique<SlabBlock>(
                kSizeClasses[i],
                kSlabSlotCount,
                alignment_
            );
        }
    }
    void* allocate(std::size_t size, std::size_t alignment = alignof(std::max_align_t)) {
        //对齐过大Slab无法满足,直接fallback到系统分配
        if (alignment > alignment_) { 
            return system_alloc(size, alignment);
        }
        //找符合大小的
        if (auto idx = pick_slab(size)) {
            if (void* p = slabs_[*idx]->allocate()) {
                return p;
            }
        }
        //slab没有合适的/或者满了
        return system_alloc(size, alignment);
    }
    void  deallocate(void* p, std::size_t  size, std::size_t alignment = alignof(std::max_align_t)) {
        if (!p) return;
        //尝试归还到slab
        if (auto idx = pick_slab(size)) {
            auto& slab = slabs_[*idx];
            if (slab->owns(p)) {
                slab->deallocate(p);
                return;
            }
        }
        // fallback 内存
        system_free(p, alignment);
    }
    // ===== 模板接口 =====
    template<typename T>
    T* allocate() {
        void* mem = allocate(sizeof(T), alignof(T));
        return static_cast<T*>(mem);
    }

    template<typename T>
    void deallocate(T* p) {
        deallocate(p, sizeof(T), alignof(T));
    }
    //===== 智能指针模板接口 =====
    template<typename T, typename... Args>
    std::unique_ptr<T, PoolDeleter<T>>
        make_unique(Args&&... args) {
        void* mem = allocate(sizeof(T), alignof(T));
        if (!mem) {
            throw std::bad_alloc();
        }

        T* obj = new (mem) T( std::forward<Args>(args)... );

        return std::unique_ptr<T, PoolDeleter<T>>(
            obj,
            PoolDeleter<T>{ this }
        );
    }

private:
    //选择一个size-class
    static std::optional<std::size_t> pick_slab(std::size_t size) {
        for (std::size_t i = 0; i < kSizeClasses.size(); i++) {
            //选择刚好大一点的
            if (size <= kSizeClasses[i])
                return i;
        }
        //太大了,没有符合的
        return std::nullopt;
    }
    //系统的调用
    static void* system_alloc(std::size_t size, std::size_t alignment) {
        return ::operator new(size, std::align_val_t{ alignment });
    }
    static void system_free(void* p, std::size_t alignment) {
        ::operator delete(p, std::align_val_t{ alignment });
    }
private:
    std::array<std::unique_ptr<SlabBlock>, kSizeClasses.size()> slabs_;
    std::size_t alignment_;

};

注意事项!!!

这个内存池是非线程安全的,不能多线程并发申请内存。

真正多线程安全的内存池有点复杂,我还暂且不太会

相关推荐
m0_736034852 小时前
1.27笔记
linux·服务器·笔记
日更嵌入式的打工仔2 小时前
(实用向)中断服务程序(ISR)的优化方向
笔记·单片机
离离茶2 小时前
【笔记1-11】Qt 关闭QToolbar的拓展菜单
开发语言·笔记·qt
了一梨3 小时前
SQLite3学习笔记6:UPDATE(改)+ DELETE(删)数据(C API)
笔记·学习·sqlite
霸王蟹3 小时前
Uni-app 跨端开发框架Unibest快速体验
前端·笔记·微信·uni-app·unibest
mango_mangojuice4 小时前
C++ 学习笔记(string类)
开发语言·c++·笔记·学习
hetao17338374 小时前
2026-01-27~28 hetao1733837 的刷题记录
c++·笔记·算法
蓝田生玉1234 小时前
Deepstack论文阅读笔记
论文阅读·笔记
淬炼之火5 小时前
基于Docker Desktop 和 Ubuntu 在 Windows上部署轻量化大模型(Qwen-LLM)
笔记·ubuntu·docker·语言模型·容器