代码质量分析-整数处理问题

1、整形范围

数字类型,由三个维度来定义:

  1. 整数 or 浮点数:int or float/double
  2. 有符号 or 无符号:signed or unsigned
  3. 长度: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

综上可知,在写代码时要尽量避免以下行为:

  1. 将长的类型赋值给短的类型;
  2. 在有符号和无符号类型之间做转换(尤其是有负数存在时);
  3. 对有符号和无符号类型的参数做运算(尤其是有负数存在时);
  4. 做计算时,尽量用可以容纳结果范围的类型去存储结果。

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)
格式化输出

参考资料

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 116,512评论 1 235
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 50,365评论 1 198
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 71,433评论 0 163
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 35,090评论 0 126
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 41,883评论 1 204
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 34,719评论 1 124
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 26,696评论 2 204
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 25,858评论 0 117
  • 想象着我的养父在大火中拼命挣扎,窒息,最后皮肤化为焦炭。我心中就已经是抑制不住地欢快,这就叫做以其人之道,还治其人...
    爱写小说的胖达阅读 24,816评论 5 169
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 28,871评论 0 177
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 26,131评论 1 166
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 27,360评论 1 173
  • 白月光回国,霸总把我这个替身辞退。还一脸阴沉的警告我。[不要出现在思思面前, 不然我有一百种方法让你生不如死。]我...
    爱写小说的胖达阅读 21,734评论 0 24
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 24,319评论 2 162
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 28,107评论 3 171
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 23,081评论 0 4
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 23,217评论 0 112
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 29,303评论 2 185
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 29,728评论 2 186

推荐阅读更多精彩内容