記一個非常詭異的關於 shared_ptr 的 bug
- 2021 年 10 月 27 日
- 筆記
- C++, shared_ptr, 智慧指針
問題描述
今天寫項目的時候遇見一個特別詭異的 bug,體現在在執行某條語句時,程式會莫名崩潰,並且給出的錯誤資訊也非常難懂,只有一個malloc(): invalid size (unsorted)
錯誤資訊,從直觀上看起來是 malloc 函數無法分配到記憶體,就想著應該是哪個動態分配記憶體的地方變數沒獲取到值,但是調試的時候才發現沒這麼簡單。
問題排查
調試的時候,發現程式崩潰的時候的調用棧最後竟然是一個 vector,並且是在 push_back 的時候,心裏面就隱隱感覺不對了,因為這個程式中的數據遠遠達不到記憶體超限的地步,而 vector 的記憶體是動態分配的,所以說基本上不可能獲取不到足夠的記憶體。
看源碼時注意到另一個 vector 就不會崩潰,於是就增加幾個類型的 vector,逐一試驗,發現在基本數據類型中,std::int32_t,std::int64_t 和他對應的無符號類型就不會導致程式崩潰,但是 std::int16_t,std::int8_t,bool 和 char 就會導致程式崩潰,分析到這裡,看起來好像是大於等於 32 個位元組的數據類型就不會崩潰。但是,我自定義的一個 struct 也會導致崩潰,而這個 struct 有 48 個位元組,調試到這,感覺這個 bug 越來越詭異了。
按理來說,在 C++ 裡面,普通的結構體如果沒有虛函數的話和自帶的數據類型是完全相同的,都是一個記憶體地址,對應著其大小的位元組流,但是在這,不同大小的類型竟然有不同的反應。
於是就調試到 STL 的源碼,發現最後一個調用的語句是::operator new()
,這個是一個按照位元組分配記憶體的語句,語句把語句單獨拿出來,放到崩潰語句的前面,發現程式的確會直接報malloc(): invalid size (unsorted)
錯誤,但如果放在 main 函數最前面的話,卻不會崩潰,最後反覆定位,定位到最終會引起 bug 的地方。這個函數如下:
storage::SQLBinaryData Pager::readRow(std::uint32_t pos) {
if (pos <= getFileSize()) {
std::uint32_t size;
dataFile.seekg(pos, dataFile.beg);
dataFile.read((char *) &size, sizeof(size));
// data 裡面是一個 new 出來的 char 數組 的 shared_ptr
SQLBinaryData data(size);
auto addr = data.data.get();
dataFile.read(addr, size);
// 這就是能造成崩潰的 ::operator new 語句
auto test = ::operator new(1);
return data;
} else {
spdlog::error("read file out of file size");
return SQLBinaryData(0);
}
}
解決方案
可以看出,這個 bug 大概率和 shared_ptr 有關,在網上查閱了很長時間資料,最後才知道在 C++17 之前,shared_ptr 並不支援動態數組,在析構的時候 shared_ptr 只會調用 delete,而不是 delete[],如果要管理 new[]構造出來的數組,需要在構造的時候傳入自定義的 delete 刪除器 std::default_delete,要麼就使用 unique_ptr。
其實大部分情況下智慧指針並不需要 shared_ptr,用 unique_ptr 就夠了,沒有這麼多要共享的東西。
還有一種比較簡便的做法,就是直接用 vector 來管理動態數組,這就已經能滿足很多 new[] 的情況了。一般情況下,寫 C++ 的時候,還是得遵循能不用指針就不用指針的原則。