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)的数?
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 位地址,有 2^32 个“可以存放有电/没电状态的存储单元”,
64 位地址,有 2^64 个“可以存放有电/没电状态的存储单元”,
请完成下面的单位换算:
2^32 bit 等于多少 GB?
2^64 bit 等于多少 GB?
答案:
2^32 bit = 4 GB // 32 位,4 字节 2^64 bit = 2^32 GB // 64 位,8 字节
对我们生活的意义
32 位的计算机,它最多只有 2^32 个“存储单元”,也就是 2^32 个位(Bit),也就是 4 GB 。
这意味,32 位的电脑,他的内存不能超过 4 GB。
而 64 位的计算机,它的内存最多可以支持 2^32 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;