本节将详细介绍C语言的基本数据类型,包括如何声明变量、如何表示字面值常量(如,5或2.78),以及典型的用法。
一些老式的C语言编译器无法支持这里提到的所有类型,请查阅你使用的编译器文档,了解可以使用哪些类型。
3.4.1 int类型
C语言提供了许多整数类型,为什么一种类型不够用?因为C语言让程序员针对不同情况选择不同的类型。特别是,C语言中的整数类型可表示不同的取值范围和正负值。一般情况使用int类型即可,但是为满足特定任务和机器的要求,还可以选择其他类型。
int类型是有符号整型,即int类型的值必须是整数,可以是正整数、负整数或零。其取值范围依计算机系统而异。一般而言,储存一个int要占用一个机器字长。因此,早期的16位IBM PC兼容机使用16位来储存一个int值,其取值范围(即int值的取值范围)是-32768~32767。目前的个人计算机一般是32位,因此用32位储存一个int值。现在,个人计算机产业正逐步向着64位处理器发展,自然能储存更大的整数。ISO C规定int的取值范围最小为-32768~32767。一般而言,系统用一个特殊位的值表示有符号整数的正负号。第15章将介绍常用的方法。
1.声明int变量
第2章中已经用int声明过基本整型变量。先写上int,然后写变量名,最后加上一个分号。要声明多个变量,可以单独声明每个变量,也可在int后面列出多个变量名,变量名之间用逗号分隔。下面都是有效的声明:
int erns;
int hogs, cows, goats;
可以分别在4条声明中声明各变量,也可以在一条声明中声明4个变量。两种方法的效果相同,都为4个int大小的变量赋予名称并分配内存空间。
以上声明创建了变量,但是并没有给它们提供值。变量如何获得值?前面介绍过在程序中获取值的两种途径。第1种途径是赋值:
cows = 112;
第2种途径是,通过函数(如,scanf())获得值。接下来,我们着重介绍第3种途径。
2.初始化变量
初始化(initialize)变量就是为变量赋一个初始值。在C语言中,初始化可以直接在声明中完成。只需在变量名后面加上赋值运算符(=)和待赋给变量的值即可。如下所示:
int hogs = 21;
int cows = 32, goats = 14;
int dogs, cats = 94; /* 有效,但是这种格式很糟糕 */
以上示例的最后一行,只初始化了cats,并未初始化dogs。这种写法很容易让人误认为dogs也被初始化为94,所以最好不要把初始化的变量和未初始化的变量放在同一条声明中。
简而言之,声明为变量创建和标记存储空间,并为其指定初始值(如图3.4所示)。
图3.4 定义并初始化变量
3.int类型常量
上面示例中出现的整数(21、32、14和94)都是整型常量或整型字面量。C语言把不含小数点和指数的数作为整数。因此,22和-44都是整型常量,但是22.0和2.2E1则不是。C语言把大多数整型常量视为int类型,但是非常大的整数除外。详见后面“long常量和long long常量”小节对long int类型的讨论。
4.打印int值
可以使用printf()函数打印int类型的值。第2章中介绍过,%d指明了在一行中打印整数的位置。%d称为转换说明,它指定了printf()应使用什么格式来显示一个值。格式化字符串中的每个%d都与待打印变量列表中相应的int值匹配。这个值可以是int类型的变量、int类型的常量或其他任何值为int类型的表达式。作为程序员,要确保转换说明的数量与待打印值的数量相同,编译器不会捕获这类型的错误。程序清单3.2演示了一个简单的程序,程序中初始化了一个变量,并打印该变量的值、一个常量值和一个简单表达式的值。另外,程序还演示了如果粗心犯错会导致什么结果。
程序清单3.2 print1.c程序
/* print1.c - 演示printf()的一些特性 */
#include <stdio.h>
int main(void)
{
int ten = 10;
int two = 2;
printf("Doing it right: ");
printf("%d minus %d is %dn", ten, 2, ten - two);
printf("Doing it wrong: ");
printf("%d minus %d is %dn", ten); // 遗漏2个参数
return 0;
}
编译并运行该程序,输出如下:
Doing it right: 10 minus 2 is 8
Doing it wrong: 10 minus 16 is 1650287143
在第一行输出中,第1个%d对应int类型变量ten;第2个%d对应int类型常量2;第3个%d对应int类型表达式ten - two的值。在第二行输出中,第1个%d对应ten的值,但是由于没有给后两个%d提供任何值,所以打印出的值是内存中的任意值(读者在运行该程序时显示的这两个数值会与输出示例中的数值不同,因为内存中储存的数据不同,而且编译器管理内存的位置也不同)。
你可能会抱怨编译器为何不能捕获这种明显的错误,但实际上问题出在printf()不寻常的设计。大部分函数都需要指定数目的参数,编译器会检查参数的数目是否正确。但是,printf()函数的参数数目不定,可以有1个、2个、3个或更多,编译器也爱莫能助。记住,使用printf()函数时,要确保转换说明的数量与待打印值的数量相等。
5.八进制和十六进制
通常,C语言都假定整型常量是十进制数。然而,许多程序员很喜欢使用八进制和十六进制数。因为8和16都是2的幂,而10却不是。显然,八进制和十六进制记数系统在表达与计算机相关的值时很方便。例如,十进制数65536经常出现在16位机中,用十六进制表示正好是10000。另外,十六进制数的每一位的数恰好由4位二进制数表示。例如,十六进制数3的二进制数是0011,十六进制数5是的二进制数0101。因此,十六进制数35的位组合(bit pattern)是00110101,十六进制数53的位组合是01010011。这种对应关系使得十六进制和二进制的转换非常方便。但是,计算机如何知道10000是十进制、十六进制还是二进制?在C语言中,用特定的前缀表示使用哪种进制。0x或0X前缀表示十六进制值,所以十进制数16表示成十六进制是0x10或0X10。与此类似,0前缀表示八进制。例如,十进制数16表示成八进制是020。第15章将更全面地介绍进制相关的内容。
要清楚,使用不同的进制数是为了方便,不会影响数被储存的方式。也就是说,无论把数字写成16、020或0x10,储存该数的方式都相同,因为计算机内部都以二进制进行编码。
6.显示八进制和十六进制
在C程序中,既可以使用也可以显示不同进制的数。不同的进制要使用不同的转换说明。以十进制显示数字,使用%d;以八进制显示数字,使用%o;以十六进制显示数字,使用%x。另外,要显示各进制数的前缀0、0x和0X,必须分别使用%#o、%#x、%#X。程序清单3.3演示了一个小程序(回忆一下,在某些集成开发环境(IDE)下编写的代码中插入getchar();语句,程序在执行完毕后不会立即关闭执行窗口)。
程序清单3.3 bases.c程序
/* bases.c--以十进制、八进制、十六进制打印十进制数100 */
#include <stdio.h>
int main(void)
{
int x = 100;
printf("dec = %d; octal = %o; hex = %xn", x, x, x);
printf("dec = %d; octal = %#o; hex = %#xn", x, x, x);
return 0;
}
编译并运行该程序,输出如下:
dec = 100; octal = 144; hex = 64
dec = 100; octal = 0144; hex = 0x64
该程序以3种不同记数系统显示同一个值。printf()函数做了相应的转换。注意,如果要在八进制和十六进制值前显示0和0x前缀,要分别在转换说明中加入#。
3.4.2 其他整数类型
初学C语言时,int类型应该能满足大多数程序的整数类型需求。尽管如此,还应了解一下整型的其他形式。当然,也可以略过本节跳至3.4.3节阅读char类型的相关内容,以后有需要时再阅读本节。
C语言提供3个附属关键字修饰基本整数类型:short、long和unsigned。应记住以下几点。
- short int类型(或者简写为short)占用的存储空间可能比int类型少,常用于较小数值的场合以节省空间。与int类似,short是有符号类型。
- long int或long占用的存储空间可能比int多,适用于较大数值的场合。与int类似,long是有符号类型。
- long long int或long long(C99标准加入)占用的储存空间可能比long多,适用于更大数值的场合。该类型至少占64位。与int类似,long long是有符号类型。
- unsigned int或unsigned只用于非负值的场合。这种类型与有符号类型表示的范围不同。例如,16位unsigned int允许的取值范围是0~65535,而不是-32768~32767。用于表示正负号的位现在用于表示另一个二进制位,所以无符号整型可以表示更大的数。
- 在C90标准中,添加了unsigned long int或unsigned long和unsigned short int或unsigned short类型。C99标准又添加了unsigned long long int或unsigned long long。
- 在任何有符号类型前面添加关键字signed,可强调使用有符号类型的意图。例如,short、short int、signed short、signed short int都表示同一种类型。
1.声明其他整数类型
其他整数类型的声明方式与int类型相同,下面列出了一些例子。不是所有的C编译器都能识别最后3条声明,最后一个例子所有的类型是C99标准新增的。
long int estine;
long johns;
short int erns;
short ribs;
unsigned int s_count;
unsigned players;
unsigned long headcount;
unsigned short yesvotes;
long long ago;
2.使用多种整数类型的原因
为什么说short类型“可能”比int类型占用的空间少,long类型“可能”比int类型占用的空间多?因为C语言只规定了short占用的存储空间不能多于int,long占用的存储空间不能少于int。这样规定是为了适应不同的机器。例如,过去的一台运行Windows 3.x的机器上,int类型和short类型都占16位,long类型占32位。后来,Windows和苹果系统都使用16位储存short类型,32位储存int类型和long类型(使用32位可以表示的整数数值超过20亿)。现在,计算机普遍使用64位处理器,为了储存64位的整数,才引入了long long类型。
现在,个人计算机上最常见的设置是,long long占64位,long占32位,short占16位,int占16位或32位(依计算机的自然字长而定)。原则上,这4种类型代表4种不同的大小,但是在实际使用中,有些类型之间通常有重叠。
C标准对基本数据类型只规定了允许的最小大小。对于16位机,short和int的最小取值范围是[−32767,32767];对于32位机,long的最小取值范围是[−2147483647,2147483647]。对于unsigned short和unsigned int,最小取值范围是[0,65535];对于unsigned long,最小取值范围是[0,4294967295]。long long类型是为了支持64位的需求,最小取值范围是[−9223372036854775807,9223372036854775807];unsigned long long的最小取值范围是[0,18446744073709551615]。如果要开支票,这个数是一千八百亿亿六千七百四十四万亿零七百三十七亿零九百五十五万一千六百一十五。但是,谁会去数?
int类型那么多,应该如何选择?首先,考虑unsigned类型。这种类型的数常用于计数,因为计数不用负数。而且,unsigned类型可以表示更大的正数。
如果一个数超出了int类型的取值范围,且在long类型的取值范围内时,使用long类型。然而,对于那些long占用的空间比int大的系统,使用long类型会减慢运算速度。因此,如非必要,请不要使用long类型。另外要注意一点:如果在long类型和int类型占用空间相同的机器上编写代码,当确实需要32位的整数时,应使用long类型而不是int类型,以便把程序移植到16位机后仍然可以正常工作。类似地,如果确实需要64位的整数,应使用long long类型。
如果在int设置为32位的系统中要使用16位的值,应使用short类型以节省存储空间。通常,只有当程序使用相对于系统可用内存较大的整型数组时,才需要重点考虑节省空间的问题。使用short类型的另一个原因是,计算机中某些组件使用的硬件寄存器是16位。
3.long常量和long long常量
通常,程序代码中使用的数字(如,2345)都被储存为int类型。如果使用1000000这样的大数字,超出了int类型能表示的范围,编译器会将其视为long int类型(假设这种类型可以表示该数字)。如果数字超出long可表示的最大值,编译器则将其视为unsigned long类型。如果还不够大,编译器则将其视为long long或unsigned long long类型(前提是编译器能识别这些类型)。
八进制和十六进制常量被视为int类型。如果值太大,编译器会尝试使用unsigned int。如果还不够大,编译器会依次使用long、unsigned long、long long和unsigned long long类型。
有些情况下,需要编译器以long类型储存一个小数字。例如,编程时要显式使用IBM PC上的内存地址时。另外,一些C标准函数也要求使用long类型的值。要把一个较小的常量作为long类型对待,可以在值的末尾加上l(小写的L)或L后缀。使用L后缀更好,因为l看上去和数字1很像。因此,在int为16位、long为32位的系统中,会把7作为16位储存,把7L作为32位储存。l或L后缀也可用于八进制和十六进制整数,如020L和0x10L。
类似地,在支持long long类型的系统中,也可以使用ll或LL后缀来表示long long类型的值,如3LL。另外,u或U后缀表示unsigned long long,如5ull、10LLU、6LLU或9Ull。
整数溢出
如果整数超出了相应类型的取值范围会怎样?下面分别将有符号类型和无符号类型的整数设置为比最大值略大,看看会发生什么(printf()函数使用%u说明显示unsigned int类型的值)。
/* toobig.c-- 超出系统允许的最大int值*/ #include <stdio.h> int main(void) { int i = 2147483647; unsigned int j = 4294967295; printf("%d %d %dn", i, i+1, i+2); printf("%u %u %un", j, j+1, j+2); return 0; }
在我们的系统下输出的结果是:
2147483647 -2147483648 -2147483647 4294967295 0 1
可以把无符号整数j看作是汽车的里程表。当达到它能表示的最大值时,会重新从起始点开始。整数i也是类似的情况。它们主要的区别是,在超过最大值时,unsigned int类型的变量j从0开始;而int类型的变量i则从−2147483648开始。注意,当i超出(溢出)其相应类型所能表示的最大值时,系统并未通知用户。因此,在编程时必须自己注意这类问题。
溢出行为是未定义的行为,C标准并未定义有符号类型的溢出规则。以上描述的溢出行为比较有代表性,但是也可能会出现其他情况。
4.打印short、long、long long和unsigned类型
打印unsigned int类型的值,使用%u转换说明;打印long类型的值,使用%ld转换说明。如果系统中int和long的大小相同,使用%d就行。但是,这样的程序被移植到其他系统(int和long类型的大小不同)中会无法正常工作。在x和o前面可以使用l前缀,%lx表示以十六进制格式打印long类型整数,%lo表示以八进制格式打印long类型整数。注意,虽然C允许使用大写或小写的常量后缀,但是在转换说明中只能用小写。
C语言有多种printf()格式。对于short类型,可以使用h前缀。%hd表示以十进制显示short类型的整数,%ho表示以八进制显示short类型的整数。h和l前缀都可以和u一起使用,用于表示无符号类型。例如,%lu表示打印unsigned long类型的值。程序清单3.4演示了一些例子。对于支持long long类型的系统,%lld和%llu分别表示有符号和无符号类型。第4章将详细介绍转换说明。
程序清单3.4 print2.c程序
/* print2.c--更多printf()的特性 */
#include <stdio.h>
int main(void)
{
unsigned int un = 3000000000; /* int为32位和short为16位的系统 */
short end = 200;
long big = 65537;
long long verybig = 12345678908642;
printf("un = %u and not %dn", un, un);
printf("end = %hd and %dn", end, end);
printf("big = %ld and not %hdn", big, big);
printf("verybig= %lld and not %ldn", verybig, verybig);
return 0;
}
在特定的系统中输出如下(输出的结果可能不同):
un = 3000000000 and not -1294967296
end = 200 and 200
big = 65537 and not 1
verybig= 12345678908642 and not 1942899938
该例表明,使用错误的转换说明会得到意想不到的结果。第1行输出,对于无符号变量un,使用%d会生成负值!其原因是,无符号值3000000000和有符号值−129496296在系统内存中的内部表示完全相同(详见第15章)。因此,如果告诉printf()该数是无符号数,它打印一个值;如果告诉它该数是有符号数,它将打印另一个值。在待打印的值大于有符号值的最大值时,会发生这种情况。对于较小的正数(如96),有符号和无符号类型的存储、显示都相同。
第2行输出,对于short类型的变量end,在printf()中无论指定以short类型(%hd)还是int类型(%d)打印,打印出来的值都相同。这是因为在给函数传递参数时,C编译器把short类型的值自动转换成int类型的值。你可能会提出疑问:为什么要进行转换?h修饰符有什么用?第1个问题的答案是,int类型被认为是计算机处理整数类型时最高效的类型。因此,在short和int类型的大小不同的计算机中,用int类型的参数传递速度更快。第2个问题的答案是,使用h修饰符可以显示较大整数被截断成short类型值的情况。第 3 行输出就演示了这种情况。把 65537 以二进制格式写成一个 32 位数是00000000000000010000000000000001。使用%hd,printf()只会查看后16位,所以显示的值是1。与此类似,输出的最后一行先显示了verybig的完整值,然后由于使用了%ld,printf()只显示了储存在后32位的值。
本章前面介绍过,程序员必须确保转换说明的数量和待打印值的数量相同。以上内容也提醒读者,程序员还必须根据待打印值的类型使用正确的转换说明。
提示
匹配printf()说明符的类型
在使用printf()函数时,切记检查每个待打印值都有对应的转换说明,还要检查转换说明的类型是否与待打印值的类型相匹配。
3.4.3 使用字符:char类型
char类型用于储存字符(如,字母或标点符号),但是从技术层面看,char是整数类型。因为char类型实际上储存的是整数而不是字符。计算机使用数字编码来处理字符,即用特定的整数表示特定的字符。美国最常用的编码是ASCII编码,本书也使用此编码。例如,在ASCII码中,整数65代表大写字母A。因此,储存字母A实际上储存的是整数65(许多IBM的大型主机使用另一种编码——EBCDIC,其原理相同。另外,其他国家的计算机系统可能使用完全不同的编码)。
标准ASCII码的范围是0~127,只需7位二进制数即可表示。通常,char类型被定义为8位的存储单元,因此容纳标准ASCII码绰绰有余。许多其他系统(如IMB PC和苹果Macs)还提供扩展ASCII码,也在8位的表示范围之内。一般而言,C语言会保证char类型足够大,以储存系统(实现C语言的系统)的基本字符集。
许多字符集都超过了127,甚至多于255。例如,日本汉字(kanji)字符集。商用的统一码(Unicode)创建了一个能表示世界范围内多种字符集的系统,目前包含的字符已超过110000个。国际标准化组织(ISO)和国际电工技术委员会(IEC)为字符集开发了ISO/IEC 10646标准。统一码标准也与ISO/IEC 10646标准兼容。
C语言把1字节定义为char类型占用的位(bit)数,因此无论是16位还是32位系统,都可以使用char类型。
1.声明char类型变量
char类型变量的声明方式与其他类型变量的声明方式相同。下面是一些例子:
char response;
char itable, latan;
以上声明创建了3个char类型的变量:response、itable和latan。
2.字符常量和初始化
如果要把一个字符常量初始化为字母A,不必背下ASCII码,用计算机语言很容易做到。通过以下初始化把字母A赋给grade即可:
char grade = 'A';
在C语言中,用单引号括起来的单个字符被称为字符常量(character constant)。编译器一发现'A',就会将其转换成相应的代码值。单引号必不可少。下面还有一些其他的例子:
char broiled; /* 声明一个char类型的变量 */
broiled = 'T'; /* 为其赋值,正确 */
broiled = T; /* 错误!此时T是一个变量 */
broiled = "T"; /* 错误!此时"T"是一个字符串 */
如上所示,如果省略单引号,编译器认为T是一个变量名;如果把T用双引号括起来,编译器则认为"T"是一个字符串。字符串的内容将在第4章中介绍。
实际上,字符是以数值形式储存的,所以也可使用数字代码值来赋值:
char grade = 65; /* 对于ASCII,这样做没问题,但这是一种不好的编程风格 */
在本例中,虽然65是int类型,但是它在char类型能表示的范围内,所以将其赋值给grade没问题。由于65是字母A对应的ASCII码,因此本例是把A赋给grade。注意,能这样做的前提是系统使用ASCII码。其实,用'A'代替65才是较为妥当的做法,这样在任何系统中都不会出问题。因此,最好使用字符常量,而不是数字代码值。
奇怪的是,C语言将字符常量视为int类型而非char类型。例如,在int为32位、char为8位的ASCII系统中,有下面的代码:
char grade = 'B';
本来'B'对应的数值66储存在32位的存储单元中,现在却可以储存在8位的存储单元中(grade)。利用字符常量的这种特性,可以定义一个字符常量'FATE',即把4个独立的8位ASCII码储存在一个32位存储单元中。如果把这样的字符常量赋给char类型变量grade,只有最后8位有效。因此,grade的值是'E'。
3.非打印字符
单引号只适用于字符、数字和标点符号,浏览ASCII表会发现,有些ASCII字符打印不出来。例如,一些代表行为的字符(如,退格、换行、终端响铃或蜂鸣)。C语言提供了3种方法表示这些字符。
第1种方法前面介绍过——使用ASCII码。例如,蜂鸣字符的ASCII值是7,因此可以这样写:
char beep = 7;
第2种方法是,使用特殊的符号序列表示一些特殊的字符。这些符号序列叫作转义序列(escape sequence)。表3.2列出了转义序列及其含义。
把转义序列赋给字符变量时,必须用单引号把转义序列括起来。例如,假设有下面一行代码:
char nerf = 'n';
稍后打印变量nerf的效果是,在打印机或屏幕上另起一行。
表3.2 转义序列
转义序列 | 含义 |
a | 警报(ANSI C) |
b | 退格 |
f | 换页 |
n | 换行 |
r | 回车 |
t | 水平制表符 |
v | 垂直制表符 |
反斜杠() | |
' | 单引号 |
" | 双引号 |
? | 问号 |