数据类型大小
了解各种数据类型的大小。
在开始之前
我们的目标是深化第一章中“内存”课程所建立的直觉。继续之前,最好重新阅读该部分内容。
引言
C语言为我们提供了多种数据类型,例如:
int用于整数float用于实数char用于字符
然而,还有更多数据类型,如 double 和 long。它们并不存储新的数据类型,区别在于它们可以存储更大的数值或具有更高的精度。
例如,long 可以存储比 int 大得多的数值。每种数据类型在内存中占用的空间不同,换句话说,每种数据类型都有不同的大小。
要查看数据类型的大小,我们可以使用 sizeof 操作符。请参考以下示例。
1#include <stdio.h> 2 3int main() 4{ 5 printf("sizeof(char) = %d\n", sizeof(char)); 6 printf("sizeof(short) = %d\n", sizeof(short)); 7 printf("sizeof(int) = %d\n", sizeof(int)); 8 printf("sizeof(float) = %d\n", sizeof(float)); 9 printf("sizeof(double) = %d\n", sizeof(double)); 10 return 0; 11}
我们看到 sizeof(char) 的结果是 1,但测量单位是什么?
输出单位是字节(1字节 = 8位):
char占用 1 字节内存,即 8 位。short占用 2 字节内存,即 16 位。int占用 4 字节内存,即 32 位。float占用 4 字节内存,即 32 位。double占用 8 字节内存,即 64 位。
注意: 必须清楚地认识到,我们绝不能假设数据类型的大小。它取决于平台、编译器以及可能的其他因素。
例如,在代码中不要硬编码int的大小,而应使用sizeof(int)来获取其大小。
然而,为了理解变量的内存布局,以及后续数组的结构,我们需要记住这些大小信息。
本课程中,我们将采用上述输出中的大小值来进行内存绘图和代码分析。
关于位与字节的进一步说明
我们可以看到最小的尺寸是 char 的 1 字节。因此,我们说可寻址的最小内存单元是字节。
这是一种更专业的说法,意思是不存在小于 1 字节(8 位)的数据类型。
让我们回顾之前讨论的一个例子:
1int i = 13;
对于这个变量,我们绘制了如下内存布局:
我们说过,操作系统会选择一个内存位置(起始点),然后根据数值将每个比特位填充为 1 或 0。
这里有一个我们尚未解决的问题:
较小的数字需要较少的比特位,较大的数字则需要更多的比特位。计算机如何知道读取到哪里为止?
我们有变量地址提供的起始点,知道从哪里开始读取。
但我们没有终点。如果只读取 4 个比特位,而某个数字实际上需要 32 个比特位,那就会过早停止读取。
终点由起始点(地址)和数据类型大小共同决定。
如果我们有一个 int,我们知道它占用 4 字节 = 32 位内存。如果起始地址是 x,那么终点位于 x + 32 之前。
或者说,该数值占据从起始点开始的接下来 32 位(包含起始点)。
因此,在我们的例子中,需要填满全部 32 位,但我们的数值只有 4 位。为了解决这个问题,其余 28 位(32 - 4)被填充为 0。
这个过程听起来比实际复杂,让我们更新一下绘图以更清晰地展示。
如图所示,我们使用了 4 字节(32 位),将 1101 放置其中,并将其余位补零。
我们不会每次都画出如此详细的比特图,因为绘制 32 位或 64 位并跟踪它们非常困难。我们仅在此处画一次,是为了理解底层发生了什么。
另一个例子
假设以下变量:
1char x = 'c';
如果操作系统决定将变量 x 放置在地址 0x7ffdee2428ec,那么接下来的 8 位(1 字节)将被设置为字符 'c' 的二进制表示。
在二进制中,c 是 1100011,它只需要 7 位,但我们必须填满 8 位。解决方案是添加一个前导 0 作为填充:01100011。
未来如需绘图,我们将仅绘制字节,不再绘制比特(以避免图形过于庞大)。
将下面的图与之前的 32 位整数图对比,会发现它更短,也更容易理解。
指针的大小
让我们尝试找出各种指针的大小。
1#include <stdio.h> 2 3int main() 4{ 5 printf("sizeof(char*) = %d\n", sizeof(char *)); 6 printf("sizeof(short*) = %d\n", sizeof(short *)); 7 printf("sizeof(int*) = %d\n", sizeof(int *)); 8 printf("sizeof(float*) = %d\n", sizeof(float *)); 9 printf("sizeof(double*) = %d\n", sizeof(double *)); 10 return 0; 11}
我们可以看到,无论指针类型为何,其大小均为 8 字节。这是存储所有可能地址所需的大小。
仔细思考一下,这很合理。指针本质上只是一个指向数据的地址,因此它的大小不应随所存储数据类型的变化而变化。
地址范围从 0 到计算机安装的总内存容量,我们需要足够的字节数来存储它。
- 使用 8 字节(64 位),我们可以存储从 0 到 1 的地址。
证明(可选)
- 我们可以证明,使用 64 位,最多可以表示 1 的地址。
为此,我们需要了解二进制转十进制的转换方法。
假设以下二进制数:
11101
我们从右往左处理每一位,对每一位进行如下计算:
位值 × 。
1 1 1 0 1 2pos3 pos2 pos1 pos0
最终结果为:
1 × + 0 × + 1 × + 1 × = 1×1 + 0×2 + 1×4 + 1×8 = 1 + 0 + 4 + 8 = 13
现在设想我们有 64 位。为了得到最大可能的数值,我们将所有位都设为 1。否则,某些 2 的幂次会被排除(乘以 0),导致总和降低。
1 1 1 .... 1 1 1 2pos63 pos62 pos2 pos1 pos0
结果为:
1 × + 1 × + 1 × + … + 1 × = + + + … +
我们记这个和为 S = + + + … + 。
第一种解法
将整个表达式乘以 2:
2S = + + … +
- 两式相减得:
2S - S =
⇒ S = - 1(其余项在两个和中均出现,一正一负相互抵消) - 因此,64 位全为 1 的二进制数转换为十进制后,结果为 1。
- 1 是使用 8 字节可表示的最大地址。
第二种解法
另一种计算和 S 的方法是使用等比数列求和公式。
注意: 对于包含 n 项的等比数列,其求和公式为:。
其中,a 是首项,此处为 1 × = 1。
r 是公比,即相邻两项之间的倍数关系。由于每一项都是前一项的两倍,故 r = 2。
我们的求和范围是从 0 到 63,共 64 项,因此 n = 64。
代入公式:
- 我们得到了公式 S = 1,其中 n = 64。
即 S = - 1。
这与我们手动计算的结果一致,无需依赖公式。