各自缓冲及实现

下面按「C++ 里最常写/最常被问到」的实现来讲:缓冲几种形态 → 软件 Cache → 内存池。硬件 CPU Cache 不手写,文末单独说清边界。


一、缓冲几种形态:怎么存、怎么调

1. 单缓冲(Single Buffer)

一块数组,写满再读(或读完再写)。最简单。

cpp 复制代码
#include <array>
#include <cstddef>

template <typename T, std::size_t N>
class SingleBuffer {
    std::array<T, N> data_{};
    std::size_t size_ = 0;

public:
    bool write(const T* src, std::size_t n) {
        if (n > N) return false;
        for (std::size_t i = 0; i < n; ++i) data_[i] = src[i];
        size_ = n;
        return true;
    }

    // 处理完再允许下一次 write
    const T* data() const { return data_.data(); }
    std::size_t size() const { return size_; }
    void clear() { size_ = 0; }
};

调法: write → 业务读 data()/size()clear()

短板: 收和处理不能真正并行。


2. 双缓冲 / Ping-Pong(DMA、音频最常用)

两块区:一块给生产者写,一块给消费者读,处理完交换角色。

cpp 复制代码
#include <array>
#include <atomic>
#include <cstddef>

template <typename T, std::size_t N>
class PingPongBuffer {
    std::array<T, N> buf_[2]{};
    std::atomic<int> ready_{-1};   // 哪一块可读;-1 表示暂无
    int write_idx_ = 0;

public:
    T* write_buf() { return buf_[write_idx_].data(); }
    std::size_t capacity() const { return N; }

    // 生产者写完调用(ISR/DMA 回调里)
    void publish() {
        ready_.store(write_idx_, std::memory_order_release);
        write_idx_ ^= 1;
    }

    // 消费者:拿到可读块;nullptr 表示还没新数据
    T* try_read() {
        int idx = ready_.exchange(-1, std::memory_order_acquire);
        return idx < 0 ? nullptr : buf_[idx].data();
    }
};

调法:

cpp 复制代码
PingPongBuffer<uint8_t, 512> pp;

// DMA/ISR
auto* w = pp.write_buf();
// ... 填满 w ...
pp.publish();

// 任务
if (auto* r = pp.try_read()) {
    process(r, 512);
}

这就是「双缓冲」;环形往往是它的流式升级版。


3. 环形缓冲(Ring Buffer)------C++ 最常手写的那种

单生产者单消费者(SPSC)最常见,嵌入式面试几乎必问。

cpp 复制代码
#include <array>
#include <atomic>
#include <cstddef>
#include <optional>

template <typename T, std::size_t N>  // N 建议 2 的幂,取模变位运算
class RingBuffer {
    static_assert((N & (N - 1)) == 0, "N must be power of 2");

    std::array<T, N> buf_{};
    std::atomic<std::size_t> head_{0};  // 写
    std::atomic<std::size_t> tail_{0};  // 读

    static std::size_t mask(std::size_t i) { return i & (N - 1); }

public:
    bool push(const T& v) {
        auto h = head_.load(std::memory_order_relaxed);
        auto next = h + 1;
        if (mask(next) == mask(tail_.load(std::memory_order_acquire)))
            return false;  // 满(牺牲一格:最多存 N-1)
        buf_[mask(h)] = v;
        head_.store(next, std::memory_order_release);
        return true;
    }

    std::optional<T> pop() {
        auto t = tail_.load(std::memory_order_relaxed);
        if (mask(t) == mask(head_.load(std::memory_order_acquire)))
            return std::nullopt;  // 空
        T v = buf_[mask(t)];
        tail_.store(t + 1, std::memory_order_release);
        return v;
    }

    std::size_t size() const {
        return mask(head_.load(std::memory_order_acquire) -
                    tail_.load(std::memory_order_acquire));
    }
};

调法:

cpp 复制代码
RingBuffer<uint8_t, 256> rx;

// UART ISR
rx.push(UART->DR);

// 任务
while (auto b = rx.pop()) {
    parse(*b);
}

要点:

  • 满/空用「牺牲一格」或另加 count/full 标志二选一。
  • SPSC + atomic 头尾指针:C++ 里最常用的无锁写法。
  • 多生产者/多消费者要加锁,或换更重的队列。

字节流版常再包一层 write(const uint8_t*, n) / read(uint8_t*, n),逻辑一样,只是一次搬多字节。


4. 队列(Queue)------库里最常用

手写环形偏「字节/元素管道」;跨任务传消息,C++ 日常更常用标准库或现成组件。

通用 C++(有锁,最常见):

cpp 复制代码
#include <queue>
#include <mutex>
#include <condition_variable>
#include <optional>

template <typename T>
class BlockingQueue {
    std::queue<T> q_;
    std::mutex m_;
    std::condition_variable cv_;

public:
    void push(T v) {
        {
            std::lock_guard lock(m_);
            q_.push(std::move(v));
        }
        cv_.notify_one();
    }

    T pop() {
        std::unique_lock lock(m_);
        cv_.wait(lock, [&] { return !q_.empty(); });
        T v = std::move(q_.front());
        q_.pop();
        return v;
    }

    std::optional<T> try_pop() {
        std::lock_guard lock(m_);
        if (q_.empty()) return std::nullopt;
        T v = std::move(q_.front());
        q_.pop();
        return v;
    }
};

底层: std::queue 默认用 std::deque;有界队列也可自己用 std::vector + 环形下标实现。

嵌入式 RTOS: 更常见是 FreeRTOS xQueueSend/Receive,概念对应,不是你在 C++ 里再写一套。

对照记:

形态 更偏 C++ 常怎么写
Ring 数据结构、流式、常无锁 SPSC 手写 RingBuffer
Queue 同步语义(阻塞/通知) BlockingQueue / FreeRTOS Queue

5. 流缓冲 / 消息缓冲(概念对应)

  • 流缓冲: 按字节进出,不保证一次 push 对应一次 pop → 就是带 read/write(n) 的环形。
  • 消息缓冲: 一次进一整帧/一条消息,出也是整条 → 更像 queue<Msg> 或「长度前缀 + 环形字节区」。

变长消息 + 环形的常见存法:

text 复制代码
[len:2][payload...][len:2][payload...] ...

push 时先看空闲是否够 2+len;pop 时先读 len 再读 payload。


二、Cache:代码里怎么「实现 / 存 / 调」

先分清两种,面试别混。

A. 硬件 CPU Cache ------ 不手写,只配合

不会在 C++ 里实现 L1/L2。你只做:

  1. 正常读写变量(CPU 自动缓存)
  2. DMA 场景做一致性(你资料 P0-6)
  3. 需要时把缓冲标成 non-cacheable / 对齐到 cache line

伪代码(Cortex-M7 思路,具体 API 看 CMSIS/HAL):

cpp 复制代码
alignas(32) uint8_t dma_rx[1024];  // 常按 cache line 对齐

// 发送前:把 Cache 里的脏数据刷到内存
SCB_CleanDCache_by_Addr(dma_tx, sizeof(dma_tx));

// 接收后:丢掉 Cache 旧副本,强制从内存再读
SCB_InvalidateDCache_by_Addr(dma_rx, sizeof(dma_rx));

存: 就是普通数组/结构体,在可缓存的 RAM 里。

调: 业务代码照常访问;和 DMA 交界处 clean/invalidate。

这不是「实现 cache」,是「正确使用带 cache 的 CPU」。


B. 软件 Cache(LRU)------C++ 里最常手写的那种

目的:用内存换时间,加速「相同 key 的重复查询」。

最常用实现:unordered_map + list(哈希找节点,链表维顺序)。

cpp 复制代码
#include <unordered_map>
#include <list>
#include <optional>
#include <utility>

template <typename K, typename V>
class LruCache {
    std::size_t cap_;
    // list: 最前=最新,最后=最旧
    std::list<std::pair<K, V>> order_;
    std::unordered_map<K, typename std::list<std::pair<K, V>>::iterator> idx_;

public:
    explicit LruCache(std::size_t capacity) : cap_(capacity) {}

    std::optional<V> get(const K& key) {
        auto it = idx_.find(key);
        if (it == idx_.end()) return std::nullopt;
        order_.splice(order_.begin(), order_, it->second);  // 挪到最前
        return it->second->second;
    }

    void put(const K& key, V value) {
        auto it = idx_.find(key);
        if (it != idx_.end()) {
            it->second->second = std::move(value);
            order_.splice(order_.begin(), order_, it->second);
            return;
        }
        if (order_.size() >= cap_) {
            idx_.erase(order_.back().first);
            order_.pop_back();
        }
        order_.emplace_front(key, std::move(value));
        idx_[key] = order_.begin();
    }
};

调法:

cpp 复制代码
LruCache<int, std::string> cache(128);

if (auto v = cache.get(id)) {
    use(*v);
} else {
    auto v = load_from_flash_or_net(id);  // 慢路径
    cache.put(id, v);
    use(v);
}

和 Buffer 的差别在调用语义上:

  • Buffer:push/pop,数据一般用过就消费掉
  • Cache:get/put,数据可反复命中;满了按策略淘汰(LRU),丢了还能再算/再读

嵌入式 MCU 上软件 LRU 相对少见(RAM 紧);更常见于 Linux 应用、协议解析结果、配置表。MCU 岗被问到 cache,多数仍是 DMA + D-Cache 一致性


三、内存池:怎么存、怎么调(C++ 最常用)

方案 1:固定对象池 + 空闲链表(嵌入式/面试最常手写)

存: 一大块静态存储 + 每个槽位要么是对象,要么串在 freelist 上。

cpp 复制代码
#include <cstddef>
#include <new>
#include <bitset>

template <typename T, std::size_t N>
class ObjectPool {
    alignas(T) unsigned char storage_[N * sizeof(T)];
    T* free_list_ = nullptr;
    std::bitset<N> used_{};

    T* slot(std::size_t i) {
        return reinterpret_cast<T*>(storage_ + i * sizeof(T));
    }

public:
    ObjectPool() {
        // 启动时把所有槽串成 freelist
        for (std::size_t i = 0; i < N; ++i) {
            auto* p = slot(i);
            *reinterpret_cast<T**>(p) = free_list_;
            free_list_ = p;
        }
    }

    template <typename... Args>
    T* alloc(Args&&... args) {
        if (!free_list_) return nullptr;
        T* p = free_list_;
        free_list_ = *reinterpret_cast<T**>(p);
        std::size_t i = static_cast<std::size_t>(
            (reinterpret_cast<unsigned char*>(p) - storage_) / sizeof(T));
        used_.set(i);
        return new (p) T(std::forward<Args>(args)...);  // 定位 new
    }

    void free(T* p) {
        if (!p) return;
        p->~T();
        std::size_t i = static_cast<std::size_t>(
            (reinterpret_cast<unsigned char*>(p) - storage_) / sizeof(T));
        used_.reset(i);
        *reinterpret_cast<T**>(p) = free_list_;
        free_list_ = p;
    }
};

调法:

cpp 复制代码
struct Packet { int id; uint8_t data[64]; };

ObjectPool<Packet, 32> pool;

Packet* pkt = pool.alloc();
if (!pkt) { /* 池耗尽 */ }
pkt->id = 1;
// 传给队列:传指针,不拷贝大块
queue.push(pkt);

// 消费完
Packet* p = queue.pop();
process(p);
pool.free(p);

为什么常用: O(1)、无碎片、时间确定;适合包、消息、传感器块。


方案 2:C++17 std::pmr 池(现代 C++ / Linux 应用最「标准」)

存: 上游给一块 buffer,或让 pool_resource 自己向上游要内存;下游用 polymorphic_allocator

cpp 复制代码
#include <memory_resource>
#include <vector>
#include <string>

void demo() {
    std::byte buf[4096];
    std::pmr::monotonic_buffer_resource arena(buf, sizeof(buf));
    // 只进不出的竞技场:适合「一阶段算完整体丢弃」

    std::pmr::unsynchronized_pool_resource pool(&arena);
    // 按尺寸分桶复用;单线程用 unsynchronized_*

    std::pmr::vector<std::pmr::string> msgs(&pool);
    msgs.emplace_back("hello");
    msgs.emplace_back("world");
    // 用完:arena 析构或 release,整块回收
}

调法: 容器/字符串构造时传入 &pool;少直接 new/delete

MCU 裸机: 有时没有完整 pmr 或成本偏高,面试仍常答「手写 ObjectPool / arena」。


方案 3:Bump / Arena(只分配不单个释放)

cpp 复制代码
class Arena {
    char* base_;
    char* cur_;
    char* end_;
public:
    Arena(char* b, std::size_t n) : base_(b), cur_(b), end_(b + n) {}

    void* alloc(std::size_t n, std::size_t align = alignof(std::max_align_t)) {
        auto addr = reinterpret_cast<std::uintptr_t>(cur_);
        auto aligned = (addr + align - 1) & ~(align - 1);
        char* p = reinterpret_cast<char*>(aligned);
        if (p + n > end_) return nullptr;
        cur_ = p + n;
        return p;
    }

    void reset() { cur_ = base_; }  // 整帧/整请求结束一次性清
};

适合:解析一帧临时对象、一局游戏临时数据------和「对象池反复借还」不同


四、对照表(怎么选、怎么调)

东西 怎么存 怎么调 C++ 最常见写法
单缓冲 一块 array write → process → clear 静态数组
双缓冲 buf[2] write_buf / publish / try_read Ping-pong 类
环形 array + head/tail push / pop 手写 RingBuffer
队列 queue/deque + 锁 push / pop(可阻塞) BlockingQueue
硬件 Cache 普通内存(CPU 管) 正常访问 + DMA 时 clean/invalidate CMSIS/HAL
软件 Cache map + list get / put LRU
对象池 静态槽位 + freelist alloc / free ObjectPool
标准池 pmr resource 分配器传给容器 std::pmr::*

五、一条「组合拳」(嵌入式最常落地)

text 复制代码
ObjectPool<Packet>  ------ 管「块」的借还
RingBuffer / Queue<Packet*> ------ 管「谁先谁后、ISR→任务」
业务任务 pop 指针 → 用完 pool.free

有 D-Cache 时:DMA 用的那块 buffer 再单独做对齐 + clean/invalidate;池和环形解决调度与归属,Cache 维护解决 CPU/DMA 看见的数据是否同一份。


如果你要往面试深度再抠,下一步最有用的是二选一:

(1)环形满空判定 + 无锁内存序为什么这样写 ,或 (2)对象池 freelist 在 ISR 里能不能用、要加什么保护。你想先啃哪块?

相关推荐
阿米亚波2 小时前
【C/C++包管理器】vcpkg(by microsoft)
c语言·c++·git·vscode·microsoft·github·vcpkg
SNAKEpc121383 小时前
OpenGL(十一)- 变换管线
c语言·c++·算法·矩阵·图形渲染
牢姐与蒯3 小时前
c++高级数据结构之图(基本概念,存储结构,BFS,DFS,最小生成树)
数据结构·c++·
c238563 小时前
MySQL 基础用法(下):查询进阶与核心特性
android·c语言·c++·mysql
码匠许师傅4 小时前
【C++ 面试真题】 C++ 中的 static 有什么作用?
c++·面试
郭涤生4 小时前
C++ 零拷贝(Zero-Copy)
linux·开发语言·c++
乐观勇敢坚强的老彭4 小时前
计算机内存中的堆和栈
c++
间歇性努力持续性发呆的野生快乐选手5 小时前
dfs回溯,bfs枚举,完全背包
c++·算法·深度优先·宽度优先
码工许师傅6 小时前
一文读懂《Effective C++ 第三版》的55条黄金法则
c++