一文帶你搞懂C++中的流量控制
流量控制
一般并發(fā)系統(tǒng)有對應(yīng)處理請求的最大能力,這里稱最大qps,也需要有閾值設(shè)置,如果超過最大qps,則可能導(dǎo)致系統(tǒng)不穩(wěn)定,產(chǎn)生雪崩效應(yīng),甚至連鎖反應(yīng)。
限流可以認為服務(wù)降級的一種,限流就是限制系統(tǒng)的輸入和輸出流量已達到保護系統(tǒng)的目的。一般來說系統(tǒng)的吞吐量是可以被測算的,為了保證系統(tǒng)的穩(wěn)定運行,一旦達到的需要限制的閾值,就需要限制流量并采取一些措施以完成限制流量的目的。比如:部分拒絕處理。
這也是最常見的場景,流控是為了保護下游有限的資源不被流量沖垮,保證服務(wù)的可用性,一般允許流控的閾值有一定的彈性,偶爾的超量訪問是可以接受的。
有的場景下,流控服務(wù)于收費模式,比如某些云廠商會對調(diào)用 API 的頻次進行計費。既然涉及到錢,一般不允許有超出閾值的調(diào)用量。
1 固定窗口
流控是為了限制指定時間間隔內(nèi)能夠允許的訪問量,因此,最直觀的思路就是基于一個給定的時間窗口,維護一個計數(shù)器用于統(tǒng)計訪問次數(shù),然后實現(xiàn)以下規(guī)則:
- 如果訪問次數(shù)小于閾值,則代表允許訪問,訪問次數(shù) +1。
- 如果訪問次數(shù)超出閾值,則限制訪問,訪問次數(shù)不增。
- 如果超過了時間窗口,計數(shù)器清零,并重置清零后的首次成功訪問時間為當(dāng)前時間。這樣就確保計數(shù)器統(tǒng)計的是最近一個窗口的訪問量。
代碼實現(xiàn)
#include <chrono> #include <cstdint> #include <cstdio> #include <ctime> #include <thread> const int NS_PER_SECOND = 1e9; // 獲取當(dāng)前時間戳, 單位納秒 int64_t Now() { struct timespec ts; clock_gettime(CLOCK_MONOTONIC, &ts); return ts.tv_sec * NS_PER_SECOND + ts.tv_nsec; } class RateLimiter { // 時間窗口, 單位秒 int64_t window_; // 時間窗口內(nèi)最大允許的閾值 int64_t threshold_; // 當(dāng)前時間窗口的起始時間 int64_t start_time_ = Now(); // 計數(shù)器 int64_t counter_; public: RateLimiter(int64_t window, int64_t threshold) : window_(window), threshold_(threshold) {} /** * @param permits 配額數(shù)量 * @return 申請成功則返回true,否則返回false */ bool tryAcquire(int permits) { long now = Now(); if (now - start_time_ > window_ * NS_PER_SECOND) { counter_ = 0; start_time_ = now; } if (counter_ + permits <= threshold_) { counter_ += permits; return true; } else { return false; } } }; int main() { // 限流300MB/s RateLimiter limiter(1, 300 * 1024 * 1024); // 每次請求256KB int64_t request_size = 256 * 1024; // 每隔約1ms發(fā)起2次請求 // 每隔1s打印一次請求數(shù)據(jù)量 int64_t start_time = Now(); int64_t last_time = start_time; int64_t last_size = 0; while (true) { int64_t now = Now(); if (now - last_time >= NS_PER_SECOND) { printf("time: %lf s, request size: %lf MB\n", (now - last_time) / 1e9, (last_size / 1024.0 / 1024.0)); last_time = now; last_size = 0; } for (int i = 0; i < 2; ++i) { if (limiter.tryAcquire(request_size)) { last_size += request_size; } } std::this_thread::sleep_for(std::chrono::milliseconds(1)); } return 0; }
注意: 示例為單線程,多線程需要考慮線程安全問題。
臨界突變問題
固定窗口的流控實現(xiàn)非常簡單,以 1 分鐘允許 100 次訪問為例,如果流量均勻保持 200 次/分鐘的訪問速率,系統(tǒng)的訪問量曲線大概是這樣的(按分鐘清零):
但如果流量并不均勻,假設(shè)在時間窗口開始時刻 0:00 有幾次零星的訪問,一直到 0:50 時刻,開始以 10 次/秒的速度請求,就會出現(xiàn)這樣的訪問量圖線:
在臨界的 20 秒內(nèi)(0:50~1:10)系統(tǒng)承受的實際訪問量是 200 次,換句話說,最壞的情況下,在窗口臨界點附近系統(tǒng)會承受 2 倍的流量沖擊,這就是固定窗口不能解決的臨界突變問題。
2 滑動窗口
如何解決固定窗口算法的臨界突變問題?既然一個窗口統(tǒng)計的精度低,那么可以把整個大的時間窗口切分成更細粒度的子窗口,每個子窗口獨立統(tǒng)計。同時,每過一個子窗口大小的時間,就向右滑動一個子窗口。這就是滑動窗口算法的思路。
如上圖所示,將一分鐘的時間窗口切分成 6 個子窗口,每個子窗口維護一個獨立的計數(shù)器用于統(tǒng)計 10 秒內(nèi)的訪問量,每經(jīng)過 10s,時間窗口向右滑動一格。
回到固定窗口出現(xiàn)臨界跳變的例子,結(jié)合上面的圖再看滑動窗口如何消除臨界突變。如果 0:50 到 1:00 時刻(對應(yīng)灰色的格子)進來了 100 次請求,接下來 1:00~1:10 的 100 次請求會落到黃色的格子中,由于算法統(tǒng)計的是 6 個子窗口的訪問量總和,這時候總和超過設(shè)定的閾值 100,就會拒絕后面的這 100 次請求。
(代碼實現(xiàn)請參考 Sentinel)
精度問題
現(xiàn)在思考這么一個問題:滑動窗口算法能否精準(zhǔn)地控制任意給定時間窗口 T 內(nèi)的訪問量不大于 N?
答案是否定的,還是將 1 分鐘分成 6 個 10 秒大小的子窗口的例子,假設(shè)請求的速率現(xiàn)在是 20 次/秒,從 0:05 時刻開始進入,那么在 0:050:10 時間段內(nèi)會放進 100 個請求,同時接下來的請求都會被限流,直到 1:00 時刻窗口滑動,在 1:001:05 時刻繼續(xù)放進 100 個請求。如果把 0:05~1:05 看作是 1 分鐘的時間窗口,那么這個窗口內(nèi)實際的請求量是 200,超出了給定的閾值 100。
如果要追求更高的精度,理論上只需要把滑動窗口切分得更細。像 Sentinel 中就可以通過修改單位時間內(nèi)的采樣數(shù)量 sampleCount 值來設(shè)置精度,這個值一般根據(jù)業(yè)務(wù)的需求來定,以達到在精度和內(nèi)存消耗之間的平衡。
平滑度問題
使用滑動窗口算法限制流量時,我們經(jīng)常會看到像下面一樣的流量曲線。
突發(fā)的大流量在窗口開始不久就直接把限流的閾值打滿,導(dǎo)致剩余的窗口內(nèi)所有請求都無法通過。在時間窗口的單位比較大時(例如以分為單位進行流控),這種問題的影響就比較大了。在實際應(yīng)用中我們要的限流效果往往不是把流量一下子掐斷,而是讓流量平滑地進入系統(tǒng)當(dāng)中。
3 漏桶
滑動窗口無法很好地解決平滑度問題,再回過頭看我們對于平滑度的訴求,當(dāng)流量超過一定范圍后,我們想要的效果不是一下子切斷流量,而是將流量控制在系統(tǒng)能承受的一定的速度內(nèi)。假設(shè)平均訪問速率為 v, 那我們要做的流控其實是流速控制,即控制平均訪問速率 v ≤ N / T。
在網(wǎng)絡(luò)通信中常常用到漏桶算法來實現(xiàn)流量整形。漏桶算法的思路就是基于流速來做控制。想象一下上學(xué)時經(jīng)常做的水池一邊抽水一邊注水的應(yīng)用題,把水池換成水桶(還是底下有洞一注水就開始漏的那種),把請求看作是往桶里注水,桶底漏出的水代表離開緩沖區(qū)被服務(wù)器處理的請求,桶口溢出的水代表被丟棄的請求。在概念上類比:
- 最大允許請求數(shù) N :桶的大小
- 時間窗口大小 T :一整桶水漏完的時間
- 最大訪問速率 V :一整桶水漏完的速度,即 N/T
- 請求被限流 :桶注水的速度比漏水的速度快,最終桶滿了, 裝不了水
假設(shè)起始時刻桶是空的,每次訪問都會往桶里注入一單位體積的水量,那么當(dāng)我們以小于等于 N/T 的速度往桶里注水時,桶內(nèi)的水就永遠不會溢出。反之,一旦實際注水速度超過漏水速度,桶里就會積累越來越多的水,直到桶滿。同時漏水的速度永遠被控制在 N/T 以內(nèi),這就實現(xiàn)了平滑流量的目的。
漏桶算法的訪問速率曲線如下:
附上一張網(wǎng)上常見的漏桶算法原題圖:
代碼實現(xiàn) LeakyBucket
class LeakyBucket { int64_t last_time_ = Now(); int64_t rate_; int64_t debt_; // 精度 int64_t precision_; int64_t quota_; public: LeakyBucket(int64_t rate, double precision = 1000) : rate_(rate), precision_(precision) { quota_ = rate_ / precision_; } bool tryAcquire(int permits) { int64_t now = Now(); int64_t pay_off = (now - last_time_) * rate_ / NS_PER_SECOND; int64_t left_debt = std::max(0LL, debt_ - pay_off); if (left_debt > quota_) { return false; } debt_ = left_debt + permits; last_time_ = now; return true; } };
class LeakyBucket { int64_t last_time_ = Now(); int64_t rate_; int64_t debt_; // 精度 int64_t precision_; int64_t quota_; public: LeakyBucket(int64_t rate, double precision = 1000) : rate_(rate), precision_(precision) { quota_ = rate_ / precision_; } bool tryAcquire(int permits) { int64_t now = Now(); int count = (now - last_time_) / (NS_PER_SECOND / precision_); int64_t pay_off = count * quota_; int64_t left_debt = std::max(0LL, debt_ - pay_off); if (left_debt > quota_) { return false; } debt_ = left_debt + permits; last_time_ += count * (NS_PER_SECOND / precision_); return true; } };
漏桶的問題
漏桶的優(yōu)勢在于能夠平滑流量,如果流量不是均勻的,那么漏桶算法與滑動窗口算法一樣無法做到真正的精確控制。
雖然可以通過限制桶大小的方式使得訪問量控制在 N 以內(nèi),但這樣做的副作用是流量在還未達到限制條件就被禁止。
還有一個隱含的約束是,漏桶漏水的速度最好是一個整數(shù)值(即容量 N 能夠整除時間窗口大小 T ),否則在計算剩余水量時會有些許誤差。
4 令牌桶
漏桶模型中,請求來了是往桶里注水; 令牌桶的思想則完全相反,把請求放行變成從桶里抽水,對應(yīng)的,把注水看作是補充系統(tǒng)可承受流量。
令牌桶算法的原理是系統(tǒng)以恒定的速率往桶里放入令牌,令牌桶有一個容量,當(dāng)令牌桶滿了,再向桶里放的令牌會被丟棄;當(dāng)一個請求需要被處理,需要從令牌桶中取出一個令牌,如果此時令牌桶中沒有令牌可取,那么拒絕該請求。
代碼實現(xiàn)
class TokenBucket { public: TokenBucket(int64_t capacity, int64_t rate) : capacity_(capacity), rate_(rate), supply_unit_time_(NS_PER_SECOND / rate) {} // 嘗試獲取令牌 bool tryAcquire(int permits) { int64_t cur = Now(); int64_t new_tokens = (cur - last_time_) / supply_unit_time_; // 更新補充時間, 不能直接=cur, 否則會導(dǎo)致時間丟失 last_time_ += new_tokens * supply_unit_time_; tokens_ = std::min(capacity_, tokens_ + new_tokens); if (tokens_ >= permits) { tokens_ -= permits; return true; } else { return false; } } private: // 當(dāng)前桶內(nèi)的令牌數(shù) int64_t tokens_ = 0; // 上次補充令牌的時間(單位納秒) int64_t last_time_ = Now(); // 令牌桶大小 int64_t capacity_; // 補充令牌的速率 int64_t rate_; // 補充令牌的單位時間 const int64_t supply_unit_time_; };
class TokenBucket { public: TokenBucket(int64_t capacity, int64_t rate) : capacity_(capacity), rate_(rate), supply_unit_time_(NS_PER_SECOND / rate) {} // 嘗試獲取令牌 bool tryAcquire(int permits) { int64_t now = Now(); int64_t count = (now - last_time_) / supply_unit_time_; int64_t new_tokens = count * supply_unit_time_ * rate_ / NS_PER_SECOND; // 更新補充時間, 不能直接=now, 否則會導(dǎo)致時間丟失 last_time_ += count * supply_unit_time_; tokens_ = std::min(capacity_, tokens_ + new_tokens); if (tokens_ >= permits) { tokens_ -= permits; return true; } else { return false; } } private: // 當(dāng)前桶內(nèi)的令牌數(shù) int64_t tokens_ = 0; // 上次補充令牌的時間(單位納秒) int64_t last_time_ = Now(); // 令牌桶大小 int64_t capacity_; // 補充令牌的速率 int64_t rate_; // 補充令牌的單位時間 const int64_t supply_unit_time_ = NS_PER_US; };
class DynamicTokenBucket { double zeroTime_; public: explicit DynamicTokenBucket(double zeroTime = 0) noexcept : zeroTime_(zeroTime) {} bool tryAcquire(double permits, double rate, double burstSize, int64_t now = Now()) { double tokens = std::min((now / 1e9 - zeroTime_) * rate, burstSize); if (tokens < permits) { return false; } tokens -= permits; zeroTime_ = now / 1e9 - tokens / rate; return true; } }; class TokenBucket { DynamicTokenBucket tokenBucket_; double rate_; double burstSize_; public: TokenBucket(double rate, double burstSize, double zeroTime = 0) noexcept : tokenBucket_(zeroTime), rate_(rate), burstSize_(burstSize) {} bool tryAcquire(double permits, int64_t now = Now()) { return tokenBucket_.tryAcquire(permits, rate_, burstSize_, now); } };
漏桶、令牌桶的區(qū)別
雖然兩者本質(zhì)上只是反轉(zhuǎn)了一下,不過在實際使用中,適用的場景稍有差別:
1)漏桶:用于控制網(wǎng)絡(luò)中的速率。在該算法中,輸入速率可以變化,但輸出速率保持恒定。常常配合一個 FIFO 隊列使用。
2)令牌桶:按照固定速率往桶中添加令牌,允許輸出速率根據(jù)突發(fā)大小而變化。
舉個例子,一個系統(tǒng)限制 60 秒內(nèi)的最大訪問量是 60 次,換算速率是 1 次/秒,如果在一段時間內(nèi)沒有訪問量,那么對漏桶而言此刻是空的?,F(xiàn)在,一瞬間涌入 60 個請求,那么流量整形后,漏桶會以每秒 1 個請求的速度,花上 1 分鐘將 60 個請求漏給下游。換成令牌桶的話,則是從令牌桶中一次性取走 60 個令牌,一下子塞給下游。
5 滑動日志
一般情況下,上述的算法已經(jīng)能很好地用于大部分實際應(yīng)用場景了,很少有場景需要真正完全精確的控制(即任意給定時間窗口T內(nèi)請求量不大于 N )。如果要精確控制的話,我們需要記錄每一次用戶請求日志,當(dāng)每次流控判斷時,取出最近時間窗口內(nèi)的日志數(shù),看是否大于流控閾值。這就是滑動日志的算法思路。
設(shè)想某一個時刻 t 有一個請求,要判斷是否允許,我們要看的其實是過去 t - N 時間段內(nèi)是否有大于等于 N 個請求被放行,因此只要系統(tǒng)維護一個隊列 q,里面記錄每一個請求的時間,理論上就可以計算出從 t - N 時刻開始的請求數(shù)。
考慮到只需關(guān)心當(dāng)前時間之前最長 T 時間內(nèi)的記錄,因此隊列 q 的長度可以動態(tài)變化,并且隊列中最多只記錄 N 條訪問,因此隊列長度的最大值為 N。
滑動日志與滑動窗口非常像,區(qū)別在于滑動日志的滑動是根據(jù)日志記錄的時間做動態(tài)滑動,而滑動窗口是根據(jù)子窗口的大小,以子窗口維度滑動。
偽代碼實現(xiàn)
算法的偽代碼表示如下:
# 初始化 counter = 0 q = [] # 請求處理流程 # 1.找到隊列中第一個時間戳>=t-T的請求,即以當(dāng)前時間t截止的時間窗口T內(nèi)的最早請求 t = now start = findWindowStart(q, t) # 2.截斷隊列,只保留最近T時間窗口內(nèi)的記錄和計數(shù)值 q = q[start, q.length - 1] counter -= start # 3.判斷是否放行,如果允許放行則將這次請求加到隊列 q 的末尾 if counter < threshold push(q, t) counter++ # 放行 else # 限流
findWindowStart 的實現(xiàn)依賴于隊列 q 使用的數(shù)據(jù)結(jié)構(gòu),以簡單的數(shù)組為例,可以使用二分查找等方式。后面也會看到使用其他數(shù)據(jù)結(jié)構(gòu)如何實現(xiàn)。
如果用數(shù)組實現(xiàn),一個難點可能是如何截斷一個隊列,一種可行的思路是使用一組頭尾指針 head 和 tail 分別指向數(shù)組中最近和最早的有效記錄索引來解決, findWindowStart 的實現(xiàn)就變成在 tail 和 head 之間查找對應(yīng)元素。
復(fù)雜度問題
雖然算法解決了精確度問題,但代價也是顯而易見的。
首先,我們要保存一個長度最大為 N 的隊列,這意味著空間復(fù)雜度達到 O(N),如果要針對不同的 key 做流控,那么空間上會占用更多。當(dāng)然,可以對不活躍 key 的隊列進行復(fù)用來降低內(nèi)存消耗。
其次,我們需要在隊列中確定時間窗口,即通過 findWindowStart 方法尋找不早于當(dāng)前時間戳 t - N 的請求記錄。以二分查找為例,時間復(fù)雜度是 O(logN)。
總結(jié)
這里按我的個人理解總結(jié)了上述幾種流控算法的復(fù)雜度和適用場景。
算法 | 時間復(fù)雜度 | 空間復(fù)雜度 | 適用場景 |
---|---|---|---|
固定窗口 | O(1) | O(1) | 容易實現(xiàn),適用于一些簡單的流控場景,流量比較均勻,或者允許臨界突變 |
滑動窗口 | O(1) | O(M) - M為子窗口數(shù) | 適用大多數(shù)場景,可以通過調(diào)節(jié)采樣子窗口數(shù)來平衡開銷 |
漏桶算法 | O(1) | O(1) | 要求輸出速率恒定的場景,能夠平滑流量 |
令牌桶算法 | O(1) | O(1) | 與漏桶類似,區(qū)別在于允許一定的突發(fā)流量 |
滑動日志 | O(log(N)) 取決于選擇的數(shù)據(jù)結(jié)構(gòu) | O(N) - N為時間窗口內(nèi)允許的最大請求量 | 要求完全精確的控制,保證任意T時刻內(nèi)流量不超過N,高時間和高空間復(fù)雜度,性能最差 |
完整代碼
#include <chrono> #include <cstdint> #include <cstdio> #include <ctime> #include <thread> const int NS_PER_SECOND = 1e9; const int NS_PER_MS = 1e6; const int NS_PER_US = 1e3; // 獲取當(dāng)前時間戳, 單位納秒 int64_t Now() { struct timespec ts; clock_gettime(CLOCK_MONOTONIC, &ts); return ts.tv_sec * NS_PER_SECOND + ts.tv_nsec; } class RateLimiter { // 時間窗口, 單位秒 int64_t window_; // 時間窗口內(nèi)最大允許的閾值 int64_t threshold_; // 當(dāng)前時間窗口的起始時間 int64_t start_time_ = Now(); // 計數(shù)器 int64_t counter_; public: RateLimiter(int64_t window, int64_t threshold) : window_(window), threshold_(threshold) {} bool tryAcquire(int permits) { long now = Now(); if (now - start_time_ > window_ * NS_PER_SECOND) { counter_ = 0; start_time_ = now; } if (counter_ + permits <= threshold_) { counter_ += permits; return true; } else { return false; } } }; class LeakyBucket { int64_t last_time_ = Now(); int64_t rate_; int64_t debt_; // 精度 int64_t precision_; int64_t quota_; public: LeakyBucket(int64_t rate, double precision = 1000) : rate_(rate), precision_(precision) { quota_ = rate_ / precision_; } bool tryAcquire(int permits) { int64_t now = Now(); int count = (now - last_time_) / (NS_PER_SECOND / precision_); int64_t pay_off = count * quota_; int64_t left_debt = std::max(0LL, debt_ - pay_off); if (left_debt > quota_) { return false; } debt_ = left_debt + permits; last_time_ += count * (NS_PER_SECOND / precision_); return true; } }; class TokenBucket { public: TokenBucket(int64_t capacity, int64_t rate) : capacity_(capacity), rate_(rate), supply_unit_time_(NS_PER_SECOND / rate) {} // 嘗試獲取令牌 bool tryAcquire(int permits) { int64_t now = Now(); int64_t count = (now - last_time_) / supply_unit_time_; int64_t new_tokens = count * supply_unit_time_ * rate_ / NS_PER_SECOND; // 更新補充時間, 不能直接=now, 否則會導(dǎo)致時間丟失 last_time_ += count * supply_unit_time_; tokens_ = std::min(capacity_, tokens_ + new_tokens); if (tokens_ >= permits) { tokens_ -= permits; return true; } else { return false; } } private: // 當(dāng)前桶內(nèi)的令牌數(shù) int64_t tokens_ = 0; // 上次補充令牌的時間(單位納秒) int64_t last_time_ = Now(); // 令牌桶大小 int64_t capacity_; // 補充令牌的速率 int64_t rate_; // 補充令牌的單位時間 const int64_t supply_unit_time_ = NS_PER_US; }; int main() { // 限流300MB/s // RateLimiter limiter(1, 300 * 1024 * 1024); // LeakyBucket limiter(300 * 1024 * 1024); TokenBucket limiter(1024 * 1024, 300 * 1024 * 1024); // 每次請求256KB int64_t request_size = 256 * 1024; // 每隔約1ms發(fā)起2次請求 // 每隔1s打印一次請求數(shù)據(jù)量 int64_t start_time = Now(); int64_t last_time = start_time; int64_t last_size = 0; while (true) { int64_t now = Now(); if (now - last_time >= NS_PER_SECOND) { printf("time: %lf s, request size: %lf MB\n", (now - last_time) / 1e9, (last_size / 1024.0 / 1024.0)); last_time = now; last_size = 0; } for (int i = 0; i < 2; ++i) { if (limiter.tryAcquire(request_size)) { last_size += request_size; } } std::this_thread::sleep_for(std::chrono::milliseconds(1)); } return 0; }
到此這篇關(guān)于一文帶你搞懂C++中的流量控制的文章就介紹到這了,更多相關(guān)C++流量控制內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
ubuntu系統(tǒng)下C++調(diào)用matlab程序的方法詳解
學(xué)習(xí)c++與matlab混合編程一般是通過c++調(diào)用matlab函數(shù),因為matlab具有強大的數(shù)學(xué)函數(shù)庫,然而vc++具有界面設(shè)計靈活的優(yōu)點,下面這篇文章主要給大家介紹了關(guān)于在ubuntu系統(tǒng)下C++調(diào)用matlab程序的方法,需要的朋友可以參考下。2017-08-08C語言數(shù)據(jù)結(jié)構(gòu)之雙向循環(huán)鏈表的實例
這篇文章主要介紹了C語言數(shù)據(jù)結(jié)構(gòu)之雙向循環(huán)鏈表的實例的相關(guān)資料,需要的朋友可以參考下2017-06-06C語言實現(xiàn)BMP圖像處理(彩色圖轉(zhuǎn)灰度圖)
這篇文章主要為大家詳細介紹了C語言實現(xiàn)BMP圖像處理,彩色圖轉(zhuǎn)灰度圖,文中示例代碼介紹的非常詳細,具有一定的參考價值,感興趣的小伙伴們可以參考一下2021-10-10C++ Boost Serialization庫超詳細獎金額
Boost是為C++語言標(biāo)準(zhǔn)庫提供擴展的一些C++程序庫的總稱。Boost庫是一個可移植、提供源代碼的C++庫,作為標(biāo)準(zhǔn)庫的后備,是C++標(biāo)準(zhǔn)化進程的開發(fā)引擎之一,是為C++語言標(biāo)準(zhǔn)庫提供擴展的一些C++程序庫的總稱2022-12-12C語言的getc()函數(shù)和gets()函數(shù)的使用對比
這篇文章主要介紹了C語言的getc()函數(shù)和gets()函數(shù)的使用對比,從數(shù)據(jù)流中一個是讀取字符一個是讀取字符串,需要的朋友可以參考下2015-08-08