C++中整數(shù)類型(Integer?Types)的避雷指南與正確使用姿勢詳解
背景
C++繼承自C語言。作為一門以零開銷抽象為主要特征的底層語言,不同于Python或JavaScript等高抽象層次的語言,C++擁有一套較為完整、但又包含有一定歷史包袱的內(nèi)建整數(shù)類型。
在實際開發(fā)中,如果對C++內(nèi)建整數(shù)類型的機制不熟悉,或者不遵循一定的使用規(guī)范,則非常容易引入難以排查和調(diào)試的Bug。因此學(xué)習(xí)了解C++中內(nèi)建整數(shù)類型的特性,以及一套行之有效的使用規(guī)范,是非常有必要的。
內(nèi)建整數(shù)類型的坑 or 歷史包袱
C++ 標(biāo)準沒有規(guī)定具體位數(shù)
雖然在實際實踐中,我們知道在x64平臺,對絕大多數(shù)編譯器來說:
- short => 16 bit
- int => 32 bit
- long => 32 bit(Windows)或64 bit(Linux)
- long long => 64 bit
但坑爹的地方在于,C++ 標(biāo)準沒有規(guī)定 int、long 等類型的具體位數(shù)。
C++ 標(biāo)準只規(guī)定了最小寬度(比如int的最小寬度是16 bit)和相對大小(比如sizeof(short) <= sizeof(int) <= sizeof(long))。
這意味著如果我們要追求代碼的嚴謹性和在未來的可移植性,就不能在使用時假定這些內(nèi)建類型的具體位數(shù)。
坑爹的 unsigned 類型
人類直覺認為“大小、長度、年齡等不可能為負數(shù)”,所以很自然地在這種場景下傾向于用 unsigned。但 C++ 規(guī)定,無符號整數(shù)的溢出是合法的模運算(Modulo Arithmetic)。
這意味著,無符號數(shù)永遠不可能為負,當(dāng)它為 0 時再減 1,不會變成 -1,而是會回繞成該類型的最大值(如 32 位下變成 2³² - 1,即 4294967295)。
一旦掉進這個坑里,會導(dǎo)致以下幾種致命 Bug:
死循環(huán)
// 災(zāi)難:如果用 unsigned 表示數(shù)組下標(biāo),執(zhí)行倒序遍歷
for (unsigned int i = vec.size() - 1; i >= 0; --i) {
// i 為 0 時,--i 變成 4294967295,依然 >= 0。
// 死循環(huán) !?。?
}
差值計算災(zāi)難
usigned int a = 2020;
usigned int b = 2026;
if (a - b < 0) {
std::cout << "a < b" << std::endl;
} else {
std::cout << "a >= b" << std::endl;
}
結(jié)果會輸出a >= b,因為a-b的結(jié)果是一個極大的正數(shù),導(dǎo)致邏輯判斷完全相反。
坑爹的隱式類型提升
混合運算引發(fā)的類型提升
C++ 為了讓不同類型的數(shù)字能在一起做數(shù)學(xué)運算,制定了一套極其復(fù)雜的整型提升規(guī)則(Integer Promotion Rules) 。最反直覺的一條是:當(dāng)有符號數(shù)和無符號數(shù)混合運算時,有符號數(shù)會被隱式強制轉(zhuǎn)換為無符號數(shù)。
這會引發(fā)類似下面的Bug:
int a = -1;
unsigned int b = 1;
if (a < b) {
// 你以為會執(zhí)行這里?錯!
} else {
// 實際會執(zhí)行這里!
// 因為 a 被偷偷轉(zhuǎn)換成了 unsigned,-1 變成了 4294967295
// 4294967295 < 1 顯然是 false。
}
這種錯誤如果出現(xiàn)在緩沖區(qū)檢查、長度驗證、邊界判斷等敏感地帶,就非常容易被攻擊者設(shè)計繞過檢查,從而引發(fā)更嚴重的安全問題。
算術(shù)運算引發(fā)的類型提升
例子:
uint8_t a = 254; mov byte ptr [a],0FEh uint8_t b = 255; mov byte ptr [b],0FFh uint8_t c = a + b; movzx eax,byte ptr [a] // 隱式提升a為uint32_t movzx ecx,byte ptr [b] // 隱式提升b為uint32_t add eax,ecx // 計算(uint32_t)a+(uint32_t)b mov byte ptr [c],al // 將eax當(dāng)中的計算結(jié)果強行截斷成8bit,然后寫回c變量
分析匯編代碼,可知計算a+b時,a和b中的值會被隱式提升成32bit。
盡管如此,但寫回計算結(jié)果時仍然會發(fā)生截斷。c中的計算結(jié)果仍然是錯誤的。
有符號溢出 = 未定義行為(UB)
不同于剛才聊的無符號類型溢出會引發(fā)"回繞"現(xiàn)象,在C++中,有符號整型的溢出被視作一種UB行為。
舉個例子:
int x = std::numeric_limit<int>::max(); x += 1; // UB
理論上編譯器可能會直接決定將這行UB代碼優(yōu)化掉,或者引發(fā)其他異?,F(xiàn)象。
一個更極端的例子:
int f(int x) {
if (x + 1 > x)
return 1;
else
return 0;
}
在高優(yōu)化編譯模式(比如release)下,編譯器可能會認為:既然 signed 溢出是 UB,那么我直接忽略處理 UB 的情況,即假設(shè) x+1 一定不溢出。因此我直接將f優(yōu)化成永遠return 1。
那么此時你調(diào)用f(std::numeric_limits<int>::max())就會得到錯誤的結(jié)果。
在實踐中,如果編譯器優(yōu)化掉的恰好是重要的安全檢查,那么就可能引發(fā)更嚴重的安全漏洞。
坑爹的靜默截斷
大類型 → 小類型 => 靜默截斷
比如:
int64_t n = 5000000000;
int x = n;
int limit = 800000000;
if (x < limit) {
std::cout << x << std::endl; // 得到垃圾值705032704
}
在你的編譯器沒有經(jīng)過特殊設(shè)置的情況下,以上代碼會通過編譯。并且盡管n遠大于limit,if塊中的代碼仍然被執(zhí)行了。
在實踐中,如果x被用于表示文件大小、網(wǎng)絡(luò)長度或用戶輸入長度,那么攻擊者可以通過構(gòu)造一個超大數(shù)字n并依靠靜默截斷來繞過檢查if (x < limit)
標(biāo)準庫的世紀失誤
早期 C++ 標(biāo)準委員會為了讓容器(如 std::vector)能容納盡可能多的元素,利用了無符號數(shù)比有符號數(shù)正向范圍大一倍的特點,將容器的 size() 返回值和 operator[] 的參數(shù)硬性規(guī)定為 size_t(一個無符號類型)。
坑爹的地方就在這兒,由于size_t是一個無符號類型,因此你一旦調(diào)用STL庫容器的size(),就必須警惕掉進前述的任何與無符號整型有關(guān)的坑。
為了讓你加深印象,這里再強調(diào)一遍。
逆向迭代陷阱(Underflow)
for (size_t i = v.size() - 1; i >= 0; --i) { // 永遠不會停止!
// 當(dāng) i 為 0 時,--i 會變成一個巨大的正數(shù)(溢出/繞回)
}
隱式類型轉(zhuǎn)換與比較錯誤
std::vector<int> v;
int x = -1;
if (x < v.size()) {
// 如果 v 為空(size 為 0),這個條件居然是 FALSE!
// 因為 -1 被轉(zhuǎn)換成了 18446744073709551615 (2^64-1)
}
C++ 之父 Bjarne Stroustrup 和多位委員會成員后來公開承認:這是一個巨大的錯誤(A Historical Mistake)。但為了 ABI 兼容,永遠無法修改了。
Google C++ Style Guide 規(guī)范是怎么說的
為了避雷前述的C++內(nèi)建整型類型的各種坑或歷史包袱,Google 制定了一系列可實操的工程規(guī)范。
下面我對這部分規(guī)范進行了梳理和拓展。平時開發(fā)中遵循這些規(guī)范,就能避免掉一大部分的坑~
推薦使用<stdint.h>或<cstdint>的固定寬度類型
既然short、long long、unsigned long long等類型的位寬是不確定的,那干脆我們就不要去用了。
取而代之,我們使用固定寬度類型,比如int16_t、uint32_t、int64_t。
注意,這些類型直接使用即可,不必加std::前綴!
int類型的正確使用姿勢
1.在 C++ 內(nèi)置整數(shù)類型中,唯一推薦經(jīng)常使用的是 int。比如在數(shù)據(jù)范圍適用的前提下,在以下場景:
- 循環(huán)計數(shù)器
- 一般的小整數(shù)
- 下標(biāo)
2.如果一個值可能 ≥ 2³¹(約 21 億),就應(yīng)使用 64 位類型(int64_t)。
特別的,如果某個值/變量本身不大,但在中間計算過程中可能溢出,也應(yīng)當(dāng)使用int64_t。
3.如果程序明確需要特定大小的整數(shù)類型,應(yīng)使用int16_t、int32_t、int64_t等精確寬度類型。
比如,TCP協(xié)議規(guī)范中端口字段明確為32bit,那么你就應(yīng)該明確地用int32_t而不是int。
強烈抵制無符號(unsigned)類型
原則
既然混用signed和unsigned容易翻車(比如剛才提到的隱式類型提升),Google 的做法非常簡單粗暴——絕大多數(shù)業(yè)務(wù)代碼里直接禁用 unsigned,全部用有符號整型。這直接消滅了混用的可能性。
特別強調(diào),不要為了“保證非負”而用 unsigned。
錯誤示范:
unsigned int age; // ? 只是想讓 age >= 0
正確做法:
int age; // 如果你想保證它不能為負數(shù),在代碼里寫斷言。 assert(age >= 0);
豁免
只有當(dāng)你明確在以下場景時,才能使用無符號類型:
- 需要進行位操作(如移位、按位邏輯操作)
- 對于有符號整數(shù)(如 int),進行右移操作(>>)時,到底是“邏輯右移(補0)”還是“算術(shù)右移(補符號位)”在 C++20 以前不確定的(通常是算術(shù)右移,會補符號位)
- 這會帶來跨平臺的不確定性。
- 而無符號類型進行位運算(&, |, ^, <<, >>)有著絕對一致的跨平臺表現(xiàn)。
- 表示位掩碼(bitmask)或位域(bitfields)
- 需要利用無符號類型的溢出回繞特性(比如密碼學(xué)或哈希算法)
- 用于表示單個二進制字節(jié)的值
- 當(dāng)我們在進行網(wǎng)絡(luò)編程、文件 IO、序列化時,處理的基礎(chǔ)單位是“字節(jié)”。一個字節(jié)就是 8 個比特,它沒有正負之分。
- 如果你用
char(有符號),當(dāng)讀取到大于 127 的字節(jié)時,它會被解釋為負數(shù),這在作為數(shù)組索引或進行寬類型轉(zhuǎn)換時會引發(fā)嚴重的 Bug。
例子1:
LevelDB 使用了自定義的 MurmurHash 變種。你看這里清一色使用的是 uint32_t。
// 來源:google/leveldb
// 這里的 seed, m, r 以及 h 都在進行位操作和故意的溢出計算
uint32_t Hash(const char* data, size_t n, uint32_t seed) {
// 常量 m 充當(dāng)乘法因子,利用無符號乘法溢出截斷的特性
const uint32_t m = 0xc6a4a793;
const uint32_t r = 24;
const char* limit = data + n;
// seed 和 (n * m) 進行異或,n*m 極可能溢出,但 uint32_t 保證了其安全性
uint32_t h = seed ^ (n * m);
// 一段典型的每次處理 4 字節(jié)的哈?;旌线^程
while (data + 4 <= limit) {
uint32_t w = DecodeFixed32(data); // 讀取 4 個原始字節(jié)
data += 4;
h += w; // 這里的加法依賴模 2^32 運算
h *= m; // 乘法依賴模 2^32 運算
h ^= (h >> 16); // 位移與異或,打亂比特位
}
// ...
return h;
}
例子2:
在 Protobuf 的底層序列化格式中,一個字段的標(biāo)簽(Tag)由“字段編號(Field Number)”和“數(shù)據(jù)類型(Wire Type)”壓縮進同一個整數(shù)中。
// 來源:google/protobuf
// 使用無符號整數(shù)來進行左移、按位或、按位與等操作
inline uint32_t WireFormatLite::MakeTag(int field_number, WireType type) {
// 將 field_number 左移 3 位,然后與低 3 位的 type 進行按位或 (|)
// uint32_t 保證了移位操作絕對不會受符號位影響
return (static_cast<uint32_t>(field_number) << 3) | static_cast<uint32_t>(type);
}
inline WireFormatLite::WireType WireFormatLite::GetTagWireType(uint32_t tag) {
// 使用按位與 (&) 提取低 3 位的數(shù)據(jù)
return static_cast<WireType>(tag & 7);
}
例子3:
Base64 編碼解碼時,針對的是原始字節(jié)流。
// 來源:google/abseil (absl)
// 使用 uint8_t 數(shù)組來表示原始的字節(jié)流序列
static const uint8_t kBase64DecoderRules[256] = {
// ... 大量解析規(guī)則狀態(tài)碼 ...
};
bool Base64UnescapeInternal(const char* src_param, size_t szsrc,
char* dest, size_t* szdest) {
// 將輸入的字符指針強制轉(zhuǎn)換為無符號的字節(jié)流指針
// 因為 Base64 處理過程中,我們只關(guān)心這 8 個 bit 是什么,不關(guān)心它代表什么字符或正負數(shù)
const uint8_t* src = reinterpret_cast<const uint8_t*>(src_param);
const uint8_t* src_end = src + szsrc;
while (src < src_end) {
// 作為數(shù)組索引時,如果是 signed char 遇到大于 127 的值會變成負數(shù)而越界崩潰
// uint8_t 完美避免了這個問題
uint8_t rule = kBase64DecoderRules[*src++];
// ...
}
}
size_t正確使用姿勢
雖然前文中我們細數(shù)了size_t作為無符號整型的一系列罪狀,但由于其較為明確的語義(比如用于表示內(nèi)存數(shù)據(jù)塊的字節(jié)數(shù)、偏移量),Google規(guī)范中也沒有一棍子打死它,而是允許在適當(dāng)?shù)那闆r下使用。
原文:When appropriate, you are welcome to use standard type aliases like size_t and ptrdiff_t.
舉例:TensorFlow 代碼中 size_t 與 int64_t 并存
來源于 TensorFlow 官方 C++ API 文檔示例:
size_t TotalBytes() const // returns memory usage int64_t dim_size(int d) const // returns shape dimension
TotalBytes()用的是size_t,很自然用于表示內(nèi)存 尺寸/字節(jié)數(shù)(不可能為負)。dim_size()返回int64_t,用于表示 tensor 的 邏輯維度大小/形狀,因為:- TensorFlow 的維度整數(shù)可能參與算術(shù)計算
- 需要 signed 類型有助于防止 signed/unsigned 隱式轉(zhuǎn)換問題
容器大小要謹慎
針對表示STL容器大小的size_t存在的缺陷,Google建議:盡量使用迭代器(iterators)和容器(containers),而不是指針(pointers)和大小(sizes)。
// ? Good for (auto it = v.begin(); it != v.end(); ++it); // ? Good for (auto& x : v); // ? Bad(混用signed和unsigned) for (int i = 0; i < v.size(); i++);
另外,需要盡量避免無意義的 unsigned 擴散到業(yè)務(wù)代碼。
以下是一個 Good case:
size_t size = container.size(); // 與 STL 兼容
int64_t count = static_cast<int64_t>(size); // 內(nèi)部轉(zhuǎn)換防止 signed/unsigned 混用
for (int64_t i = 0; i < count; ++i) { ... } // 內(nèi)部循環(huán)
這種方式既兼容了容器接口的 size_t,又避免了 signed/unsigned 混用引發(fā)的 bug。
以上就是C++中整數(shù)類型(Integer Types)的避雷指南與正確使用姿勢詳解的詳細內(nèi)容,更多關(guān)于C++整數(shù)類型的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
typedef_struct與struct之間的區(qū)別
本篇文章主要是對typedef struct與struct之間的區(qū)別進行了介紹,需要的朋友可以過來參考下,希望對大家有所幫助2013-12-12
Opencv2.4.9函數(shù)HoughLinesP分析
這篇文章主要為大家詳細介紹了Opencv2.4.9函數(shù)HoughLinesP,具有一定的參考價值,感興趣的小伙伴們可以參考一下2019-01-01

