第 11 章:指针核心知识(上)
- 第 11 章:指针核心知识(上)
- 1. 阅读前问题卡:C 语言指针核心知识(上)
- 1.1 阅读前先看这几个问题
- 1.2 读完后完成这 3 个任务
- 1.2.1 任务 1:梳理内存、地址与指针
- 1.2.2 任务 2:运行取址与解引用示例
- 1.2.3 任务 3:拆解指针类型与大小
- 1.3 自检清单
- 2. 目标
- 3. 内存和地址
- 4. 指针变量和地址
- 4.1 取地址操作符(&)
- 4.2 指针变量
- 4.2.1 指针变量和初始化
- 4.2.2 如何拆解指针类型
- 4.2.3 解引用操作符
- 4.3 指针变量的大小
1. 阅读前问题卡:C 语言指针核心知识(上)
建议先读:先读第 3 节建立“内存单元—地址—指针”的关系,再按 4.1、4.2.1、4.2.3 节理解取址、保存地址和解引用。
暂时跳过:掌握取地址、指针初始化和解引用后,再细看 4.3 节的数据模型表。
1.1 阅读前先看这几个问题
- 文档怎样从“房间号”类比推导出内存单元、地址和指针的关系?
- CPU 读写数据时,虚拟地址、MMU 和物理内存之间是什么关系?哪些特殊领域可能直接使用物理地址?
- 对整型变量
a使用&a会得到什么?为什么只记录其 4 个字节中地址较小的那个地址就够了? - 指针变量保存的是什么?
NULL指针与未初始化的局部指针分别表示什么,又有什么使用风险? - 声明
int *pa时,*与int分别说明什么?它与表达式*pa中的解引用操作符有什么区别? *pa = 0为什么能够修改a?这条语句体现了地址、指针变量和被指向对象怎样的联系?- 指针变量的大小由什么决定?为什么不同指针类型在同一平台下大小相同,而 X86 与 X64 的测试结果不同?
1.2 读完后完成这 3 个任务
1.2.1 任务 1:梳理内存、地址与指针
- 做什么:画出“内存单元—地址—指针—CPU”的关系,并在图中补上虚拟地址经 MMU 映射到物理内存的方向。
- 验证标准:能说明一个内存单元为 1 个字节、一个字节含 8 个比特位,并能区分程序调试时看到的虚拟地址与物理地址。
1.2.2 任务 2:运行取址与解引用示例
- 做什么:运行文档中的取地址和解引用示例,观察
&a、pa与a的值,再执行*pa = 0。 - 验证标准:能确认
pa保存&a的结果,%p输出变量地址,并观察到通过*pa赋值后a的值变为 0。
1.2.3 任务 3:拆解指针类型与大小
- 做什么:为
char ch = 'w';写出匹配的指针声明,再运行sizeof示例比较 X86 与 X64 环境中的各类指针大小。 - 验证标准:能解释声明中
*与前置类型各自的含义,并对照运行结果说明 X86 环境下各类指针为 4 个字节、X64 环境下为 8 个字节。
1.3 自检清单
- 我能用一句话说明地址与指针的关系。
- 我能解释内存为何按字节划分并为每个内存单元编号。
- 我能区分虚拟地址、物理地址及 MMU 的映射作用。
- 我能说明
&、指针变量和解引用操作符*在示例中的职责。 - 我能区分
NULL指针与未初始化的局部野指针。 - 我能拆解
int *pa的类型含义,并说明指针大小与平台数据模型的关系。
2. 目标
- 理解指针的本质就是地址。
- 理解指针类型的意义,能够使用正确的指针类型。
- 理解指针运算的本质就是地址的偏移。
- 掌握传值调用和传址调用的区别。
- 掌握二级指针。
- 为后续指针进阶打好基础。
3. 内存和地址
学习指针必须先理解内存和地址。
在讲解之前,先看一个生活中的案例:
假设有一栋宿舍楼,把你放在楼里,楼上有 100 个房间,但是房间没有编号。你的一个朋友来找你玩,如果想找到你,就得挨个房子去找,这样效率很低。但是,如果我们根据楼层和楼层的房间情况,给每个房间编上号,如:
一楼:101,102,103... 二楼:201,202,203... ...有了房间号,如果你的朋友得到房间号,就可以快速地找到房间,找到你。
生活中,每个房间有了房间号,就能提高效率,能快速地找到房间。
如果把上面的例子对照到计算机中,又是怎么样呢?
我们知道,计算机上 CPU(中央处理器)在处理数据的时候,需要的数据是在内存中读取的,处理后的数据也会放回内存中。
常见的一台电脑上内存是 8 GB/16 GB/32 GB 等,那这些内存空间如何高效地管理呢?
解决办法和上面的例子相似:把内存划分为一个个的内存单元,每个内存单元的大小取 1 个字节,然后给每一个内存单元一个编号,就类似于房间编号。
计算机中常见的单位介绍:
一个比特位可以存储一个二进制的位 1 或者 0。
bit - 比特位 Byte - 字节 KB MB GB TB PB1Byte = 8bit 1KB = 1024Byte 1MB = 1024KB 1GB = 1024MB 1TB = 1024GB 1PB = 1024TB ...🎈 注意,上面我们是为了方便讲解,对真实的内存访问进行了简化。实际上,我们编写的运行在 X86 架构的计算机上的 C 语言程序,是不会直接访问物理内存的。
程序运行时为变量分配的地址,在程序调试过程中,我们看到的地址都是虚拟地址;虚拟地址不是物理地址。虚拟地址是虚拟地址空间中的地址。
虚拟地址空间可以看作操作系统为每个程序提供的一个“假想的、独占的、连续的内存空间”。
程序中的虚拟地址,会由 CPU 的内存管理单元(MMU)映射到物理内存中,其实程序最终还是会访问物理内存。这个由虚拟地址换成物理地址的过程,暂时我们不需要关心,学习到操作系统的知识,慢慢再理解就可以了。
我们在学习编程语言的时候,都是在使用虚拟地址的。
🎈 是否存在 C 程序直接使用物理内存的情况?
有,但属于非常特殊的领域:
- 嵌入式系统:没有 MMU(叫单片机或 MCU,如 ARM Cortex-M 系列、Arduino)。C 程序直接使用物理地址。此时,编译器和程序员要亲自管理整个内存布局。
- 操作系统内核:内核拥有最高权限,它可以访问物理地址。
4. 指针变量和地址
4.1 取地址操作符(&)
理解了内存和地址的关系,我们再回到 C 语言。在 C 语言中创建变量,其实就是向内存申请空间,比如:
X86 环境:
#include<stdio.h>intmain(){inta=10;return0;}比如,上述的代码就是创建了整型变量a,在内存中申请 4 个字节,用于存放整数 10,其中每个字节都有地址。上图中 4 个字节的地址分别是:
0x006FFD70 0x006FFD71 0x006FFD72 0x006FFD73那我们如何能得到a的地址呢?
这里就得学习一个操作符&——取地址操作符。
X86 环境:
#include<stdio.h>intmain(){inta=10;&a;//取出a的地址printf("%p\n",&a);return0;}按照我画图的例子,会打印出来:006FFD70。
&a取出的是a所占 4 个字节中地址较小的字节的地址。
虽然整型变量占用 4 个字节,我们只要知道了第一个字节地址,顺藤摸瓜访问到 4 个字节的数据也是可行的。
4.2 指针变量
4.2.1 指针变量和初始化
我们通过取地址操作符&拿到的地址是一个数值,比如0x006FFD70。这个数值有时候也需要存储起来,方便后期再使用。那我们把这样的地址值存放在哪里呢?答案是:指针变量中。
比如:
#include<stdio.h>intmain(){inta=10;int*pa=&a;//取出a的地址并存储到指针变量pa中return0;}🎈 指针变量也是一种变量,这种变量就是用来存放地址的,存放在指针变量中的值都会理解为地址。
这里我们是拿&a(也就是a的地址)给pa进行了初始化。
有时候我们创建了一个指针变量,但是暂时不需要指向变量,可以使用NULL来初始化。NULL是 C 语言中定义的一个标识符常量,值是 0。0 也是地址,这个地址是无法使用的,读写该地址会报错。
#ifdef__cplusplus#defineNULL0#else#defineNULL((void*)0)#endif一个指针变量被初始化为NULL,意思就是这个指针变量暂时不指向任何有效的内存位置。等后续使用的时候,可以给它赋值新的地址。
使用NULL初始化指针变量:
intmain(){int*pa=NULL;return0;}❓ 那如果一个指针变量没有初始化,会怎样呢?
- 指针变量如果是全局的,则默认会被初始化为 0,也就是
NULL指针。 - 指针变量如果是局部的,则指针变量的值是未知的,这样的指针就是野指针;强制使用野指针会导致非法访问内存,程序崩溃。
4.2.2 如何拆解指针类型
我们看到pa的类型是int*,该如何理解指针的类型呢?
inta=10;int*pa=&a;这里pa左边写的是int*,int*是pa的类型;但是*和int应该分开理解。
*是在说明pa是指针变量,而前面的int是在说明pa指向的是整型int类型的对象。
那如果有一个char类型的变量ch,ch的地址要放在什么类型的指针变量中呢?
charch='w';pc=&ch;//pc的类型怎么写呢?4.2.3 解引用操作符
在现实生活中,我们使用地址要找到一个房间,在房间里可以拿取或者存放物品。
程序中我们将地址保存起来,未来是要使用的,那怎么使用呢?C 语言中其实也是一样的。我们只要拿到了地址(指针),就可以通过地址(指针)找到地址(指针)指向的对象。这里必须学习一个操作符,叫解引用操作符(*)。
#include<stdio.h>intmain(){inta=100;int*pa=&a;*pa=0;return0;}上面代码中第 7 行就使用了解引用操作符。*pa的意思就是通过pa中存放的地址,找到pa指向的空间;*pa其实就是a变量了。所以,*pa = 0这个操作是把a改成了 0。
有同学肯定在想,这里如果目的就是把a改成 0 的话,写成a = 0;不就完了,为啥非要使用指针呢?
其实这里是把a的修改交给了pa来操作。这样对a的修改就多了一种途径,写代码就会更加灵活,后期慢慢就能理解了。
4.3 指针变量的大小
C 语言标准并未规定指针变量的长度。我们现在分别在 VS 2026 的 X86 和 X64 环境下测试:
#include<stdio.h>intmain(){printf("%zd\n",sizeof(char*));printf("%zd\n",sizeof(short*));printf("%zd\n",sizeof(int*));printf("%zd\n",sizeof(double*));return0;}X86 环境输出结果,X64 环境输出结果。
其实指针变量的大小是取决于数据模型的,下面列出了常见的 4 个数据模型:
数据模型指的是在特定操作系统、CPU 架构和 ABI(应用程序二进制接口)下,C/C++ 等语言中基本整数类型(short、int、long、long long)以及指针类型的具体位宽(字节数)的约定。
| 数据模型 | int | long | long long | pointer(指针) | 操作系统 |
|---|---|---|---|---|---|
LP32 | 16 位 | 32 位 | 64 位 | 32 位 | Win16 API |
ILP32 | 32 位 | 32 位 | 64 位 | 32 位 | Win32 API;Unix 及类 Unix 系统(Linux、Mac OS X) |
LLP64 | 32 位 | 32 位 | 64 位 | 64 位 | Win64 API |
LP64 | 32 位 | 64 位 | 64 位 | 64 位 | Unix 与类 Unix 系统(Linux、Mac OS X) |
结论:
- 32 位平台下地址是 32 个 bit 位,指针变量大小是 4 个字节。
- 64 位平台下地址是 64 个 bit 位,指针变量大小是 8 个字节。
- 注意,指针变量的大小和类型是无关的,只要是指针类型的变量,在相同的平台下,大小都是相同的。
5.回答问题卡问题
问题1:文档怎样从“房间号”类比推导出内存单元、地址和指针的关系?
答:
一个内存单元类似于一个独立房间,内存地址就是房间门牌号,指针变量就是是专门保存地址值的变量代表存放房间号的记事本,指针本质上是地址。每个内存单元为 1 个字节,一个字节包含 8 个比特位。
问题2:CPU 读写数据时,虚拟地址、MMU 和物理内存之间是什么关系?哪些特殊领域可能直接使用物理地址?
程序中虚拟地址,会由CPU的内存管理单元(MMU)映射物理内存中,其实程序最终还是会访问物理内存。没有MMU的嵌入式系统,操作系统内核
问题3:对整型变量a使用&a会得到什么?为什么只记录其 4 个字节中地址较小的那个地址就够了?
答:
会得到a的地址。只要知道了第一个字节地址,顺藤摸瓜访问到 4 个字节的数据也是可行的。
问题4:指针变量保存的是什么?NULL指针与未初始化的局部指针分别表示什么,又有什么使用风险?
答:
存放地址值。NULL是 C 语言中定义的一个标识符常量,值是 0。0 也是地址,这个地址是无法使用的,读写该地址会报错。NULL指针是初始化为0的指针,后期可以填上新地址,未初始化的指针是野指针,强制使用野指针会导致非法访问内存,程序崩溃。
问题5:声明int *pa时,*与int分别说明什么?它与表达式*pa中的解引用操作符有什么区别?
答:
在声明int *pa中,*表明pa是指针变量,int表明它指向的是int类型的对象。这里的*属于声明的一部分,只说明变量类型,不执行计算。在表达式*pa中,*是解引用操作符,表示使用pa保存的地址找到所指对象,从而读取或修改该对象。
问题6:*pa = 0为什么能够修改a?这条语句体现了地址、指针变量和被指向对象怎样的联系?
答:
*pa的意思就是通过pa中存放的地址,找到pa指向的空间;*pa其实就是a变量了。所以,*pa = 0这个操作是把a改成了 0。
问题7:指针变量的大小由什么决定?为什么不同指针类型在同一平台下大小相同,而 X86 与 X64 的测试结果不同?
答:
指针变量的大小是取决于数据模型的。指针变量的大小和类型是无关的,只要是指针类型的变量,在相同的平台下,大小都是相同的。文档给出的原因是平台地址位宽不同:X86 环境使用 32 位地址,指针占 4 字节;X64 环境使用 64 位地址,指针占 8 字节。同一平台的各种指针都用于保存该平台的地址,因此大小相同。