计算机内存探秘:物理存储器、地址空间与内存地址

 更新时间:2025年05月24日 14:26:09   作者:web安全工具库  
程序和数据放在内存里运行,听说过“内存地址”这个词,但它到底代表什么?物理内存条、显卡显存、主板上的ROM...这些都是存储器,它们是如何被统一管理的?本文将带你探索计算机存储器的不同层面,理解物理存储器、存储地址空间以及程序所感知的“内存地址”之间的关系

对于初学者来说,计算机的“内存”概念有时会让人感到困惑。我们知道程序和数据放在内存里运行,也听说过“内存地址”这个词,但它到底代表什么?物理内存条、显卡显存、主板上的ROM...这些都是存储器,它们是如何被统一管理的?

本文将带你探索计算机存储器的不同层面,理解物理存储器、存储地址空间以及程序所感知的“内存地址”之间的关系。

1. 物理存储器:硬件层面的“仓库”

首先,我们来谈谈物理存储器 (Physical Storage)。顾名思义,这是指计算机硬件中实际存在的、用于存储数据的芯片或设备。最常见的物理存储器包括:

  • 主内存 (Main Memory / RAM): 插在主板上的内存条,是CPU主要的工作区域。
  • 显卡显存 (VRAM): 位于显卡上的专用高速存储器,用于存储图形数据和纹理。
  • 各种适配器上的 ROM 或 RAM: 例如,网卡、声卡等设备上也可能有存储固件(ROM)或少量用于缓冲数据的RAM。

这些物理存储器是分散在计算机系统中的独立硬件单元。它们各自有自己的控制器,以及访问其内部数据的机制。

2. 存储地址空间 (Per-Device): 各自为政的地址范围

每个物理存储设备都有其内部的存储地址空间 (Storage Address Space)。这指的是该设备内部用来标识其存储单元(通常是字节)的地址范围。

例如,一个 8GB 的内存条,它内部可能有从地址 0 到 8GB-1 的存储单元。一块显卡的 4GB 显存,它内部也有从地址 0 到 4GB-1 的存储单元。一个设备上的 ROM 可能有从地址 0 到 ROM 大小-1 的地址。

你可以把这想象成不同的建筑物,每栋建筑物里的房间都有从 1 开始编号。建筑物 A 的 1 号房间和建筑物 B 的 1 号房间是完全不同的两个地方。每个物理设备就是一栋“建筑物”,它的内部地址空间就是这栋建筑物里“房间”的编号范围。

问题来了:CPU 如何统一管理和访问这些分散在不同物理设备、拥有各自独立地址空间的存储单元呢?CPU 不能直接说“请给我建筑物 B 的 1 号房间的东西”。

3. 统一的视图:内存地址空间与线性地址

为了让 CPU 和软件能够方便地访问和管理这些分散的物理存储资源,操作系统和硬件(特别是内存管理单元 MMU)会将这些物理设备的地址映射 (Mapping) 到一个统一的、线性的地址空间中。这个统一的地址空间,就是我们通常在讨论计算机系统时所说的内存地址空间,或者更精确地说,是线性地址空间 (Linear Address Space),在现代操作系统中,它往往是虚拟地址空间 (Virtual Address Space) 的一部分。

这个过程可以理解为:系统为所有的物理存储资源(包括 RAM、显存、各种设备的寄存器和内存等)编制了一个统一的“地图”。地图上的每一个地址都对应着某个物理设备上的某个具体的存储单元。

例如,在一个 32 位系统中,这个统一的线性地址空间通常是从地址 0x000000000xFFFFFFFF,总共 2^32 = 4GB 的地址范围。系统会将 4GB 物理内存的地址范围映射到这个线性地址空间的一部分,将显存映射到另一部分,将设备 ROM/RAM 映射到再一部分,等等。

这样一来,CPU 只需要使用这个统一的线性地址(例如 0x80001234),系统硬件就会负责将这个线性地址翻译成对应的物理设备上的物理地址(例如“显卡显存上的地址 0x101234”),从而完成数据的访问。

内存地址 (Memory Address) 这个概念,在程序开发者的视角看来,通常指的就是在这个统一的线性/虚拟地址空间中的地址。当我们在 C/C++ 中使用指针获取变量地址时,获取到的就是这个线性/虚拟地址空间中的地址。

将内存抽象成字节数组:

从软件(特别是操作系统和应用程序)的角度看,这个统一的内存地址空间可以被抽象成一个巨大的、一维的字节数组 (Byte Array)。这个数组的每一个“格子”就是一个字节(8 bits),并且都有一个唯一的、从 0 开始的编号,这个编号就是该字节的内存地址

  • 地址 0x00000000 对应第一个字节。
  • 地址 0x00000001 对应第二个字节。
  • ...
  • 地址 0xFFFFFFFF 对应最后一个字节(在 32 位系统中)。

不同类型的数据,如 char (1 字节), int (通常 4 字节), float (通常 4 字节), double (通常 8 字节),以及更复杂的结构体和数组,它们在内存中会占据连续的若干个字节空间。一个变量的地址通常指的就是它所占用的第一个字节的地址。

4. 代码示例:窥探程序眼中的内存地址

通过一个简单的 C 语言程序,我们可以直观地看到变量在程序所感知的这个“内存地址空间”中是如何被分配地址的。

#include <stdio.h> // 包含标准输入输出库,用于使用 printf 函数
#include <stddef.h> // 包含 stddef.h 以使用 size_t 类型

int main() {
    // 声明不同类型的变量
    char my_char = 'A';        // 字符类型,通常占 1 字节
    int my_int = 12345;        // 整型,通常占 4 字节
    float my_float = 3.14f;    // 浮点型,通常占 4 字节
    double my_double = 2.71828; // 双精度浮点型,通常占 8 字节

    // 声明一个数组
    int my_array[5] = {10, 20, 30, 40, 50}; // 包含 5 个整型的数组

    // 声明一个结构体
    struct Point {
        int x;
        int y;
    };
    struct Point p = {100, 200}; // 结构体变量

    // 声明一个函数 (实际上是获取函数的入口地址)
    // 注意:函数地址通常在代码段,与数据段/栈段的地址在不同的内存区域
    void (*print_msg)(void) = main; // 获取 main 函数的地址 (示例)
    // 实际调用函数指针的例子:
    // print_msg(); // 这会尝试再次执行 main 函数,可能会导致栈溢出或其他问题,不建议在实际代码中这样做!
    // 这里的目的是演示如何获取函数地址

    // 打印变量的值、地址和占用的字节数
    // %p 用于打印指针的值 (地址),需要转换为 (void*) 类型以保证跨平台兼容性
    // sizeof 运算符用于获取变量或类型占用的字节数
    printf("变量 my_char:\n");
    printf("  值: %c\n", my_char);
    printf("  地址: %p\n", (void*)&my_char);
    printf("  占用字节数: %zu\n", sizeof(my_char)); // %zu 用于 size_t 类型

    printf("\n变量 my_int:\n");
    printf("  值: %d\n", my_int);
    printf("  地址: %p\n", (void*)&my_int);
    printf("  占用字节数: %zu\n", sizeof(my_int));

    printf("\n变量 my_float:\n");
    printf("  值: %f\n", my_float);
    printf("  地址: %p\n", (void*)&my_float);
    printf("  占用字节数: %zu\n", sizeof(my_float));

    printf("\n变量 my_double:\n");
    printf("  值: %lf\n", my_double); // %lf 用于 double 类型
    printf("  地址: %p\n", (void*)&my_double);
    printf("  占用字节数: %zu\n", sizeof(my_double));

    printf("\n数组 my_array:\n");
    // 数组名本身通常代表数组第一个元素的地址
    printf("  数组首地址: %p\n", (void*)my_array);
    printf("  第一个元素 my_array[0] 的地址: %p\n", (void*)&my_array[0]);
    printf("  第二个元素 my_array[1] 的地址: %p\n", (void*)&my_array[1]);
    printf("  占用总字节数: %zu\n", sizeof(my_array));
    printf("  每个元素占字节数: %zu\n", sizeof(my_array[0]));
    // 注意观察相邻元素地址之间的差异,它等于元素的大小 (这里是 sizeof(int))

    printf("\n结构体 p:\n");
    printf("  结构体首地址: %p\n", (void*)&p);
    printf("  成员 p.x 的地址: %p\n", (void*)&p.x);
    printf("  成员 p.y 的地址: %p\n", (void*)&p.y);
    printf("  占用总字节数: %zu\n", sizeof(p));
    // 注意成员地址与结构体首地址的关系

    printf("\n函数 main 的地址:\n");
    printf("  地址: %p\n", (void*)print_msg); // 打印函数指针的值

    return 0; // 程序正常结束
}

编译和运行:

  1. 将上述代码保存为 address_example.c 文件。
  2. 打开终端或命令提示符。
  3. 使用 C 编译器(如 GCC)编译代码:
gcc address_example.c -o address_example
  1. 运行生成的可执行文件:
./address_example

运行结果示例:

请注意,输出的内存地址是示例值,具体数值在您的系统上运行或每次运行时都可能不同,因为操作系统会动态分配内存,并且涉及到虚拟内存地址。关键在于观察地址的相对关系和不同类型占用的字节数

变量 my_char:
  值: A
  地址: 0x7ffd7533f0b7
  占用字节数: 1

变量 my_int:
  值: 12345
  地址: 0x7ffd7533f0b0
  占用字节数: 4

变量 my_float:
  值: 3.140000
  地址: 0x7ffd7533f0ac
  占用字节数: 4

变量 my_double:
  值: 2.718280
  地址: 0x7ffd7533f0a0
  占用字节数: 8

数组 my_array:
  数组首地址: 0x7ffd7533f080
  第一个元素 my_array[0] 的地址: 0x7ffd7533f080
  第二个元素 my_array[1] 的地址: 0x7ffd7533f084
  占用总字节数: 20
  每个元素占字节数: 4

结构体 p:
  结构体首地址: 0x7ffd7533f078
  成员 p.x 的地址: 0x7ffd7533f078
  成员 p.y 的地址: 0x7ffd7533f07c
  占用总字节数: 8

函数 main 的地址:
  地址: 0x563e556c4179

结果分析:

  1. 每个变量都被分配了一个唯一的地址。这些地址是程序在运行时看到的线性/虚拟地址
  2. sizeof 运算符显示了不同数据类型占用的字节数,这决定了它们在内存中占据的空间大小。
  3. 对于数组 my_array,数组名 my_array 的地址与第一个元素 &my_array[0] 的地址相同。相邻元素 &my_array[0] 和 &my_array[1] 的地址相差 4 个字节 (0x7ffd7533f084 - 0x7ffd7533f080 = 0x4),正好是一个 int 类型的大小,这印证了数组元素是连续存储的。
  4. 对于结构体 p,结构体的首地址就是其第一个成员 &p.x 的地址。第二个成员 &p.y 的地址紧随其后(或者根据编译器的对齐策略有微小的间隔),地址相差 4 个字节 (0x7ffd7533f07c - 0x7ffd7533f078 = 0x4),正好是 p.x (int) 的大小,这说明结构体成员也是按顺序存储的。结构体的总大小是其成员大小的总和(加上可能的对齐填充)。
  5. 函数 main 也有一个地址,这是函数代码在内存中的起始位置。函数的地址通常位于内存的不同区域(代码段)与变量(数据段/栈段)的地址区分开来。

这个例子清晰地展示了程序如何看待内存——一个拥有连续地址的字节序列,各种数据类型根据其大小占据其中的一部分。操作系统和硬件在底层默默地将这些程序可见的地址翻译成物理设备上的实际地址。

到此这篇关于计算机内存探秘:物理存储器、地址空间与内存地址的文章就介绍到这了,更多相关计算机内存:物理存储器、地址空间与内存地址内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

您可能感兴趣的文章:

相关文章

  • 到初创公司工作的五个理由

    到初创公司工作的五个理由

    这篇文章主要介绍了到初创公司工作的五个理由,在职业选择上并不来一定要去大公司,去初创公司工作一次会让你有更多的收获,需要的朋友可以参考下
    2014-09-09
  • Markdown语法手册—完整笔记整理

    Markdown语法手册—完整笔记整理

    Markdown是一种轻量级标记语言,创始人为约翰·格鲁伯(John Gruber), 它允许人们使用易读易写的纯文本格式编写文档,然后转换成有效的 XHTML(或者HTML)文档,由于Markdown的轻量化、易读易写特性,并且对于图片,图表、数学式都有支持,许多网站都广泛使用Markdown
    2024-08-08
  • Uint 和 int 的区别解析

    Uint 和 int 的区别解析

    Int与Uint的区别在于带符号与不带符号,在计算机中根据补码进行互相转换,很多语言是有方法支持的,但是也有一些并没有转换方法,比如SQLserver(SQLserver不支持Uint类型),下面详细介绍Uint 和 int 的区别,感兴趣的朋友一起看看吧
    2023-08-08
  • 有意思的数据结构默克树 Merkle tree应用介绍

    有意思的数据结构默克树 Merkle tree应用介绍

    这篇文章主要为大家介绍了有意思的数据结构默克树 Merkle tree应用介绍,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2022-09-09
  • centos部署open-webui的完整流程记录

    centos部署open-webui的完整流程记录

    这篇文章主要介绍了centos部署open-webui的完整流程,OpenWebUI是一个开源的Web用户界面工具,用于与本地或私有化部署的大语言模型交互,文中将步骤介绍的非常详细,需要的朋友可以参考下
    2025-02-02
  • 鸿蒙系统中的Webview技术使用方法详解

    鸿蒙系统中的Webview技术使用方法详解

    webView类是View类的一个扩展,用来显示网页,它不包含任何的网页浏览器的特征,像没有导航控制和地址栏,使用起来也很方便,这篇文章主要给大家介绍了关于鸿蒙系统中Webview技术使用的相关资料,需要的朋友可以参考下
    2024-07-07
  • idea启动后CPU飙升的问题解决

    idea启动后CPU飙升的问题解决

    IDEA运行大型项目,项目启动卡顿,CPU使用率占用过高,下面这篇文章主要给大家介绍了关于idea启动后CPU飙升的问题解决,文中通过实例代码介绍的非常详细,需要的朋友可以参考下
    2023-05-05
  • github版本库使用详细图文教程(命令行及图形界面版)

    github版本库使用详细图文教程(命令行及图形界面版)

    今天我们就来学习github的使用,我们将用它来管理我们的代码,你会发现它的好处的,当然是要在本系列教程全部完成之后,所以请紧跟站长的步伐,今天是第一天,我们来学习如何在git上建立自己的版本仓库,并将代码上传到仓库中
    2015-08-08
  • 详情解析TCP与UDP传输协议

    详情解析TCP与UDP传输协议

    本文通过讲解TCP与UDP传输协议传输数据是的过程及详细介绍什么是 socket及现在我么们和大家一起来学习吧
    2021-08-08
  • 最新IDEA永久激活教程(支持最新2019.2版本)

    最新IDEA永久激活教程(支持最新2019.2版本)

    这篇文章主要介绍了最新IDEA永久激活教程,此教程已支持最新2019.2版本,适用Windows、Mac、Ubuntu等所有平台,非常不错,具有一定的参考借鉴价值,需要的朋友可以参考下
    2020-05-05

最新评论