當程序因為內存耗盡而拋出 std::bad_alloc
異常時,這并不意味著程序必須崩潰或停止運行。我們應該考慮“內存不足”作為一種可能正常出現的情況(“Out of memory? Business as usual.”),并設計應用程序能優雅地處理這種異常。
具體點說:
- 有些應用可能因為內存耗盡而進入死循環或卡住,表現為“不終止”。
- 這是因為程序沒有合理地捕獲和處理
std::bad_alloc
,導致無法恢復或清理資源。 - 更健壯的設計是在內存分配失敗時能捕獲異常,進行資源回收、釋放內存,甚至降級服務或重啟部分邏輯。
- 這對于內存受限或實時系統尤其重要。
換句話說,這段話強調:
遇到內存分配失敗時,程序不必驚慌失措,而應該做好異常處理,確保即使在極端條件下依然保持可控和可恢復狀態。
這是在討論和澄清內存不足異常(std::bad_alloc
)相關的問題,具體分三點:
1. What is “out of memory” and what is “bad allocation”
- **“Out of memory”**指的是系統無法滿足程序請求的內存分配請求,因為可用內存已耗盡。
- **
std::bad_alloc
**是C++標準庫中在new
操作符內存分配失敗時拋出的異常,用于通知程序分配內存失敗。
簡單來說,“out of memory”是一個系統狀態,“bad allocation”是該狀態在C++層面上的具體表現。
2. What do those who catch std::bad_alloc
do with it
- 捕獲
std::bad_alloc
的程序員通常做什么?- 嘗試釋放資源或回收內存,減輕內存壓力。
- 降級功能或通知用戶,比如提示“內存不足,部分功能不可用”。
- 日志記錄,以便后續分析。
- 優雅退出,確保程序不崩潰,保存用戶數據。
- 有些系統可能重試分配或延遲操作。
3. How common (or rare) are these applications
- 捕獲并處理
std::bad_alloc
的應用程序并不常見,特別是普通桌面或后臺程序,很多程序默認遇到內存耗盡直接終止。 - 需要高度健壯性的應用,如金融系統、嵌入式系統、服務器軟件或實時系統,更可能設計成捕獲并處理這類異常。
- 但總體來說,處理內存不足異常的程序屬于少數,且編寫難度較大。
總結:
這三點幫助理解內存不足的本質、異常處理實踐和現實中應用程序的普遍性。它鼓勵我們設計更健壯的程序,主動捕獲std::bad_alloc
,避免程序因內存耗盡而直接崩潰。
這段話講的是資源(resource)和資源獲取失敗的情況,具體點解讀如下:
1. 資源的定義和分類
- 資源 ≈ 有限的可用量的東西(根據Wikipedia和cppreference的定義)。
- 假定資源(Assumed resources):
- CPU時間、CPU核心、CPU緩存
- 網絡帶寬
- 隨機數發生器的熵
- 電力供應
- 棧內存
這些通常被系統或平臺保證或默認可用,程序不直接檢測它們是否耗盡。
- 需檢測的資源(Checked resources):
- 磁盤空間
- 硬件設備(文件描述符、套接字描述符)
- 線程、鎖
- 其他軟件資源
- 堆內存
這些資源申請可能會失敗,程序需要主動處理。
2. 資源申請失敗時的異常類型
std::system_error
:- 線程相關對象(如thread、unique_lock、shared_lock)
- 可能shared_ptr
- 網絡套接字(basic_socket)
- 圖形顯示對象(display_surface)
- 這類異常代表系統調用失敗或底層操作失敗。
std::bad_alloc
:- 主要表示內存申請失敗(堆分配失敗)。
- 發生在很多標準庫容器和資源分配中,如vector、list、map/set、string、function、shared_ptr等。
- 還包括一些更高級的組件如promise、packaged_task、正則表達式等。
總結:
- 資源不是單一的“內存”,而是多種有限系統資源。
- 資源申請失敗時,C++標準庫通過不同的異常類型來通知程序員,主要是
std::bad_alloc
和std::system_error
。 - 理解這點對編寫健壯、可靠的程序尤其重要,必須捕獲并合理處理這些異常。
這段是在解釋操作系統中“內存耗盡(Out Of Memory, OOM)”的真實含義,尤其從虛擬內存和系統管理角度來講。我們逐條分析:
1. “Memory” means page-based virtual memory
? “Memory” 指的是基于分頁的虛擬內存。
- 在現代操作系統中,每個進程看到的是一個線性的虛擬地址空間(如從
0x00000000
到0xFFFFFFFF
),而這個地址空間被**按頁(通常4KB)**劃分。 - 每頁有不同的屬性:
- 私有 or 共享(private/shared)
- 只讀 or 寫時復制寫入(COW, Copy-On-Write)
- 干凈 or 臟頁(clean/dirty)
- 常駐內存 or 已被換出(paged-out)
這意味著:你看到的“內存地址”并不代表真實物理內存位置,它是操作系統通過頁表管理的抽象。
2. “Unused memory is wasted memory”
? 操作系統會用空閑內存做緩存或緩沖,如果程序不在用,那系統就拿去干別的。
- 所以現代操作系統(如 Linux、Windows)通常會盡量填滿物理內存,用于:
- 文件緩存(page cache)
- IO buffer
- 共享庫等
- 空閑內存越少≠出問題,反而可能是系統在高效工作。
3. Commit charge
? Commit charge 指的是所有可寫但沒有文件后備的頁的總和,包括:
- 棧(stack)
- 數據段(data)
- 堆(heap)
- 私有
mmap
- 共享庫的 .GOT 表(全局偏移表)等
這些內容不是來自文件,因此一旦寫入,系統必須提供**物理內存或交換空間(swap)**來存它們。
4. When commit charge > (RAM + Swap), is it OOM?
? 當 commit charge 超過 可用RAM + 可用swap 時,是不是一定內存溢出(OOM)?
答案是:未必!
- 操作系統通常是**懶分配(lazy allocation)**的 ——
malloc
申請內存成功 ≠ 系統馬上分配物理頁。 - 真正使用(寫入)時才分配頁框(稱為“觸發頁面錯誤”)。
- 所以,可能程序申請了一堆內存,但還沒實際用 → commit charge 沒增加多少。
- 一旦你寫入太多頁,系統發現“沒有物理內存或swap能保證這些數據”時,才觸發OOM。
總結
概念 | 說明 |
---|---|
虛擬內存 | 每個進程有自己的地址空間,分頁管理 |
頁面屬性 | 可讀/可寫、共享/私有、駐留/換出 |
commit charge | 所有需要內存支持的、可寫、非文件映射頁的總量 |
OOM條件 | 當系統無法為潛在寫入頁提供內存或swap時,可能OOM |
懶分配機制 | 實際用內存(寫入頁)才會真正占用資源 |
這段是在講 Linux 下 fork()
與大內存分配之間的經典問題,特別是在 fork/exec 模式下的大程序啟動機制對內存的隱性影響。
fork/exec 問題解釋
背景:
在 Unix/Linux 上,執行新程序通常分兩步:
fork()
:父進程復制自己(內存等資源)→ 得到一個子進程exec()
:子進程替換自己,加載新程序(二進制)
然而,這個“復制”的動作有代價。
問題描述:
當你的進程分配了大量內存(比如 10GB 或 20GB),即使你只是想調用 exec()
啟動一個新程序,在這之前你必須成功調用 fork()
。
? 但
fork()
會復制整個地址空間(虛擬地址頁表) —— 這可能會因為內存不足而失敗。
為什么小程序沒問題?
- Linux 的
fork()
使用了 寫時復制(COW):- 父子共享內存頁,直到某一方嘗試寫入。
- 所以 fork() 通常 不立即復制全部內容,看起來是“便宜”的。
但!
為什么 fork()
會失敗?
即使使用了 COW,系統仍然必須保證“如果將來子進程寫入,它能支持分離寫入”,這意味著:
fork() 成功 ? 系統擁有足夠的 commit space 來承諾:
“即使你們都寫入每一頁,我也能支持”。
于是:
malloc(20GB)
成功,但未寫入fork()
失敗:系統發現你請求的20GB,再加上子進程可能的20GB,系統撐不住 → 拒絕
對比 Windows:
Windows 沒有 POSIX 的 fork()
模型,常用 CreateProcess()
:
- 一步到位加載新進程
- 不會復制當前進程的內存
- 避免了 Linux 的 fork/exec 模式下的 OOM 問題
替代方案:
方案 | 描述 | 問題 |
---|---|---|
vfork() | 暫時共享地址空間,不復制內存 | 子進程不能訪問內存(容易出錯) |
posix_spawn() | 內核級替代 fork/exec,效率高 | 語法復雜,不如 fork/exec 靈活 |
使用線程池 + 管道 | 父進程控制資源,避免復制 | 不適用于 exec |
總結核心點:
問題 | 解釋 |
---|---|
fork() 復制整個虛擬內存空間 | 即使使用了 COW,系統也要保證“萬一全寫了”也能撐住 |
大量 malloc + fork 就可能觸發 “Out of Memory” | 尤其是在總內存 + swap 不足時 |
exec() 會丟掉子進程的內存 | 但得等 fork() 成功后才能調用 exec() |
Windows 不用 fork,因此沒有這個問題 | 它直接用 CreateProcess() |
如果你在構建一個 高內存使用后臺服務,需要頻繁調用子進程,可以考慮: |
- 避免用
fork()
,改用posix_spawn()
或 worker 模型 - 把子進程啟動放在內存膨脹之前
理解 —— 這段在討論不同操作系統如何處理**內存 overcommit(過度分配)**的問題,也即:操作系統是否允許你申請比物理內存(+swap)還多的內存,以及在不夠時是否立刻失敗。
什么是 Overcommit?
Overcommit:允許進程分配的虛擬內存總量超過系統實際可用物理內存 + swap。
原因是:
實際分配 ≠ 實際使用,大多數內存分配(如 malloc)最終未被訪問。
兩種策略的對比:
嚴格 commit accounting(不允許 overcommit)
- 系統在你分配內存時就檢查是否“將來”能提供這塊內存。
- 如果不能保證,將立即返回
std::bad_alloc
/ENOMEM
。 - OS:Windows, Solaris, HP-UX 等
- fork() 更容易失敗:因為 fork() 理論上可能復制全部內存。
Overcommit + OOM killer(允許分配,必要時強制殺死進程)
- 系統允許幾乎任意分配,但一旦物理內存耗盡,會觸發 OOM 殺手。
- 被殺的進程是由內核算法決定的,不可預測。
- OS:Linux(默認使用 heuristic 模式)
Linux 的 overcommit 策略
在 Linux 中,控制 overcommit 的 sysctl 變量是:
/proc/sys/vm/overcommit_memory
可取值:
值 | 含義 |
---|---|
0 | 默認:啟發式。可能 overcommit,內核根據啟發式算法判斷 |
1 | 始終允許 overcommit(不做檢查,分配成功 ≠ 最終成功) |
2 | 嚴格限制:禁止 overcommit,分配必須有 backing store |
相關機制:
oom_adj
/oom_score_adj
:影響進程被 OOM 殺手選中的概率cgroups
:可用來對進程群組設定內存限額,配合 OOM 控制更有效rlimits
:即使在 overcommit 模式下,進程資源限制仍有效(如RLIMIT_AS
)
其他系統特色
系統 | 行為 | 說明 |
---|---|---|
AIX | 默認嚴格,但可通過 SIGDANGER opt-out | 信號通知進程“快沒內存了” |
FreeBSD | 可系統關閉 overcommit,也可進程層面控制 | 使用 protect(1) |
Linux | 默認 heuristic,可調 | 兼顧靈活性與安全性 |
補充細節
- 在 Linux 上,即使啟用了 overcommit,fork() 仍有可能失敗,如果啟用了 COW accounting(如 strict 模式
2
)。 - 在
"never"
模式下,Linux 會預留一部分內存專門給 shell/top/kill —— 以便用戶可以殺掉 OOM 進程。這是防止系統徹底卡死的一個機制。 "always"
模式能帶來性能,但也更容易導致非確定性崩潰。
總結重點
項目 | 內容 |
---|---|
“Overcommit” 是什么 | 虛擬內存分配可以超過物理內存 |
嚴格模式 | 更安全、更可預測,但 fork() 等操作容易失敗 |
寬松模式 | 更靈活但不可控,可能觸發 OOM 殺手 |
Linux 默認行為 | 啟發式(heuristic),可配置 |
fork 問題 | 和 overcommit 策略強相關(特別是 fork + 大內存) |
這段內容揭示了圍繞內存分配失敗的一些**“常見誤區(myths)”**,尤其是針對 Linux 系統上的 new
/malloc
行為。
核心觀點:別太相信“分配總成功”的說法
Myth(誤區):
“在 Linux 上,內存分配總是成功的。”
這是錯誤的泛化,很多人(包括流行庫的作者)誤認為:
new
或malloc
永遠不會失敗;- 或者,只有地址空間耗盡才會拋出
std::bad_alloc
。
舉例
- Herb Sutter 曾批判這類想法(見他的《To new, perchance to throw part 2》)。
- LevelDB issue #335:也在討論異常安全時提到:“Linux only throws
std::bad_alloc
when address space is exhausted”。
實際情況:Linux 并非總是 overcommit!
Linux 的默認設置是:
vm.overcommit_memory = 0
這是啟發式(heuristic)模式,而不是 always-overcommit。
所以在以下情況中,你的分配 可以失敗:
- 內核估算 commit charge 過高,拒絕了分配;
- 顯式切到 strict 模式(
vm.overcommit_memory = 2
); - 使用 cgroup、rlimit 等限制;
mmap()
的匿名映射在某些配置下會檢查 backing store;- fork() 由于寫時復制(COW)導致潛在的高 commit,系統提前拒絕。
誤信 myth 的后果
如果你寫的是系統級代碼、高可靠服務、或者內存敏感代碼(如數據庫、圖形引擎、瀏覽器):
- 忽視
new
失敗會導致非預期 crash; - 無法優雅處理 OOM 場景;
- 程序可能在壓力下行為不穩定、不安全。
總結
Myth | Reality |
---|---|
Linux 上 new 永遠成功 | 錯誤。Linux 的默認行為是啟發式 overcommit,有可能拒絕 |
只有地址空間耗盡才拋 std::bad_alloc | 錯誤。commit charge 超過估算也可能觸發 |
不用關心 new 是否失敗 | 錯誤。在部分配置和工作負載下,失敗是可能的而且嚴重的 |
如果你希望,我可以幫你: |
- 檢查你的 Linux 當前的 overcommit 配置;
- 寫一段 C++/C 代碼來實驗
malloc
或new
是否失敗; - 設置或查詢 overcommit 參數。
這段話闡述了一個重要安全概念:
“內存分配失敗(bad allocation)不等于內存不足(OOM)”
核心觀點
std::vector<int>(-1); // 這是壞分配 (bad allocation),不是 OOM
- 這個代碼試圖創建一個大小為
-1
的vector
(即size_t(-1)
), - 這相當于分配幾乎
2^64 - 1
個元素 —— 明顯是程序員邏輯錯誤,不是內存用光了才失敗的。
為什么這很重要?
很多安全漏洞(甚至是 CVE)都源于:
- 未經驗證的輸入
- 導致了極端的內存分配
- 最終程序崩潰或被拒服(DoS)
真實世界的例子(OOM DoS via bad allocation)
CVE | 漏洞組件 | 問題描述 |
---|---|---|
CVE-2016-2109 | OpenSSL | 惡意短編碼引起解碼器嘗試分配大量內存 |
CVE-2016-2463 | Android | 偽造媒體文件觸發異常大分配 |
CVE-2016-6170 | ISC BIND | 偽造 DNS UPDATE 消息引發 OOM |
CVE-2015-7540 | samba AD-DC | 惡意網絡包觸發異常內存使用 |
CVE-2015-1819 | libxml | 精心構造 XML 導致 OOM |
CVE-2014-3506 | OpenSSL | 偽造 DTLS 握手消息引發過度分配 |
CVE-2013-7447 | cairo | 特制圖像數據導致 cairo 分配過大緩沖區 |
正確的防御措施
- 永遠不要相信外部輸入(如
Content-Length
)。 - 對分配前的值進行 范圍檢查。
- 使用
std::vector::reserve()
或std::string::resize()
之前先確保大小合理。 - 若使用
new
,在分配前驗證:if (size > MAX_SAFE_SIZE) throw std::runtime_error("Size too big");
總結重點
項 | 內容 |
---|---|
bad_alloc ≠ OOM | 有時是由無效輸入導致的邏輯錯誤 |
不信任輸入 | 任何外部數據都可能是惡意的 |
測試不足 | 邏輯漏洞往往在單元測試中未暴露 |
CVE 案例 | 多個成熟庫都曾因此類問題中招 |
防護建議 | 輸入驗證 + 分配前檢查大小上限 |
如果你正在開發處理外部數據的系統(如網絡、圖像、音頻、XML、協議棧),請始終將輸入視為不可信。這種“簡單的長度驗證”常常是安全的第一道防線。 |
這段內容深入探討了 C 與 C++ 在處理內存分配失敗(尤其是 malloc
失敗)時的現實問題,尤其是在面對攻擊者輸入或內存耗盡的場景下。
核心問題:內存分配失敗了怎么辦?
在 C 中,如果 malloc(n_bytes)
失敗,會返回 NULL
。
但現實是 —— 你通常:
不知道如何優雅地把這個錯誤往上傳達(尤其是跨多層函數調用)。
C中的處理方式
以下是幾種現實中看到的處理策略:
方式 | 示例 | 缺點 |
---|---|---|
返回錯誤碼 | if (!ptr) return ERR_ALLOC_FAIL; | 錯誤傳播代碼繁瑣,容易漏 |
longjmp /setjmp | 跳出多層函數 | 極度易錯,破壞棧結構,調試困難 |
直接中止進程 | g_error("failed to allocate...") | 非庫友好:調用方無法恢復 |
忽略失敗 | 直接解引用 | 崩潰、漏洞、DoS |
示例代碼中 GLib 的行為: |
mem = malloc(n_bytes);
if (mem)return mem;
g_error("failed to allocate ..."); // 中止程序
C++ 如何改進?
在 C++ 中:
try {_input_buffer.resize(_isize); // 內部可能 new[],如果失敗會拋 bad_alloc
} catch (bad_alloc) {// 統一異常處理邏輯log_error(...);drop_connection();
}
C++ 優點:
- 標準庫
new
默認在失敗時拋出std::bad_alloc
- 你可以
catch
到它 - 允許你在靠近“業務邏輯”層做集中處理
- 如果你愿意,還可以
set_new_handler()
做自定義清理或回退
但即使在 C++中,也不是銀彈
- 如果沒有仔細
try-catch
,程序仍會終止 - STL 容器里隱含分配的地方多(如
vector::resize
,map::insert
) - 異常傳播對性能敏感路徑不利(如實時系統)
- 一些庫(如
absl::btree
) 可能選擇不拋異常
更好的設計方式(C/C++ 通用)
- 驗證所有外部數據 —— 特別是分配大小前:
if (_isize > MAX_SIZE) return error();
- 封裝內存分配 —— 便于統一錯誤處理:
void* safe_malloc(size_t sz) {void* p = malloc(sz);if (!p) throw bad_alloc();return p; }
- 區分可恢復 vs 致命錯誤 —— 不要總是
exit()
。
總結對比
比較點 | C | C++ |
---|---|---|
內存失敗信號 | NULL | bad_alloc 異常 |
錯誤傳播 | 手動返回碼/longjmp | try/catch 或傳遞異常 |
默認行為 | 多數庫直接 abort | 拋出異常可捕獲 |
可恢復性 | 依賴設計 | 更易于結構化恢復 |
安全隱患 | 忽略返回值、崩潰 | 忽略異常、資源泄露 |
建議
- 不管是 C 還是 C++,永遠不要直接相信輸入可安全用于分配
- 在分配前進行上限檢查
- 用 RAII、異常處理(在 C++),或者封裝錯誤檢查(在 C)來實現健壯行為
- 在大型項目中統一封裝內存分配接口
這部分深入分析了 C++ 中的內存分配失敗(std::bad_alloc
)的處理現狀,并通過真實案例展示其影響力和實際處理缺失的問題。
一次“分配”摧毀整個世界:CVE-2009-1692
這個 CVE 展示的是一種 JavaScript 濫用內存 的方式,能導致:
瀏覽器/平臺 | 行為 |
---|---|
IE, Firefox, Safari, Chrome, Opera | 內存激增后崩潰 |
Konqueror、Wii、PS3、iPhone | 整個設備掛死,需要硬重啟 |
Chrome | 僅標簽崩潰,稍好一些 |
原因:大部分 JavaScript 引擎未對異常內存使用做合理限制。例如: |
let s = "A";
while (true) s += s;
或者構造巨大的數組、字符串、對象,誘使引擎分配超過系統可承受的內存。
那么 std::bad_alloc 真有人處理嗎?
真實搜索:Debian Code Search
對 catch (std::bad_alloc)
的顯式捕獲進行統計:
- 共 341 個包、3043 處代碼。
- 抓出幾類處理方式:
| 類型 | 占比 | 行為 |
| -------------------------------- | — | ----------------------------- |
| 忽略(“Somebody else’s problem”) | 46% | 沒有做任何 meaningful 處理 |
| 轉換為錯誤碼 | 23% | 設置 error flag / 返回錯誤碼 |
| 轉為自定義異常 | 13% | 更高級錯誤管理(但仍依賴上層) |
| 轉為其他語言 OOM 異常 | 5% | Python 的PyErr_NoMemory()
,等 |
| 原樣 rethrow | 4% | 不處理,只傳播 |
結論:大多數項目并沒有有效處理bad_alloc
,即使是大型項目或庫。
深層次啟示
為什么這么少人處理?
- 罕見性:現代系統 overcommit,有 swap,
bad_alloc
變得少見。 - 難以測試:制造 OOM 非常困難,特別是在 CI 或開發機上。
- 恢復復雜:一旦分配失敗,很多對象可能已經部分構造,狀態不明。
- 設計缺陷:默認 C++ 構造過程不適合 rollback/恢復,RAII 有局限。
建議與啟發
? 如果你關心健壯性或面對不受信任輸入:
- 任何外部輸入都必須驗證分配大小上限
- try-catch
bad_alloc
是必要但不充分條件,你需要設計出清晰的 錯誤傳播路徑 - 封裝資源敏感操作,比如:
std::optional<std::vector<char>> safe_allocate(size_t n) {try {return std::vector<char>(n);} catch (const std::bad_alloc&) {return std::nullopt;}
}
- 在大型系統中考慮更強的 OOM 策略,如:
- 內存池+監控機制
- 分配失敗時釋放低優先級資源
- 限制最大內存占用
總結
- 現實中一次失控的內存分配可以導致整個設備掛死(如 CVE-2009-1692)
- 即使在現代開源軟件中,大多數
bad_alloc
是 未被優雅處理 的 - 你若做的是面向用戶、網絡、或嵌入設備的軟件,必須認真對待 OOM
- C++ 提供了異常處理機制,但不意味著它自動解決了恢復問題
這一頁繼續分析了在 C++ 中如何應對 std::bad_alloc
,并結合真實代碼倉庫中的例子展示了多種常見的應對策略。
主題總結:C++ 庫中如何“處理”分配失敗
C++ 的內存分配失敗默認拋出 std::bad_alloc
異常,但實際應用中,開發者的應對方式五花八門,本頁主要展示了兩類更“實際”的做法:
① “Convert to Error Code”(轉換為錯誤碼)——占 23%
這類庫通常采用如下策略:
- 在內部分配失敗時 catch 異常
- 然后返回某種錯誤碼(而不是讓異常繼續傳播)
示例:Notepad++ / Scintilla
try {if (_pscratchTilla->execute(SCI_GETSTATUS) != SC_STATUS_OK)throw;InsertString(position, data, length);
} catch (std::bad_alloc &) {return SC_STATUS_BADALLOC;
}
優點:調用者可以統一地檢查錯誤碼并處理,不需要處理異常
缺點:你得永遠記得去檢查返回值,否則就白做了
來源代碼:
- Scintilla Buffer.cpp
- Scintilla Document.cxx
② 置空指針 + 狀態標志(member variable update)
一些更底層的庫(如 libstdc++)不會直接拋異常,而是在失敗時設置標志位或返回空指針。
示例:libstdc++ / GCC 的 ios
stream 處理
try {_words = new (std::nothrow) _Words[_newsize];
} catch (const std::bad_alloc&) {_words = nullptr;
}
if (!_words)_M_streambuf_state |= badbit;
std::nothrow
會阻止拋出異常,失敗時返回 nullptr
狀態標志 badbit
會告訴流使用者出了問題,但不致 crash
來源代碼:
- GCC libstdc++ ios.cc
小結:這種做法的“哲學”是?
穩定優先:比起拋出異常破壞流程,不如用返回碼/狀態位盡量“穩住”
可恢復性強:尤其在嵌入式或交互式應用中,更傾向于用錯誤碼回傳錯誤
對比 catch 異常的做法:
方法 | 表現 | 優點 | 缺點 |
---|---|---|---|
拋出 bad_alloc | 直接跳出到上層 | 自動傳播 | 穩定性差,易崩 |
返回錯誤碼 | 調用者自行判斷 | 更穩健 | 容易忘記檢查 |
設置狀態位 | 延遲處理 | 不會中斷流程 | 易被忽視 |
實戰建議:
- 若你寫的是“控制邏輯類庫”,建議用 返回錯誤碼
- 若你寫的是“算法或組件庫”,可以保留
bad_alloc
異常,讓上層處理 - 若你是做 UI、嵌入式、或系統層調用,盡量別崩,catch
bad_alloc
并恢復 是最佳實踐
本頁內容要點:
繼續講 C++ 實戰中對 std::bad_alloc
的應對方式,尤其聚焦于:
③ Convert to custom exception(轉換為自定義異常)——占 13%
示例:POCO 項目中的 PostgreSQL 模塊
代碼片段摘自 Poco::Data::PostgreSQL::Binder
:
try {if (aPosition >= _bindVector.size())_bindVector.resize(aPosition + 1);InputParameter inputParameter(aFieldType, aBufferPtr, aLength);_bindVector[aPosition] = inputParameter;
} catch (std::bad_alloc&) {throw PostgreSQLException("Memory allocation error while binding");
}
解釋與動機:
這段代碼的作用是:在 PostgreSQL 參數綁定時,如果分配內存失敗,拋出特定的數據庫異常。
為什么不直接讓 bad_alloc
傳播?
std::bad_alloc
語義太籠統:只表示“內存不足”,調用者難以分辨哪里出錯- 業務邏輯希望知道:“這是數據庫層的綁定問題”,而不是其他未知的系統異常
- 拋出更語義明確的異常
PostgreSQLException
,便于上層精確捕獲和處理
通用做法:
這種模式適用于 中間件、數據庫接口、網絡通信庫 等系統:
“底層異常 + 語義變換 = 業務語境中的異常”
具體實現步驟通常是:
- 捕獲如
std::bad_alloc
這種通用異常 - 轉換成你的領域自定義異常(加上有意義的信息)
- 重新拋出給上層
自定義異常的優點:
優點 | 說明 |
---|---|
語義清晰 | 可以根據上下文區分哪一層出了問題 |
更好調試 | 帶有錯誤文本,便于記錄日志或提示用戶 |
更好管理 | 可將所有異常統一包裹成某種“領域異常” |
注意事項:
- 如果你拋出自定義異常,務必讓其繼承自
std::exception
,否則catch (std::exception&)
捕不到 - 如果你使用異常做控制流,記得別在性能關鍵路徑上濫用
總結:
這一類處理方式體現了 “異常語義升維”:把底層的問題提升成高層可以理解和應對的業務語境。
如果你在寫自己的 C++ 框架,建議在模塊邊界用這個策略,例如:
catch (const std::bad_alloc&) {throw MyApp::DatabaseError("OOM when preparing query buffer");
}
本頁內容:std::bad_alloc
的第四種處理方式
④ Rethrow as-is(原樣重新拋出)——占 4%
示例代碼片段:
m_allocations.push_back(ptr);
catch (const std::bad_alloc& e)throw e; // 顯式重新拋出異常
delete[] ptr;
catch (...) throw; // 捕獲所有異常并重新拋出
_allocator.deallocate(_instance, 1);
_instance = 0;
throw;
含義解釋:
- “Rethrow as-is” 指的是:捕獲異常只是為了清理資源,清理完成后立即重新拋出原始異常。
- 異常不轉換、不吞掉、不降級為 error code。
換句話說:
我只打掃戰場,不阻止你繼續報警。
為什么這么做?
在 C++ 中使用 RAII(資源獲取即初始化)可以自動釋放大多數資源。但 某些資源不是 RAII 管理的,比如:
- 顯式堆分配但沒有用智能指針管理的資源
- 顯卡資源、文件描述符、數據庫連接等外部資源
- 全局狀態(比如
_instance
)
這種情況下,如果直接拋異常,你可能會資源泄漏或 狀態混亂。
實用模式:
try {ptr = new int[1024];m_allocations.push_back(ptr);
} catch (const std::bad_alloc& e) {delete[] ptr; // 手動清理throw; // 原樣拋出
}
或在 Singleton 模式中:
try {_instance = _allocator.allocate(1);
} catch (const std::bad_alloc&) {_instance = nullptr;throw; // 讓調用者知道失敗原因
}
注意事項:
風險點 | 描述 |
---|---|
throw e; | 這樣會 slicing(異常對象復制)且重置 what() 棧信息。應使用 throw; 保持異常原樣 |
忘記清理 | 僅適用于你確實在 catch 中完成了非 RAII 資源的清理 |
復雜代碼中 | 若 catch block 很長,重新拋出前可能會混淆上下文邏輯,要留意可維護性 |
總結:
Rethrow-as-is 是一種 嚴謹負責的清理行為,適用于“我做了一點事,然后繼續讓上層來決定”。
優點:
- 不吞異常、不篡改語義
- 能清理局部非 RAII 資源
缺點: - 不能完全避免資源泄漏(建議配合智能指針)
- 不能添加業務語義(不像前面講的“Convert to custom exception”)
本頁重點:
“Cleanup and terminate” —— 占總樣本 21%
主要分類:
類型 | 比例 | 說明 | 代表項目 |
---|---|---|---|
Not from main | 12% | 在非主線程(非 main 函數)捕獲異常后清理并終止 | rethinkdb, ipopt, fluxbox, krita 等 |
From main | 8% | 在主線程的 main 函數里捕獲異常后清理并終止 | tripwire, smartmontools, tango 等 |
Configurable handler, default abort | 1% | 配置異常處理器,默認調用 abort | igraph, gdal 等 |
“Parachute” | <1% | 代碼中帶有“安全降落傘”風格的異常處理(參考《Code Complete》) | scantailor |
意思是什么?
- 捕獲異常后,程序不能安全繼續運行,就進行清理資源并優雅退出。
- 不是試圖“恢復”或轉換異常,而是“最后保護”,保證資源正確釋放,輸出錯誤信息,然后終止程序。
- “Not from main”通常是子線程或庫內部異常,這種情況下清理后終止避免不確定狀態。
- “From main”意味著在程序入口集中處理所有異常。
“Parachute” 異常處理:
- 來自 Steve McConnell《Code Complete》概念
- 程序中加入異常“安全網”,防止任何異常逃逸到操作系統導致崩潰。
- 保障程序的最小安全退出,保證日志和資源回收。
總結:
這種策略通常適用于:
- 對內存安全和穩定性要求極高的系統(服務器、嵌入式、圖形驅動)
- 不能容忍異常破壞狀態的程序
- 只希望在異常時盡可能干凈退出
如果你要設計 Vulkan 框架底層,也應該考慮在頂層捕獲異常,做最后清理和終止,避免崩潰時資源泄漏或硬件狀態異常。
需要我幫你寫一個 C++ 程序主入口的異常安全模板嗎?包括統一異常捕獲,日志記錄,資源釋放,安全退出。
核心要點補充:
- “Cleanup” 通常是指:
- 刪除臨時文件、鎖文件,釋放有限資源,避免“僵尸”或資源泄漏。
- 例如
unlink_ofile(oname);
刪除臨時輸出文件。
- 異常捕獲后立即清理并退出:
- 例子中捕獲
std::bad_alloc
,打印錯誤信息,退出程序。
- 例子中捕獲
- 庫內部直接崩潰(
CRASH()
)示例:- 在
fastMalloc
里,如果malloc
返回空指針,直接調用崩潰函數。 - 這種做法代價高,但防止異常傳播導致狀態不確定。
- 在
設計啟示
- 在 Vulkan 或高性能計算框架中,底層內存分配失敗可考慮:
- 統一捕獲 bad_alloc,安全清理后終止,防止狀態崩潰。
- 對臨時資源(緩沖區、鎖文件等)要有明確的釋放策略。
- 對于無法恢復的分配失敗,寧可安全崩潰,避免隱蔽錯誤。
這部分意思是:
面對 std::bad_alloc
,一些程序選擇 “繼續向前走”,采取不同的策略,而不是直接終止:
- 嘗試分配更少的內存(3%)
- 例如用更小的緩沖區,或改為即時計算,減少內存需求。
- 例子:Audacity、Eigen3、LibreOffice 等。
- 在析構函數中吞掉
bad_alloc
(3%)- 為了緩存或延遲操作,在析構時悄悄處理內存不足。
- 例子:LibreOffice、OpenCV 等。
- 使用替代算法(2%)
- 比如改用“就地”算法避免額外內存分配。
- 例子:VTK、Krita、Octave 等。
- 釋放內存(清理緩存、騰出空閑鏈表)(2%)
- 嘗試通過釋放緩存或內部資源來恢復內存。
- 例子:libstdc++、Sonic Visualizer。
- 重試分配(1%)
- 直接嘗試重新申請內存,期望短期內資源釋放后成功。
- 例子:GNU Radio。
設計啟示
- Vulkan框架設計時,可以提供多級降級策略,面對OOM:
- 優先減小數據量(如分塊計算)
- 使用更節省內存的算法路徑
- 動態清理緩存
- 甚至嘗試重試
- 只有最后才徹底失敗或終止
- 這樣能增強系統健壯性和靈活性,提升用戶體驗。
這段代碼示范了遇到內存分配失敗(std::bad_alloc
)時,優雅切換到“就地”算法(in-place algorithm),以節省內存,保證程序繼續運行。
代碼邏輯解析:
bool inline inplace_transpose(arma::Mat<eT>& X)
{try {X = arma::trans(X); // 先嘗試直接轉置(需要額外內存)return false; // 成功,返回false表示沒有用“就地”方法}catch (std::bad_alloc&) {
#if (ARMA_VERSION_MAJOR >= 4) || ((ARMA_VERSION_MAJOR == 3) && (ARMA_VERSION_MINOR >= 930))arma::inplace_trans(X, "lowmem"); // 失敗后調用就地轉置,節省內存return true; // 表示用了“就地”轉置
#endif}
}
- 先嘗試普通轉置,失敗時拋出
bad_alloc
- 捕獲異常后,切換到就地轉置,避免開辟新的內存空間
- 返回值表示是否用就地方法
啟示:
- 設計高性能或內存敏感框架時,預設“備選算法”非常重要。
- 在OOM時,盡量避免直接崩潰,切換到內存占用更小的“降級方案”。
- 對外接口上,告知調用方是否使用了備選算法,有利于調優或告警。
這部分講的是面對 std::bad_alloc
(內存分配失敗),軟件通常采取“回滾并做別的事情”的策略,避免程序崩潰,提升用戶體驗和系統穩定性。具體做法分成幾類:
主要做法:
- 交互式應用拒絕用戶操作(約12%)
- 比如打開文件失敗,彈錯誤框,提醒用戶。
- 例子:Notepad++, LibreOffice, Inkscape, TeXstudio 等。
- 服務器拒絕服務請求(約5%)
- 服務器因資源不足,直接丟棄新請求,保證現有服務穩定。
- 包括網絡服務器(ntopng, apt-cacher-ng等)和數據庫(scylladb, tarantool等)。
- 預設降級或替代方案(約1%)
- 使用默認資源代替失敗的加載,比如錯誤紋理、無聲驅動等。
- 例子:游戲0ad、模擬器desmume。
設計啟發
- 在 Vulkan 或其它底層框架設計中,應允許調用者檢測內存失敗并優雅“退回”,比如拒絕提交任務、通知用戶、加載備用資源。
- 設計接口時,支持錯誤返回或異常捕獲,允許“放棄當前操作但繼續運行”。
- 對于服務器或批處理任務,提供負載調節或請求拒絕機制,避免崩潰。
這段內容細化了“回滾并做別的事情”里交互式應用的典型處理:
- 最常見場景是文件太大,無法加載,比如
Poppler::Document::load(fileName)
失敗時拋std::bad_alloc
,然后返回錯誤狀態(PopplerErrorBadAlloc
),并返回一個空的智能指針,避免程序崩潰。 - 另一個例子是
vtkstd::bad_alloc
拋出時,用自定義異常IRISException
包裝,再通過 UI 彈窗警告用戶“生成網格時內存不足”,這樣用戶能感知錯誤且程序保持穩定。
這體現了: - 優雅地捕獲內存分配失敗異常,避免直接崩潰
- 返回錯誤狀態或拋出自定義異常
- 及時向用戶反饋錯誤信息
- 保證程序其余部分繼續穩定運行
這里講的是服務器端遇到 std::bad_alloc
時的典型處理:
- 服務器通常會捕獲異常后放棄當前請求,避免整個服務崩潰或卡死。
- 具體例子里,
processPacket(...)
可能拋出bad_alloc
,catch 捕獲后打印日志(或其他輕量級處理),然后直接跳過這條請求,繼續處理后續數據包。
這體現了服務器穩定性和容錯性的設計思路: - 服務器優先保證持續服務能力
- 某些請求內存不足導致失敗時,寧愿放棄該請求,也不影響整體系統正常運行
- 記錄日志幫助排查和監控
這部分講的是單元測試中對 std::bad_alloc
異常的捕獲和驗證:
- 單元測試(Unit Tests)會故意觸發
bad_alloc
,檢查代碼在內存分配失敗時的表現是否符合預期。 - 通過宏或斷言(如
ASSERT_THROWS_IN_TEST
)確認特定操作會拋出std::bad_alloc
。 - 這樣可以確保程序在異常情況下依然穩定或做出正確反應。
舉例來自 TBB 的測試代碼,模擬內存壓力,確認容器操作拋出異常,保證健壯性。
這一段給出了幾個領域(辦公軟件、代碼編輯器、瀏覽器、數據庫)在遇到C++內存耗盡(OOM)時的典型表現和應對狀況總結,體現了現實世界中std::bad_alloc
處理的復雜性和挑戰:
- 辦公軟件中,很多程序在大文件或大粘貼操作時崩潰或異常退出,有些能存活但表現不穩定,極少數能優雅處理。
- 代碼編輯器/IDE也類似,有的優雅拒絕操作,有的直接崩潰。
- 瀏覽器表現尤為復雜,JavaScript運行時OOM導致標簽頁崩潰,或者瀏覽器崩潰,部分能阻止腳本但仍會逐漸崩潰。
- 數據庫有些能存活(ScyllaDB、Tarantool),大多數會崩潰,且社區呼聲強烈希望數據庫能更優雅地處理OOM,如取消導致OOM的查詢并釋放內存,而不是直接崩潰服務。
最后提到OOM處理的核心要點: - 用戶確實需要OOM保護,尤其是避免數據丟失或服務中斷時
- 一致的RAII風格編程是基礎,避免析構時拋出異常
- 多種策略并存,極端情況可預分配全部資源
- 庫的角色極其重要,有的庫會使用戶陷入困境,有的庫(如Scintilla)則幫用戶優雅處理OOM
整體來說,這是一段現實案例的總結和建議,告訴我們OOM是非常棘手的問題,必須從設計、代碼、庫、架構多方面入手,結合實際場景選擇合適策略。