指针的定义、初始化和引用

官方信息技术老师·22 页·深入(追求细节与边界)·0 次浏览·2 天前
指针基础内存地址解引用C语言

指针的定义、初始化和引用

从交换数据的需求出发,彻底搞懂指针的定义、初始化与引用

按 空格/→ 演示下一步

1 / 22 页

全部页面点击任意一页,跳回舞台从这页播放

指针基础内存地址解引用C语言

指针的定义、初始化和引用

从交换数据的需求出发,彻底搞懂指针的定义、初始化与引用

1第 1 页 · 指针的定义、初始化和引用

变量的地址:内存的门牌号

上页说指针是「装地址的盒子」——那地址本身是什么?去酒店前台领房卡,工作人员不会说「花园景观房」,而是直接报一个号码:1208。这个号码就是地址。

内存按字节编址
每个字节有唯一编号,编号本身就是一个 64 位整数
& 取首字节地址
&x 拿到的是变量 x 占据的第一个字节的编号
地址是只读数值
地址分配后基本不变,不能对地址本身再赋值
类型决定步长
相邻同类型变量的地址差等于 sizeof(类型)
酒店房间号对应 →内存地址

整栋楼=内存;房间号=地址;前台报号=&;拿号入住=指针存地址

2第 2 页 · 变量的地址:内存的门牌号

地址的获取与查看

c

用 & 取地址、%p 打印,亲眼看到地址就是一个可运算的数值。

代码高亮加载中…

& 取地址,%p 十六进制展示;强转后地址就是整数;地址相减得字节距离——地址可参与算术。

3第 3 页 · 地址的获取与查看

指针变量的定义语法

上一页我们用 `&` 拿到了变量的地址,但每次只是'看一眼'。要把地址存下来反复用,就得用指针变量——这一页看它的定义语法。

基本语法
由「类型 + * + 名字」三部分组成,如 `int *p;`
星号归属变量
`*` 修饰紧跟的变量,不修饰类型;空格位置不影响语义
写法完全等价
`int *p`、`int* p`、`int * p` 三种写法含义相同
类型决定步长
`int*` 指针 +1 通常跳 4 字节,`double*` 通常跳 8 字节
多变量陷阱
`int* a, b;` 中只有 a 是指针,b 是普通 int
带刻度单位的尺对应 →指针类型

指向 int 的尺每格 4 字节,指向 double 的每格 8 字节——指针 +1 跳多少字节,由类型决定

4第 4 页 · 指针变量的定义语法

指针变量的内存模型

对比看:普通变量存值,指针变量存地址,地址再指向那个值。

图解渲染中…
Pp 自己也有地址 &p,但里面装的是 a 的地址 &aQ&a 是 a 在内存里的位置编号,相当于门牌号
5第 5 页 · 指针变量的内存模型

指针的初始化:指向已有变量

c

完整演示从变量定义、地址获取、指针初始化到解引用的全过程,并验证两者绑定关系。

代码高亮加载中…

L4 定义变量;L5 是指针初始化的正确写法(&a 取出地址);L8~L9 验证 p 与 &a 是同一地址;L10 通过 *p 读出 a 的值;L12 演示通过 *p 改写 a。

6第 6 页 · 指针的初始化:指向已有变量

空指针 vs 野指针

空指针和野指针都叫'指针不能用',但一个可控一个失控,差别巨大。

空指针 NULL
  • 明确赋值为 NULL,不指向任何对象
  • 定义时初始化为 NULL,或暂时不用时置 NULL
  • 解引用会触发段错误,程序崩溃
  • 使用前判断 if(p!=NULL),用完置 NULL
野指针
  • 未初始化,或指向已释放/越界的地址
  • 局部变量忘赋初值,free 后未置 NULL
  • 解引用行为未定义,可能崩溃也可能读到垃圾
  • 定义时立即初始化,free 后立即置 NULL
空指针明确'不指任何东西',行为可控;野指针'不知道指哪',行为未定义,必须消除。
7第 7 页 · 空指针 vs 野指针

解引用运算符 * 的含义

上一页我们把地址塞进了指针变量 p。但只拿到地址还不够——我们要的是地址背后那个真正的值。怎么从地址取出值?这就轮到解引用运算符 * 出场了。

* 的双重身份
声明语句里 * 标记「这是个指针」;表达式里 * 才是解引用运算符
*p 的两步访问
先从 p 读出地址,再去那块内存单元取出实际的值
p 是地址,*p 是值
p 是某个内存单元的编号;*p 是那个编号对应单元里放的东西
解引用必须合法
目标地址无效(如 NULL、野指针)时,*p 会段错误或读到垃圾值
*p 可作左值
把 *p 写在赋值号左边,相当于直接改写那块内存里的数据
快递柜取件码对应 →*p 解引用

取件码告诉你柜门位置(地址 p);扫码开柜取出包裹才是 *p。只看码不扫码,永远拿不到里面的东西

pMEM[p]*p \equiv \text{MEM}[p]
8第 8 页 · 解引用运算符 * 的含义

解引用的读写操作

c

用指针 *p 完成一次读、一次写,直观看清解引用的两种角色。

代码高亮加载中…

L7 中 *p 出现在 = 右侧做读取,L8 中 *p 出现在 = 左侧做写入;输出印证了对同一块内存的两种操作。

9第 9 页 · 解引用的读写操作

指针读写的内存流程图

纵向看五阶段:先建变量与指针,再用赋值把指针指向变量,最后演示 *p 读写对变量值的双向影响。

图解渲染中…
a2变量 a 占用一块内存,地址 0x1000 里存着值 10b2未初始化的 p 自身有地址 0x2000,但内容是随机垃圾c2执行 p=&a 后,p 的内存里写入 0x1000 这串数字f1通过 *p 写入等价于直接对 a 赋值,操作同一块内存
10第 10 页 · 指针读写的内存流程图

指针操作自测

点击作答

执行下面代码后,变量 a 的最终值是? int a=5, b=8; int *p=&a; p=&b; *p=20;

11第 11 页 · 指针操作自测

值传递的局限:为何 swap 失败

前面我们拿到了变量的地址,也学会了用 * 读写真实内存。可写一个 swap(a, b) 交换两个整数,运行后 a、b 却纹丝不动——问题出在哪?

实参→形参:复制副本
调用 swap(a,b) 时,a、b 的值各被复制一份给形参 x、y
副本与原值互相独立
x、y 是新的局部变量,和外界的 a、b 住在不同内存地址
函数内只交换副本
swap 里 t=x; x=y; y=t 改的全是副本,跟外面的 a、b 无关
函数返回后原值不变
swap 执行完,外面的 a 仍是原值,b 也仍是原值
给原件拍照得到复印件对应 →值传递得到副本

在复印件上涂改画叉,原件不会有任何变化;形参就是那张复印件

12第 12 页 · 值传递的局限:为何 swap 失败

错误的 swap 函数

c

用一段最经典的 C 代码演示——swap(a,b) 调用后,a 和 b 居然纹丝不动。问题出在哪?

代码高亮加载中…

x、y 是形参,函数内交换的只是 a、b 的副本;原变量 a、b 在 main 里纹丝不动。这就是值传递——把值的拷贝交出去。

13第 13 页 · 错误的 swap 函数

指针传递:让函数看到实参地址

上次 swap(a,b) 失败是因为函数只拿到值的副本,根本碰不到原变量。这次换个思路:把 a、b 的地址交给函数,让它顺着地址直接修改原变量。

传地址给函数
调用时传入 &a、&b,把实参所在的内存地址交给被调函数
形参是指针类型
void swap(int *x, int *y),x、y 各自指向一个 int
*x 即原变量
*x 等价于 x 所指向的那个变量,可直接读写
实参加 & 取地址
必须用 &a、&b 取地址再传入,不能直接传 a、b
内外共享同一内存
函数内外操作的是同一份数据,修改立即生效
给钥匙上门搬家具对应 →传地址改原变量

上门能直接搬动原家具;只口头报布局,师傅只能在他自己家照样摆

swap(&a,&b)    xa,  yb\text{swap}(\&a, \&b) \;\Rightarrow\; *x \leftrightarrow a,\; *y \leftrightarrow b
14第 14 页 · 指针传递:让函数看到实参地址

正确的 swap 函数实现

c

形参换成指针后,函数内部就能直接改写实参本体。

代码高亮加载中…

形参改为 int *p1, int *p2,调用时传入 &a, &b,函数内通过 *p1、*p2 的读写直接修改 a、b 本体所在的内存单元。

15第 15 页 · 正确的 swap 函数实现

指针传递交换数据的完整流程

从左到右、从上到下读:main 发起调用,swap 通过形参指针直接操作实参所在内存,最终实参值互换。

图解渲染中…
R实参变量,存放真实数据的内存位置F形参指针,保存的是实参的地址,可通过 * 解引用访问实参
16第 16 页 · 指针传递交换数据的完整流程

传值 vs 传指针:调用栈对比

两种参数传递方式在调用栈上走了完全不同的路,swap 的成败正是分水岭。

值传递
  • 压栈:复制实参的值到新栈帧
  • 形参与实参地址不同,是两份数据
  • 改形参不影响实参,swap 因此失败
  • 函数返回,栈帧弹出,形参消失
指针传递
  • 压栈:复制的是地址(一个整数)
  • 形参是指针,但指向同一块实参
  • 用 * 解引用改的就是实参本体
  • 返回后指针销毁,实参数据仍保留
需要让函数修改实参,或传递大结构体省拷贝,用指针传递;只读小数据用值传递更安全简单。
17第 17 页 · 传值 vs 传指针:调用栈对比

指针参数应用题

点击作答

希望 swap(a, b) 能真正交换调用者两个 int 变量的内容(而非副本),下列哪个实现正确?

18第 18 页 · 指针参数应用题

野指针的成因与危害

c

两个会真实触发的野指针崩溃场景:未初始化指针与悬空指针

代码高亮加载中…

L10 是未初始化的「种子」,L11 触发崩溃;L14 接过已死地址,L15 读取悬空——野指针的两种成因都指向未定义行为。

19第 19 页 · 野指针的成因与危害

安全使用指针的 checklist

野指针像没系安全带的司机——撞上去才发现没保护。光知道有雷不够,每次用指针前过一遍清单,才能把隐患堵在门外。

定义时初始化
声明指针的同一行就赋值 NULL 或合法地址,绝不留'未知状态'
使用前判空
解引用前用 if(p!=NULL) 确认非空,否则 *p 可能直接段错误
避免越界访问
确保指针在 [array, array+len) 区间内移动,超出即未定义行为
开车前的安全检查对应 →使用指针的 checklist

绕车一周看胎压/灯光/油量,对应初始化/判空/不越界三项逐条过

20第 20 页 · 安全使用指针的 checklist

本周知识要点回顾

  • 指针本质是存储地址的变量,类型决定解读方式
  • 解引用就是按地址找到内存并读写数据
  • 传指针让函数能修改实参,打破值传递局限
  • 指针使用前必须初始化,警惕空指针和野指针
延伸主题:指针与数组的关系指针运算与步长const修饰指针
21第 21 页 · 本周知识要点回顾

课后思考

先合上课件独立想一想,别急着翻参考答案。

1既然指针存的是地址,定义时为什么还要指定类型?int* 与 char* 存的不都是数字吗?

参考答案指针类型决定了 +1 跳多少字节、解引用读几个字节。int* 步长 4、char* 步长 1,跨类型访问会读到错位的内存。

2正确的 swap 要求传 &x 和 &y,调用时若忘了加 &,编译器和运行时各会怎样?

参考答案类型不匹配会触发编译警告或错误;若强转通过,函数会把 int 当地址解引用,立刻段错误或读到任意内存。

3数组名在大多数场景退化为指针,但有两个例外不会退化为普通指针。你能想到吗?

参考答案例外是 &arr 和 sizeof(arr)。前者取整个数组的地址(步长为整个数组),后者返回整个数组字节数而非指针大小。

22第 22 页 · 课后思考