1兼耀、整形范圍
數(shù)字類型,由三個(gè)維度來定義:
- 整數(shù) or 浮點(diǎn)數(shù):int or float/double
- 有符號(hào) or 無符號(hào):signed or unsigned
- 長(zhǎng)度:short or long(看編譯器,此處均采用32位編譯器)
長(zhǎng)度決定了位數(shù):
- short:2字節(jié)蕾管,即16位
- long:== int线衫,4字節(jié),即32位
在此基礎(chǔ)上繁仁,看符號(hào):
- 如果是有符號(hào)數(shù)涉馅,那么最高位需要表示符號(hào)(0表示正數(shù),1表示負(fù)數(shù))黄虱,可表示最大值會(huì)減半稚矿,但是可以表示負(fù)數(shù)(范圍等同于正數(shù))。
- 如果是無符號(hào)數(shù)捻浦,那么就全部是非負(fù)數(shù)晤揣,最高位也可以用于表示數(shù)字,最大值會(huì)是有符號(hào)數(shù)的兩倍朱灿。
所以可以簡(jiǎn)單得出各個(gè)整形類型的范圍(方括號(hào)表示可不填昧识,系默認(rèn)值):
- [signed] short [int]:-2^15 ~ 2^15-1
- unsigned short [int]:0 ~ 2^16-1
- [signed] int:-2^31 ~ 2^31-1
- unsigned int:0 ~ 2^32-1
- [signed] long [int]:-2^31 ~ 2^31-1
- unsigned long [int]:0 ~ 2^32-1
- [signed] long long [int]:-2^63 ~ 2^63-1
- unsigned long long [int]:0 ~ 2^64-1
問:C語言中的uint8_t\uint_16_t\uint32_t\uint64_t是什么?
實(shí)際上就是不同位長(zhǎng)度的上述基礎(chǔ)類型盗扒,比如:
uint32_t 表示 unsigned int跪楞。
問:_t 的后綴表示什么缀去?
_t 表示這些數(shù)據(jù)類型是通過typedef定義的,而不是新的數(shù)據(jù)類型甸祭。使用他們是為了明確得定義長(zhǎng)度缕碎,避免直接使用基礎(chǔ)類型時(shí),在不同編譯機(jī)器上出現(xiàn)差異池户,從定義文件中可以窺見:
# if __WORDSIZE == 64
typedef long int int64_t;
# else
__extension__
typedef long long int int64_t;
# endif
提問:為什么有符號(hào)數(shù)的正數(shù)范圍是-1咏雌?
回答:因?yàn)樽罡呶挥糜诒硎痉?hào),所以-1校焦。
提問:為什么有符號(hào)數(shù)的負(fù)數(shù)范圍不用-1处嫌?
回答:因?yàn)樵谟蟹?hào)數(shù)的規(guī)則下,0出現(xiàn)了+0和-0兩個(gè)表示方法斟湃,浪費(fèi)熏迹,所以把-0(1000 0000 0000 0000 0000 0000 0000 0000)額外定義成了最小的負(fù)數(shù),也就是-2^31(實(shí)際上因?yàn)樽罡呶皇欠?hào)位凝赛,本不應(yīng)該出現(xiàn)這個(gè)數(shù))注暗。
2、常見錯(cuò)誤
2.1墓猎、無意的整數(shù)外溢(OVERFLOW_BEFORE_WIDEN)
用窄長(zhǎng)度的參數(shù)計(jì)算捆昏,然后將結(jié)果賦值給寬長(zhǎng)度的變量,如果這個(gè)計(jì)算的結(jié)果超出了窄長(zhǎng)度的范圍毙沾,其高位會(huì)被丟棄骗卜,值保留窄長(zhǎng)度的范圍內(nèi)的內(nèi)容,如果是有符號(hào)類型左胞,結(jié)果會(huì)更不可知(最高位是符號(hào)位)寇仓。
極容易忽略,人們總是按照自己數(shù)字的范圍來定義變量類型烤宙,而不會(huì)考慮他會(huì)被用于計(jì)算什么遍烦。
gcc目前無法告警,Coverity靜態(tài)分析器將發(fā)出OVERFLOW_BEFORE_WIDEN警告躺枕。
建議在對(duì)變量做計(jì)算賦值時(shí)服猪,必須考慮其計(jì)算參數(shù)的類型是否至少有一個(gè)和自己類型相同。
CR建議加上對(duì)計(jì)算時(shí)參數(shù)的類型檢查拐云。
// wrong
uint32_t a = 123456;
uint64_t b = a * 1000000000; // 結(jié)果可能會(huì)溢出罢猪,b不會(huì)得到正確的結(jié)果
// right
uint64_t a = 123456;
uint64_t b = a * 1000000000;
// right
uint32_t a = 123456;
uint64_t b = a * (uint64_t)1000000000;
2.2、除以零或求零的模(DIVIDE_BY_ZERO)
在計(jì)算除法或者求模的時(shí)候叉瘩,傳入的變量可能為0膳帕,從而引起不確定的行為,對(duì)C++來說房揭,會(huì)引起程序中斷备闲。
對(duì)編譯來說晌端,除數(shù)是個(gè)變量,是不會(huì)告警的恬砂。
cout << 1 / 0 << endl; // 編譯會(huì)報(bào)錯(cuò)
int a = 0; cout << 1 / a << endl; // 編譯不會(huì)報(bào)錯(cuò)咧纠,運(yùn)行時(shí)報(bào)錯(cuò)。
本質(zhì)上是一種異常判斷不嚴(yán)謹(jǐn)?shù)那闆r泻骤,建議對(duì)所有除法和求模操作漆羔,如果對(duì)象是變量,那么必須要做非0判斷狱掂。
CR建議加上對(duì)除法/求模運(yùn)算的參數(shù)判斷檢查演痒。
2.3、不適當(dāng)?shù)厥褂昧素?fù)值(NEGATIVE_RETURNS)
通常指將一個(gè)有符號(hào)類型的參數(shù)趋惨,傳給一個(gè)無符號(hào)類型的參數(shù)鸟顺。
最容易弄錯(cuò)的是對(duì)于時(shí)間的計(jì)算:
uint32_t cur_time = time(nullptr); // 錯(cuò)誤
void SomeFunc(uint32_t time);
SomeFunc(time(nullptr)); // 錯(cuò)誤
time(nullptr) 函數(shù)實(shí)際返回的是一個(gè) time_t 類型的結(jié)果。這個(gè)time_t類型器虾,實(shí)際上就是對(duì)long類型的一個(gè)typedef讯嫂。
typedef long time_t;
問:為什么time_t要被定義為一個(gè)有符號(hào)數(shù)?猜測(cè)是可以表述1970年之前的時(shí)間兆沙?
由于我們一般意義上理解time(nullptr)是一個(gè)秒數(shù)欧芽,不可能為負(fù)數(shù),所以會(huì)把它當(dāng)正數(shù)使用葛圃,實(shí)際上它的返回值是個(gè)有符號(hào)數(shù)千扔。
由此引申,其他的變量也是库正,我們可能覺得一個(gè)數(shù)一定是正數(shù)曲楚,所以把它當(dāng)無符號(hào)數(shù)用,實(shí)際上如果它被定義為有符號(hào)數(shù)诀诊,那就是有風(fēng)險(xiǎn)的洞渤。
2.4、操作數(shù)不影響結(jié)果(CONSTANT_EXPRESSION_RESULT)属瓣、宏將無符號(hào)值與 0 做了比較(NO_EFFECT)
主要是對(duì)變量的范圍做判斷時(shí),做了無效判斷讯柔。
比如判斷一個(gè)無符號(hào)數(shù)是否小于0抡蛙,或者判斷一個(gè)32位的數(shù)是否大于一個(gè)64位數(shù)的最大值等。其結(jié)果一定是否魂迄。
雖說無害燃辖,但是增加了圈復(fù)雜度婉宰。
uint32_t a = 100;
if (a < 0) {xxx} // 永遠(yuǎn)不會(huì)進(jìn)分支
2.5、邏輯與按位運(yùn)算符(CONSTANT_EXPRESSION_RESULT)
直接把數(shù)字當(dāng)做布爾型的值來計(jì)算宝与,有效但是不應(yīng)該。
如下面的用法嗓节,猜測(cè)他是要判斷ret是否等于兩者中的之一,但這種寫法,會(huì)導(dǎo)致永遠(yuǎn)會(huì)進(jìn)分支灭美。非常不應(yīng)該。
在CR時(shí)如果出現(xiàn)這種代碼昂利,相信也會(huì)很容易發(fā)現(xiàn)届腐。
if (ret == 269807148 || 269807149) {
return ret;
}
2.6、非正常符號(hào)擴(kuò)展(SIGN_EXTENSION)
這里涉及的其實(shí)是有符號(hào)數(shù)和無符號(hào)數(shù)在不同長(zhǎng)度的類型之間轉(zhuǎn)換時(shí)的問題蜂奸。
我們分成幾類:
// 1. 無符號(hào)數(shù)轉(zhuǎn)為更長(zhǎng)的無符號(hào)數(shù)
uint8_t a = 5; // 00000101
uint16_t b = a; // 0000000000000101犁苏,b也會(huì)是5
// 2. 無符號(hào)數(shù)轉(zhuǎn)為更短的無符號(hào)數(shù)
uint16_t a = 1021; // 0000001111111101
uint8_t b = a; // 11111101,b會(huì)變成253
// 3. 有符號(hào)數(shù)轉(zhuǎn)為更長(zhǎng)的有符號(hào)數(shù)
int8_t a = -5; // 10000101
int16_t b = a; // 1111111110000101扩所,b也會(huì)是-5
// 4. 有符號(hào)數(shù)轉(zhuǎn)為更短的有符號(hào)數(shù)
int16_t a = 1925; // 0000011110000101
int8_t b = a; // 10000101围详,由于符號(hào)位的存在,b變成-5祖屏,不但數(shù)值被縮短了短曾,正負(fù)也變了
// 5. 有符號(hào)數(shù)變?yōu)闊o符號(hào)數(shù)
int8_t a = -20; // 10010100
uint8_t b = a; // 10010100,由于符號(hào)位被當(dāng)做數(shù)據(jù)位赐劣,b變成148
// 6. 無符號(hào)數(shù)變?yōu)橛蟹?hào)數(shù)
uint8_t a = 148; // 10010100
int8_t b = a; // 10010100嫉拐,由于最高位被視為符號(hào)位,b變成-20
// 7. 有符號(hào)和有符號(hào)數(shù)的計(jì)算
int8_t a = -84; // 11010100
int8_t b = -84; // 11010100
int8_t c = a + b; // 首先正常計(jì)算結(jié)果-168超過了8位魁兼,1000000010101000
// 由于結(jié)果是8位婉徘,所以被截?cái)嗪螅S嗟?0101000咐汞,結(jié)果變成了-40
// 8. 無符號(hào)和無符號(hào)數(shù)的計(jì)算
int8_t a = 212; // 11010100
int8_t b = 212; // 11010100
int8_t c = a + b; // 首先正常計(jì)算結(jié)果424超過了8位盖呼,0000000110101000
// 由于結(jié)果是8位,所以被截?cái)嗪蠡海S嗟?0101000几晤,結(jié)果變成了168
// 9. 有符號(hào)數(shù)和無符號(hào)數(shù)的計(jì)算
uint8_t a = 6; // 00000110
int8_t b = -20; // 10010100 bool c = (a + b) > 6;
// 正常的理解c應(yīng)該是false,a+b=-14
// 但實(shí)際上計(jì)算式由于兩個(gè)參數(shù)類型不同植阴,會(huì)先進(jìn)行隱式類型轉(zhuǎn)換蟹瘾,有符號(hào)數(shù)會(huì)轉(zhuǎn)為無符號(hào)數(shù)
// 于是結(jié)果b變成了148,相加后掠手,結(jié)果必然大于6憾朴,c變成true
綜上可知,在寫代碼時(shí)要盡量避免以下行為:
- 將長(zhǎng)的類型賦值給短的類型喷鸽;
- 在有符號(hào)和無符號(hào)類型之間做轉(zhuǎn)換(尤其是有負(fù)數(shù)存在時(shí))众雷;
- 對(duì)有符號(hào)和無符號(hào)類型的參數(shù)做運(yùn)算(尤其是有負(fù)數(shù)存在時(shí));
- 做計(jì)算時(shí),盡量用可以容納結(jié)果范圍的類型去存儲(chǔ)結(jié)果砾省。
PS:C對(duì)類型隱式轉(zhuǎn)換的順序?yàn)椋?/p>
double > float > unsigned long > long > unsigned int > int
即操作數(shù)類型排在后面的與操作數(shù)類型排在前面的進(jìn)行運(yùn)算時(shí)鸡岗,排在后面的類型將隱式轉(zhuǎn)換為排在前面的類型。
2.7编兄、錯(cuò)誤的移位操作(BAD_SHIFT)
在做移位操作時(shí)轩性,如果被移位的數(shù)以及被賦結(jié)果的變量是低位數(shù),移動(dòng)的位置是個(gè)高位數(shù)翻诉,就可能出現(xiàn)不可預(yù)知的結(jié)果炮姨。比如:
uint64_t a = 0;
// 此處省略一些對(duì)a的修改操作
uint32_t b = 1 << a; // 由于a是64位,當(dāng)對(duì)1左移超過31位時(shí)碰煌,就可能發(fā)生不可知的結(jié)果
只需在申明移位的數(shù)量的變量時(shí)舒岸,注意其長(zhǎng)度不要超過允許的長(zhǎng)度即可。
另外芦圾,如果要做移位操作蛾派,最好使用無符號(hào)數(shù),避免移位后出現(xiàn)符號(hào)位的數(shù)字个少。
2.8洪乍、常量表達(dá)式結(jié)果(CONSTANT_EXPRESSION_RESULT)
一種看似正常,實(shí)際上存在邏輯問題的表達(dá)式夜焦,其判斷結(jié)果永遠(yuǎn)為true或false壳澳。
舉個(gè)例子:
if (ret != comm::AAA || ret != comm::BBB) {
// do something
}
看似是想說如果ret不等于這兩個(gè)結(jié)果就做某事,實(shí)際上因?yàn)閞et永遠(yuǎn)不可能同時(shí)等于兩個(gè)值茫经,因此這兩個(gè)條件至少有一個(gè)成立巷波,也就是這個(gè)分支判斷永遠(yuǎn)為true。
2.9卸伞、格式化輸出
打印日志時(shí)抹镊,對(duì)于整形,需要使用對(duì)應(yīng)的格式符來輸出參數(shù)內(nèi)容荤傲。
比如不要對(duì)無符號(hào)數(shù)使用%d垮耳,應(yīng)該使用%u。
如果對(duì)整形打印時(shí)使用了%s遂黍,那還可能會(huì)直接報(bào)錯(cuò)(編譯無法告警)终佛。
3、編譯告警情況
各個(gè)問題是否在編譯時(shí)會(huì)給出告警妓湘?
問題 | 是否編譯告警 |
---|---|
無意的整數(shù)外溢(OVERFLOW_BEFORE_WIDEN) | 否 |
除以零或求零的模(DIVIDE_BY_ZERO) | 否 |
不適當(dāng)?shù)厥褂昧素?fù)值(NEGATIVE_RETURNS) | 否 |
操作數(shù)不影響結(jié)果(CONSTANT_EXPRESSION_RESULT) | 否 |
非正常符號(hào)擴(kuò)展(SIGN_EXTENSION) | 否 |
錯(cuò)誤的移位操作(BAD_SHIFT) | 否 |
常量表達(dá)式結(jié)果(CONSTANT_EXPRESSION_RESULT) | 否 |
格式化輸出 | 否 |