C 语言数据在内存中的存储基本原理

我们都知道,C语言是一门面向过程的、抽象化的通用程序设计语言,广泛应用于底层开发。C语言能以简易的方式编译、处理低级存储器。同是正是因为 C 语言可以使用指针,从而对物理内存地址进行直接操作,这是 C 的最大特点之一。要理解好 C 语言,也需要对数据在内存中存储方式有基本的认识,本文以 C 为例子,介绍数据在内存中存储的基本原理。

内存空间与地址

下图展示了一个计算机内存条,其中每个黑色方块都包含一块内存空间。我们可以将内存想象成一个巨大的 Excel 表格,其中每个单元格都可以存储一定大小的数据。系统通过内存地址来访问目标位置的数据。计算机根据特定规则为表格中的每个单元格分配编号,确保每个内存空间都有唯一的内存地址。有了这些地址,程序便可以访问内存中的数据。

最重要的概念:基本数据类型以二进制的形式存储在计算机中。一个二进制位即为 1 比特。在绝大多数现代操作系统中,1字节(byte)由 8 比特(bit)组成。

1. 数据类型

1.1 类型的基本归类

当谈及计算机中的数据时,我们会想到文本、图片、视频、语音、3D 模型等各种形式。尽管这些数据的组织形式各异,但它们都由各种基本数据类型构成。

C语言中有多种基本数据类型,包括整数、浮点数、字符等。这些数据类型在内存中占用的空间大小不同,例如,int通常占用4个字节,float占用4个字节,char占用1个字节。数据类型决定了数据在内存中的存储方式和表示形式,可以分为以下的五大类型:

1.1.1 整型

1
2
3
4
5
6
7
8
9
10
11
12
char
unsigned char
signed char
short
unsigned short [int]
signed short [int]
int
unsigned int
signed int
long
unsigned long
signed long [int]

1.1.2 浮点型

1
2
float
double

1.1.3 构造类型

1
2
3
4
array // 数组类型
struct // 结构体类型
enum // 枚举类型
union // 联合类型

1.1.4 指针类型

1
2
3
4
int *ptr
char *ptr
float *ptr
void *ptr

1.1.5 空类型

1
void // 函数参数、返回类型、指针

2. 整形在内存中的存储

C 变量的创建是根据数据类型在内存中开辟空间。

2.1 原码、反码、补码

数据在内存中以 2 进制的形式存储,以整数举例,其 2 进制存储有三种表示形式。

正整数:原码、反码、补码相同。
负整数:原码、反码、补码要进行计算。

举例代码:

1
2
3
4
int a = -10;
// 10000000 00000000 00000000 00001010 - 原码(原始 2 进制表示形式)
// 11111111 11111111 11111111 11110101 - 反码(符号位不变,其它位按位取反)
// 11111111 11111111 11111111 11110110 - 补码(反码 + 1)

VS Code 调试取 &a 的地址后可以观察发现 16 进制的内存地址所存储的数据为 F6 FF FF FF,这恰好是 2 进制补码的 16 进制数,同时表明了整数在内存中存储的是补码。



为什么数据在内存中存放的是补码:因为在计算机组成原理中,补码可以将符号位和数值域统一处理,加法和减法也可以统一处理(CPU只有加法器),此外补码和原码相互转换,运算过程是相同的,无需额外的逻辑硬件电路。

  • 更深入的解释是原码虽然最直观,但存在一些局限性。一方面,负数的原码不能直接用于运算。例如在原码下计算 1 + (−2),得到的结果是 −3,这显然是不对的。为了解决此问题,计算机引入了反码。如果我们先将原码转换为反码,并在反码下计算 1 + (−2),最后将结果从反码转换回原码,则可得到正确结果 −1。
  • 另一方面,数字零的原码有 +0 和 −0 两种表示方式。这意味着数字零对应两个不同的二进制编码,这可能会带来歧义。比如在条件判断中,如果没有区分正零和负零,则可能会导致判断结果出错。而如果我们想处理正零和负零歧义,则需要引入额外的判断操作,这可能会降低计算机的运算效率。
  • 后面,计算机进一步引入了补码,在负零的反码基础上加 1 会产生进位,但 byte 类型的长度只有 8 位,因此溢出到第 9 位的 1 会被舍弃。也就是说,负零的补码为 0000 0000 ,与正零的补码相同。这意味着在补码表示中只存在一个零,正负零歧义从而得到解决。

举例代码:

1
2
3
4
5
6
7
8
9
10
int main()
{
// 1 - 1;
// 1 + (-1);
// 00000000 00000000 00000000 00000001 - 1的原码
// 10000000 00000000 00000000 00000001 - -1的原码
// 如果计算机采用原码进行逻辑加减(1+(-1)),得到结果数值如下
// 10000000 00000000 00000000 00000010 - 是-2?可以看到数值是错误的。
// 如果采用补码运算,数值才是正确的,这是计算机组成底层原理的逻辑。
}

2.2 大端字节序和小端字节序

字节序(Byte Order)是指多字节数据在计算机内存中存储时的顺序。字节序分为两种主要类型:大端字节序(Big-Endian)和小端字节序(Little-Endian)。

  1. 大端字节序(Big-Endian)

    • 在大端字节序中,多字节数据的最高有效字节(Most Significant Byte,MSB)存储在内存的最低地址处,而最低有效字节(Least Significant Byte,LSB)存储在内存的最高地址处。
    • 大端字节序的表示方式类似于阅读书写的方式,先读取最高位的字节,然后是次高位,以此类推。

    例如,十六进制数0x12345678在大端字节序中存储为:

    1
    0x12 0x34 0x56 0x78
  2. 小端字节序(Little-Endian)

    • 在小端字节序中,多字节数据的最低有效字节(LSB)存储在内存的最低地址处,而最高有效字节(MSB)存储在内存的最高地址处。
    • 小端字节序的表示方式与计算机内部的数据存储方式相符,因此在许多计算机架构中使用较为广泛。

    例如,十六进制数0x12345678在小端字节序中存储为:

    1
    0x78 0x56 0x34 0x12

字节序的选择在不同计算机架构和操作系统中可以有所不同。例如,x86 和 x86-64 架构通常使用小端字节序,而某些大型服务器架构(如SPARC)使用大端字节序。这种差异可能会在数据交换和网络通信中引起问题,因此在跨平台应用程序中,需要特别注意字节序的处理,以确保数据正确解释和传递。通常使用一种标准的字节序协议(如网络字节序)来协调不同架构之间的数据交换。

3. 总结

以上就是数据存储的基本原理,C 语言根据数据的类型在内存中开辟空间,操作系统根据内存地址来进行读取操作。整数在计算机中是以补码的形式存储的。在补码表示下,计算机可以对正数和负数的加法一视同仁,不需要为减法操作单独设计特殊的硬件电路,并且不存在正负零歧义的问题。如果深入,还有数据结构、内存管理、浮点数、字符编码等等方向可以探讨,从这里也可以窥探到计算机组成原理和它的体系实在是精妙。


C 语言数据在内存中的存储基本原理
https://imccchoi.github.io/2023/09/25/post-4/
作者
ccchoi
发布于
2023年9月25日
许可协议