Logo
Section 1 / 6

数据类型大小

了解各种数据类型的大小。

导入

数据类型大小

了解各种数据类型的大小。

在开始之前

我们的目标是深化第一章中“内存”课程所建立的直觉。继续之前,最好重新阅读该部分内容。

引言

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;

对于这个变量,我们绘制了如下内存布局:

...
Previous memory drawing for i

我们说过,操作系统会选择一个内存位置(起始点),然后根据数值将每个比特位填充为 1 或 0。
这里有一个我们尚未解决的问题:
较小的数字需要较少的比特位,较大的数字则需要更多的比特位。计算机如何知道读取到哪里为止?

我们有变量地址提供的起始点,知道从哪里开始读取。
但我们没有终点。如果只读取 4 个比特位,而某个数字实际上需要 32 个比特位,那就会过早停止读取。

终点由起始点(地址)和数据类型大小共同决定。
如果我们有一个 int,我们知道它占用 4 字节 = 32 位内存。如果起始地址是 x,那么终点位于 x + 32 之前。
或者说,该数值占据从起始点开始的接下来 32 位(包含起始点)。

因此,在我们的例子中,需要填满全部 32 位,但我们的数值只有 4 位。为了解决这个问题,其余 28 位(32 - 4)被填充为 0。

这个过程听起来比实际复杂,让我们更新一下绘图以更清晰地展示。

...
Full 32 bits of an integer

如图所示,我们使用了 4 字节(32 位),将 1101 放置其中,并将其余位补零。

我们不会每次都画出如此详细的比特图,因为绘制 32 位或 64 位并跟踪它们非常困难。我们仅在此处画一次,是为了理解底层发生了什么。

另一个例子

假设以下变量:

1char x = 'c';

如果操作系统决定将变量 x 放置在地址 0x7ffdee2428ec,那么接下来的 8 位(1 字节)将被设置为字符 'c' 的二进制表示。
在二进制中,c 是 1100011,它只需要 7 位,但我们必须填满 8 位。解决方案是添加一个前导 0 作为填充:01100011。

...
Start and end address of a char

未来如需绘图,我们将仅绘制字节,不再绘制比特(以避免图形过于庞大)。
将下面的图与之前的 32 位整数图对比,会发现它更短,也更容易理解。

...
Drawing an integer at the byte level

指针的大小

让我们尝试找出各种指针的大小。

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 到 2642^{64}1 的地址。

证明(可选)

  • 我们可以证明,使用 64 位,最多可以表示 2642^{64}1 的地址。

为此,我们需要了解二进制转十进制的转换方法。
假设以下二进制数:

11101

我们从右往左处理每一位,对每一位进行如下计算:
位值 × 2位置索引2^{位置索引}。

1 1 1 0 1 2pos3 pos2 pos1 pos0

最终结果为:
1 × 202^0 + 0 × 212^1 + 1 × 222^2 + 1 × 232^3 = 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 × 202^0 + 1 × 212^1 + 1 × 222^2 + … + 1 × 2632^{63} = 202^0 + 212^1 + 222^2 + … + 2632^{63}

我们记这个和为 S = 202^0 + 212^1 + 222^2 + … + 2632^{63}。

第一种解法

将整个表达式乘以 2:

2S = 212^1 + 222^2 + … + 2642^{64}

  • 两式相减得:
    2S - S = 2642^{64}202^0
    ⇒ S = 2642^{64} - 1(其余项在两个和中均出现,一正一负相互抵消)
  • 因此,64 位全为 1 的二进制数转换为十进制后,结果为 2642^{64}1。
  • 2642^{64}1 是使用 8 字节可表示的最大地址。

第二种解法

另一种计算和 S 的方法是使用等比数列求和公式。

注意: 对于包含 n 项的等比数列,其求和公式为:a⋅(1−rn)1−r\frac{a \cdot (1 - r^n)}{1 - r}。

其中,a 是首项,此处为 1 × 202^0 = 1。
r 是公比,即相邻两项之间的倍数关系。由于每一项都是前一项的两倍,故 r = 2。
我们的求和范围是从 0 到 63,共 64 项,因此 n = 64。

代入公式:

S=1⋅(1−264)1−2=(−1)⋅(1−264)=−1+264=264−1S = \frac{1 \cdot (1 - 2^{64})}{1 - 2} = (-1) \cdot (1 - 2^{64}) = -1 + 2^{64} = 2^{64} - 1

  • 我们得到了公式 S = 2n2^n1,其中 n = 64。
    即 S = 2642^{64} - 1。

这与我们手动计算的结果一致,无需依赖公式。

堆与动态内存

加法