《C语言程序设计现代方法》----第七章 基本类型

x33g5p2x  于2022-04-10 转载在 其他  
字(6.3k)|赞(0)|评价(0)|浏览(310)

第七章 基本类型

7.1 整数类型

说明符的顺序没有什么影响,所以unsigned short int 和short unsigned int是一样的。

标准要求int类型不能比short int类型短,long int类型不能比int类型段。但是short int 类型的取值范围有可能和int类型的范围是一样的,int类型的取值范围也可以和long int的一样。

注意:无论是哪一种类型,它们的取值范围都不是C标准强制的,会随着编译器的不同而不同。对于特定的实现,确定整数类型范围的一种方法是检查<limits.h>头。该头是标准库的一部分,其中定义了表示每种整数类型的最大值和最小值的宏。

7.1.2 整数常量

八进制常量只包含0~7中的数字,而且必须要以0开头。

十六进制常量包含0~9中的数字和a~f中的字母,而且总是以0x开头。
注意:十六进制常量中的字母既可以是大写字母,也可以是小写字母。

为了强制编译器把常量当作长整数来处理,只需要在后边加上一个字母L(或l),为了指明是无符号常量,可以在常量后边加上字母U(或u),U和L可以结合使用(字母U、L的顺序和大小写无所谓)。

7.1.4 整数溢出

有符号整数运算中发生溢出时,程序的行为是未定义的。

无符号整数运算过程中发生溢出时,结果是有定义的:正确答案是对2n取模,其中n是用于存储结果的位数。例如,如果无符号的16位数65535加1,其结果可以保证为0。

7.1.5 读/写整数

假设有一个程序因为其中一个int变量发生溢出而无法工作。我们的第一反应是把变量的类型从int变为long int。但仅仅这样是不够的,我们还必须检查数据类型的改变对程序其它部分的影响,尤其是需要检查该变量是否用在printf函数或者scanf函数的调用中。如果用了,需要改变调用的格式串,因为%d只适用于int类型。

  • 读/写无符号整数时,使用字母u、o或x代替转换说明中的d。如果使用u说明符,该数将以十进制形式读写,o表示八进制形式,而x表示十六进制形式。
  • 读写短整数时,在d、o、u或x前面加上字符h。
  • 读写长整数时,在d、o、u或x前面加上字符l。
  • (C99)读写长长整数时,在d、o、u或x前面加上字母ll。

7.2 浮点类型

long double支持极高精度的要求,很少会用到。

7.2.1 浮点常量

默认情况下,浮点常量都以双精度的形式进行存储。

为了表明只需要单精度,可以在常量的末尾处加上字母F或f;而为了说明常量必须以long double格式存储,可以在常量的末尾处加上字母L或l。

7.2.2 读/写浮点数

转换说明符%e、%f和%g用于读写单精度浮点数。

C99允许printf函数调用中使用%le、%lf和%lg,不过字母l不起作用。

7.3 字符类型

字符常量需要用单引号括起来,而不是双引号。

7.3.2 有符号字符和无符号字符

标准C允许使用单词signed和unsigned来修饰char类型:

unsigned char x;
signed char x;

可移植性技巧 不要假设char类型默认为signed或unsigned。如果有区别,用signed char或unsigned char代替char。

7.3.4 转移序列

名称转义序列
警报(响铃)符\a
回退符\b
换页符\f
换行符\n
回车符\r
水平制表符\t
垂直制表符\v
反斜杠\
问号?
单引号\’
双引号"
  • 八进制转义序列由字符\和跟随其后的一个最多含有三位数字的八进制数组成。(此数必须表示为无符号字符,所以最大值通常是八进制的377。)例如,可以将转义字符写成\33或\033。跟八进制常量不同,转义序列中的八进制数不一定要用0开头。
  • 十六进制转义序列由\x和跟随其后的一个十六进制数组成。虽然标准C对于十六进制数的位数没有限制,但必须表示成无符号字符(因此,如果字符长度是8位,那么16进制数的值不能超过FF)。若采用这种表示法,可以把转义字符写成\x1b或\x1B的形式。字符x必须小写,但是十六进制的数字(例如b)不限大小写。x不可省略。

7.3.5 字符处理函数

toupper()函数在被调用时检测参数是否为小写字母,如果是,他会把参数转换成相应的大写字母;否则,该函数会返回参数的值。

调用toupper函数的程序要添加预处理指令包含相应的头文件<ctype.h>。

7.3.6 用scanf和printf读/写字符

scanf函数不会跳过空白字符。为了强制scanf函数在读入字符前跳过空白字符,需要在格式串中的转换说明%c前面加上一个 空格。

注意:scanf格式串中的空白意味着“跳过零个或多个空白字符”。

7.3.7 用getchar和putchar读/写字符

注意:getchar含糊返回的是一个int类型的值而不是char类型的值。和scanf函数一样,getchar函数也不会在读取时跳过空白字符。

在执行程序时,使用getchar函数和putchar函数可以节约不少时间(胜于scanf和printf)。原因有两个:

  1. 这两个函数比scanf函数和printf函数简单的多,因为scanf函数和printf函数时设计用来按照不同的格式读/写多种不同类型的数据的。
  2. 为了额外的速度提升,通常getchar函数和putchar函数是作为宏来实现的。

惯用法:

while(getchar()!='\n')
···
while((ch = getchar())==' ')
···

7.4 类型转换

隐式转换:

  • 当算术表达式或者逻辑表达式中操作数的类型不相同时。(C语言执行所谓的常用算术转换
  • 当赋值运算符右侧表达式的类型和左侧变量的类型不匹配时。
  • 当函数的调用中的实参类型与其对应的形参类型不匹配时。
  • 当return语句中表达式的类型和函数返回值的类型不匹配时。

7.4.1 常用算术转换

整值提升:它把字符或短整型整数转换成int类型(或者某些情况下是unsigned int类型)。

整值提升有下列两种情况:

  • 任一操作数的类型是浮点类型的情况。按照下图将类型较狭小的操作数进行提升:

注意:只要双目运算符左右的数据有一个的类型是某一个类型,那么另一个比其等级低的类型将会向上进行转换。也就是说,如果一个操作数的类型为long double,那么另一个操作数的类型转换成long double。

  • 两个操作数的类型都不是浮点类型的情况。首先对其两个操作数进行整值提升(保证没有一个操作数的是字符类型或短整型)。然后按照下图对类型较狭小的操作数进行提升:

有一种特殊情况,只有在long int类型和unsigned int类型长度(比如36位)相同时才会发生。在这种情况下,如果一个操作数的类型是long int,而另一个的类型是unsigned int,那么两个操作数都会转换成unsigned long int类型。

注意:strlen()函数和sizeof()操作数的类型都是unsigned int类型。

7.4.2 赋值过程中的转换

常用算术转换不适用于赋值运算。C语言会遵循另一条简单的转换规则,那就是把赋值运算右边的表达式转换成左边变量的类型。如果变量的类型至少和表达式类型一样“宽”,那么这种转换都将没有任何障碍。当然,把某种类型的值赋给类型更狭小的变量,也将会得到无意义的结果。

下面是一个例子:

int a = 0;
unsigned x = 5;
int y = 5;
a = x + y;
//上面这个表达式将会发生两次类型转换,第一次,y将从有符号类型转换为有符号类型,第二次,x+y表达式的结果,将从无符号类型转换成有符号类型。

7.4.3 C99中的隐式转换

为了定义转换规则,C99中允许每个整数类型具有“整数转换等级”。下面按从最高级到最低级的顺序排列。

  1. long long int、unsigned long long int
  2. long int、unsigned long int
  3. int、unsigned int
  4. short int、unsigned short int
  5. char、signed char、unsigned char
  6. _Bool

C99用整数提升取代了C89中的整值提升,可以将任何等级低于int和unsigned int的类型转换为itn(只要该类型的所有值都可以用int类型表示)或unsigned int。

C99中执行常用算术替换的规则可以划分为两种情况:

  • 任一操作数的类型是浮点类型的情况。只要两个操作数的都不是复数型,规则与前面一样。

  • 两个操作数的类型都不是浮点类型的情况。首先对两个数进行整数提升,如果这时两个操作数的类型相同,过程结束。否则,依次尝试下面的规则,一旦遇到可应用的规则就不再考虑其它的规则:

  • 如果两个操作数都是有符号型或者都是无符号型,将整数转换等级较低的操作数转换为等级较高的操作数的类型。

  • 如果无符号数的等级高于或等于有符号操作数的等级,将有符号操作数转换为无符号操作数的类型。

  • 如果有符号操作数类型可以表示无符号操作数类型的所有值,将无符号操作数转换为有符号操作数的类型。

  • 否则,将两个操作数都转换为与有符号操作数的类型相对应的无符号类型。

另外,==所有算术类型都可以转换为_Bool类型。==如果原始值为0则转换结果为0,否则结果为1。

7.4.4 强制类型转换

  1. C语言把(类型名)视为一元运算符。一元运算符的优先级高于二元运算符。所以编译器会把表达式
(float)dividend / divisor

解释为

((float)dividend)/divisor
  1. 有时候,需要使用强制类型转换来避免溢出。
long i;
int j = 1000;
i = j*j;

当两个int类型值相乘时,结果也应该是int类型的,但是j*j的结果太大,以致在机器上无法表示成int型,从而导致溢出,此时就可以使用强制类型转换避免这种问题的发生:

i = (long)j*j;

7.5 类型定义

类型定义可以设置一个新类型。

typedef int Bool;

==注意:所定义的类型的名字放在最后。(#define是把新的名字(我们使用的字符名)放在前面)==同时,我们常常将定义的类型名的首字母设置为大写。

7.5.1 类型定义的优点

  1. 类型定义更加有利于理解,因为我们可以将类型名赋予实际的意义。
  2. 类型定义使程序员更加便于理解。
  3. 类型定义是产生新的数据类型,不像#define只是简单的替换。下面将给出例子:
#define Pint int*;
typedef int* Ptr_int;
Pint a,b;//被替换后是这样的 int *a,b;
Ptr_int c,d;

此处a是指针类型,b是整型类型,c和d都是整型指针数据类型。

  1. typedef命名的对象具有和变量相同的作用域规则:定义在函数体内的typedef名字在函数外是无法识别的。

7.5.2 类型定义和可移植性

类型定义能够使程序具有更好的可移植性。

在C99中,<stdint.h>头使用typedef定义占用特定位数的整数类型名,例如,int 32_t是恰好占用32位的有符号类型。这是一种有效的定义方式,能使程序更易于移植。

7.6 sizeof运算符

sizeof(表达式);

注意:

  1. sizeof()的值是一个无符号整数,代表存储属于类型名的值所需要的字节数。
  2. sizeof后面当加某一种数据类型时,必须加括号,但是如果加一个变量名的时候,就可以不加括号。
  3. sizeof()运算符是一种特殊的运算符,因为编译器本身通常就能够确定sizeof表达式的值。
  4. 一般表达式的运算是在运行时执行的,而sizeof是一个编译阶段就执行的运算符,在其内的任何运算都不执行,只推测出其中表达式结果的类型求其大小。**所以sizeof里面的表达式并没有真正的进行执行,即使里面的表达式有副作用,也不会对变量带来任何的影响。下面是例子:

虽然在sizeof()括号中变量a进行了++操作,但是a仍然没有改变。

注意:在C89中,编译器本身通常就能确定sizeof表达式的值。但是编译器不能确定变长数组的大小,因为数组中的元素个数在程序执行期间是可变的。

问与答

  1. 问:%o和%x分别用于以八进制和十六进制书写无符号整数,那么如何以八进制和十六进制书写普通的(有符号)整数呢?

答:只要有符号整数的值不是负值,就可以用%o和%x显示。这些转换导致print函数会把有符号整数看成是无符号的;换句话说,printf函数将假设符号位是数的绝对值部分。只要符号位为0,就没有问题。如果符号位为1,那么print函数将显示出一个超出预取的大数。

  1. 问:但是,如果数是负数怎么办呢?如何以八进制或十六进制形式书写它?

答:我们可以判定这个数是否是负数,然后自己显示一个负号:

if(i < 0)
	printf("-%x",-i);
else
	printf("%x",i);
  1. 问:为什么使用%lf读取double类型的值,而用%f进行显示呢?

答:这是一个十分难回答的问题。首先,注意,scanf函数和printf函数都是不同寻常的函数,因为它们都没有将函数的参数限制为固定数量。scanf函数和prinf函数有可变长度的参数列表。当调用带有可变长度参数列表的函数时,编译器会安排float参数自动转换成为double类型,其结果是printf函数无法区分float类型和double类型的参数。这解释了在printf函数调用中为何可以用%f既表示float类型又表示double类型的参数。

另一方面,scanf函数是通过指针指向变量的。%f告诉scanf函数在所传地址位置上存储一个float类型值,而%lf告诉scanf函数在该地址上存储一个double类型值。这里float和double的区别是非常重要的。如果给出了错误的转换说明,那么scanf函数将可能存储错误的字节数量(没有提到的是,float类型的位模式可能不同于double类型的位模式)。

  1. 问:使用转义序列?的目的是什么?

答:转义序列?与三字符序列有关,因为三字符序列以??开头。如果需要在字符串中加入??,那么编译器很可能会把它当作三字符序列的开始。用?代替第二个?可以解决这个问题。

  1. 问:既然getchar函数的读取速度更快,为什么仍然需要使用scanf函数读取单个的字符呢?

答:虽然scanf函数没有getchar速度快,但是它更加灵活。正如前面已经看到的,格式串"%c",可以使scanf函数读入下一个输入字符;" %c"可以读入下一个非空白字符。而且,scanf很擅长读取混入了其它数据类型的字符。

  1. 问:在什么情况下,整值提升会把字符或短整数转换为unsigned int类型?

答:如果int型整数没有大到足以包含所有可能的原始类型值,整值提升会产生unsigned int类型。因为字符通常是8位的长度,几乎总会转化为int类型(可以保证int类型至少为16位长度)。有符号短整数也总可以转换为int类型,但无符号短整数却是有疑问的。如果短整数和普通整数的长度相同(例如在16位机上),那么无符号短整数必须转化为unsigned int类型,因为最大的无符号短整数(在16位机上为65535)要大于最大的int类型整数(32767)。

  1. 问:如果把超出变量取值范围的值赋给变量,究竟会发生什么?

答:如果是整值类型并且变量是无符号类型,那么会丢掉超出的位数:如果变量是有符号类型,那么结果是由实现定义的。把符号数赋值给整型或浮点型变量的话,如果变量太小而无法承受,会产生未定义的行为:任何事都有可能发生,包括程序终止。

相关文章