環形緩沖區(Ring Buffer)是一種常見的用于數據流緩沖的結構,通常用于生產者-消費者模型、音視頻處理等場景。
因為環形緩沖區使用的場景大多為性能敏感的場景,我們采用數組的數據結構和位運算來實現,以提高代碼效率。位運算的效率要高于模運算,但是用位運算替代模運算的前提是緩沖區的大小必須為 2 的整數次冪,因為對于 2 的冪來說,模運算就是屏蔽高位,這個在下面展示代碼的時候細說。
因為要適配不同的類型,在頭文件中使用模板,由于模板類和模板函數是在使用時才實例化的,編譯器需要在包含模板的地方就能看到其完整實現,如果編譯器看不到它的實現,在鏈接時就會報錯(undefined reference),通常不能將模板的實現寫在.cpp
文件中。但是也不推薦把模板類的聲明和定義全寫在.h
文件里,推薦的方式是.h + .tpp
的方式,這樣可以分離接口與實現,提高可讀性,也可以避免不必要的重復編譯——如果都寫在.h
文件中,每次這個頭文件被#include
,就會重新編譯一遍模板定義,編譯時間會變長。
環形緩沖區類的頭文件 RingBuffer.h
#ifndef RINGBUFFER_H
#define RINGBUFFER_Htemplate<typename T, size_t Capacity>
class RingBuffer {static_assert((Capacity & (Capacity - 1)) == 0, "Capacity must be a power of 2");public:RingBuffer();bool push(const T& item);bool pop(T& item);bool empty() const;bool full() const;size_t size() const;void reset();private:T buffer_[Capacity];size_t head_;size_t tail_;bool full_;
};#include "RingBuffer.tpp"#endif
環形緩沖區類的實現部分RingBuffer.tpp
:
#ifndef RINGBUFFER_TPP
#define RINGBUFFER_TPPtemplate<typename T, size_t Capacity>
RingBuffer<T, Capacity>::RingBuffer(): head_(0),tail_(0),full_(false) {}template<typename T, size_t Capacity>
bool RingBuffer<T, Capacity>::push(const T &item) {if (full_) return false;buffer_[head_] = item;head_ = (head_ + 1) & (Capacity - 1);if (head_ == tail_) full_ = true;return true;
}template<typename T, size_t Capacity>
bool RingBuffer<T, Capacity>::pop(T &item) {if (empty()) return false;item = buffer_[tail_];tail_ = (tail_ + 1) & (Capacity - 1);full_ = false;return true;
}template<typename T, size_t Capacity>
bool RingBuffer<T, Capacity>::empty() const {return (!full_ && (head_ == tail_));
}template<typename T, size_t Capacity>
bool RingBuffer<T, Capacity>::full() const {return full_;
}template<typename T, size_t Capacity>
size_t RingBuffer<T, Capacity>::size() const {if (full_) return Capacity;if (head_ >= tail_) return head_ - tail_;return head_ + Capacity - tail_;
}template<typename T, size_t Capacity>
void RingBuffer<T, Capacity>::reset() {head_ = tail_ = 0;full_ = false;
}#endif
主函數代碼:
#include <iostream>
#include "RingBuffer.h"using namespace std;int main() {RingBuffer<int, 8> buffer;for (int i = 0; i < 7; ++i) {if (buffer.push(i))cout << "Pushed: " << i << "\n";elsecout << "Buffer full, cannot push: " << i << "\n";}int val;while (buffer.pop(val)) {cout << "Popped: " << val << "\n";cout << buffer.size() << endl;}return 0;
}
如何判斷一個數是 2 的冪?可以通過:
(Capacity & (Capacity - 1)) == 0
因為 2 的冪都是形如 1000 這樣的數字,減一后除了首位外全為 1,利用&
的位運算之后全為 0。
如何用位運算替代模運算?就是利用位運算屏蔽高位。例如用 a & (b - 1)
替代 a % b
,前提 b 是 2 的整數次冪。例如 1111 % 1000,就是把高于 000 的位數全部去掉,因此可以利用 1000 - 1 = 0111 的高位 0 來“與”掉所有的高位,因為 0 與任何數還是 0 ,1 與任何數還是數本身。例如:
15 % 8 == 7
0b1111 & 0b0111 = 0b0111
注意這里不能用移位操作( >> 或者 << ),左移和右移操作替代的是除法和乘法:
x / 2^n → x >> n
x * 2^n → x << n
環形緩沖區的頭尾初始值都是 0,符合條件時進行 push 和 pop 操作時,head_ 和 tail_ 的值都后移。
每次執行 push 操作,先檢測一下緩沖區是否是滿的,如果不滿就將數據插入頭位置,然后把頭位置后移一位,如果 head_ + 1 大于 Capacity,則觸發一次回繞,通過求余(通過模運算或者位運算)得到新的 head_。除了初始狀態head_
= tail_
= 0,full_ = false
,后續的操作中,如果head_
和tail_
的值相同,則判定現在緩沖區已滿(因為 head_ == tail_
時既可能是空也可能是滿,必須通過 full_
標志來區分)。
size() 函數在處理 head_ < tail_ 的情況時(這種情況出現在多次 push 操作使得回繞被觸發,且 pop 的操作次數少于 push 操作的時候),計算緩沖區中的數據量時需要用 Capacity 減去 head_ 和 tail_ 的差值。