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

以下内容摘抄自《改善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; }


原文发布于微信公众号 - 玄魂工作室(xuanhun521)

原文发表时间:2016-08-29

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏MasiMaro 的技术博文

C函数原理

C语言作为面向过程的语言,函数是其中最重要的部分,同时函数也是C种的一个难点,这篇文章希望通过汇编的方式说明函数的实现原理。

1183
来自专栏Java成神之路

PHP中 对象自动调用的方法:__set()、__get()、__tostring()

 (1)__get($property_name):获取私有属性$name值时,此对象会自动调用该方法,将属性name值传给参数$property_name,通...

1044
来自专栏C/C++基础

C++inline函数简介

inline函数是由inline关键字来定义,引入inline函数的主要原因是用它替代C中复杂易错不易维护的宏函数。

1502
来自专栏ShaoYL

深刻理解----修饰变量----关键字

35011
来自专栏java一日一条

基础类型转化成String

在程序中你可能时常会需要将别的类型转化成String,有时候可能是一些基础类型的值。在拼接字符串的时候,如果你有两个或者多个基础类型的值需要放到前面,你需要显式...

852
来自专栏www.96php.cn

PHP关键字、PHP 语言结构(Language constructs)和函数的区别

1、 什么是语言结构和函数 语言结构: 就是PHP语言的关键词,语言语法的一部分; 它不可以被用户定义或者添加到语言扩展或者库中; ...

3899
来自专栏mukekeheart的iOS之旅

OC学习10——内存管理

1、对于面向对象的语言,程序需要不断地创建对象。这些对象都是保存在堆内存中,而我们的指针变量中保存的是这些对象在堆内存中的地址,当该对象使用结束之后,指针变量指...

2315
来自专栏爱撒谎的男孩

Struts2之类型转换器

3625
来自专栏前端儿

理解运用JS的闭包、高阶函数、柯里化

JS的闭包,是一个谈论得比较多的话题了,不过细细想来,有些人还是理不清闭包的概念定义以及相关的特性。

2503
来自专栏java一日一条

Java字符串之性能优化

在程序中你可能时常会需要将别的类型转化成String,有时候可能是一些基础类型的值。在拼接字符串的时候,如果你有两个或者多个基础类型的值需要放到前面,你需要显式...

862

扫码关注云+社区

领取腾讯云代金券