首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >《改善C程序代码的125个建议》-防止整数类型产生回绕与溢出

《改善C程序代码的125个建议》-防止整数类型产生回绕与溢出

作者头像
用户1631416
发布2018-04-12 11:53:03
1.8K0
发布2018-04-12 11:53:03
举报
文章被收录于专栏:玄魂工作室玄魂工作室

以下内容摘抄自《改善C程序代码的125个建议》:

建议2:防止整数类型产生回绕与溢出

到C99为止,C语言为我们提供了12个相关的数据类型关键字来表达各种数据类型。如表1-2所示,K&R C提供了7个,C89/C90新增了2个,C99新增了3个。

表1-2 C的数据类型关键字

整型是C语言最基本的数据类型,它以二进制编码的方式进行存储,具体可以包括字符、短整型、整型和长整型等。例如,整数2的二进制表示为10,它在8位与32位的操作系统中存储方式如图1-3所示。

图1-3 整数2的二进制编码存储方式

虽然在计算机中整数是以二进制编码方式进行存储的,但为了便于表达,有时候又会用十六进制编码方式表示(例如,在32位操作系统下,整数2的十六进制编码方式为0x00000002),二进制和十六进制之间能够很方便地进行转换。

与此同时,整数类型又可分为有符号(signed)和无符号(unsigned)两种类型,limits.h文件定义了整型数据类型的表达值范围。在GCC 4.8.3中,limits.h文件定义如下:



表1-3描述了以ANSI标准定义的整数类型。

表1-3 ANSI标准定义的整数类型

简单地讲,有符号和无符号整数间的区别在于怎样解释整数的最高位。如果定义一个有符号整数,则C编译程序生成的代码认为该数最高位是符号标志:符号标志为0,则该数为正;符号标志为1,则该数为负。

负数采用2的补码的形式来表示,即对原码各位求反(符号位除外),再将求反的结果加1,最后将符号位设置为1。例如,在32位操作系统中,有符号整数-2的存储方法如下。

第一步:取绝对值2的二进制编码。

00000000 00000000 00000000 00000010

第二步:求反(符号位除外)。

01111111 11111111 11111111 11111101

第三步:将求反的结果加1。

01111111 11111111 11111111 11111110

第四步:将符号位设置为1。

11111111 11111111 11111111 11111110

因此,有符号整数-2的二进制编码为11111111 11111111 11111111 11111110,十六进制编码为0xFFFFFFFE。

最后还需要说明的是,当类型修饰符被自身使用时(即它不在基本类型之前时),假定其为int型。也就是说,表1-4的两种类型是等效的。

表1-4 等效的整数类型

建议2-1:char类型变量的值应该限制在signed char与unsigned char的交集范围内

大家应该都知道,C语言设计char类型的目的是存储字母和标点符号之类的字符。实际上,char类型存储的是整数而不是字符。为了处理字符,计算机使用一种数字编码的方式来操作,如常见的ASCII就是用特定整数来表示特定字符的。例如,要在ASCII码中存储字母B,实际上只需要存储整数66。因此,可以使用下面的方法为char类型的变量赋值。


char c=66;


在ASCII码中,整型数据66在char类型的大小范围之内,所以这样的赋值方式是完全允许的,但不推荐使用这样的赋值方式。

这里需要注意的是,采用这样的赋值方式有个前提条件,即必须是在ASCII码中。有时候不同的计算机系统也会使用完全不同的编码,如一些IBM主机就使用一种称为EBCDIC(Extended Binary-Coded Decimal Interchange Code,扩充的二进制编码的十进制交换码)的编码方式。如果采用的是其他编码方式,这样的赋值方式所得到的结果就不一样了。因此,我们推荐使用字符常量的方式进行赋值,如下面的代码所示:


char c='B';


除此之外,在表1-3中还可以看出,默认的char类型可以是signed char类型(取值范围为-127~127),也可以是unsigned char类型(取值范围为0~255),具体取决于编译器。也就是说,不同的机器上char可能拥有不同范围的值。因此,为了使程序保持良好的可移植性,我们所声明的char类型变量的值应该限制在signed char与unsigned char的交集范围内。例如,ASCII字符集中的字符都在这个范围内。

当然,在一个把字符当做整数值的处理程序中,可以显式地把这类变量声明为signed char或unsigned char,从而确保不同的机器中在字符是否为有符号值方面保持一致,以此来提高程序的可移植性。另一方面,许多处理字符的库函数把它们的参数都声明为char,如果我们把这些参数显式地声明为signed char或unsigned char,可能会带来兼容性问题;并且有些机器处理signed char的效率更高些,如果硬要把它改成unsigned char,效率很可能会因此而受损。所以把所有的char变量统一声明为signed char或unsigned char未必就是好的解决方案。因此,最佳的解决方案就是把char类型变量的值限制在signed char与unsigned char的交集范围内,这样既可以获得最大程度的可移植性,同时又不会牺牲效率。

建议2-2:使用显式声明为signed char或unsigned char的类型来执行算术运算

在讨论本建议话题之前,我们先看看下面的这段代码的输出结果,如代码清单1-1所示。

代码清单1-1 char使用示例


#include int main (void) { char c=150; int i=900; printf("i/c=%d\n", i/c); return 0;}


在代码清单1-1中,或许大多数人都认为它输出的结果应该是“i/c=6”,但实际的输出结果却大相径庭。前面已经讲过,char类型的变量c可以有两种类型:有符号的(signed char)和无符号的(unsigned char)。这里假设char是8位的补码字符类型,那么代码清单1-1就可能输出“i/c=-8”(signed char)或者“i/c=6”(unsigned char)两种结果。其中,在Microsoft Visual Studio 2010与GCC中的输出结果都是“i/c=-8”,如图1-4与图1-5所示。

图1-4 代码清单1-1在Microsoft Visual Studio 2010中的输出结果

图1-5 代码清单1-1在GCC中的输出结果

其实,导致这种结果最根本的原因就在于我们不能够准确地确定char类型的变量c究竟是signed char类型还是unsigned char类型。因此,我们把决策权交给编译器,而不同的编译器默认的char类型是不同的,所以最后得到的结果也就不相同。

解决这种问题的办法很简单,就是显式地将char类型的变量c声明为signed char或unsigned char类型,这样可保证结果的唯一性,如代码清单1-2所示。

代码清单1-2 unsigned char使用示例


#include int main (void) {

unsigned char c=150;

int i=900;

printf("i/c=%d\n", i/c);

return 0;

}


这样就显式地将char类型的变量c声明为unsigned char类型,现在,后面的除法运算(i/c)与char的符号无关,所以代码清单1-2输出的结果为“i/c=6”。

建议2-3:使用rsize_t或size_t类型来表示一个对象所占用空间的整数值单位

C语言标准规定size_t是一种无符号整数类型,编译器可以根据操作系统的不同而用typedef来定义不同的size_t类型,即在不同的操作系统上所定义的size_t可能不一样。例如在32位操作系统上可以将size_t定义为unsigned int类型,而在64位操作系统上则可以定义为unsigned long int类型,甚至还可以将size_t定义为unsigned long long int类型等,如下面的示例所示。

在GCC的stddef.h文件中将size_t定义为:


#ifndef __SIZE_TYPE__

#define __SIZE_TYPE__ long unsigned int

#endif

#if !(defined (__GNUG__) && defined (size_t))

typedef __SIZE_TYPE__ size_t;

#ifdef __BEOS__ typedef long ssize_t;

#endif /* __BEOS__ */


而在VC++2010的crtdefs.h文件中将size_t定义为:


#ifndef _SIZE_T_DEFINED #ifdef _WIN64 typedef unsigned __int64 size_t; #else typedef _W64 unsigned int size_t; #endif #define _SIZE_T_DEFINED #endif


从上面的定义可以看出,size_t类型的引入增强了程序在不同平台上的可移植性,而它也正是为了方便系统之间的移植而定义的。size_t类型的变量大小足以保证存储内存中对象的大小,任何表示对象长度的变量,包括作为大小、索引、循环计数和长度的整数值,都可以声明为size_t类型。比如我们常用的sizeof操作符的结果返回的就是size_t类型,该类型保证能容纳实现所建立的最大对象的字节大小。size_t类型的限制是由SIZE_MAX宏指定的。

接下来看看size_t类型的使用示例,如代码清单1-3所示。

代码清单1-3 size_t类型的使用示例


char *copy(size_t n, const char *str) { int i; char *p; if (n == 0) { /* 处理n==0的情况*/ } p = (char *)malloc(n); if (p == NULL) { /*处理p==NULL的情况 */ } for ( i = 0; i < n; ++i ) { p[i] = *str++; } return p; }


不难发现,代码清单1-3中存在着一个严重的问题:当p所引用的动态分配的缓冲区在n>INT_MAX时将会发生溢出。我们知道,int类型的限制是由INT_MAX宏指定的,而size_t类型代表的是一个无符号整数类型,它可能包含一个大于INT_MAX的值。因此,当n的值为0 <n<=int_max时,执行循环n次,代码如预期一样正常运行;但当n的值为int_max<n<=size_max,且整型变量i的增值超过int_max时,i的值将是从int_min开始的负值。这时,p[i]所引用的内存位置是在p所引用的内存之前,这就会导致写入发生在数组边界之外。< span="">

因此,为了避免发生这种潜在性的错误,应该将变量i也声明成size_t类型,如代码清单1-4所示。

代码清单1-4 代码清单1-3的解决方法


char *copy(size_t n, const char *str) { size_t i; char *p; if (n == 0||n>SIZE_MAX) { /* 处理n==0的情况 */ } p = (char *)malloc(n); if (p == NULL) { /*处理p==NULL的情况*/ } for ( i = 0; i < n; ++i ) { p[i] = *str++; } return p; }


除了size_t类型之外,ISO/IEC TR 24731-1:2007中引入了一种新类型rsize_t,虽然它被定义为size_t类型,但它明确地表示是用于保存单个对象的长度的。

在VC++2010的crtdefs.h文件中将rsize_t定义为:


#if __STDC_WANT_SECURE_LIB__ #ifndef _RSIZE_T_DEFINED typedef size_t rsize_t;#define _RSIZE_T_DEFINED #endif #endif


在支持rsize_t类型的代码中,你可以检查对象的长度,验证它不大于RSIZE_MAX(一个正常单个对象的最大长度),库函数也可以使用rsize_t进行输入校验。

在VC++2010的limits.h文件中将RSIZE_MAX定义为:


#if __STDC_WANT_SECURE_LIB__ #ifndef RSIZE_MAX #define RSIZE_MAX SIZE_MAX #endif #endif


这样就消除了示例整数溢出的可能性,现在我们可以将代码清单1-3中的变量i声明成rsize_t类型,同时也可将参数n修改成rsize_t类型,并与RSIZE_MAX进行比较以验证数据的合法范围,如代码清单1-5所示。

代码清单1-5 代码清单1-3的rsize_t解决方法


char *copy(rsize_t n, const char *str) { rsize_t i; char *p; if (n == 0 || n > RSIZE_MAX) { /* 处理n==0|| n > RSIZE_MAX的情况 */ } p = (char *)malloc(n); if (p == NULL) { /*处理p==NULL的情况 */ } for (i = 0; i < n; ++i) { p[i] = *str++; } return p; }


本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2016-08-29,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 玄魂工作室 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 以下内容摘抄自《改善C程序代码的125个建议》:
    • 建议2:防止整数类型产生回绕与溢出
    领券
    问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档