C现代方法(第7章)笔记——基本类型

第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种整数类型的每一种所表示的取值范围都会根据机器的不同而不同,但是有两条所有编译器都必须遵守的原则。

  1. C标准要求short intintlong int中的每一种类型都要覆盖一个确定的最小取值范围(详见23.2节)。
  2. 其次,标准要求int类型不能比short int类型短,long int类型不能比int类型短。但是,short int类型的取值范围有可能和int类型的范围是一样的,int类型的取值范围也可以和long int的一样。
  • 16位机上的整数类型, 通常short intint有相同的取值范围(int2个字节,16bits)。

  • 32位机上的整数类型, 通常intlong int有相同的取值范围(int4个字节,32bits)。

  • 64位机上的整数类型, short2两个字节,16bits;int4个字节,32bits;long8个字节,64bits。(注意,有的64位机器情况可能不一样,尝试使用sizeof()函数确定具体大小)

对于特定的实现,确定整数类型范围的一种方法是检查<limits.h>头(23.2节)。该头是标准库的一部分,其中定义了表示每种整数类型的最大值和最小值的宏。

7.1.1 C99中的整数类型

C99提供了两个额外的标准整数类型: long long intunsigned long long int这两个long long类型要求至少64位宽

C99中把short intintlong intlong long int类型[以及signed char类型( 7.3 节)]称为标准有符号整型,而把 unsigned short intunsigned intunsigned long intunsigned long long int类型[以及unsigned char类型( 7.3 节)和_Bool类型( 5.2 节)]称为标准无符号整型

除了标准的整数类型以外,C99标准还允许在具体实现时定义扩展的整数类型(包括有符号的和无符号的)。例如,编译器可以提供有符号和无符号的128位整数类型。

7.1.2 整型常量

常量——在程序中以文本形式出现的数,而不是读、写或计算出来的数。

C语言允许用十进制(基数为10)、八进制(基数为8)和十六进制(基数为16)形式书写整型常量。

  • 十进制常量包含0~9中的数字,但是一定不能以零开头: 1525532767
  • 八进制常量包含0~7中的数字,而且必须以零开头: 0170377077777
  • 十六进制常量包含0~9中的数字和a~f中的字母(可以是大写也可以是小写),而且总是以0x开头: 0xf0xff0x7ff

请注意!!八进制和十六进制只是书写数的方式,它们不会对数的实际存储方式产生影响。(整数都是以二进制形式存储的,跟表示方式无关。)

任何时候都可以从一种书写方式切换到另一种书写方式,甚至可以混合使用:10 + 015 + 0x20 的值为55(十进制)。八进制和十六进制更适用于底层程序的编写。

十进制整型常量的类型通常为int,但如果常量的值大得无法存储在int型中,那就用long int类型。如果出现long int不够用的罕见情况,编译器会用unsigned long int做最后的尝试。确定八进制和十六进制常量的规则略有不同:编译器会依次尝试intunsigned intlong intunsigned long int类型,直至找到能表示该常量的类型。(有符号->无符号->有符号->无符号)

要强制编译器把常量作为长整数来处理,只需在后边加上一个字母L(or l)
15L0377L0x7fffL

要指明是无符号常量,可以在常量后边加上字母U(or u)
15U0377U0x7fffU

LU可以结合使用,以表明常量既是长整型又是无符号的:0xffffffffUL。(字母LU的顺序和大小写无所谓。)

7.1.3 C99中的整型常量

在C99中,以LLll(两个字母大小写要一致)结尾的整型常量是long long int型的。如果在LLll的前面或后面增加字母U(或u),则该整型常量为unsigned long long int型。

C99确定整型常量类型的规则与C89有些不同。对于没有后缀(UuLlLLll)的十进制常量,其类型是intlong intlong long int中能表示该值的“最小”类型。对于八进制或者十六进制常量,可能的类型顺序为intunsigned intlong intunsigned long intlong long intunsigned long long int。常量后面的任何后缀都会改变可能类型的列表。例如,以U(或u)结尾的常量类型一定是unsigned intunsigned long intunsigned long long int中的一种,以L(或l)结尾的十进制常量类型一定是long intlong long int中的一种。如果常量的数值过大,以至于不能用标准的整数类型表示,则可以使用扩展的整数类型。

7.1.4 整数溢出

对整数执行算术运算时,其结果有可能因为太大而无法表示。例如,对两个int值进行算术运算时,结果必须仍然能用int类型来表示;否则(表示结果所需的数位太多)就会发生溢出。

整数溢出时的行为要根据操作数是有符号型还是无符号型来确定。有符号整数运算中发生溢出时,程序的行为是未定义的。回顾4.4节的介绍可知,未定义行为的结果是不确定的。最可能的情况是,仅仅是运算的结果出错了,但程序也有可能崩溃,或出现其他意想不到的状况。

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

7.1.5 读/写整数

因为%d只适用于int类型。读写无符号整数、短整数和长整数需要一些新的转换指定符。

  • 读写无符号整数时,使用字母uox代替转换说明中的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 */
  • 读写短整数时,在doux前面加上字母h
short s; 

scanf("%hd", &s); 
printf("%hd", s);
  • 读写长整数时,在doux前面加上字母l
long l; 

scanf("%ld",  &l); 
printf("%ld",  l);
  • 读写长长整数时(仅限C99),在doux前面加上字母ll
long long ll; 

scanf("%lld",  &ll); 
printf("%lld",  ll);

7.2 浮点类型

有些时候需要变量能存储带小数点的数,或者能存储极大数极小数。这类数可以用浮点(因小数点是“浮动的”而得名)格式进行存储。C语言提供了3种浮点类型,对应三种不同的浮点格式。

  • float:单精度浮点数。
  • double:双精度浮点数。
  • long double:扩展精度浮点数。

当精度要求不严格时(例如,计算带一位小数的温度),float类型是很适合的类型。double提供更高的精度,对绝大多数程序来说够用了。long double支持极高精度的要求,很少会用到。

C标准没有说明floatdoublelong double类型提供的精度到底是多少,因为不同计算机可以用不同方法存储浮点数。大多数现代计算机遵循IEEE 754标准(即IEC 60559)的规范。

单精度(32位)和双精度(64位)。数值以科学记数法的形式存储,每一个数都由三部分组成:符号指数小数指数部分的位数说明了数值的可能大小程度,而小数部分的位数说明了精度

  • float类型的精度为6个数字,double15个数字。
  • long double类型的长度随着机器的不同而变化,而最常见的大小是80位和128位。
  • 在一些机器上,float可以有和double相同的数值集合,或者double可以有和long double相同的数值集合。可以在头<float.h>( 23.1 节)中找到定义浮点类型特征的宏。

在C99中,浮点类型分为两种:一种是实浮点类型,包括floatdoublelong double类型;另一种是C99新增的复数类型(27.3 节,包括float complexdouble complexlong double complex)。

7.2.1 浮点常量

浮点常量可以有许多种书写方式。例如,下面这些常量全都是表示数57.0的有效方式:

57.057.57.0e057E05.7e15.7e+1.57e2570.e-1

浮点常量必须包含小数点指数;其中,指数指明了对前面的数进行缩放所需的10的幂次。如果有指数,则需要在指数数值前放置字母E(或e)。可选符号+-可以出现在字母E(或e)的后边。

默认情况下,浮点常量以双精度数(double)的形式存储。这条规则通常不会引发任何问题,因为在需要时double类型的值可以自动转换为float类型值。

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

冷门小知识:C99提供了十六进制浮点常量的书写规范。十六进制浮点常量以0x0X开头(跟十六进制整型常量类似)。这一特性很少用到。

7.2.2 读/写浮点数

转换说明%e%f%g用于读写单精度浮点数(float)。读写doublelong double类型的值所需的转换说明略有不同。

  • 读取double类型的值时,在efg前放置字母l
double d; 

scanf("%lf", &d);

/*
注意:只能在scanf函数格式串中使用l,不能在printf函数格式串中使用。在printf函数格式串中,转换e、f和g可以用来写float类型或double类型的值。( C99允许printf函数调用中使用%le、%lf 和%lg,不过字母l不起作用。)
*/
  • 读写long double类型的值时,在efg前放置字母L
long double ld; 

scanf("%Lf", &ld); 
printf("%Lf", ld)

7.3 字符类型

char类型的值可以根据计算机的不同而不同,因为不同的机器可能会有不同的字符集

当今最常用的字符集是美国信息交换标准码(ASCII)字符集( 附录E),它用7位代码表示128个字符。在ASCII码中,数字0~90110000~0111001码来表示,大写字母A~Z1000001~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,而' '的值为32C语言中,字符和整数之间的关联是非常强的

当计算中出现字符时,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允许使用单词signedunsigned来修饰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库中常见的一些函数:

  1. isalnum(int c):检查字符是否为字母或数字;
  2. isalpha(int c):检查字符是否为字母;
  3. iscntrl(int c):检查字符是否为控制字符;
  4. isdigit(int c):检查字符是否为数字
  5. isgraph(int c):检查字符是否可打印但不是空白字符;
  6. islower(int c):检查字符是否为小写字母;
  7. isprint(int c):检查字符是否可打印;
  8. ispunct(int c):检查字符是否为标点字符;
  9. isspace(int c):检查字符是否为空白字符(空格、制表符、换行符等);
  10. isupper(int c):检查字符是否为大写字母;
  11. isxdigit(int c):检查字符是否为十六进制数字;
  12. tolower(int c):将字符转换为小写;
  13. 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函数。有下面两个原因:

  1. 这两个函数比scanf函数和printf函数简单得多,因为scanf函数和printf函数是设计用来按不同的格式读/写多种不同类型数据的。
  2. 为了额外的速度提升,通常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编译器可能需要生成一些指令,将某些操作数转换成不同类型,使得硬件可以对表达式进行计算。

例如,如果对16short型数和32int型数进行加法操作,那么编译器将安排把16short型值转换成32位值。如果是int型数据和float型数据进行加法操作,那么编译器将安排把int型值转换成为float格式。这个转换过程稍微复杂一些,因为int型值和float型值的存储方式不同。

由编译器自动处理的转换称为隐式转换(implicit conversion)
同时,允许程序员使用强制运算符执行显示转换(explicit conversion)

当发生下列情况是会进行隐式转换

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

7.4.1 常规算术转换

常规算术转换可用于大多数二元运算符(包括算术运算符、关系运算符和判等运算符)的操作数。

例如,假设变量ffloat类型,变量iint类型。常规算术转换将应用在表达式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增加了一些类型(_Boollong long类型、扩展的整数类型和复数类型)

为了定义转换规则,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整数提升(integer promotion)取代了C89中的整值提升(integral promotion),可以将任何等级低于intunsigned 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,并且变量ilong 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 类型定义和可移植性

类型定义是编写可移植程序的一种重要工具。程序从一台计算机移动到另一台计算机可能引发的问题之一就是不同计算机上的类型取值范围可能不同。如果iint类型的变量,那么赋值语句

i = 100000;

在使用32位整数的机器上是没问题的,但是在使用16位整数的机器上就会出错。

可移植性技巧:为了更大的可移植性,可以考虑使用typedef定义新的整数类型名。

可惜的是,这种技术无法解决所有的问题,因为类型定义的变化可能会影响对应变量的使用方式。我们至少需要改动使用了对应类型变量的printf函数调用和scanf函数调用(比如用转换说明%ld替换%d)。

C语言库自身使用typedef为那些可能因C语言实现的不同而不同的类型创建类型名。这些类型的名字经常以_t结尾,比如ptrdiff_tsize_twchar_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运算符也可以应用于常量、变量和表达式。如果ij是整型变量,那么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类型则被看作次要的。思考KernighanRitchieThe C Programming Language一书中关于float的论述:“使用float类型的主要原因是节省大型数组的存储空间,或者有时是为了节省时间,因为在一些机器上双精度计算的开销格外大。”经典C要求所有浮点计算都采用双精度的格式。(C89C99没有这样的要求。)

问4:十六进制的浮点常量是什么样子?使用这种浮点常量有什么好处?

答:十六进制浮点常量以0x0X开头,且必须包含指数(指数跟在字母Pp后面)。指数可以有符号,常量可以以fFlL结尾。指数以十进制数表示,但代表的是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类型值。这里floatdouble的区别是非常重要的。如果给出了错误的转换说明,那么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是指针,qr都成了普通的整型变量。类型定义不会有这样的问题。

其次,typedef命名的对象具有和变量相同的作用域规则;定义在函数体内的typedef名字在函数外是无法识别的。另外,宏的名字在预处理时会在任何出现的地方被替换。

问12:本书中提到“编译器本身通常就能够确定sizeof表达式的值”。难道编译器不总能确定sizeof表达式的值吗?

答:在C89中编译器总是可以的,但在C99中有一个例外。编译器不能确定变长数组(8.3节)的大小,因为数组中的元素个数在程序执行期间是可变的


总结

本文是作者阅读《C语言程序设计:现代方法(第2版·修订版)》时所做笔记,日后会持续更新后续章节笔记。欢迎各位大佬阅读学习,如有疑问请及时联系指正,希望对诸位有所帮助,Thank you very much!

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

New_Teen

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值