文章目录
通过前面的学习,我们已经了解了C语言的基本内容,认识到程序 = 算法+数据,而C语言中数据是有类型的,这章对C语言的基本数据类型做一个较全面的介绍。
- 数据类型
- 基本数据类型
- 整型 int
- 实型(浮点型)float double
- 字符型 char
- 构造数据类型
- 数组、结构、联合、枚举
- 指针类型
- 基本数据类型
8.1 整数类型
C语言支持两种根本不同的数值类型:整数类型(整型)和浮点类型(浮点型)。整数类型的值是整数,而浮点类型的值可能有小数部分。整数类型又可以从有无符号分为两大类:有符号和无符号,从其表示范围分为几类:短、中、长。之前使用的int类型属于有符号、中长度的整型。
有符号和无符号整数的区别在于表示整数的二进制位最高位(最左边的位)是否用来表示符号位,有符号整数用最高位表示数的符号,1来表示负数,0表示正数或者0(具体来说用有符号整数用补码表示数)。而无符号整数没有符号位,全部二进制位都用来表示正数或者0。因此,4个字节,共32位的无符号整数可以表示的数的范围是[0, 232-1],即[000000000000000000000000000000002,111111111111111111111111111111112]。如果是32位的有符号整数,那么由于最高位是符号位,故其表示的数的范围是[-231, 231-1]。基本上是将无符号可以表示的整数分两半,一半表示负数,一半表示正数。在有符号整数类型名称前面加上unsigned就是无符号整数类型。
用不同的字节数来存储整数造成整数的范围不同,C语言规定了从小到大3种不同长度的整数类型:short int、int、long int。每种长度的整数类型,又可以分为有符号和无符号两种类型。所以在C89中有6种整数类型。C99增加了long long int型和相应的无符号类型,以扩大整数的表示范围。
类型名 | 简称 | ----- | 类型名 | 简称 |
---|---|---|---|---|
short int | short | ----- | unsigned short int | unsigned short |
int | ----- | unsigned int | unsigned | |
long int | long | ----- | unsigned long int | unsigned long |
long long int | long long | ----- | unsigned long long int | unsigned long long |
C语言没有规定这些类型的变量具体的字节数和范围,下面列出了在目前常见电脑系统下,即32位环境下(注意,这里的位数与操作系统位数不是一个位数,因为目前常见电脑系统是64位操作系统),这些类型的字节数和范围如下表所示,
类型名 | 简称 | 字节数 | 范围 | ----- | 类型名 | 简称 | 字节数 | 范围 |
---|---|---|---|---|---|---|---|---|
short int | short | 2字节/16位(有符号) | -215 to 215-1 (-32, 768 to 32,767) | ----- | unsigned short int | unsigned short | 2字节/16位(无符号) | 0 to 216-1 (65535) |
int | 4字节/32位(有符号) | -231 to 231-1 ((-2147483648 to 2147483647) | ----- | unsigned int | unsigned | 4字节/32位(无符号) | 0 to 232-1 (4294967295) | |
long int | long | 4字节/32位(有符号) | -231 to 231-1 ((-2147483648 to 2147483647) | ----- | unsigned long int | unsigned long | 4字节/32位(无符号) | 0 to 232-1 (4294967295) |
long long int | long long | 8字节/64位(有符号) | -263 to 263-1 ((-9223372036854775808 to 9223372036854775807) | ----- | unsigned long long int | unsigned long long | 8字节/64位(无符号) | 0 to 264-1 (18446744073709551615) |
可以看到,在目前常见环境下,long int和int范围是完全一样的。
如果在64位环境下,short和int与32位环境下相同,分别占2字节和4字节,但是long占8字节,故有:
类型名 | 简称 | 字节数 | 范围 | ----- | 类型名 | 简称 | 字节数 | 范围 |
---|---|---|---|---|---|---|---|---|
short int | short | 2字节/16位(有符号) | -215 to 215-1 (-32, 768 to 32,767) | ----- | unsigned short int | unsigned short | 2字节/16位(无符号) | 0 to 216-1 (65535) |
int | 4字节/32位(有符号) | -231 to 231-1 ((-2147483648 to 2147483647) | ----- | unsigned int | unsigned | 4字节/32位(无符号) | 0 to 232-1 (4294967295) | |
long int | long | 8字节/64位(有符号) | -263 to 263-1 ((-9223372036854775808 to 9223372036854775807) | ----- | unsigned long int | unsigned long | 8字节/64位(无符号) | 0 to 264-1 (18446744073709551615) |
如果在16位环境下,int和short一样,占2个字节,long占4个字节
类型名 | 简称 | 字节数 | 范围 | ----- | 类型名 | 简称 | 字节数 | 范围 |
---|---|---|---|---|---|---|---|---|
short int | short | 2字节/16位(有符号) | -215 to 215-1 (-32, 768 to 32,767) | ----- | unsigned short int | unsigned short | 2字节/16位(无符号) | 0 to 216-1 (65535) |
int | 2字节/16位(有符号) | -215 to 215-1 (-32, 768 to 32,767) | ----- | unsigned int | unsigned | 2字节/16位(无符号) | 0 to 216-1 (65535) | |
long int | long | 4字节/32位(有符号) | -231 to 231-1 ((-2147483648 to 2147483647) | ----- | unsigned long int | unsigned long | 4字节/32位(无符号) | 0 to 232-1 (4294967295) |
尽管在不同位数的环境下,相同类型的字节数不同,但是值得庆幸的是,由于整数采用补码表示,所以字节数和范围的关系是确定的。只要知道字节数,就能知道整数的范围。同时,不同类型的大小关系是确定的,即short int类型字节数小于等于int的,int类型字节数小于等于long int的,long int类型字节数小于等于long long int的。而且C语言对各种类型的字节数做了“最低标准”的规定,即short int至少2个字节,int至少2个字节,long int至少4个字节。
在某个特定的系统上,到底怎样确定整数类型的字节数是多少呢?常见的有两种方法:
方法1. 可以使用sizeof运算符来得到各种类型占了几个字节。根据字节数可以确定其值取值范围。其用法是
sizeof(类型名)
或者sizeof(表达式)
sizeof(表达式)
的含义是先计算表达式的值,然后得到存储该值的类型占几个字节。
#include <stdio.h>
int main()
{
printf("The bytes of short int = %d\n", sizeof(short int));
printf("The bytes of int = %d\n", sizeof(10)); // 可以使用整数变量或者常量
printf("The bytes of long = %d\n", sizeof(long));
printf("The bytes of long long = %d\n", sizeof(long long));
return 0;
}
实际上,sizeof是运算符,因此在sizeof应用于表达式时不要求加圆括号,因此我们可以用sizeof i
代替sizeof(i)
。但是由于sizeof
运算符是一元运算符,其优先级较高,故sizeof i * f
会被解释为(sizeof i) * f
,可能与我们的期望不同,故建议始终加上圆括号。
方法2. 检查<limits.h>头。该头是标准库的一部分,其中定义了表示每种整数类型的最大值和最小值的宏。
#include <stdio.h>
#include <limits.h>
int main()
{
printf("The minimum value of SHORT INT = %d\n", SHRT_MIN);
printf("The maximum value of SHORT INT = %d\n", SHRT_MAX);
printf("The minimum value of INT = %d\n", INT_MIN);
printf("The maximum value of INT = %d\n", INT_MAX);
printf("The minimum value of LONG = %ld\n", LONG_MIN);
printf("The maximum value of LONG = %ld\n", LONG_MAX);
printf("The minimum value of LONG LONG = %lld\n", LLONG_MIN);
printf("The maximum value of LONG LONG = %lld\n", LLONG_MAX);
getchar();
return 0;
}
实际编程中,怎样选择这么多的类型呢?
首先,不用无符号数,尤其不能无符号和有符号整数混用,因为可能会造成意想不到的问题,无符号整数主要用在底层程序中。其次,因为目前内存比较大,所以跳过short,直接使用int类型,如果int类型的范围不够,再考虑long int或者long long int。
附:
4比特下,有符号和无符号整数的二进制表示。4比特二进制数,如果是无符号数,表示范围是[0, 24-1],即[0,15],如果是有符号数,表示范围是[-23, 23-1],即[-8,7]。
无符号 | 二级制 | 有符号 |
---|---|---|
7 | 0111 | 7 |
6 | 0110 | 6 |
5 | 0101 | 5 |
4 | 0100 | 4 |
3 | 0011 | 3 |
2 | 0010 | 2 |
1 | 0001 | 1 |
0 | 0000 | 0 |
15 | 1111 | -1 |
14 | 1110 | -2 |
13 | 1101 | -3 |
12 | 1100 | -4 |
11 | 1011 | -5 |
10 | 1010 | -6 |
9 | 1001 | -7 |
8 | 1000 | -8 |
8.1.1 C99中的整数类型
C99中提供了两种额外的标准整数类型:long long int和unsigned long long int。增加这两种整数类型有两个原因,一是为了满足日益增长的对超大型整数的需求,而是为了适应支持64位运算的新处理器的能力。这两个long long类型要求至少8个字节,64位宽,所以long long int类型值的范围通常为-263(-9 223 372 036 854 75 808)到263-1(9 223 372 036 854 75 807),而unsigned long long int类型值的范围通常为0到264-1(18 446 744 073 709 551 615)。
8.1.2 整数常量
在程序中以文本形式出现的数,而不是读、写或计算出来的数,称为整数常量,或者整数字面量。比如10,453。C语言除了允许用十进制书写整数常量,还支持用八进制和十六进制书写整数。
如同十进制的每个位是以10的幂为基底一样,八进制的数以8的幂为基底,而且只包含0~7,比如,八进制数237表示成十进制就是2 × 82 + 3 × 81 + 7 × 80 = 128 + 24 + 7 = 159。
十六进制的数以16的幂为基底,而且用数字0~9加上字母A~F来书写,其中A~F表示10~15之间的各个数。比如,十六进制数1AF表示成十进制就是1 × 162 + 10 × 161 + 15 × 160 = 256 + 160 + 15 = 431。
假如没有特殊的标志,很多数我们无法区别它们采用的是哪种进制,因此C语言有如下规定:
- 十进制常量包含0~9中的数字,但是一定不能以零开头。
如,15、345、-6574 - 八进制常量包含0~7中的数字,而且必须以零开头。
如,034、017、-0645 - 十六进制常量包含0~9中的数字和A~F中的字母,而且必须以0x开头,十六进制常量中的所有字母既可以是大写,也可以是小写。如,0xff、0xfF 、0xFF、0Xff、0xfF
注意,无论是十进制、八进制还是十六进制,它们都是数的书写方式,真正存储在计算机中的是二进制。一个式子中出现多种书写形式是可以的,比如:10 + 015 + 0x11。八进制和十六进制更适合于底层程序的编写,一般情况不需要使用。
十进制整数常量的类型通常为int,但如果常量的值大到无法存储在int中,就用long int类型。如果出现long int类型也无法存储,编译器用unsigned long int做尝试。确定八进制和十六进制常量的规则略有不同:编译器会依次尝试int、unsigned int、long int和unsigned long int类型,直到找到能表示该常量的类型。
有时候可以强制编译器把常量当做长整数来处理,只需在后面加一个字母L(或者l):
15L 0377L 0x4567L
为了指明常量是无符号的,可以加上字母U(或者u):
15U 456U 0xFFFFU
L和U可以结合使用,表示常量既是长整型又是无符号整型。(字母的顺序和大小写无所谓)
8.1.3 C99中的整数常量
在C99中用LL或者ll(两个字母的大小写要一致)表示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中的一种。
#include <stdio.h>
int main()
{
printf("%d\n", sizeof(1));
printf("%d\n", sizeof(300000000)); // 3亿
printf("%d\n", sizeof(3000000000)); // 30亿
getchar();
return 0;
}
注意,上述程序是在Dev-C++下设置运行时标准是C99后运行的结果,VS2010的结果是
与预期不符,原因很可能是VS2010对C99标准支持不完全造成的。在能用无符号整数存储30亿的情况下,VS2010先用unsigned int存储它。而按照C99标准,Dev C++优先用能存储它的有符号长长整数,long long int来存储30亿。
如果对整数后缀的作用有疑问,可以思考下面的代码
long long int x = 100000 * 100000;
printf("%lld", x);
由于10万乘以10万,结果是100亿,用long long int类型是足够存储它的,但是运行结果却是
这样的错误结果,原因在于,做乘法的两个10万都是int整数类型,所以它们的结果也一定是int整数类型,而int整数的范围不足以存储100亿,所以乘法本身就溢出了。最后即使把溢出的结果再赋值给long long int,结果仍然是错误的。为了解决这个问题,可以为其中任何一个10万整数常量添加ll后缀,表示它是长长整数常量。这样程序的结果就是正确的了。
long long int x = 100000ll * 100000; // 添加ll后缀,表示它是长长整数常量
printf("%lld", x);
在后续类型转换小节,可以看到,由于左边10万是长长整数类型,右边的10万会被隐式转换为长长整数类型进行乘法运算。故算出的乘积是正确的。
8.1.4 读写整数
假如修改了程序中变量的类型,那么还需要修改print和scanf语句中的转换说明符。
-
读写无符号整数时,使用字母u、o或x替代转换说明中的d。如果使用u说明符,该数以十进制形式读写,o表示八进制形式,而x表示十六进制形式。
unsigned int u; scanf("%u", &u); // 以十进制读 printf("%u", u); // 以十进制写 scanf("%o", &u); // 以八进制读,加0或不加0都可以 printf("%o", u); // 以八进制写 scanf("%x", &u); // 以十六进制读,加0x或不加0x都可以 printf("%x", u); // 以十六进制写
-
读写短整数时,在d、u、o或x前面加上字母h:
short s; scanf("%hd", &s); printf("%hd", s);
-
读写长整数时,在d、u、o或x前面加上字母l:
long l; scanf("%ld", &l); printf("%ld", l);
-
读写长长整数时(仅限C99),在d、u、o或x前面加上字母ll:
long long ll; scanf("%lld", &ll); printf("%lld", ll);
读写函数的转换说明符使用说明:
有符号十进制 | 无符号十进制 | 无符号八进制 | 无符号十六进制 | |
---|---|---|---|---|
正常整型 | d | u | o | x |
短整型 | hd | hu | ho | hx |
长整型 | ld | lu | lo | x |
长长整型(C99标准) | lld | llu | llo | llx |
附:
可能有人会问,在输入有符号整数的时候,能否以八进制或者十六进制输入?这是可以的,只需要将%d替换为%i,输入八进制整数时,以0开头,输入十六进制时,以0x开头。
int a = 0;
scanf("%i", &a); // 输入:017 (15的八进制 )
printf("%d\n", a);
scanf("%i", &a); // 输入:0xf (15的十六进制 )
printf("%d\n", a);
那么在输出有符号整数的时候,能否以八进制或者十六进制的形式输出整数值呢?由于C语言没有对有符号输出为八进制或者十六进制的转换说明符,故要借助于无符号整数可以输出为八进制或者十六进制,比如
int x = 20;
printf("%o", x); // 将x看成为无符号整数来输出
输出:
如果要输出的是负值,需要输出其绝对值,并添加输出负号
int x = -20;
printf("-%o", -x); // 输出负号和负数的绝对值
输出:
8.2 浮点类型
整数类型无法存储带小数点的数,这就需要浮点数。相比整数的范围,浮点类型的数的范围也是很大的,因此需要很大或者很小的数时也可以考虑浮点数。浮点类型由于小数点是“浮动的”得名,C语言提供了3种浮点类型:
- float:单精度浮点数
- double:双精度浮点数
- long double:扩展精度浮点数
当精度要求不严格时(例如,计算带一位小数的温度),float类型是很适合的类型。double提供更高的精度,对绝大多数程序都够用了。long double支持极高精度的要求,很少会用到。
类似整数类型,C标准没有说明float、double和long double类型提供的范围和精度到底是多少,不同的计算机可以用不同方法存储浮点数。大多数现代计算机都遵循IEEE 754(即IEC 60559)的规范。
IEEE标准提供了两种主要的浮点数格式:单精度(32位)和双精度(64位)。数值以科学计数法的形式存储,每一个数都由三部分组成:符号、指数和小数。指数部分的位数说明了数值的可能大小程度,而小数部分的位数说明了精度。显然,总位数越多,指数和小数部分相应分到的位数也越多,范围也就越大,精度也越高。
类型 | 最小正值 | 最大值 | 精度 |
---|---|---|---|
float | 1.175 49 × 10 -38 | 3.402 82 × 10 38 | 6个数字 |
double | 2.225 07 × 10 -308 | 1.797 69 × 10 308 | 15个数字 |
在不遵循IEEE标准的计算机上,上表是无效的。事实上,在一些机器上,float可以有和double相同的数值集合,或者double可以有和long double相同的数值集合。不同于在头<limits.h>中查看整型范围的宏,在<float.h>头文件中查看浮点类型特征的宏。
8.2.1 浮点数常量
浮点常量可以有很多种书写方式。例如,下面这些常量全都是表示数57.0的有效方式:
- 57.0
- 57.
- 57.0e0
- 57E0
- 5.7e1
- 5.7e+1
- .57e2
- 570.e-1
浮点常量必须包含小数点或者指数(否则会当做整数常量):其中指数指明了对前面的数进行缩放的10的幂次。如果有指数,需要在指数前面放置字母E或者e。指数为0的话0也需要写出来,像57E是错误的写法。可选符号+或-可以出现在字母的后边,注意,这里的+或-是指数幂次的正负。可以参考科学计数法。再次强调一下,57.0和57E0表示浮点常量,而不是整数常量。
默认情况下, 浮点常量以双精度数的形式存储。比如,C程序中的浮点常量57.0,是以double类型存储的,如果要以float类型存储,可以在浮点常量的末尾加上F或者f,比如57.0F;为了说明浮点常量必须以long double格式存储,可以在浮点常量末尾加上字母L或者l(小写的L),比如57.0L。
8.2.2 读写浮点数
读取double类型的值时,在e、f或g前放置字母l:
double d;
scanf("%lf", &d);
C99之前,用printf输出double类型的值时,不要加l,直接用e、f或g,C99允许printf函数使用%le、%lf和%lg,这样与float类型不加l形成对比,但是字母l不起作用。
读写long double类型的值时,在e、f或g前放置字母L:
long double ld;
scanf("%Lf", &ld);
printf("%Lf", ld);
8.3 数值类型小结
8.3.1 整数类型的问题
整型相比于浮点型,对数值的表示是精确的,但是问题是每一种整型变量的范围都是有限的,相比于浮点型,范围更要小很多。整数运算的时候很容易出现“溢出”现象。比如
int i = 100000 * 100000;
printf("%d\n", i);
显示结果不正确,因为正确的结果超出了int型的范围。还有
short i = 32767 + 1;
printf("%hd\n", i);
结果显示为负数。这也是因为溢出问题。
整数类型有好多种,注意不要使用无符号整型,尤其是和有符号整型混用,建议范围足够的话就使用int,不够的话再考虑使用更大范围的有符号整数。
8.3.2 浮点类型的问题
与整数类型相反,浮点类型的优点是表示的数值范围很大,但是精度有限。(这也很好理解,像32位机器上,int和float都用4个字节表示,那么在精度和范围之中只能二取一,无法做到兼顾,毕竟用来存储的二进制位是有限的。)
float a = 1000.43f, b = 1000.0f;
printf("%f\n", a - b);
就像1/3是无限循环小数0.3333…一样,有的数本身也就无法用固定的位数表示。比如,
printf("%.16f\n", 2.0 - 1.1);
浮点型有几种,建议日常就使用double类型,一般情况下精度足够了。
8.4 字符类型
字符类型变量用来存储字符
char ch;
ch = 'a';
ch = '0';
ch = ' '; // 单引号里面是一个空格
问题来了,内存中变量都是二进制形式的,字符变量ch是如何存储字母’a’、'0’以及空格的呢,保存成什么样的二进制呢?还有它还可以存储其它的字符有哪些呢?在字符类型上还有哪些操作呢?
实际上,这几个问题是紧密相关的。C语言中存储字符,实际上存储的是字符在ASCII字符集下的码值(的二进制形式),比如,字符’a’的码值为97,‘A’的码值为65,而空格字符’ '的码值为32。就像用int类型变量存储整数那样,char类型变量也是存储了这些字符的ASCII码值的二进制形式。实际上,C语言把char类型当做小整数来处理。因为一般情况下,char类型变量只占1个字节,即8个位,故其表示的整数范围有限。在ASCII编码中,字符的取值范围是00000002~11111112,可以看成是0~127的整数,它们是可以被8位char类型变量全部存储的(最高位为0)。参考ASCII码字符对照表
故C语言不但用char类型存储字符,还把它当做小整数来处理(1个字节的整数),可以对它进行整数运算。由于在ASCII码中字母、数字字符的码值顺序排布的关系,这样规定有利于对操作字符变量进行操作,比如
char ch;
int i;
ch = 'a'; // ch里面存储了'a'的码值97
printf("%c", ch); // 显示字母a
printf("%d", ch); // 显示字母a的码值97
char ch;
int i;
ch = 98; // ch里面存储了值98,即字母'b'的码值
i = ch; // i等于98
char ch;
ch = 'a'; // ch里面存储了'a'的码值97
c++; // ch里面存储了值98,即字母'b'的码值
char ch;
ch = 'a'; // ch里面存储了'a'的码值97
ch -= 32; // ch里面存储了值65,即字母'A'的码值,减去32可以将小写字母转换为大写字母
判断字符是否是小写字母,如果是,转换为对应的大写字母
if ('a' <= ch && ch <= 'z'){
ch = ch - ('a' - 'A'); // 将ch转换为了其对应的大写字母
}
上述两块代码有意义是因为’a’~‘z’以及’A’~‘Z’的码值是连续分布的,任何一个字母对应的大小写直接的码值相差’a’ - ‘A’。尽管字符类型可以当成小整数来处理,但是处理的方式还是与字符相关的,并不适合做一般意义上的算术运算,比如乘法或者除法,像'a' * 3
或者'a' + ‘b’
就是语法允许,但是没有一般意义的运算。
8.4.1 转义字符
一般情况下,字符常量是用单引号括起来的。而然有些字符是无法用上述方式书写的,有些字符无法键盘输入。因此,C语言提供了一种特殊的表示法:转义序列。
转义序列共有两种形式:字符转义序列和数字转义序列。
下表给出完整的字符转义序列
名称 | 转义序列 |
---|---|
警报(响铃)符 | \a |
回退符 | \b |
换页符 | \f |
换行符 | \n |
回车符 | \r |
水平制表符 | \t |
垂直制表符 | \v |
反斜杠 | \\ |
问号 | \? |
单引号 | \’ |
双引号 | \" |
最常用的转义字符是’\n’,表示输出一个换行,接下来的输出会另起一行。如果printf输出内容中有反斜杠、单引号或者双引号,需要用反斜杠’\'对它们进行转义。
转义序列列表没有包含所有无法打印的ASCII字符,只包含了最常用的。而数字转义序列可以表示任何字符。
为了把特殊字符写成数字转义序列,首先需要在ASCII码表中查找字符的八进制或十六进制值。比如,字母’A’的ASCII码值是65,对应的八进制值为0101,对应的十六进制值为0x41。
- 八进制转义序列 由字符\和跟随其后的一个最多含有三位数字的八进制数组成。比如,‘A’写成\101。注意,可写可不写八进制开头的0,但是不能超过3位数字,故这里不能写开头的0。而像字符’?‘的码值是63,它可以写成’\077’或者’\77’。
- 十六进制转义序列 由\x和跟随其后的一个十六进制数组成。字符’A’写成\x41的形式。字符x必须小写,这与十六进制常量中字母大小写都可以有所不同。注意这里与十六进制整数常量不同,一定不要写开头的0。十六进制数中的字母大小写都可以,比如字母’j’的转义序列可以是\x6a或者\x6A。
作为字符常量使用时,无论直接用字符,还是转义序列,都必须用一对单引号括起来,比如,字母a要写成’a’、’\141’或者’\x61’。在字符串中也可以使用字符的转义序列,比如printf("\x61");
,字符串是一串字符组成的字符序列,字符串常量用双引号括起来。
转义序列可能有些隐晦,所以采用#define的方式给它们起个容易理解的名字是个不错的主意,比如:
#define ESX '\33'
8.4.2 用scanf和printf读/写字符
字符变量的读写转换说明使用%c:
char ch;
scanf("%c", &ch);
printf("%c", ch);
这里有一个容易出错的地方是所有的字符,包括空格、换行符都是ASCII的字符,都可以被读取到字符变量中,这与读取数值类型是不同的,读取数值类型的时候是会自动跳过空格、换行符的,但是读取字符的时候不会。如果下一个未读字符是空格,那么例子中scanf函数将把空格读到ch中。而一般这都不是我们的本意。比如,在读入字符之前,读入了一个整数
char ch;
int i;
scanf("%d", &i);
scanf("%c", &ch);
printf("整数是%d\n", i);
printf("字符是%c, 码值是%d\n", ch, ch);
按照我们使用输入函数来输入数值的惯例,输入时,会输入一个整数,加换行或者空格,然后再输入字符。但是,读取字符的输入函数会把换行或者空格当成需要读入的字符,真正想读入的字符没有被读入到字符变量ch中。
为了强制scanf函数在读入实际需要读入的字符前跳过空白字符(换行、空格、制表符等字符),可以在格式串中的转换说明符%c前加上一个空格:
scanf(" %c", &ch);
scanf格式串中的空白字符意味着跳过输入的零个或多个空白字符,直到遇到一个非空白字符。
char ch;
int i;
scanf("%d", &i);
scanf(" %c", &ch); // %c之前有一个空格
printf("整数是%d\n", i);
printf("字符是%c, 码值是%d\n", ch, ch);
用这种方式,就可以读入真正想读入的字符。
反过来,因为通常情况下scanf函数不会跳过空白,所以它很容易检查到输入行的结尾:检查刚读入的字符是否为换行符。例如,下面的循环将读入并且忽略掉当前输入行中剩下的所有字符:
do{
scanf("%c", &ch);
}while(ch != '\n');
那么下次再调用scanf函数时,将从下一行开始读入。这个方法可以忽略当前行剩余未读的字符。
为了验证输入缓冲区的存在,可以尝试运行下面的程序
char ch;
do{
printf("tea\n");
scanf(" %c", &ch);
}while(ch == 'y');
printf("循环结束\n");
这样的程序可以连续输入多个y,这样循环体中printf输出语句可以在不等待用户输入的时候连续循环执行,这是因为多个y被暂存到输入缓冲区中,待后续的scanf函数来读取,而不需要用户输入。只有在输入缓冲区为空,scanf函数才会等待让用户从控制台输入。
8.4.3 用getchar()和putchar()读/写字符
C语言还提供了单独的两个函数专门用来读/写字符,读字符是getchar()函数,写字符是putchar()函数。
每次调用getchar()时,它会读入一个字符并将其返回。如果需要保存这个字符,需要用赋值语句将它存储到变量中:
ch = getchar();
这句话相当于
scanf("%c", &ch);
getchar()返回的是int类型,因此也可以用int变量接收返回值。
和scanf函数一样,getchar()函数也不会在读取的时候跳过空白字符。
putchar(ch)
写单个字符ch,这句话相当于
printf("%c", ch);
读/写字符时,使用getchar函数和putchar函数比scanf和printf函数效率要高一些。
8.4.4 读字符常见问题
混合使用scanf和getchar时注意scanf函数倾向于遗留下它“扫视”过但是未读取的字符(包括空格、换行符等,读取数值类型时这个现象非常常见)。假如下面的程序的输入内容与我们常规的输入一致,即输入一个整数后按下换行,那么getchar读回的就是这个换行符。或者是空格,反正就是紧跟着整数之后的第一个字符。
int i;
char c;
printf("请输入一个整数:");
scanf("%d", &i);
printf("请输入一个字符:");
c = getchar(); // 或者scanf("%c", &c);
如果可能出现这样的问题,建议还是使用scanf读字符,并在读字符的格式串前加空格:
scanf(" %c", &c);
8.5 类型转换
上面我们已经看了,C语言中有不同的类型,由于字符类型本质上是一种小整数类型,因此也可以把它看成是一种整数类型。因此,C语言中可以进行数值运算的“算术类型”有:
- 整型:
- 字符(char)
- 有符号整数(short int、int、long int)
- 无符号整数(unsigned short int、unsigned int、unsigned long int)
- 浮点型(float 、double 、long double)
那么支持在一个算术表达式中出现不同的“算术类型”吗,如果支持,那么运算的时候遵循什么规则呢?
C语言支持不同的“算术类型”参与运算,但是会做一些转换。转换的原则是:
- 会将它们转换为同一类型再进行运算。
- 尽量保留数值信息,减少转换信息损失。
比如,
- 16位的short int和32位的int求和,编译器会将short的值转换为32位的int再进行求和。
- int和float求和,编译器会将int的值转换为float形式再进行运算。
下面就从隐式转换和显式转换两种转换介绍转换规则。隐式转换指编译器自动处理不同类型的运算,不需要程序员介入,显式转换指程序中用强制类型转换运算符进行显式的转换。
8.5.1 隐式类型转换
下面的情况会发生隐式类型转换:
- 当算术表达式或逻辑表达式中操作数的类型不同时。
- 当赋值运算符右侧表达式的类型和左侧变量的类型不同时。
- 当函数调用中的实参类型与其对应的形式参数不同时。
- 当return语句中表达式的类型和函数返回值类型不同时。
由于函数还没有介绍,这里将讨论前两种情况,3、4种函数的情况本质上与第2种情况是类似的。
8.5.1.1 隐式类型转换之算术转换
算术转换的规则是将“窄”的类型的值提升到“宽”的类型的值再进行计算,比如float类型的值和int类型的值求和,会将int类型的值转换为float类型的值,显然这样更安全一些,float类型的值表示的范围更大,反过来,将float类型的值转换为int类型的值,可能会溢出,即超出int类型的值的范围,而且,会丢失小数部分。
执行常用算术转换的规则还可以划分为两种情况。
-
操作数含有浮点类型的情况(任一个操作数是浮点类型)。按照下图将类型较狭小的操作数进行提升:
写成伪代码的形式即,if (有一个操作数是long double) 另一个操作数转换为long double else if (有一个操作数是double) 另一个操作数转换为double else 另一个操作数转换为float
比如,long int和double求和,long int会被转换为double。
-
两个操作数的类型都不是浮点型的情况(两个操作数都是整数类型,不考虑long long int和unsigned long long int)。
首先对两个操作数进行整数提升(将类型低于int类型的值转换为int型或者unsigned int,比如字符型,短整型)。然后按照下图对类型较狭小的类型进行提升:
总体来看,就是
写成伪代码的形式即,将short等窄于int类型的值转换为int类型 if (有一个操作数是unsigned long int) 另一个操作数转换为unsigned long int else if (有一个操作数是long int) 另一个操作数转换为long int else if (有一个操作数是unsigned int) 另一个操作数转换为unsigned int else 两个操作数都是int,不需要转换
比如,有long int和short int求和,那么首先short int被转换为int,然后int被转换为long int再进行求和运算。
有一种特殊情况,在unsigend int类型和long int类型长度相同时(比如32位),那么这两个操作数都会转化为unsigned long int类型。
这个规则看似合理,实际在使用中,两个操作数一个是有符号的,一个是无符号的,那么很可能出现意想不到的问题。比如,一个操作数是int型变量x,值为-10,一个是unsigned int型变量y,值为10。显然,数值上,x是小于y的,但是,如果写出
x < y
这样的式子,得到的结果却是假。这是因为x < y
会将int型的x值转换为unsigned int再进行比较,而-1转换为unsigned int值为一个很大的无符号整数4294967286,所以x < y
的结果为假,不是期望的真。由于此类陷阱的存在,所以最好避免使用无符号整数,尤其是不要把它和有符号整数混用。int x = -10; unsigned int y = 10; if (x < y){ printf("合理的数学结果\n"); }else{ printf("不合理的数学结果\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 转换为 int */ i = i + s; /* s 转换为 int */ u = u + i; /* i 转换为 unsigned int */ l = l + u; /* u 转换为 long int */ ul = ul + l; /* l 转换为 unsigned long int */ f = f + ul; /* ul 转换为 float */ d = d + f; /* f 转换为 double */ ld = ld + d; /* d 转换为 long double */
8.5.1.2 隐式类型转换之赋值转换
赋值过程中的转换遵循另外的规则,就是把赋值号右边表达式的值转换为左边变量的类型。如果变量的“宽度”比表达式的值要大,那么转换不会损失信息,否则,就丢失信息,甚至得到无意义的结果。
比如,不损失信息的转换:
char c = 'a';
int i;
float f;
double d;
i = c;
f = i;
d = f;
会损失信息的转换:
int i;
i = 829.5; // i是829
如果把超过变量类型范围的值赋值给变量,结果没有意义:
char c;
c = 1000;
i = 1.0e20;
f = 1.0e100;
这些变量赋值得到了无意义的值。
浮点常量赋值给float型变量时,一个很好的方法是在浮点常量尾部加上后缀f,f = 3.14159f;
,如果没有后缀,常量3.14159是double类型,将double类型赋值给float类型,编译器认为有精度损失,可能会发出警告信息。
8.5.2 显式类型转换
有时候,我们想要强制将一种类型的值转换为另一种类型的值参与运算,此时就可以使用强制类型转换:
[强制类型转换表达式] (类型名)表达式
其中,类型名表示的是将表达式强制类型转换的目标类型。
比如,下面的例子使用强制类型转换将float类型转换为int型,得到浮点数的小数部分:f - (int)f
比如,C语言中整数除整数,结果还是整数,如果想在整数变量之间进行浮点数相除:(float)i1/i2
。
C语言把强制类型转换:(类型名),视为一元运算符。由于一元运算符的优先级高于二元运算符,故先对i1进行强制类型转换运算,得到float类型的值,然后再与i2进行除法运算。所以除法运算时,i2会被隐式转换为float类型,最终结果是两个float类型相除的结果。即相当于((float)i1)/i2
。
注意,如果强制类型转换的表达式是变量,强制类型转换过后变量本身是不发生改变的,只是强转会依据变量的值得到目标类型的值。所以以上两个例子中,f
、i1
、i2
的值并没有改变。
int i1 = 10, i2 = 4;
float f;
f = i1 / i2;
printf("%f\n", f); // 2
f = (float)i1 / i2; // 强制将i1的值转换为float类型的值再和i2进行除法运算
printf("%f\n", f); // 2.5
printf("%d\n", i1); // 强制类型转换不改变i1本身的值,i1仍然为10