虽说C语言是一门很成熟的编程语言,但是近些年来也是有所发展的,从早期的C89到后来的C99、C11等新标准,C语言逐步增加了许多好用的功能,例如新标准头文件“stdint.h”的添加。
C语言逐步增加了许多好用的功能
stdint.h头文件
如果程序员希望写出可移植的C语言程序,首先最重要的一点是不能假定位宽。C语言标准并没有明确指定的 short、int、long 等类型的位宽,因此可能在某些平台 sizeof(int) 等于 2,在其他平台 sizeof(int) 等于 4,所以如果编写的C语言代码假定 sizeof(int) 是一个固定值,显然就属于不可移植的代码。
为了解决这样的问题,在新标准文件“stdint.h” 之前,程序员必须做些额外的工作,以确定C语言代码运行的平台的各种数据位宽,这样的工作着实烦人,稍不留神就会出错。
“stdint.h”头文件的添加就是为了便于程序员写出不假定位宽的程序的。其内部通过 typedef 和宏判断定义了不少好用的整数类型,例如 int8_t 类型表示 8 位的有符号的整数类型,uint32_t 则表示 32 位的无符号的整数类型。类似的,还有 int16_t、int64_t、uint64_t 等类型,都是比较好理解的。
C语言中的“快”类型
不过,如果读者打开 stdint.h 头文件,应该能够看到一些更有趣的类型,如下图:
更有趣的类型
可以看出,这些类型被称作“fast type”,类型名中也有 fast 的字样(如 int_fast16_t),直译成中文即“快类型”,那么它们到底有什么含义呢?
观察力敏锐的读者应该发现了,int_fast16_t 和 int_fast32_t 其实是一样的,以上图黄框为例,它们都表示 int 类型,这是怎么回事呢?int 类型怎么能同时表示 16 位宽和 32 位宽的整数类型呢?
int 类型当然不能同时表示两种位宽的整数类型,事实上,int_fastxx_t 类型并不是准确的 xx 位宽类型,它表示不低于 xx 位宽的类型,因此,只要 int 的位宽大于或者等于 32 位,它就能同时表示 int_fast16_t 和 int_fast32_t 类型。
有读者看到这里可能会有疑问,如果 int 的位宽等于 32,那么使用它来表示 16 位宽的 int_fast16_t 整数类型,不是造成资源浪费了吗?
不是造成资源浪费了吗?
其实读者应将注意力放在“fast”一词上。CPU 从内存取数据一般是逐字取的,这里的“字”并不是字节的意思,在 32 位主机上,字长常常是 4 个字节,也即 CPU 单次取出数据的最小单位是 4 个字节。
字长也可以理解为 CPU 读取数据的“步长”。
也就是说,CPU 读取数据的“步长”是字长(下文以4字节为例),也就是说假设这次读取了地址 0~3 的数据,接下来若是希望读取相邻的数据,最接近的地址也得是地址 4~7。其实从这里可以看出,CPU 每次读取数据的起始地址都是字长的整数倍。
这也是“数据对齐”的原因——为了CPU读取数据的效率。
如果需要读取的数据只有一个字节(char 型),那么显然,无论该字节放在哪里,它总是在某个字长段的范围内的(例如地址 0~3,地址 4~7 内),此时 CPU 一次就能读取完毕。
CPU 一次就能读取完毕。
如果需要读取的数据有两个字节(16bits),情况就不同了——它的地址可能是 3~4,而 CPU 读取数据的“步长”是 4 个字节,若要读取该值,CPU 只能先读取 0~3 字节的数据,再读取 4~7 字节的数据,最后还需要组合拼凑,才能得到该值。这样的一系列操作显然非常低效。
所以 stdint.h 将“快”类型定义为字长的整数倍的意图就一目了然了,无非就是牺牲一些“空间”换取“时间”。当然了,读者在使用“快类型”时需要注意:int_fast16_t 并不一定恰好是 16 位宽,它只是不少于 16 位宽的类型。
C语言中的“小”类型
前面提到,int_fastxx_t 类型牺牲了“空间”换取“时间”,如果在某个C语言项目中,“空间”效率并不是特别重要,而“空间”效率却非常重要,那么 int_fastxx_t 类型显然就不合适了。此时可以使用 stdint.h 中定义的 “small type”,也即“小类型”。
C语言中的“小类型”
int_leastxx_t 系列的数据类型基本上保证了其恰好是 xx 位宽,避免了空间浪费,但是按照前文的分析,“小类型”付出的代价是损失了一部分时间效率。