跳至正文
View Categories

2 min read

1. 变量地址 #

1.1 回顾内存 #

前面,我们说过,一个存在内存中的数,其实是由一段连续存储空间上的“有电/没电”状态表示的。

一个 int 的大小如下图所示。

在这个图中,我们特地画成两行的表格,其实是有意义的。

思考:计算机是怎么定位到某个变量,所对应的内存区域呢?

1.2 在内存中定位变量 #

我们先思考一下,老师点了一份外卖,快递小哥是怎么知道老师的位置呢?

对了,是老师在手机上输入的“地址”

大家请思考一下,“地址” 有什么特点?
唯一

1.2.1 内存地址 #

在我们的计算机内存中,其实也有“地址”
和生活中的地址不同,内存中当然不会有“某某街道”,所以,内存中的地址,都是用数字来编号的。而且,我们一般习惯上写作十六进制。

下面我们先来讨论一下这个地址。

1.2.2 十六进制 #

使用十六进制表示的好处是什么呢?请回忆一下:

    1111 0000 1111 0000 (2进制)
0x  F    0    F    0;     
  • 二进制中,可以直接看出来,内存中的“有电没电”状态
  • 十六进制的每 1 位,正好对应二进制的 4 位
  • 十六进制表达,既可以较为直观的看出内存状态,也较为简洁
  • 2 位十六进制数,对应于 1 字节(Byte)内存空间

请问,下面的地址,是一个占多少字节(Byte)的数?

0x7cff15e8

答案是 4 字节。
因为这是一个 8 位的十六进制数,而每两位十六进制数表示一个字节,所以,这个数一共占 8/2 = 4 字节。

请看上面的地址变化

0x7cff15e8 -> 0x7cff15e9 -> 0x7cff15ea 

相邻的两个地址,相差 1 。而且,这里的每个存储单元,只能保持两个状态,有电 / 没电

1.2.3 地址范围 #

请问,下面的地址,是一个占多少位(Bit)的数?

注意:内存单元的 “地址编号” 是硬件定义的,不占用内存空间;但 “表示地址的数值”(如指针、指令中的地址)会作为数据存储在内存中,内存中的基本单位为1字节。

0x7cff15e8

解析:
在上面的分析中,这个数是一个 8 字节的数。
1 字节 = 8 位
所以,这个数是 4*8 = 32 位(Bit)。

这个 32 位其实是我们电脑上的一个重要参数。先来看一下,在电脑上哪里可以看到这个参数:
请打开“计算机” -> “右键” -> “属性” -> “系统类型”

大家电脑上。这里应该也是显示的是 64 位。

这是因为电脑的进步速度太快了,在几年前,市面上还是以 32 位的计算机居多。现在已经几乎都是 64 位的计算机了。
为了方便讲解,我们把 “32 位”和 “64 位” 放在一起讲解。

来看一下,32 位和 64 位 计算机的地址差异

示例:(十六进制数书写的间隔只是为了方便阅读,在代码中,不能有空格)

0x0c0f 15e8              // 32 位地址
0x1c4f 2c3f 13e8 160d    // 64 位地址

请用十六进制,分别写出 4 字节 和 8 字节数的表示范围

4 字节 和 8 字节数的表示范围:

0x0000 0000            - 0xFFFF FFFF              // 32 位,4 字节
0x0000 0000 0000 0000  - 0xFFFF FFFF FFFF FFFF    // 64 位,8 字节

请将以上的十六进制数,转化为十进制数

十进制:

0x0000 0000            - 0xFFFF FFFF              // 32 位,4 字节
0                      - 4,294,967,295
0                      - 2^32

0x0000 0000 0000 0000  - 0xFFFF FFFF FFFF FFFF    // 64 位,8 字节
0                      - 18,446,744,073,709,551,616
0                      - 2^64

从上面的计算中,我们可以得出这样的结论:
32 位地址(一个地址存 1个字节Byte),有 2^32*8 个位“可以存放有电/没电状态的存储单元”,
64 位地址(一个地址存 1个字节Byte),有 2^64*8 个位“可以存放有电/没电状态的存储单元”,
请完成下面的单位换算:
2^32 字节 等于多少 GB?
2^64 字节 等于多少 GB?

答案:

2^32 Byte = 4 GB     // 32 位,4 字节
2^64 Byte = 2^34 GB  // 64 位,8 字节

对我们生活的意义
32 位的计算机,它最多只有 2^32 个“存储单元”,也就是 2^32 个位(Bit),也就是 4 GB 。
这意味,32 位的电脑,他的内存不能超过 4 GB。

而 64 位的计算机,它的内存最多可以支持 2^34 GB。
现在,我们主流的计算机配置,内存都在 8 GB 以上,所以只有 64 位的计算机才可以支持。

1.3 内存的真实样子 #


上图中,要注意的地方有:
内存地址是递增 1 变化的
使用 1 / 0 来表示“有电/没电”状态
这里演示的是 32 位计算机上的内存地址

1.4 内存模型,合并表示 #

我们建立模型的作用,是为了更容易地讨论一些问题。
现在我们给出来的,是最基本的内存模型。请务必理解:它可以看作“bool 变量在内存中的分布”

那么,由于 C++ 中还有很多其他的数据类型,怎么用模型表示它们呢?
char / unsigned char ,请注意,内存地址是递增 8 变化

// 一般也写作
地址 | ... | 0x7cff15e8 | 0x7cff15f0 | 0x7cff15f8 | ... |
数值 | ... | 202        | 214        | 148        | ... |


short / unsigned short ,请注意,内存地址是递增 16 变化

// 一般也写作
地址 | ... | 0x7cff0000 | 0x7cff0010 | 0x7cff0020 | ... |
数值 | ... | 20211      | 202        | 148        | ... |


int / unsigned int / long / unsigned long / float,请注意,内存地址是递增 32 变化

// 一般也写作
地址 | ... | 0x7cff0000 | 0x7cff0020 | 0x7cff0040 | ... |
数值 | ... | 202        | 214        | 148        | ... |


int / unsigned int / long / unsigned long / float,请注意,内存地址是递增 64 变化

// 一般也写作
地址 | ... | 0x7cff0000 | 0x7cff0040 | 0x7cff0080 | ... |
数值 | ... | 202        | 214        | 148        | ... |

1.5 小实验 – 感受内存大小 #

我们现在做一个实验,向操作系统申请 400 MB 的内存。

首先,请计算一下,

假如我们使用 char 类型的数组,400 MB 应该对应多少长度的数组呢?
请思考并计算。
/*




*/

解析:

一个 char 的大小,正好是一个字节。
400 MB 是 400 * 1024 * 1024 B,所以,数组的长度应该为 400 * 1024 * 1024

在这个实验中,由于需要申请的数组大小,超过了操作系统能够自动分配的上限(Windows 1MB,Linux 10 MB),
所以我们将使用 new 。new 不作为大家需要掌握的语法知识,在这里将它理解为普通的数组初始化即可。

请打开任务管理器,监控程序的内存变化。
请运行如下代码

#include <iostream>
using namespace std;
int main()
{
    cout << "请找到当前程序的进程,记录当前的内存使用量。" << endl;
    cout << "请输入任意一个字符,然后点击回车。" << endl;
    char key;
    cin >> key;     //【注意】在这里,cin 用作一个程序暂停


    // 测试 400 MB 的数据
    cout << "正在申请 400 MB 的内存空间。" << endl;
    char* data = new char [400 * 1024 * 1024];


    cout << "请再次找到当前程序的进程,记录当前的内存使用量。" << endl;


    cout << "请再输入任意一个字符,然后点击回车,释放 400 MB 内存。" << endl;
    cin >> key;
    delete[] data;


    cout << "请再输入任意一个字符,然后点击回车,结束程序。" << endl;
    cin >> key;
    return 0;
}

大家也可以尝试一下,使用 short / int / double 等不同长度的数据类型,向操作系统申请总大小为 400 MB 的数组。

2. 指针 #

指针,和之前提到的变量地址,有着非常重要的关系。

2.1 输出地址 #

学习到现在,希望大家的心中,要有一个这样的变量模型。

在这个变量模型中,应该有两个部分
地址
数值
注意:以后我们谈到【变量】,大家脑海中一定要有这样的两个部分【地址】和【数值】。

我们已经知道,如何输出变量的数值,那么,如何输出变量的地址呢?

#include <iostream>
using namespace std;
int main()
{
    int a(50);

    cout << "a 的数值为:" <<  a << endl;
    cout << "a 的地址为:" << &a << endl; // 【注意】 & 符号

    return 0;
}

C++ 中使用 符号(&)表示表示变量的地址。该符号称为 “取地址符”

那么,一个很自然的问题就来了,【数值】可以用【数值类型的变量】来存储;那么,指针呢?
请看代码

2.2 指针类型 #

int a(50);

int * p = &a; // int * ,表示 int 类型的指针

C++ 中,指针需要使用符号(*)来表示。

如上面的代码中 int * p ,一般我们就称“p 是 int 类型的指针”
请注意。指针的英文为:pointer,所以,我们习惯上将指针变量,命名为 p,或者以 p开头。这是一个比较好的习惯。
比如

int a(50);

int * p   = &a;
int * pA  = &a;
int * p_a = &a;

2.3 指针模型 #

注意:
【指针】其实也是【变量】
【指针变量】也有地址
【指针变量】的数值,是一个地址,(就是那个普通变量的地址)。
指针在赋值的时候,是将变量的地址,写入到指针的值中。如上图所示,请特别注意!

2.4 小结 #

和数组类似,指针也有不同的类型,比如

char               * p_c  ;
short              * p_s  ;
int                * p_i  ;
long               * p_l  ;
long long          * p_ll ;
float              * p_f  ;
double             * p_d  ;
long double        * p_ld ;
unsigned char      * p_uc ;
unsigned short     * p_us ;
unsigned int       * p_ui ;
unsigned long      * p_ul ;
unsigned long long * p_ull;