第7章 基本类型
7.1 整数类型
C
语言支持两种根本不同的数值类型:整数类型(也称整型)和浮点类型(也称浮点型)。整数类型的值是整数,而浮点类型的值则可能还有小数部分。整数类型又分为两类:有符号整数和无符号整数。
有符号整数
如果为正数或零,那么最左边的位(符号位)为0
;如果是负数,则符号位为1
。因此,最大的16
位整数的二进制表示是0111_1111_1111_1111
,对应的数值是32767(即2^15 - 1)
。
最大的32
位整数的二进制表示是0111_1111_1111_1111_1111_1111_1111_1111
,对应的数值是
2147483647(即2^31 - 1)
。不带符号位(最左边的位是数值的一部分)的整数称为
无符号整数
。最大的16
位无符号整数是65535(即2^16 - 1)
,而最大的32
位无符号整数是4294967295(即2^32 - 1)
。默认情况下,C 语言中的整型变量都是有符号的,也就是说最左位保留为符号位。若要告诉编译器变量没有符号位,需要把它声明成unsigned
类型。无符号整数主要用于系统编程和底层与机器相关的应用。第20章将讨论无符号整数的常见应用,在此之前,我们通常回避无符号整数。
C语言的整数类型有不同的大小,int
类型通常为32
位,老的CPU上可能为16
位。为了存储很大的整型,C语言提供了长整型;为了节省空间,指示编译器以比正常存储小的空间来存储一些数,这样的数称为短整型。
为了使构造的整数类型正好满足需要,可以指明变量是
long
类型或short
类型、singed
类型或unsigned
类型,共有下列6
中组合可以产生不同的类型:
- short int (或者short)
- unsigned short int (或者unsigned short)
- ==========================
- int
- unsigned int
- ==========================
- long int (或者long)
- unsigned long int (或者unsigned long)
请注意!!C语言允许通过省略单词
int
来缩写整数类型的名字,就像上面括号里那样,C程序员经常会省略int
。
6种整数类型的每一种所表示的取值范围都会根据机器的不同而不同,但是有两条所有编译器都必须遵守的原则。
- C标准要求
short int
、int
和long int
中的每一种类型都要覆盖一个确定的最小取值范围(详见23.2节)。 - 其次,标准要求
int
类型不能比short int
类型短,long int
类型不能比int
类型短。但是,short int
类型的取值范围有可能和int
类型的范围是一样的,int
类型的取值范围也可以和long int
的一样。
在
16
位机上的整数类型, 通常short int
和int
有相同的取值范围(int
占2
个字节,16
bits)。在
32
位机上的整数类型, 通常int
和long int
有相同的取值范围(int
占4
个字节,32
bits)。在
64
位机上的整数类型,short
占2
两个字节,16
bits;int
占4
个字节,32
bits;long
占8
个字节,64
bits。(注意,有的64
位机器情况可能不一样,尝试使用sizeof()
函数确定具体大小)对于特定的实现,确定整数类型范围的一种方法是检查
<limits.h>
头(23.2节)。该头是标准库的一部分,其中定义了表示每种整数类型的最大值和最小值的宏。
7.1.1 C99中的整数类型
C99
提供了两个额外的标准整数类型: long long int
和unsigned long long int
。这两个long long
类型要求至少64
位宽。
C99
中把short int
、int
、long int
和long long int
类型[以及signed char
类型( 7.3 节)]称为标准有符号整型,而把 unsigned short int
、unsigned int
、unsigned long int
和unsigned long long int
类型[以及unsigned char
类型( 7.3 节)和_Bool
类型( 5.2 节)]称为标准无符号整型。
除了标准的整数类型以外,C99
标准还允许在具体实现时定义扩展的整数类型(包括有符号的和无符号的)。例如,编译器可以提供有符号和无符号的128
位整数类型。
7.1.2 整型常量
常量——在程序中以文本形式出现的数,而不是读、写或计算出来的数。
C语言允许用十进制(基数为
10
)、八进制(基数为8
)和十六进制(基数为16
)形式书写整型常量。
- 十进制常量包含
0~9
中的数字,但是一定不能以零开头:15
、255
、32767
- 八进制常量包含
0~7
中的数字,而且必须以零开头:017
、0377
、077777
- 十六进制常量包含
0~9
中的数字和a~f
中的字母(可以是大写也可以是小写),而且总是以0x
开头:0xf
、0xff
、0x7ff
请注意!!八进制和十六进制只是书写数的方式,它们不会对数的实际存储方式产生影响。(整数都是以二进制形式存储的,跟表示方式无关。)
任何时候都可以从一种书写方式切换到另一种书写方式,甚至可以混合使用:10 + 015 + 0x20 的值为55
(十进制)。八进制和十六进制更适用于底层程序的编写。
十进制整型常量的类型通常为int
,但如果常量的值大得无法存储在int
型中,那就用long int
类型。如果出现long int
不够用的罕见情况,编译器会用unsigned long int
做最后的尝试。确定八进制和十六进制常量的规则略有不同:编译器会依次尝试int
、unsigned int
、long int
和unsigned long int
类型,直至找到能表示该常量的类型。(有符号->无符号->有符号->无符号)
要强制编译器把常量作为长整数来处理,只需在后边加上一个字母
L
(orl
)
15L
、0377L
、0x7fffL
要指明是无符号常量,可以在常量后边加上字母
U
(oru
)
15U
、0377U
、0x7fffU
L
和U
可以结合使用,以表明常量既是长整型又是无符号的:0xffffffffUL
。(字母L
、U
的顺序和大小写无所谓。)
7.1.3 C99中的整型常量
在C99中,以
LL
或ll
(两个字母大小写要一致)结尾的整型常量是long long int
型的。如果在LL
或ll
的前面或后面增加字母U
(或u
),则该整型常量为unsigned long long int
型。
C99
确定整型常量类型的规则与C89
有些不同。对于没有后缀(U
、u
、L
、l
、LL
、ll
)的十进制常量,其类型是int
、long int
或long long int
中能表示该值的“最小”类型。对于八进制或者十六进制常量,可能的类型顺序为int
、unsigned int
、long int
、unsigned long int
、long long int
和unsigned long long int
。常量后面的任何后缀都会改变可能类型的列表。例如,以U
(或u
)结尾的常量类型一定是unsigned int
、unsigned long int
和unsigned long long int
中的一种,以L
(或l
)结尾的十进制常量类型一定是long int
或long long int
中的一种。如果常量的数值过大,以至于不能用标准的整数类型表示,则可以使用扩展的整数类型。
7.1.4 整数溢出
对整数执行算术运算时,其结果有可能因为太大而无法表示。例如,对两个
int
值进行算术运算时,结果必须仍然能用int
类型来表示;否则(表示结果所需的数位太多)就会发生溢出。整数溢出时的行为要根据操作数是有符号型还是无符号型来确定。有符号整数运算中发生溢出时,程序的行为是未定义的。回顾4.4节的介绍可知,未定义行为的结果是不确定的。最可能的情况是,仅仅是运算的结果出错了,但程序也有可能崩溃,或出现其他意想不到的状况。
无符号整数运算过程中发生溢出时,结果是有定义的:正确答案对
2的N次方
取模,其中n
是用于存储结果的位数。例如,如果对无符号的16
位数65535
加1
,其结果可以保证为0
。
7.1.5 读/写整数
因为
%d
只适用于int
类型。读写无符号整数、短整数和长整数需要一些新的转换指定符。
- 读写无符号整数时,使用字母
u
、o
或x
代替转换说明中的d
。如果使用u
说明符,该数将按十进制读写,o
表示八进制,x
表示十六进制。
unsigned int u;
scanf("%u", &u); /* reads u in base 10 */
printf("%u", u); /* writes u in base 10 */
scanf("%o", &u); /* reads u in base 8 */
printf("%o", u); /* writes u in base 8 */
scanf("%x", &u); /* reads u in base 16 */
printf("%x", u); /* writes u in base 16 */
- 读写短整数时,在
d
、o
、u
或x
前面加上字母h
:
short s;
scanf("%hd", &s);
printf("%hd", s);
- 读写长整数时,在
d
、o
、u
或x
前面加上字母l
:
long l;
scanf("%ld", &l);
printf("%ld", l);
- 读写长长整数时(仅限C99),在
d
、o
、u
或x
前面加上字母ll
:
long long ll;
scanf("%lld", &ll);
printf("%lld", ll);
7.2 浮点类型
有些时候需要变量能存储带小数点的数,或者能存储极大数或极小数。这类数可以用浮点(因小数点是“浮动的”而得名)格式进行存储。C语言提供了3种浮点类型,对应三种不同的浮点格式。
float
:单精度浮点数。double
:双精度浮点数。long double
:扩展精度浮点数。
当精度要求不严格时(例如,计算带一位小数的温度),float
类型是很适合的类型。double
提供更高的精度,对绝大多数程序来说够用了。long double
支持极高精度的要求,很少会用到。
C标准没有说明float
、double
和long double
类型提供的精度到底是多少,因为不同计算机可以用不同方法存储浮点数。大多数现代计算机遵循IEEE 754标准(即IEC 60559)的规范。
单精度(32
位)和双精度(64
位)。数值以科学记数法的形式存储,每一个数都由三部分组成:符号
、指数
和小数
。指数部分的位数说明了数值的可能大小程度,而小数部分的位数说明了精度。
float
类型的精度为6
个数字,double
为15
个数字。long double
类型的长度随着机器的不同而变化,而最常见的大小是80
位和128
位。- 在一些机器上,
float
可以有和double
相同的数值集合,或者double
可以有和long double
相同的数值集合。可以在头<float.h>
( 23.1 节)中找到定义浮点类型特征的宏。
在C99中,浮点类型分为两种:一种是实浮点类型,包括
float
、double
和long double
类型;另一种是C99新增的复数类型(27.3 节,包括float complex
、double complex
和long double complex
)。
7.2.1 浮点常量
浮点常量可以有许多种书写方式。例如,下面这些常量全都是表示数
57.0
的有效方式:
57.0
、 57.
、 57.0e0
、 57E0
、 5.7e1
、 5.7e+1
、 .57e2
、 570.e-1
浮点常量必须包含小数点
或指数
;其中,指数指明了对前面的数进行缩放所需的10
的幂次。如果有指数,则需要在指数数值前放置字母E
(或e
)。可选符号+
或-
可以出现在字母E
(或e
)的后边。
默认情况下,浮点常量以双精度数(double
)的形式存储。这条规则通常不会引发任何问题,因为在需要时double
类型的值可以自动转换为float
类型值。
为了表明只需要单精度,可以在常量的末尾处加上字母F
或f
(如57.0F
);而为了说明常量必须以long double
格式存储,可以在常量的末尾处加上字母L
或l
(如57.0L
)。
冷门小知识:C99提供了十六进制浮点常量的书写规范。十六进制浮点常量以
0x
或0X
开头(跟十六进制整型常量类似)。这一特性很少用到。
7.2.2 读/写浮点数
转换说明
%e
、%f
和%g
用于读写单精度浮点数(float
)。读写double
和long double
类型的值所需的转换说明略有不同。
- 读取
double
类型的值时,在e
、f
或g
前放置字母l
:
double d;
scanf("%lf", &d);
/*
注意:只能在scanf函数格式串中使用l,不能在printf函数格式串中使用。在printf函数格式串中,转换e、f和g可以用来写float类型或double类型的值。( C99允许printf函数调用中使用%le、%lf 和%lg,不过字母l不起作用。)
*/
- 读写
long double
类型的值时,在e
、f
或g
前放置字母L
:
long double ld;
scanf("%Lf", &ld);
printf("%Lf", ld)
7.3 字符类型
char
类型的值可以根据计算机的不同而不同,因为不同的机器可能会有不同的字符集。
当今最常用的字符集是
美国信息交换标准码(ASCII)
字符集( 附录E),它用7
位代码表示128
个字符。在ASCII码中,数字0~9
用0110000~0111001
码来表示,大写字母A~Z
用1000001~1011010
码来表示。ASCII
码常被扩展用于表示256
个字符,相应的字符集Latin-1
包含西欧语言和许多非洲语言中的字符。
char
类型的变量可以用任意单字符赋值:
char ch;
ch = 'a'; /* lower-case a */
ch = 'A'; /* upper-case A */
ch = '0'; /* zero */
ch = ' '; /* space */
//注意,字符常量需要用单引号括起来,而不是双引号
7.3.1 字符操作
在
C
语言中字符的操作非常简单,因为存在这样一个事实:C
语言把字符当作小整数进行处理。毕竟所有字符都是以二进制的形式进行编码的,而且无须花费太多的想象力就可以将这些二进制代码看成整数。例如,在
ASCII
码中,字符的取值范围是0000000~1111111
,可以看成0~127
的整数。字符'a'
的值为97
,'A'
的值为65
,'0'
的值为48
,而' '
的值为32
。在C
语言中,字符和整数之间的关联是非常强的。
当计算中出现字符时,C
语言只是使用它对应的整数值。假设下面的例子采用ASCII
字符集。
char ch;
int i;
i = 'a'; /* i is now 97 */
ch = 65; /* ch is now 'A' */
ch = ch + 1; /* ch is now 'B' */
ch++; /* ch is now 'C' */
可以像比较数那样对字符进行比较。下面的if
语句测试ch
中是否含有小写字母,如果有,那么它会把ch
转换为相应的大写字母。
if ('a' <= ch && ch <= 'z')
ch = ch - 'a' + 'A';
字符拥有和数相同的属性,这一事实会带来一些好处。例如,可以让for
语句中的控制变量遍历所有的大写字母:
for (ch = 'A'; ch <= 'Z'; ch++){
//.......
}
7.3.2 有符号字符和无符号字符
通常有符号字符的取值范围是
-128~127
,无符号字符的取值范围是0~255
。
C
语言标准没有说明普通char
类型数据是有符号型还是无符号型,有些编译器把它们当作有符号型来处理,有些编译器则将它们当作无符号型来处理。
标准C
允许使用单词signed
和unsigned
来修饰char
类型:
signed char sch;
unsigned char uch;
/*
注意,不要假设char类型默认为signed或unsigned。如果有区别,用signed char或unsigned char代替char。
*/
由于字符和整数之间的密切关系,
C89
采用术语整值类型(integral type)
来统称整数类型和字符类型。枚举类型(16.5节)也属于整值类型。
C99
不使用术语“整值类型”
,而是扩展了整数类型的含义,使其包含字符类型和枚举类型。C99
中的_Bool
型(5.2节)是无符号整数类型。
7.3.3 算术类型
整数类型和浮点类型统称为
算术类型
。
下面是C99的算术类型总结分类:
- 整数类型:
- 字符类型(char);
- 有符号整型,包括标准的(signed char、short int、int、long int、long long int)和扩展的;
- 无符号整型,包括标准的(unsigned char、unsigned short int、unsigned int、unsigned long int、unsigned long long int、_Bool)和扩展的;
- 枚举类型。
- 浮点类型:
- 实数浮点类型(float、double、long double);
- 复数类型(float_Complex、double_Complex、long double_Complex)
7.3.4 转义序列
转义序列共有两种:
字符转义序列(character escape)
和数字转义序列(numeric escape)
。
转义序列\a
(警报符)、\b
(回退符)、\f
(换页符)、\r
(回车符)、\t
(水平制表符)和\v
(垂直制表符)表示常用的ASCII控制字符, 转义序列\n
表示ASCII 码的回行符,转义序列\\
允许字符常量或字符串包含字符\
,转义序列\'
允许字符常量包含字符'
,而转义序列\"
则允许字符串包含字符"
,转义序列\?
很少使用。
字符转义序列使用起来很容易,但是它们有一个问题:转义序列列表没有包含所有无法打印的ASCII字符,只包含了最常用的字符。字符转义序列也无法用于表示基本的128个ASCII字符以外的字符。
数字转义序列可以表示任何字符
,所以它可以解决上述问题。
为了把特殊字符书写成数字转义序列,首先需要查找字符的八进制或十六进制值。例如,ASCII码中的ESC字符(十进制值为
27
)对应的八进制值为33
,对应的十六进制值为1B
。上述八进制或十六进制码可以用来书写转义序列。
八进制转义序列
由字符\
和跟随其后的一个最多含有三位数字
的八进制数组成。(此数必须表示为无符号字符,所以最大值通常是八进制的377
)例如,可以将转义字符写成\33
或\033
。跟八进制常量不同,转义序列中的八进制数不一定要用0开头。十六进制转义序列
由\x
和跟随其后的一个十六进制数组成。虽然标准C对十六进制数的位数没有限制,但其必须表示成无符号字符(因此,如果字符长度是8
位,那么十六进制数的值不能超过FF
)。若采用这种表示法,可以把转义字符写成\x1b
或\x1B
的形式。字符x
必须小写,但是十六进制的数字(例如b
)不限大小写。
作为字符常量使用时,转义序列必须用一对单引号括起来。例如,表示转义字符的常量可以写成'\33'
(或'\x1b'
)的形式。转义序列可能有点隐晦,所以采用#define
的方式给它们命名通常是个不错的主意:
#define ESC '\33' /* ASCII escape character */
转义序列不是唯一一种用于表示字符的特殊表示法。
三联序列
(25.3节)提供了一种表示字符#、[、\、]、^、{、|、}
和~
的方法,这些字符在一些语言的键盘上是打不出来的。
C99
增加了通用字符名(25.4节,采用unicode
编码,以\uXXXX
的形式表示字符,X
为十六进制数)。通用字符名跟转义序列相似,不同之处在于通用字符名可以用在标识符中。
7.3.5 字符处理函数
之前提到过如何将小写字母转换为大写字母,然而一种更快捷的方法是调用C语言的toupper
库函数:
ch = toupper(ch); /* convert ch to upper case */
toupper
函数在被调用时检测参数(本例中为ch
)是否为小写字母。如果是,它会把参数转换成相应的大写字母;否则,toupper
函数会返回参数的值。
调用toupper
函数的程序需要在顶部放置下面这条#include
指令:
#include <ctype.h>
这个库叫做字符分类函数库
,包含了一些用于处理字符的函数,用于测试字符的性质(如字母、数字、空白字符等),以及进行字符大小写转换等操作。
下面是ctype.h
库中常见的一些函数:
isalnum(int c)
:检查字符是否为字母或数字;isalpha(int c)
:检查字符是否为字母;iscntrl(int c)
:检查字符是否为控制字符;isdigit(int c)
:检查字符是否为数字isgraph(int c)
:检查字符是否可打印但不是空白字符;islower(int c)
:检查字符是否为小写字母;isprint(int c)
:检查字符是否可打印;ispunct(int c)
:检查字符是否为标点字符;isspace(int c)
:检查字符是否为空白字符(空格、制表符、换行符等);isupper(int c)
:检查字符是否为大写字母;isxdigit(int c)
:检查字符是否为十六进制数字;tolower(int c)
:将字符转换为小写;toupper(int c)
:将字符转换为大写;
这些函数通常以整数作为参数,接受一个字符的
ASCII
码值作为输入,并返回一个非零值(真)或零值(假),表示字符是否具有特定的性质。这些函数可以用于字符的分类、验证和转换,对于文本处理和字符处理非常有用。
7.3.6 用scanf和printf读/写字符
转换说明
%c
允许scanf
函数和printf
函数对单个字符进行读/写操作;
char ch;
scanf("%c", &ch); /* reads a single character */
printf("%c", ch); /* writes a single character */
在读入字符前,scanf
函数不会跳过空白字符。如果下一个未读字符是空格,那么在前面的例子中,scanf
函数返回后变量ch
将包含一个空格。为了强制scanf
函数在读入字符前跳过空白字符,需要在格式串中的转换说明%c
前面加上一个空格:
scanf(" %c", &ch); /* skips white space, then reads ch */
//scanf格式串中的空白意味着“跳过零个或多个空白字符”
7.3.7 用getchar和putchar读/写字符
C语言还提供了另外一些读/写单个字符的方法。特别是,可以使用getchar
函数和
putchar
函数来取代scanf
函数和printf
函数。putchar
函数用于写单个字符:
putchar(ch);
每次调用
getchar
函数时,它会读入一个字符并将其返回。为了保存这个字符,必须使用赋值操作将其存储到变量中;
ch = getchar(); /* reads a character and stores it in ch */
事实上,getchar
函数返回的是一个int
类型的值而不是char
类型的值(原因将在后续章节中讨论)。因此,如果一个变量用于存储getchar
函数读取的字符,其类型设置为int
而不是char
也没啥好奇怪的。和scanf
函数一样,getchar
函数也不会在读取时跳过空白字符。
执行程序时,使用
gerchar
函数和putchar
函数是可以节约时间的,相比scanf
函数和printf
函数。有下面两个原因:
- 这两个函数比
scanf
函数和printf
函数简单得多,因为scanf
函数和printf
函数是设计用来按不同的格式读/写多种不同类型数据的。 - 为了额外的速度提升,通常
getchar
函数和putchar
函数是作为宏(14.3节)来实现的。
getchar
函数还有一个优于scanf
函数的地方:因为返回的是读入的字符(而scanf
函数返回的是成功匹配的输入项数量),所以getchar
函数可以应用在多种不同的C语言惯用法中,包括搜索字符
或跳过所有出现的同一字符的循环
。
下面是两个常用的惯用法,含义十分隐晦,但是值得学习。
//跳过输入行的剩余部分:
while (getchar() != '\n') /* skips rest of line */
;
/*
getchar函数对搜索字符的循环和跳过字符的循环都很有用。
下面这个语句利用getchar函数跳过不定数量空格字符,
当循环终止时,变量ch将包含getchar函数遇到的第一个非空白字符。
*/
while ((ch = getchar()) == ' ') /* skips blanks */
;
小知识:如果在同一个程序中混合使用
getchar
函数和scanf
函数,请一定要注意。scanf
函数往往会遗留下它“扫视”过但未读取的字符(包括换行符)。思考一下,如果试图先读入数再读入字符的话,下面的程序片段会发生什么:
printf("Enter an integer: ");
scanf("%d", &i);
printf("Enter a command: ");
command = getchar();
在读入i
的同时,scanf
函数调用将留下没有消耗掉的任意字符,包括(但不限于)换行符。getchar
函数随后将取回第一个剩余字符(结果是command存储的是换行符),但这不是我们所希望的结果。
为了说明字符的读取方式,下面编写一个程序来计算消息的长度。在用户输入消息后,程序显示长度:
/*
Enter a message: Brevity is the soul of wit.
Your message was 27 character(s) long
消息的长度包括空格和标点符号,但是不包含消息结尾的换行符。
*/
#include <stdio.h>
int main() {
int count = 0;
printf("Enter a message: ");
while((getchar()) != '\n')
++count;
printf("Your message was %d character(s) long", count);
return 0;
}
7.4 类型转换
C语言则允许在表达式中混合使用基本类型。在单个表达式中可以组合整数、浮点数,甚至是字符。当然,在这种情况下C编译器可能需要生成一些指令,将某些操作数转换成不同类型,使得硬件可以对表达式进行计算。
例如,如果对
16
位short
型数和32
位int
型数进行加法操作,那么编译器将安排把16
位short
型值转换成32
位值。如果是int
型数据和float
型数据进行加法操作,那么编译器将安排把int
型值转换成为float
格式。这个转换过程稍微复杂一些,因为int
型值和float
型值的存储方式不同。由编译器自动处理的转换称为隐式转换(implicit conversion)。
同时,允许程序员使用强制运算符执行显示转换(explicit conversion)。
当发生下列情况是会进行隐式转换:
- 当算术表达式或逻辑表达式中操作数的类型不相同时。(C语言执行所谓的常规算术转换。)
- 当赋值运算符右侧表达式的类型和左侧变量的类型不匹配时。
- 当函数调用中的实参类型与其对应的形参类型不匹配时。
- 当
return
语句中表达式的类型和函数返回值的类型不匹配时
7.4.1 常规算术转换
常规算术转换可用于大多数二元运算符(包括算术运算符、关系运算符和判等运算符)的操作数。
例如,假设变量
f
为float
类型,变量i
为int
类型。常规算术转换将应用在表达式f + i
的操作数上,因为两者的类型不同。显然把变量i
转换成float
类型(匹配变量f
的类型)比把变量f
转换成int
类型(匹配变量i
的类型)更安全。常规算术转换的策略是把操作数转换成可以安全地适用于两个数值的“最狭小的”数据类型。(粗略地说,如果某种类型要求的存储字节比另一种类型少,那么这种类型就比另一种类型更狭小。)为了统一操作数的类型,通常可以将相对较狭小类型的操作数转换成另一个操作数的类型来实现(这就是所谓的
提升
)。最常用的提升是整值提升(integral promotion),它把字符或短整数转换成int
类型(或者某些情况下是unsigned int
类型)。
执行常规算术转换的规则可以划分成两种情况:
- 任一操作数的类型是浮点类型的情况。按照
float -> double -> long double
将类型较狭小的操作数进行提升。也就是说,如果一个操作数的类型为long double
,那么把另一个操作数的类型转换成long double
类型。否则,如果一个操作数的类型为double
类型,那么把另一个操作数转换成double
类型。否则,如果一个操作数的类型是float
类型,那么把另一个操作数转换成float
类型。注意,这些规则涵盖了混合整数和浮点类型的情况。例如,如果一个操作数的类型是long int
类型,并且另一个操作数的类型是double
类型,那么把long int
类型的操作数转换成double
类型。 - 两个操作数的类型都不是浮点类型的情况。首先对两个操作数进行整值提升(保证没有一个操作数是字符类型或短整型)。然后按照
int -> unsigned int -> long int -> unsigned long int
对较狭小的操作数进行提升。有一种特殊情况,只有在long int
类型和unsigned int
类型长度相同(比如32
位)时才会发生。在这类情况下,如果一个操作数的类型是long int
,而另一个操作数的类型是unsigned int
,那么两个操作数都会转换成unsigned long int
类型。
小知识,当有符号操作数和无符号操作数组合起来时,有符号操作数会被“转换”为无符号的值。转换过程中需要加上或者减去
n+1
的倍数,其中n
是无符号类型能表示的最大值。这条规则可能会导致某些隐蔽的编程错误。因为此类陷阱的存在,所以最好尽量避免使用无符号整数,特别是不要把它和有符号整数混合使用。
下面的例子显示了常规算术转换的实际执行情况:
char c;
short int s;
int i;
unsigned int u;
long int l;
unsigned long int ul;
float f;
double d;
long double ld;
i = i + c; /* c is converted to int */
i = i + s; /* s is converted to int */
u = u + i; /* i is converted to unsigned int */
l = l + u; /* u is converted to long int */
ul = ul + l; /* l is converted to unsigned long int */
f = f + ul; /* ul is converted to float */
d = d + f; /* f is converted to double */
ld = ld + d; /* d is converted to long double *
7.4.2 赋值过程中的转换
与常规算术转换不同,赋值过程中,C语言会遵循另一条简单的转换规则,那就是把赋值运算右边的表达式转换成左边变量的类型。如果变量的类型至少和表达式类型一样“宽”,那么这种转化将没有任何障碍。
char c;
int i;
float f;
double d;
i = c; /* c is converted to int */
f = i; /* i is converted to float */
d = f; /* f is converted to double */
其他情况下是有问题的。把浮点数赋值给整型变量会丢掉该数的小数部分:
int i;
i = 842.97; /* i is now 842 */
i = -842.97; /* i is now –842 */
此外,把某种类型的值赋给类型更狭小的变量时,如果该值在变量类型范围之外,那么将得到无意义的结果(甚至更糟)。
c = 10000; /*** WRONG ***/
i = 1.0e20; /*** WRONG ***/
f = 1.0e100; /*** WRONG ***/
这类赋值可能会导致编译器或lint
之类的工具发出警告。
如果浮点常量被赋值给
float
型变量,那么建议在浮点常量尾部加上后缀f
,例如f = 3.1415926f;
如果没有后缀,常量
3.14159
将是double
类型,可能会触发警告消息。
7.4.3 C99中的隐式转换
C99
中的隐式转换和C89
中的隐式转换略有不同,这主要是因为C99
增加了一些类型(_Bool
、long long
类型、扩展的整数类型和复数类型)为了定义转换规则,
C99
允许每个整数类型具有“整数转换等级”。下面按从最高级到最低级的顺序排列:
long long int, unsigned long long int
long int, unsigned long int
int, unsigned int
short int, unsigned short int
char, signed char, unsigned char
_Bool
C99
用整数提升(integer promotion)取代了C89
中的整值提升(integral promotion),可以将任何等级低于int
和unsigned int
的类型转换为int
(只要该类型的所有值都可以用int
类型表示)或unsigned int
。
C99
中执行常规算术转换的规则可以分为两种情况:
- 任一操作数的类型是浮点类型的情况。只要两个操作数都不是复数型,规则与前面一样。
- 两个操作数的类型都不是浮点类型的情况。首先对两个操作数进行整数提升。如果这时两个操作数的类型相同,过程结束。否则,依次尝试下面的规则,一旦遇到可应用的规则就不再考虑别的规则。
- 如果两个操作数都是有符号型或者都是无符号型,将整数转换等级较低的操作数转换为等级较高的操作数的类型。
- 如果无符号操作数的等级高于或等于有符号操作数的等级,将有符号操作数转换为无符号操作数的类型。
- 如果有符号操作数类型可以表示无符号操作数类型的所有值,将无符号操作数转换为有符号操作数的类型。
- 否则,将两个操作数都转换为与有符号操作数的类型相对应的无符号类型。
另外,所有算术类型都可转换为_Bool类型。如果原始值为0则转换结果为0,否则结果为1。
7.4.4 强制类型转换
虽然C语言的隐式转换使用起来非常方便,但我们有些时候还需要从更大程度上控制类型转换。基于这种原因,C语言提供了强制类型转换。
下面的例子显示了使用强制类型转换表达式计算float
类型值小数部分的方法:
float f, frac_part;
frac_part = f - (int)f;
强制类型转换表达式可以用于显示那些肯定会发生的类型转换:
i = (int)f; /* f is converted to int */
顺便提一下,C语言把(类型名)视为一元运算符。一元运算符的优先级高于二元运算符,因此编译器会把表达式:
(float) dividend / divisor
解释为
((float) dividend) / divisor
有些时候,需要使用强制类型转换来避免溢出。思考下面这个例子:
long i;
int j = 1000;
i = j * j; /* overflow may occur */
乍看之下,这条语句没有问题。表达式j * j
的值是1 000 000
,并且变量i
是long int
类型的,所以应该能很容易地存储这种大小的值,不是吗?问题是,当两个int
类型值相乘时,结果也应该是int
类型的,但是j * j
的结果太大,以致在某些机器上无法表示为int
型,从而导致溢出。幸运的是,可以使用强制类型转换避免这种问题发生:
i = (long) j * j;
因为强制运算符的优先级高于*
,所以第一个变量j
会被转换成long int
类型,同时也迫使第二个j
进行转换。注意,语句
i = (long) (j * j); /*** WRONG ***/
是不对的,因为溢出在强制类型转换之前就已经发生了。
7.5 类型定义
前面的章节中,我们利用#define
指令创建了一个宏,但是一个更好的办法是利用所谓的类型定义特性,比如:
typedef int Bool;
注意,Bool
是新类型的名字。还要注意,我们使用首字母大写的单词Bool
。将类型名的首字母大写不是必需的,只是一些C语言程序员的习惯。
采用typedef
定义Bool
会导致编译器在它所识别的类型名列表中加入Bool
。现在,Bool
类型可以和内置的类型名一样用于变量声明、强制类型转换表达式和其他地方了。例如,可以使用Bool
声明变量:
Bool flag; /* same as int flag; */
编译器将把Bool
类型看成是int
类型的同义词;因此,变量flag
实际就是一个普通的int
类型变量。
7.5.1 类型定义的优点
类型定义使程序更加易于理解(假定程序员仔细选择了有意义的类型名)。例如,假设变量cash_in
和变量cash_out
将用于存储美元数量。可以这样做:
typedef float Dollars;
Dollars cash_in, cash_out;
这样的写法比float cash_in, cash_out;
更有实际意义;
不仅如此,类型定义还可以使程序更容易修改。如果稍后决定Dollars
实际应该定义为double
类型,那么只需要改变类型定义就足够了:
7.5.2 类型定义和可移植性
类型定义是编写可移植程序的一种重要工具。程序从一台计算机移动到另一台计算机可能引发的问题之一就是不同计算机上的类型取值范围可能不同。如果
i
是int
类型的变量,那么赋值语句
i = 100000;
在使用32
位整数的机器上是没问题的,但是在使用16
位整数的机器上就会出错。
可移植性技巧
:为了更大的可移植性,可以考虑使用typedef
定义新的整数类型名。可惜的是,这种技术无法解决所有的问题,因为类型定义的变化可能会影响对应变量的使用方式。我们至少需要改动使用了对应类型变量的
printf
函数调用和scanf
函数调用(比如用转换说明%ld
替换%d
)。
C语言库自身使用typedef
为那些可能因C语言实现的不同而不同的类型创建类型名。这些类型的名字经常以_t
结尾,比如ptrdiff_t
、size_t
和wchar_t
。这些类型的精确定义不尽相同,下面是一些常见的例子:
typedef long int ptrdiff_t;
typedef unsigned long int size_t;
typedef int wchar_t;
在C99
中,<stdint.h>
头使用typedef
定义占用特定位数的整数类型名。例如,int32_t
是恰好占用32
位的有符号整型。这是一种有效的定义方式,能使程序更易于移植。
7.6 sizeof运算符
sizeof(类型名)
sizeof
运算符允许程序获取存储指定类型的值所需要的内存空间,其值是一个无符号整数,代表存储属于类型名
的值所需要的字节数。表达式
sizeof(char)
的值始终为1
,但是对其他类型计算出的值可能会有所不同。在32
位的机器上,表达式sizeof(int)
的值通常为4
。注意,sizeof
运算符是一种特殊的运算符,因为编译器本身通常就能够确定sizeof
表达式的值。
通常情况下,sizeof
运算符也可以应用于常量、变量和表达式。如果i
和j
是整型变量,那么sizeof(i)
在32
位机器上的值为4
,这和表达式sizeof(i+j)
的值一样。跟应用于类型时不同,sizeof
应用于表达式时不要求圆括号,我们可以用sizeof i
代替sizeof(i)
。但是,由于运算符优先级的问题,圆括号有时还是需要的。编译器会把表达式sizeof i + j
解释为(sizeof i) + j
,这是因为sizeof
作为一元运算符的优先级高于二元运算符+
。为了避免出现此类问题,本书在sizeof
表达式中始终加上圆括号。
显示
sizeof
值要注意,sizeof
表达式的类型是size_t
,一种由实现定义的类型。size_t
一定是无符号整型。
问与答
问1:7.1节说到
%o
和%x
分别用于以八进制和十六进制书写无符号整数。那么如何以八进制和十六进制书写普通的(有符号)整数呢?
答:只要有符号整数的值不是负值,就可以用%o
和%x
显示。这些转换导致printf
函数把有符号整数看作无符号的;换句话说,printf
函数将假设符号位是数的绝对值部分。只要符号位为0
,就没有问题。如果符号位为1
,那么printf
函数将显示出一个超出预期的大数。
问2:但是,如果是负数该怎么办呢?如何以八进制或十六进制书写它?
答:没有直接的方法可以书写负数的八进制或十六进制形式。幸运的是,需要这样做的情况非常少。当然,我们可以判定这个数是否为负数,然后自己显示一个负号(=符号+数的绝对值):
if (i < 0)
printf("-%x", -i);
else
printf("%x", i);
问3:浮点常量为什么存储成
double
格式而不是float
格式?
答:由于历史的原因,C语言更倾向于使用double
类型,float
类型则被看作次要的。思考Kernighan
和Ritchie
的The C Programming Language
一书中关于float
的论述:“使用float
类型的主要原因是节省大型数组的存储空间,或者有时是为了节省时间,因为在一些机器上双精度计算的开销格外大。”经典C要求所有浮点计算都采用双精度的格式。(C89
和C99
没有这样的要求。)
问4:十六进制的浮点常量是什么样子?使用这种浮点常量有什么好处?
答:十六进制浮点常量以0x
或0X
开头,且必须包含指数(指数跟在字母P
或p
后面)。指数可以有符号,常量可以以f
、F
、l
或L
结尾。指数以十进制数表示,但代表的是2
的幂而不是10
的幂。例如,0x1.Bp3
表示1.6875×2^3 = 13.5
。十六进制位B
对应的位模式为1011
;因为B
出现在小数点的右边,所以其每一位代表一个2
的负整数幂,把它们(2^1 + 2^3 + 2^4
)相加得到0.6875
。
十六进制浮点常量主要用于指定精度要求较高的浮点常量(包括e
和π
等数学常量)。十进制数具有精确的二进制表示,而十进制常量在转换为二进制时则可能受到舍入误差的些许影响。十六进制数对于定义极值(例如<float.h>
头中宏的值)常量也是很有用的,这些常量很容易用十六进制表示,但难以用十进制表示。
问5:为什么使用
%lf
读取double
类型的值,却用%f
显示它呢?
答:这是一个很难回答的问题。首先注意,scanf
函数和printf
函数都是不同寻常的函数,因为它们都没有将函数的参数限制为固定数量。scanf
函数和printf
函数有可变长度的参数列表( 26.1 节)。当调用带有可变长度参数列表的函数时,编译器会安排float
参数自动转换成为double
类型,其结果是printf
函数无法区分float
类型和double
类型的参数。这解释了在printf
函数调用中为何可以用%f
既表示float
类型又表示double
类型的参数。
另外,scanf
函数是通过指针指向变量的。%f
告诉scanf
函数在所传地址位置上存储一个float
类型值,而%lf
告诉scanf
函数在该地址上存储一个double
类型值。这里float
和double
的区别是非常重要的。如果给出了错误的转换说明,那么scanf
函数将可能存储错误的字节数量(更不用说float
类型的位模式可能不同于double
类型的位模式)。
问6:什么时候需要考虑字符变量是有符号的还是无符号的?
答:如果在变量中只存储7
位的字符,那么不需要考虑,因为符号位将为零。但是,如果计划存储8
位字符,那么变量可能最好是unsigned char
类型。思考下面的例子:
ch = '\xdb'
如果已经把变量ch
声明成char
类型,那么编译器可能选择把它看作有符号的字符来处理(许多编译器这么做)。只要变量ch
仅作为字符来使用,就不会有什么问题。但是如果ch
用在一些需要编译器将其值转换为整数的上下文中,那么可能就有问题了:转换为整数的结果将是负数,因为变量ch
的符号位为1
。
还有另外一种情况:在一些程序中,习惯使用char
类型变量存储单字节的整数。如果编写了这类程序,就需要决定每个变量应该是signed char
类型还是unsigned char
类型,这就像需要决定普通整型变量应该是int
类型还是unsigned int
类型一样。
问7:使用转义序列
\?
的目的是什么?
答:转义序列\?
与三联序列(25.3节)有关,因为三联序列以??
开头。如果需要在字符串中加入??
,那么编译器很可能会把它误认为三联序列的开始。用\?
代替第二个?
可以解决这个问题。
问8:既然
getchar
函数的读取速度更快,为什么仍然需要使用scanf
函数读取单个的字符呢?
答:虽然scanf
函数没有getchar
函数读取的速度快,但是它更灵活。正如前面已经看到的,格式串"%c"
可以使scanf
函数读入下一个输入字符," %c"
可以使scanf
函数读入下一个非空白字符。而且,scanf
函数也很擅长读取混合了其他数据类型的字符。假设输入数据中包含一个整数、一个单独的非数值型字符和另一个整数。通过使用格式串"%d%c%d"
就可以利用scanf
函数读取全部三项内容。
问9:在什么情况下,整值提升会把字符或短整数转换成
unsigned int
类型?
答:如果int
类型整数没有大到足以包含所有可能的原始类型值,那么整值提升会产生unsigned int
类型。因为字符的长度通常是8
位,所以几乎总会转换为int
类型(可以保证int
类型至少为16
位长度)。有符号短整数也总可以转换为int
类型,但无符号短整数是有疑问的。如果短整数和普通整数的长度相同(例如在16
位机上),那么无符号短整数必须被转换为unsigned int
类型,因为最大的无符号短整数(在16
位机上为65 535
)要大于最大的int
类型数(即32 767
)。
问10:如果把超出变量取值范围的值赋值给变量,究竟会发生什么?
答:粗略地讲,如果值是整值类型并且变量是无符号类型,那么会丢掉超出的位数;如果变量是有符号类型,那么结果是由实现定义的。把浮点数赋值给整型或浮点型变量的话,如果变量太小而无法承受,会产生未定义的行为:任何事情都可能发生,包括程序终止。
问11:为什么C语言要提供类型定义呢?定义一个
BOOL
宏不是和用typedef
定义一个Bool
类型一样好用吗?
答:类型定义和宏定义存在两个重要的不同点。首先,类型定义比宏定义功能更强大。具体来说,数组和指针类型是不能定义为宏的。假设我们试图使用宏来定义一个“指向整数的指针”类型:
#define PTR_TO_INT int *
声明
PTR_TO_INT p, q, r;
在处理以后会变成
int * p, q, r;
可惜的是,只有p
是指针,q
和r
都成了普通的整型变量。类型定义不会有这样的问题。
其次,typedef
命名的对象具有和变量相同的作用域规则;定义在函数体内的typedef
名字在函数外是无法识别的。另外,宏的名字在预处理时会在任何出现的地方被替换。
问12:本书中提到“编译器本身通常就能够确定
sizeof
表达式的值”。难道编译器不总能确定sizeof
表达式的值吗?
答:在C89
中编译器总是可以的,但在C99
中有一个例外。编译器不能确定变长数组(8.3节)的大小,因为数组中的元素个数在程序执行期间是可变的。
总结
本文是作者阅读《C语言程序设计:现代方法(第2版·修订版)》时所做笔记,日后会持续更新后续章节笔记。欢迎各位大佬阅读学习,如有疑问请及时联系指正,希望对诸位有所帮助,Thank you very much!