我们已经习惯了 char、int 和 float 类型,但现在是时候把这事提升一个层次,看看类型这一块还有哪些别的东西了!
到目前为止,我们都把 int 当作有符号类型使用,也就是可以取负值或正值的类型。但 C 也有专门的无符号整数类型,它们只能存放正数。
这类类型以关键字 unsigned 开头。
int a; // 有符号
signed int a; // 有符号
signed a; // 有符号,是 "int" 或 "signed int" 的"简写",罕见
unsigned int b; // 无符号
unsigned c; // 无符号,是 "unsigned int" 的简写为什么?你为什么会决定只存正数呢?
答案:无符号变量里能放下的数字,比有符号变量更大。
但这是为什么呢?
你可以把整数想象成由一定数量的位来表示92。在我的电脑上,一个 int 由 64 位表示。
而每一种由 1 或 0 构成的位排列都代表一个数。我们可以决定如何瓜分这些数。
对于有符号数,我们(大致)用一半的排列表示负数,另一半表示正数。
对于无符号数,我们用全部排列来表示正数。
在我的电脑上,int 是 64 位的,用二进制补码93来表示数值,我得到的整数范围限制如下:
| 类型 | 最小值 | 最大值 |
|---|---|---|
int |
-9,223,372,036,854,775,808 |
9,223,372,036,854,775,807 |
unsigned int |
0 |
18,446,744,073,709,551,615 |
注意,正 unsigned int 的最大值大约是 int 最大正值的两倍。所以你在那里能获得一些灵活性。
还记得 char 吗?那个用来存放单个字符的类型?
char c = 'B';
printf("%c\n", c); // 输出 "B"我有个惊人的消息:它其实是个整数。
char c = 'B';
// 把这里的 %c 改成 %d:
printf("%d\n", c); // 66(!!)在深处,char 只是一个小 int,也就是只占一个字节空间的整数,这就把它的范围限制在……
这里 C 规范就有点古怪了。它向我们保证 char 就是一个字节,即 sizeof(char) == 1。但接着在 C11 §3.6¶3 中,它又特意说道:
字节由一段连续的位组成,其位数由实现定义。
等等——什么?你们中有些人可能习惯了“一个字节就是 8 位”的观念,对吧?我是说,它本来就是这样,对吧?答案是:“几乎可以肯定。”94 但 C 是一门古老的语言,当年的机器对于一个字节里有多少位,可以说持有更宽松的看法。而这么多年来,C 一直保留着这种灵活性。
但假设你 C 里的字节是 8 位——就像你所能见到的世界上几乎所有机器那样——那么 char 的范围是……
——所以在我告诉你之前,得先说明:char 可能是有符号的,也可能是无符号的,取决于你的编译器。除非你显式指定。
很多情况下,直接用 char 就够了,因为你并不关心数据的符号。但如果你需要有符号或无符号的 char,就必须写明确:
char a; // 可能是有符号或无符号
signed char b; // 肯定是有符号
unsigned char c; // 肯定是无符号好,现在终于可以算出数值范围了——前提是假设 char 是 8 位,且你的系统对有符号和无符号数都采用几乎通用的二进制补码表示95。
那么,在这些约束下,我们终于可以算出范围了:
char 类型 |
最小值 | 最大值 |
|---|---|---|
signed char |
-128 |
127 |
unsigned char |
0 |
255 |
而 char 的范围是实现定义的。
让我理清楚:char 实际上是个数字,那我们可以对它做数学运算吗?
可以!只要记得把结果保持在 char 的范围内!
那单引号里的字符常量呢,比如 'B'?它怎么会有数值?
规范在这里也含糊其辞,因为 C 并非为运行在单一类型的底层系统上而设计。
但我们先暂时假设你的字符集至少前 128 个字符基于 ASCII96。在这种情况下,字符常量会被转换成 char,其值与该字符的 ASCII 值相同。
这话真拗口。我们直接看个例子:
#include <stdio.h>
int main(void)
{
char a = 10;
char b = 'B'; // ASCII 值 66
printf("%d\n", a + b); // 76!
}这取决于你的执行环境和所用的字符集97。如今最流行的字符集之一是 Unicode98(它是 ASCII 的超集),所以对于基本的 0-9、A-Z、a-z 和标点符号,你几乎肯定能得到它们的 ASCII 值。
short、long、long long到目前为止,我们一般只用了两种整数类型:
charint而且我们最近了解了整数类型的无符号变体。我们还了解到 char 其实是伪装起来的小 int。所以我们知道 int 可以有多种位宽。
但还有几种整数类型值得我们看一看,以及它们能容纳的最小范围。(你的实现范围很可能比规范要求的更宽,但这里给出的是你能确信可移植可用的范围。)
头文件 <limits.h> 定义了保存各种类型范围的宏;要确保无误就依赖它,绝不要硬编码或假设这些值。
这些额外的类型是 short int、long int 和 long long int。通常,C 开发者使用这些类型时会把 int 部分省掉(例如 long long),编译器也完全乐意。
// 下面两行是等价的:
long long int x;
long long x;
// 下面两行也一样:
short int x;
short x;我们按符号性分组,按升序看看这些整数数据类型及其大小。再次强调,这些最小和最大限制是规范向你保证可移植的部分;你的系统范围可能更宽。
| 类型 | 最小字节数 | 最小值 | 最大值 |
|---|---|---|---|
char |
1 | -128 或 0 | 127 或 25599 |
signed char |
1 | -128 | 127 |
short |
2 | -32768 | 32767 |
int |
2 | -32768 | 32767 |
long |
4 | -2147483648 | 2147483647 |
long long |
8 | -9223372036854775808 | 9223372036854775807 |
unsigned char |
1 | 0 | 255 |
unsigned short |
2 | 0 | 65535 |
unsigned int |
2 | 0 | 65535 |
unsigned long |
4 | 0 | 4294967295 |
unsigned long long |
8 | 0 | 18446744073709551615 |
没有 long long long 这种类型。你不能那样一个劲地加 long。别犯傻。
补码爱好者可能已经注意到那些数字有点古怪。比如,为什么
signed char到 -127 就停了,而不是 -128?记住:这些只是规范要求的最小值。有些数值表示法(比如符号-数值表示法100)的上限就是 ±127。
让我们在我的 64 位补码系统上跑同一张表,看看结果如何:
| 类型 | 我的字节数 | 最小值 | 最大值 |
|---|---|---|---|
char |
1 | -128 | 127101 |
signed char |
1 | -128 | 127 |
short |
2 | -32768 | 32767 |
int |
4 | -2147483648 | 2147483647 |
long |
8 | -9223372036854775808 | 9223372036854775807 |
long long |
8 | -9223372036854775808 | 9223372036854775807 |
unsigned char |
1 | 0 | 255 |
unsigned short |
2 | 0 | 65535 |
unsigned int |
4 | 0 | 4294967295 |
unsigned long |
8 | 0 | 18446744073709551615 |
unsigned long long |
8 | 0 | 18446744073709551615 |
这样就合理一些了,不过也能看出我的系统上的极限值比规范里规定的最小值更大。
那么 <limits.h> 里有哪些宏呢?
| 类型 | 最小值宏 | 最大值宏 |
|---|---|---|
char |
CHAR_MIN |
CHAR_MAX |
signed char |
SCHAR_MIN |
SCHAR_MAX |
short |
SHRT_MIN |
SHRT_MAX |
int |
INT_MIN |
INT_MAX |
long |
LONG_MIN |
LONG_MAX |
long long |
LLONG_MIN |
LLONG_MAX |
unsigned char |
0 |
UCHAR_MAX |
unsigned short |
0 |
USHRT_MAX |
unsigned int |
0 |
UINT_MAX |
unsigned long |
0 |
ULONG_MAX |
unsigned long long |
0 |
ULLONG_MAX |
注意其中隐藏着一个判断系统使用有符号还是无符号 char 的办法。如果 CHAR_MAX == UCHAR_MAX,那它一定就是无符号的。
还要注意,unsigned 变体没有最小值宏——它们就是 0。
double 与 long double我们来看看规范在 §5.2.4.2.2¶1-2 中对浮点数是怎么说的:
下列参数用于定义每种浮点类型的模型:
参数 定义 \(s\) 符号(\(\pm1\)) \(b\) 指数表示法的基数或底(一个 \(> 1\) 的整数) \(e\) 指数(介于最小值 \(e_{min}\) 与最大值 \(e_{max}\) 之间的整数) \(p\) 精度(有效数字中基 \(b\) 的位数) \(f_k\) 小于 \(b\) 的非负整数(有效数字各位) 浮点数(\(x\))由以下模型定义:
\(x=sb^e\sum\limits_{k=1}^p f_kb^{-k},\) \(e_{min}\le e\le e_{max}\)
希望这一下就把你讲明白了。
好吧好吧。我们退后一步,看看实际中是怎么回事。
注意:本节会提到一堆宏,它们都在头文件 <float.h> 里。
浮点数按特定的比特序列编码在字节中(IEEE-754 格式102极为流行)。
再深入一点,这个数基本上由有效数字(也就是数字部分——即有效位本身,有时也叫尾数)和指数表示,指数就是把这些数字乘上的那个幂。回想一下,负指数可以让数值变小。
想象我们用 \(10\) 作为取幂的底数。只要有效数字取 \(12345\),指数分别取 \(-3\)、\(4\) 和 \(0\),我们就能编码出下面这些浮点值:
\(12345\times10^{-3}=12.345\)
\(12345\times10^4=123450000\)
\(12345\times10^0=12345\)
对所有这些数来说,有效数字始终不变,唯一的区别在指数。
在你的机器上,指数的底大概是 \(2\) 而不是 \(10\),因为计算机偏爱二进制。你可以打印 FLT_RADIX 宏来确认这一点。
于是我们有了一个用若干字节、以某种方式编码表示的数。由于比特模式的数量有限,能表示的浮点数也就是有限的。
更具体地说,只有一定数量的有效十进制位能够被精确表示。
那怎么才能多几位呢?用更大的数据类型就行!
我们手头正好有几种。我们已经知道 float,而要更高精度还有 double。要更高的精度,还有 long double(除了名字之外,它和 long int 没有关系)。
规范并没有规定每种类型应该占多少字节,但在我的系统上,可以看到相对大小是递增的:
| 类型 | sizeof |
|---|---|
float |
4 |
double |
8 |
long double |
16 |
所以(在我的系统上)每种类型都用多出来的那些位换取更高的精度。
但这里说的精度到底有多高?这些值能表示多少位十进制数字?
C 在 <float.h> 里提供了一堆宏,帮我们弄清楚这件事。
如果你用二进制(基 2)系统来存储这些数(地球上几乎所有人都是如此,你大概也不例外),事情会有点别扭,不过请耐心跟我一起把它弄明白。
关键的问题是:“在某种浮点类型里,我最多能存多少位有效十进制数字,才能在打印时取回同一个十进制数?”
你可以在某种浮点类型里存储、并且打印时保证原样取回的十进制位数,由下面这些宏给出:
| 类型 | 可存储的十进制位数 | 最小保证 |
|---|---|---|
float |
FLT_DIG |
6 |
double |
DBL_DIG |
10 |
long double |
LDBL_DIG |
10 |
在我的系统上 FLT_DIG 是 6,所以我可以确信:打印一个 6 位的 float,取回来的还是同一个数。(位数可能更多——有些数多用几位也能正确取回。但 6 位是肯定能取回的。)
例如,按这种逐位递增的模式打印 float,我们显然能撑到 8 位才出问题,但之后又回到 7 位正确。
0.12345
0.123456
0.1234567
0.12345678
0.123456791 <-- Things start going wrong
0.1234567910再来一个演示。这段代码里有两个 float,各自存着一个具有 FLT_DIG 位有效十进制位的数103。然后把它们相加,结果本应有 12 位有效十进制位。但这超出了 float 能存储、并且能正确还原成字符串的位数——所以打印出来时,从第 7 位有效数字开始就出错了。
#include <stdio.h>
#include <float.h>
int main(void)
{
// 这两个数都有 6 位有效数字,因此可以
// 精确地存进 float 里:
float f = 3.14159f;
float g = 0.00000265358f;
printf("%.5f\n", f); // 3.14159 —— 正确!
printf("%.11f\n", g); // 0.00000265358 —— 正确!
// 现在把它们加起来
f += g; // 3.14159265358 才是 f 本应有的值
printf("%.11f\n", f); // 3.14159274101 —— 错!
}(上面的代码在数值常量后面带了个 f——这表示该常量是 float 类型,而不是默认的 double。后面再详细讲。)
记住,FLT_DIG 是你能存进 float 并正确取回的安全位数。
有时你也许能多拿回一两位,但有时你只能拿回 FLT_DIG 位。可以肯定的是:只要你存进 float 的位数不超过 FLT_DIG(含等于),就一定能把它们正确取回。
故事就是这样。FLT_DIG。完。
……真的完了吗?
不过,把十进制数存进浮点数再取出来,只是故事的一半。
原来浮点数能编码一些需要更多小数位才能完整打印出来的数。只不过你那个长长的十进制数未必正好落在这些数上。
也就是说,从一个浮点数到下一个浮点数之间存在间隙。如果你想编码的十进制数落在这个间隙里,它就会用最接近的那个浮点数。这就是 float 只能编码 FLT_DIG 位的原因。
但那些不在间隙里的浮点数呢?要精确打印它们需要多少位小数?
换个说法就是:对任意给定的浮点数,如果我想把它的十进制表示转换回完全相同的浮点数,需要保留多少位十进制数字?换句话说,我得用十进制打印多少位,才能还原出原数中全部的二进制位?
有时可能只需要几位。但为了保险,你会希望按某个安全的小数位数转换成十进制。这个位数就编码在下面这些宏里:
| 宏 | 说明 |
|---|---|
FLT_DECIMAL_DIG |
float 中编码的十进制位数。 |
DBL_DECIMAL_DIG |
double 中编码的十进制位数。 |
LDBL_DECIMAL_DIG |
long double 中编码的十进制位数。 |
DECIMAL_DIG |
与最宽的编码相同,即 LDBL_DECIMAL_DIG。 |
看一个例子:DBL_DIG 是 15(所以常量里最多只能有这么多位有效数字),而 DBL_DECIMAL_DIG 是 17(所以要转换成 17 位十进制数字,才能保留原 double 的全部比特)。
把具有 15 位有效数字的数 0.123456789012345 赋给 x,再把只有 1 位有效数字的数 0.0000000000000006 赋给 y。
x is exact: 0.12345678901234500 Printed to 17 decimal places
y is exact: 0.00000000000000060但把二者相加,结果应该是 0.1234567890123456,这已经超过 DBL_DIG 了,所以可能会发生怪事……来看看:
x + y not quite right: 0.12345678901234559 Should end in 4560!这就是打印超过 DBL_DIG 位的下场,对吧?可你看看——上面那个数,它本身是完全能够精确表示的!
如果把 0.12345678901234559(17 位)赋给 z 再打印,会得到:
z is exact: 0.12345678901234559 17 digits correct! More than DBL_DIG!如果我们把 z 截断到 15 位,它就不是同一个数了。这就是为什么要把 double 的所有比特都保留下来,需要 DBL_DECIMAL_DIG,而不只是较小的 DBL_DIG。
话虽如此,但显然,一般来说当我们摆弄十进制数时,打印超过 FLT_DIG、DBL_DIG 或 LDBL_DIG 位是不安全的——那样就无法合理地对应原来的十进制数以及后续的任何运算。
但从 float 转换成十进制表示、再转回 float 时,一定要用 FLT_DECIMAL_DIG 来做,这样才能精确保留全部比特。
当你写下一个常量数字,比如 1234,它是有类型的。可到底是什么类型?我们来看看 C 如何决定常量的类型,以及如何迫使它选择某个特定类型。
除了像奶奶以前常烤的那种老式十进制,C 还支持其他进制的常量。
如果数字以 0x 开头,它就会被当作十六进制数读取:
int a = 0x1A2B; // 十六进制
int b = 0x1a2b; // 十六进制数字不区分大小写
printf("%x", a); // 打印一个十六进制数,"1a2b" 如果数字以 0 开头,它就会被当作八进制数读取:
int a = 012;
printf("%o\n", a); // 打印一个八进制数,"12"这对初学者尤其麻烦:他们试图用 0 在左边补齐十进制数,好让它们排得整整齐齐,结果却无意中改变了数字的进制:
int x = 11111; // 十进制 11111
int y = 00111; // 十进制 73(八进制 111)
int z = 01111; // 十进制 585(八进制 1111)许多 C 编译器中的一项非官方扩展104允许你用 0b 前缀表示二进制数:
int x = 0b101010; // 二进制 101010
printf("%d\n", x); // 打印出十进制的 42printf() 没有用于打印二进制数的格式说明符。你得用按位运算符一个字符一个字符地打印。
你可以在常量整数后面附加一个表示类型的后缀,强制它成为某种类型。
我们会做一些赋值来演示,但大多数情况下开发者会省略后缀,除非需要精确指定。编译器很擅长确保类型兼容。
int x = 1234;
long int x = 1234L;
long long int x = 1234LL
unsigned int x = 1234U;
unsigned long int x = 1234UL;
unsigned long long int x = 1234ULL;后缀可以是大写,也可以是小写。而 U 与 L 或 LL 谁先谁后都可以。
| 类型 | 后缀 |
|---|---|
int |
无 |
long int |
L |
long long int |
LL |
unsigned int |
U |
unsigned long int |
UL |
unsigned long long int |
ULL |
我在表里提到过,“没有后缀”意味着 int……但实际上比这更复杂。
那么,当你遇到一个像这样的无后缀数字时,会发生什么:
int x = 1234;它是什么类型?
C 通常的做法是,从 int 开始往上,选择能容纳该值的最小类型。
但具体来说,这还取决于数字的进制(十进制、十六进制还是八进制)。
规范里有一张很好的表,说明什么样的无后缀值会使用什么类型。事实上,我干脆把它原样照抄在这里。
C11 §6.4.4.1¶5 写道:“整数常量的类型,是相应列表中其值能够被表示的第一种类型。”
接着它展示了这张表:
| 后缀 | 十进制常量 | 八进制或十六进制 常量 |
|---|---|---|
| 无 | intlong int |
intunsigned intlong intunsigned long intlong long intunsigned long long int |
u 或 U |
unsigned intunsigned long intunsigned long long int |
unsigned intunsigned long intunsigned long long int |
l 或 L |
long intlong long int |
long intunsigned long intlong long intunsigned long long int |
兼有 u 或 U以及 l 或 L |
unsigned long intunsigned long long int |
unsigned long intunsigned long long int |
ll 或 LL |
long long int |
long long intunsigned long long int |
兼有 u 或 U以及 ll 或 LL |
unsigned long long int |
unsigned long long int |
这段话的意思是:例如,如果你写了一个像 123456789U 这样的数,C 首先会看它能否作为 unsigned int。如果放不下,它就尝试 unsigned long int。再不行就试 unsigned long long int。它会使用能容纳该数字的最小类型。
你大概以为像 1.23 这样的浮点常量默认类型是 float 吧?
惊喜吧!原来不带后缀的浮点数居然是 double 类型!迟到的生日快乐!
你可以附加一个 f(或 F——不区分大小写)来强制它为 float 类型。附加 l(或 L)则可以强制它为 long double 类型。
| 类型 | 后缀 |
|---|---|
float |
F |
double |
无 |
long double |
L |
例如:
float x = 3.14f;
double x = 3.14;
long double x = 3.14L;
不过,我们一直以来不就是这么写的吗?
float x = 3.14;左边不就是 float,右边不就是 double 吗?没错!但 C 很擅长自动数值转换,所以无后缀浮点常量反而更常见。这个后面再细说。
还记得前面我们讲过,浮点数可以用有效数字、底数和指数来表示吗?
这种数有一种常见的写法,下面先给出它,再给出更易辨认的等价形式——也就是你实际算一遍所得的结果:
\(1.2345\times10^3 = 1234.5\)
把数字写成 \(s\times b^e\) 的形式,称为科学记数法105。在 C 中,这类数用“E 记数法”书写,因此下面这些是等价的:
| 科学记数法 | E 记数法 |
|---|---|
| \(1.2345\times10^{-3}=0.0012345\) | 1.2345e-3 |
| \(1.2345\times10^8=123450000\) | 1.2345e+8 |
你可以用 %e 按这种记数法打印数字:
printf("%e\n", 123456.0); // 打印出 1.234560e+05关于科学记数法的几个小趣闻:
不必非得在小数点前只写一位数字。前面可以放任意多位数字。
double x = 123.456e+3; // 123456不过,打印时它会调整指数,使小数点前只剩一位数字。
指数上的加号可以省略,因为加号本来就是默认的,但据我所见,实践中并不常见。
1.2345e10 == 1.2345e+10你可以给 E 记数法常量加上 F 或 L 后缀:
1.2345e10F
1.2345e10L不过等等,浮点的事还没完!
原来还有十六进制浮点常量!
它们的用法与十进制浮点数类似,但和整数一样以 0x 开头。
问题在于,你必须指定指数,而这个指数给出的是 2 的幂。也就是:\(2^x\)。
另外,写这种数时要用 p 而不是 e:
所以 0xa.1p3 就是 \(10.0625\times2^3 == 80.5\)。
使用十六进制浮点常量时,可以用 %a 打印十六进制科学记数法:
double x = 0xa.1p3;
printf("%a\n", x); // 0x1.42p+6
printf("%f\n", x); // 80.500000