本文将详细介绍 C 语言的基本数据类型,包括如何声明变量、如何表示字面值常量(如,5 或 2.78),以及典型的用法。一些老式的 C 语言编译器无法支持这里提到的所有类型,请查阅你使用的编译器文档,了解可以使用哪些类型。
一、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。一般而言,系统用一个特殊位的值表示有符号整数的正负号。
1.1 声明 int 变量
先写上 int,然后写变量名,最后加上一个分号。要声明多个变量,可以单独声明每个变量,也可在 int 后面列出多个变量名,变量名之间用逗号分隔。下面都是有效的声明:
int erns;
int hogs, cows, goats;
可以分别在 4 条声明中声明各变量,也可以在一条声明中声明 4 个变量。两种方法的效果相同,都为 4 个 int 大小的变量赋予名称并分配内存空间。
以上声明创建了变量,但是并没有给它们提供值。变量如何获得值?前面介绍过在程序中获取值的两种途径。第 1 种途径是赋值:
cows = 112;
第 2 种途径是,通过函数(如,scanf())获得值。接下来,我们着重介绍第 3 种途径。
1.2 初始化变量
初始化(initialize)变量就是为变量赋一个初始值。在 C 语言中,初始化可以直接在声明中完成。只需在变量名后面加上赋值运算符(=)和待赋给变量的值即可。如下所示:
int hogs = 21;
int cows = 32, goats = 14;
int dogs, cats = 94; /* 有效,但是这种格式很糟糕 */
以上示例的最后一行,只初始化了 cats,并未初始化 dogs。这种写法很容易让人误认为 dogs 也被初始化为 94,所以最好不要把初始化的变量和未初始化的变量放在同一条声明中。
简而言之,声明为变量创建和标记存储空间,并为其指定初始值(如图 4 所示)。
1.3 int 类型常量
上面示例中出现的整数(21、32、14 和 94)都是整型常量或整型字面量。C 语言把不含小数点和指数的数作为整数。因此,22 和 -44 都是整型常量,但是 22.0 和 2.2E1 则不是。C 语言把大多数整型常量视为 int 类型,但是非常大的整数除外。详见后面“long 常量和 long long 常量”小节对 long int 类型的讨论。
1.4 打印 int 值
可以使用 printf() 函数打印 int 类型的值。%d
指明了在一行中打印整数的位置。%d
称为转换说明,它指定了 printf() 应使用什么格式来显示一个值。格式化字符串中的每个 %d
都与待打印变量列表中相应的 int 值匹配。这个值可以是 int 类型的变量、int 类型的常量或其他任何值为 int 类型的表达式。作为程序员,要确保转换说明的数量与待打印值的数量相同,编译器不会捕获这类型的错误。程序清单 2 演示了一个简单的程序,程序中初始化了一个变量,并打印该变量的值、一个常量值和一个简单表达式的值。另外,程序还演示了如果粗心犯错会导致什么结果。
程序清单 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() 函数时,要确保转换说明的数量与待打印值的数量相等。
1.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。
要清楚,使用不同的进制数是为了方便,不会影响数被存储的方式。也就是说,无论把数字写成 16、020 或 0x10,存储该数的方式都相同,因为计算机内部都以二进制进行编码。
1.6 显示八进制和十六进制
在 C 程序中,既可以使用也可以显示不同进制的数。不同的进制要使用不同的转换说明。以十进制显示数字,使用 %d
;以八进制显示数字,使用 %o
;以十六进制显示数字,使用 %x
。另外,要显示各进制数的前缀 0、0x 和 0X,必须分别使用 %#o
、%#x
、%#X
。程序清单 3 演示了一个小程序(回忆一下,在某些集成开发环境(IDE)下编写的代码中插入 getchar();
语句,程序在执行完毕后不会立即关闭执行窗口)。
程序清单 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 前缀,要分别在转换说明中加入 #。
二、其他整数类型
初学 C 语言时,int 类型应该能满足大多数程序的整数类型需求。尽管如此,还应了解一下整型的其他形式。
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 都表示同一种类型。
2.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.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 的最小取值范围是 [−32768,32767]
;对于 32 位机,long 的最小取值范围是 [−2147483648,2147483647]
。对于 unsigned short 和 unsigned int,最小取值范围是 [0,65535]
;对于 unsigned long,最小取值范围是 [0,4294967295]
。long long 类型是为了支持 64 位的需求,最小取值范围是 [−9223372036854775808,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 位。
2.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 标准并未定义有符号类型的溢出规则。以上描述的溢出行为比较有代表性,但是也可能会出现其他情况。
2.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 类型的值。程序清单 4 演示了一些例子。对于支持 long long 类型的系统,%lld
和 %llu
分别表示有符号和无符号类型。
程序清单 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 在系统内存中的内部表示完全相同。因此,如果告诉 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()函数时,切记检查每个待打印值都有对应的转换说明,还要检查转换说明的类型是否与待打印值的类型相匹配。
三、使用字符: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 类型。
3.1 声明 char 类型变量
char 类型变量的声明方式与其他类型变量的声明方式相同。下面是一些例子:
char response;
char itable, latan;
以上声明创建了 3 个 char 类型的变量:response、itable 和 latan。
3.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"
是一个字符串。
实际上,字符是以数值形式存储的,所以也可使用数字代码值来赋值:
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.3 非打印字符
单引号只适用于字符、数字和标点符号,浏览 ASCII 表会发现,有些 ASCII 字符打印不出来。例如,一些代表行为的字符(如,退格、换行、终端响铃或蜂鸣)。C 语言提供了 3 种方法表示这些字符。
第 1 种方法前面介绍过——使用 ASCII 码。例如,蜂鸣字符的 ASCII 值是 7,因此可以这样写:
char beep = 7;
第 2 种方法是,使用特殊的符号序列表示一些特殊的字符。这些符号序列叫作转义序列(escape sequence)。表 2 列出了转义序列及其含义。
把转义序列赋给字符变量时,必须用单引号把转义序列括起来。例如,假设有下面一行代码:
char nerf = 'n';
稍后打印变量 nerf 的效果是,在打印机或屏幕上另起一行。
表 2 转义序列
转义序列 | 含义 |
---|---|
a
|
警报(ANSI C) |
b
|
退格 |
f
|
换页 |
n
|
换行 |
r
|
回车 |
t
|
水平制表符 |
v
|
垂直制表符 |
\
|
反斜杠() |
'
|
单引号 |
"
|
双引号 |
?
|
问号 |
|