1、整形范围
数字类型,由三个维度来定义:
- 整数 or 浮点数:int or float/double
- 有符号 or 无符号:signed or unsigned
- 长度:short or long(看编译器,此处均采用32位编译器)
长度决定了位数:
- short:2字节,即16位
- long:== int,4字节,即32位
在此基础上,看符号:
- 如果是有符号数,那么最高位需要表示符号(0表示正数,1表示负数),可表示最大值会减半,但是可以表示负数(范围等同于正数)。
- 如果是无符号数,那么就全部是非负数,最高位也可以用于表示数字,最大值会是有符号数的两倍。
所以可以简单得出各个整形类型的范围(方括号表示可不填,系默认值):
- [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是什么?
实际上就是不同位长度的上述基础类型,比如:
uint32_t 表示 unsigned int。
问:_t 的后缀表示什么?
_t 表示这些数据类型是通过typedef定义的,而不是新的数据类型。使用他们是为了明确得定义长度,避免直接使用基础类型时,在不同编译机器上出现差异,从定义文件中可以窥见:
# if __WORDSIZE == 64
typedef long int int64_t;
# else
__extension__
typedef long long int int64_t;
# endif
提问:为什么有符号数的正数范围是-1?
回答:因为最高位用于表示符号,所以-1。
提问:为什么有符号数的负数范围不用-1?
回答:因为在有符号数的规则下,0出现了+0和-0两个表示方法,浪费,所以把-0(1000 0000 0000 0000 0000 0000 0000 0000)额外定义成了最小的负数,也就是-2^31(实际上因为最高位是符号位,本不应该出现这个数)。
2、常见错误
2.1、无意的整数外溢(OVERFLOW_BEFORE_WIDEN)
用窄长度的参数计算,然后将结果赋值给宽长度的变量,如果这个计算的结果超出了窄长度的范围,其高位会被丢弃,值保留窄长度的范围内的内容,如果是有符号类型,结果会更不可知(最高位是符号位)。
极容易忽略,人们总是按照自己数字的范围来定义变量类型,而不会考虑他会被用于计算什么。
gcc目前无法告警,Coverity静态分析器将发出OVERFLOW_BEFORE_WIDEN警告。
建议在对变量做计算赋值时,必须考虑其计算参数的类型是否至少有一个和自己类型相同。
CR建议加上对计算时参数的类型检查。
// wrong
uint32_t a = 123456;
uint64_t b = a * 1000000000; // 结果可能会溢出,b不会得到正确的结果
// 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)
在计算除法或者求模的时候,传入的变量可能为0,从而引起不确定的行为,对C++来说,会引起程序中断。
对编译来说,除数是个变量,是不会告警的。
cout << 1 / 0 << endl; // 编译会报错
int a = 0; cout << 1 / a << endl; // 编译不会报错,运行时报错。
本质上是一种异常判断不严谨的情况,建议对所有除法和求模操作,如果对象是变量,那么必须要做非0判断。
CR建议加上对除法/求模运算的参数判断检查。
2.3、不适当地使用了负值(NEGATIVE_RETURNS)
通常指将一个有符号类型的参数,传给一个无符号类型的参数。
最容易弄错的是对于时间的计算:
uint32_t cur_time = time(nullptr); // 错误
void SomeFunc(uint32_t time);
SomeFunc(time(nullptr)); // 错误
time(nullptr) 函数实际返回的是一个 time_t 类型的结果。这个time_t类型,实际上就是对long类型的一个typedef。
typedef long time_t;
问:为什么time_t要被定义为一个有符号数?猜测是可以表述1970年之前的时间?
由于我们一般意义上理解time(nullptr)是一个秒数,不可能为负数,所以会把它当正数使用,实际上它的返回值是个有符号数。
由此引申,其他的变量也是,我们可能觉得一个数一定是正数,所以把它当无符号数用,实际上如果它被定义为有符号数,那就是有风险的。
2.4、操作数不影响结果(CONSTANT_EXPRESSION_RESULT)、宏将无符号值与 0 做了比较(NO_EFFECT)
主要是对变量的范围做判断时,做了无效判断。
比如判断一个无符号数是否小于0,或者判断一个32位的数是否大于一个64位数的最大值等。其结果一定是否。
虽说无害,但是增加了圈复杂度。
uint32_t a = 100;
if (a < 0) {xxx} // 永远不会进分支
2.5、逻辑与按位运算符(CONSTANT_EXPRESSION_RESULT)
直接把数字当做布尔型的值来计算,有效但是不应该。
如下面的用法,猜测他是要判断ret是否等于两者中的之一,但这种写法,会导致永远会进分支。非常不应该。
在CR时如果出现这种代码,相信也会很容易发现。
if (ret == 269807148 || 269807149) {
return ret;
}
2.6、非正常符号扩展(SIGN_EXTENSION)
这里涉及的其实是有符号数和无符号数在不同长度的类型之间转换时的问题。
我们分成几类:
// 1. 无符号数转为更长的无符号数
uint8_t a = 5; // 00000101
uint16_t b = a; // 0000000000000101,b也会是5
// 2. 无符号数转为更短的无符号数
uint16_t a = 1021; // 0000001111111101
uint8_t b = a; // 11111101,b会变成253
// 3. 有符号数转为更长的有符号数
int8_t a = -5; // 10000101
int16_t b = a; // 1111111110000101,b也会是-5
// 4. 有符号数转为更短的有符号数
int16_t a = 1925; // 0000011110000101
int8_t b = a; // 10000101,由于符号位的存在,b变成-5,不但数值被缩短了,正负也变了
// 5. 有符号数变为无符号数
int8_t a = -20; // 10010100
uint8_t b = a; // 10010100,由于符号位被当做数据位,b变成148
// 6. 无符号数变为有符号数
uint8_t a = 148; // 10010100
int8_t b = a; // 10010100,由于最高位被视为符号位,b变成-20
// 7. 有符号和有符号数的计算
int8_t a = -84; // 11010100
int8_t b = -84; // 11010100
int8_t c = a + b; // 首先正常计算结果-168超过了8位,1000000010101000
// 由于结果是8位,所以被截断后,剩余的10101000,结果变成了-40
// 8. 无符号和无符号数的计算
int8_t a = 212; // 11010100
int8_t b = 212; // 11010100
int8_t c = a + b; // 首先正常计算结果424超过了8位,0000000110101000
// 由于结果是8位,所以被截断后,剩余的10101000,结果变成了168
// 9. 有符号数和无符号数的计算
uint8_t a = 6; // 00000110
int8_t b = -20; // 10010100 bool c = (a + b) > 6;
// 正常的理解c应该是false,a+b=-14
// 但实际上计算式由于两个参数类型不同,会先进行隐式类型转换,有符号数会转为无符号数
// 于是结果b变成了148,相加后,结果必然大于6,c变成true
综上可知,在写代码时要尽量避免以下行为:
- 将长的类型赋值给短的类型;
- 在有符号和无符号类型之间做转换(尤其是有负数存在时);
- 对有符号和无符号类型的参数做运算(尤其是有负数存在时);
- 做计算时,尽量用可以容纳结果范围的类型去存储结果。
PS:C对类型隐式转换的顺序为:
double > float > unsigned long > long > unsigned int > int
即操作数类型排在后面的与操作数类型排在前面的进行运算时,排在后面的类型将隐式转换为排在前面的类型。
2.7、错误的移位操作(BAD_SHIFT)
在做移位操作时,如果被移位的数以及被赋结果的变量是低位数,移动的位置是个高位数,就可能出现不可预知的结果。比如:
uint64_t a = 0;
// 此处省略一些对a的修改操作
uint32_t b = 1 << a; // 由于a是64位,当对1左移超过31位时,就可能发生不可知的结果
只需在申明移位的数量的变量时,注意其长度不要超过允许的长度即可。
另外,如果要做移位操作,最好使用无符号数,避免移位后出现符号位的数字。
2.8、常量表达式结果(CONSTANT_EXPRESSION_RESULT)
一种看似正常,实际上存在逻辑问题的表达式,其判断结果永远为true或false。
举个例子:
if (ret != comm::AAA || ret != comm::BBB) {
// do something
}
看似是想说如果ret不等于这两个结果就做某事,实际上因为ret永远不可能同时等于两个值,因此这两个条件至少有一个成立,也就是这个分支判断永远为true。
2.9、格式化输出
打印日志时,对于整形,需要使用对应的格式符来输出参数内容。
比如不要对无符号数使用%d,应该使用%u。
如果对整形打印时使用了%s,那还可能会直接报错(编译无法告警)。
3、编译告警情况
各个问题是否在编译时会给出告警?
问题 | 是否编译告警 |
---|---|
无意的整数外溢(OVERFLOW_BEFORE_WIDEN) | 否 |
除以零或求零的模(DIVIDE_BY_ZERO) | 否 |
不适当地使用了负值(NEGATIVE_RETURNS) | 否 |
操作数不影响结果(CONSTANT_EXPRESSION_RESULT) | 否 |
非正常符号扩展(SIGN_EXTENSION) | 否 |
错误的移位操作(BAD_SHIFT) | 否 |
常量表达式结果(CONSTANT_EXPRESSION_RESULT) | 否 |
格式化输出 | 否 |
网友评论