教材《C程序设计》第五版(谭浩强)第 8 章,书页 217–253,共 37 页|建议 2h
课前一句话
- 前 7 课,你都是"叫变量的名字,让编译器自己去找";这一课你要学会拿着地址,自己走过去。
- 一句话:指针就是地址;指针变量就是一张写着地址的便签。
- 学完你能写出:用指针让函数真正交换两个变量、用指针走遍一个数组、用指针一次给函数带回多个结果。
- 这是全书的分水岭:指针通了,第 9 章结构体、第 10 章文件都是延长线;没通,后面只能靠背。
一、先补地基:本章要解决的问题
1. 上一课留下的"不服气"
第 7 课你写过这个函数:
void swap(int x, int y)
{
int t;
t = x; x = y; y = t;
}在 main 里 swap(a, b);,回来一看:a、b 一动没动。
当时的解释只有四个字:值传递——你把 a 的值复印一份交给 x,函数里改的是复印件。
可你心里想的是:我就是想在函数里改外面的变量,怎么办?
答案只有一条:不把"值"交出去,把"东西放在哪"交出去。 函数拿到地址,就能直接走到那个地方动手。
"东西放在哪"就是地址。用 C 的话说,叫指针。整章第 8 章解决的就是这一个问题,剩下的全是它的变形。
2. 一个贯穿全课的比喻:宿舍楼
从这一课到第 09 课,只用一个比喻,中途不换:
内存 = 一整栋超长宿舍楼;一个字节 = 一个房间;房间号 = 地址;房间里的住户 = 数据;变量名 = 楼下大厅挂的名牌;指针变量 = 一张写着房间号的便签。
| 计算机术语 | 宿舍楼里的对应物 | 说明 |
|---|---|---|
| 内存 / 字节 | 整栋楼 / 一间房 | 每间房一个编号 |
| 地址 | 房间号,如 2000 | 从 0 开始编号 |
| 变量的值 | 房间里的住户 | 3、100、'A' |
| 变量名 | 大厅名牌 | 编译器维护"名字→房号"对照表 |
| 指针变量 | 写着房号的便签 | 便签自己也有房,上面写的是别人的号 |
&a | 抄下 a 的房号 | 取地址 |
*p | 照着号去敲门找人 | 解引用 |
两条"楼规",后面天天用:
- 不同专业住不同规格的房:
int占 4 间,float占 4 间,char只占 1 间(Visual C++ 的规定,教材 p.219)。 - 便签上必须写清"这号是几人间",否则你走到门口不知道该敲 1 下还是 4 下——这就是"基类型"。
二、知识点讲解
8.1 指针是什么
【一句话定义】
指针就是地址;更准确地说,C 的"指针"是带类型信息的地址——既告诉你房间号,也告诉你这间房住的是什么类型的人。
【拆名字】
- pointer = point(点、指向)+ -er(东西)→ 用来指向的东西 → 指针。
- address = ad-(朝向)+ dress(引导、摆正)→ 把你引导到某个位置 → 地址。
- 二者在 C 里几乎是同义词:指针的值就是一个地址。
【变量是怎么住进楼里的】
教材 p.219:定义变量后,编译时系统就会给它分配内存单元,按类型分配长度。Visual C++ 里:int 4 字节、float 4 字节、char 1 字节。
假设定义了三个 int 变量 i、j、k,编译器可能这样分房(教材图 8.1):
| 变量 | 占用字节 | 起始地址 |
|---|---|---|
i | 2000 – 2003 | 2000 |
j | 2004 – 2007 | 2004 |
k | 2008 – 2011 | 2008 |
注意三件事:每个字节一个编号;"i 的地址"指起始地址 2000;具体分到哪个号每次编译都可能不同,书上只是举例。
【直接访问:叫名字,编译器替你跑腿】
写 printf("%d\n", i); 时,实际发生的是:
- 编译器按名字查到
i的地址 2000; - 从 2000 开始,按
int的规矩读 4 个字节; - 按整型解释得到 3,按十进制输出。
教材管这叫 直接访问。同理 scanf("%d", &i); 是把输入送到 2000 开始的 4 个字节;k = i + j; 是从 2000–2003 取 3、2004–2007 取 6,和送到 2008–2011。
考点教材 p.221 画线的一句:对变量的访问都是通过地址进行的。 你写变量名,只是让编译器替你查地址;C 的特色是——这个地址你也能自己拿。
【间接访问:房卡锁在宿管处】
你要进 A 房。办法一:A 的房卡一直在你兜里,掏出来就开门(直接访问)。办法二:为了安全,你把 A 卡锁进宿管处的保险柜 B;要进 A,得先开 B 柜、取出 A 卡、再去开 A 门(间接访问)。多绕的这一道,就是指针。
i_pointer = &i; // 把 i 的地址(2000)抄到便签 i_pointer 上之后有两条路:
| 写法 | 含义 | 比喻 |
|---|---|---|
i = 3; | 直接把 3 送到 i 的房间 | 兜里有卡,直接开门 |
*i_pointer = 3; | 把 3 送到 i_pointer 所指的房间(就是 i) | 先开保险柜取卡,再开 A 门 |
* 读作"解引用"(dereference):de-(去掉)+ reference(引用) → 把"引用"这层壳去掉,露出它指向的真东西。
【指针 vs 指针变量】
| 说法 | 对不对 | 为什么 |
|---|---|---|
| "变量 i 的指针是 2000" | ✅ | 指针 = 地址,就是一个数 |
| "i 的指针变量是 2000" | ❌ | 指针变量是便签,不是数 |
"i_pointer 是一个指针变量" | ✅ | 它专门存放地址 |
一句话:指针是"号",指针变量是"写着号的便签"。
【为什么必须"带类型"】
假如只告诉你"去 1010 号房取东西",你到了门口仍不知道:该敲 1 下(取 1 字节)、2 下(短整型)还是 4 下(整型)?取出来的 4 个字节,按整数解释还是按实数解释?
所以 C 的地址信息 = 位置信息(纯地址)+ 类型信息,教材称为"带类型的地址"。&a 一般叫"变量 a 的地址",准确说法是"整型变量 a 的地址"。
考点教材 p.219 思考题:若有 int a 和 float b,先后分配到 2000 开始的存储单元,&a 和 &b 的信息完全一样吗? 答:不一样。 纯地址都是 2000,但类型信息不同。这就是 8.2 节定义指针变量必须写基类型的原因。
【本节对比表】
| 直接访问 | 间接访问 | |
|---|---|---|
| 手里拿的 | 变量名 | 存放地址的指针变量 |
| 编译器做的 | 名字 → 地址 → 取值 | 先取指针的值(地址)→ 再去那里取值 |
| 代码样子 | i、&i | *p、p |
| 教材比喻 | A 钥匙带身上 | A 钥匙锁在 B 抽屉 |
8.2 指针变量
一句话:指针变量是专门用来装地址的变量,它指向另一个对象(变量、数组、函数……)。
8.2.1 使用指针变量的例子(例 8.1)
【题目】通过指针变量访问整型变量。【思路】定义 2 个整型变量,再定义 2 个指针变量分别指向它们,通过 * 访问。
完整代码与逐行解释见 第三节 代码 1。这里只记结果与三条要点:
- 定义
pointer_1、pointer_2时,它们并未指向任何变量,只是"规定它们可以指向整型变量";指向谁要靠后面的赋值语句。 *在两处出现,含义完全不同(见下表)。- 输出结果与直接输出 a、b 一样,说明
*pointer_1就是 a。
易错同一个 *,两种身份:
| 出现位置 | 例子 | * 的身份 | 读作 |
|---|---|---|---|
| 定义语句里 | int *pointer_1; | 类型声明符 | "定义指针变量 pointer_1" |
| 执行语句/表达式里 | printf("%d", *pointer_1); | 解引用运算符 | "pointer_1 所指向的变量" |
教材 p.220 的注意:*pointer_1; 是"企图定义 pointer_1 为指针变量,出错";必须写 int *pointer_1;。指针变量名是 pointer_1,不是 *pointer_1。
8.2.2 怎样定义指针变量
【一句话】一般形式:类型名 *指针变量名;
【拆名字】这个"类型名"有专名叫基类型(base type)——"底座"的类型,规定这张便签只能写这种类型房间的号。
【怎么用】
int *pointer_1, *pointer_2; // 两个 int 指针
float *pointer_3; // float 指针
char *pointer_4; // char 指针
int *p = &a; // 定义时直接初始化【为什么必须写基类型】教材 p.224 给了两个理由:
理由一:不同类型的数据占的字节数和存放方式不同。 指向整型变量和指向实型变量,物理含义不同。
理由二(更实用):它决定"指针加 1"到底加多少。 教材 p.225 原话:指向整型变量时"移动 1 个位置"意味着移动 4 个字节;指向字符变量时增加的是 1。
| 基类型 | 写法 | 走一步跨几字节 | 能指向谁 |
|---|---|---|---|
int | int *p; | 4 | int 变量、int 数组元素 |
float | float *p; | 4 | float 变量、float 数组元素 |
char | char *p; | 1 | char 变量、char 数组元素 |
double | double *p; | 8 | double 变量、double 数组元素 |
易错一个指针变量只能指向同一种类型的变量,不能忽而指向整型、忽而指向实型。
易错类型不匹配(教材 p.224):
int a, *p;
p = &a; // ✅ 匹配
float *q;
q = &a; // ⚠️ 警告:"把一个 int * 型数据转换为 float * 数据"Visual C++ 6.0 里这是警告不是错误:编译器把 &a 的基类型自动改成 float 再赋给 q,但 q 不能用这个地址正确指向整型变量。
易错"指针变量中只能存放地址,不要把一个整数赋给指针变量。"
*pointer_1 = 100; // ❌ pointer_1 是指针变量,100 是整数
p = 100; // ❌ 系统无法辨别它是地址
p = &a; // ✅ 地址只能用 & 得到【类型怎么写、怎么读】:int * 读作"指向 int 的指针"(简称 int 指针),char * 读作 char 指针。int *、char *、float * 是三种不同类型,不能混。
易错描述时不能只说"a 是一个指针变量",要完整说:"a 是指向整型数据的指针变量"。
8.2.3 怎样引用指针变量
考点引用指针变量只有 3 种情况(教材 p.225):
| # | 干什么 | 写法 | 说明 |
|---|---|---|---|
| 1 | 给指针变量赋值 | p = &a; | p 的值变成 a 的地址,p 指向 a |
| 2 | 引用指针指向的变量 | printf("%d", *p);<br>*p = 1; | *p 就是 a;*p=1 等价于 a=1 |
| 3 | 引用指针变量本身的值 | printf("%o", p); | 以八进制输出 p 里存的地址(即 &a) |
考点两个必须背熟的运算符:
| 运算符 | 名字 | 作用 | 例子 |
|---|---|---|---|
& | 取地址运算符 | 取出变量的地址 | &a 是 a 的地址 |
* | 指针运算符("间接访问"运算符) | 取指针所指向的对象 | *p 是 p 指向的变量 |
记法:& 和 * 是一对反操作。& 把"人"变成"号",* 把"号"变成"人"。
变量 a ──&──▶ 地址(号) ──*──▶ 变量 a【教材例 8.2】输入 a、b,按先大后小输出。思路:不交换 a、b 的值,而是交换两个指针变量的值。
int *p1, *p2, *p;
int a, b;
scanf("%d,%d", &a, &b);
p1 = &a; p2 = &b;
if (a < b)
{ p = p1; p1 = p2; p2 = p; } // 三张便签倒手
printf("a=%d,b=%d\n", a, b); // 输出 5,9 —— a、b 一个都没变
printf("max=%d,min=%d\n", *p1, *p2); // 输出 9,5考点这道题的题眼:a、b 的值没变,变的只是 p1、p2 的指向。 教材 p.223 原话:"a 和 b 的值并未交换,它们仍保持原值,但 p1 和 p2 的值改变了。"
教材还提到,if 里三行可直接写成 { p1 = &b; p2 = &a; },不必用中间变量 p——这等于告诉你:这一问换的是"地址",不是"值"。
8.2.4 指针变量作为函数参数
【一句话】函数的参数不仅可以传整型、浮点型、字符型,还可以传指针类型;作用是把一个变量的地址传送到另一个函数中。
【拆名字】这里的机制叫 值传递(pass by value):实参的值复制一份给形参。传指针时,复制的就是"房间号"——拿到号的复印件,照样能走到那间房。
##### 例 8.3:用指针实现真正的 swap
完整代码见 第三节 代码 2。关键在最后三行:
void swap(int *p1, int *p2)
{
int temp; // ⚠️ 是 int,不是 int *
temp = *p1; // 把 a 的值抄到 temp
*p1 = *p2; // 把 b 的值搬进 a
*p2 = temp; // 把 5 搬进 b
}【关键理解】例 8.2 交换的是 p1、p2 自己(便签),所以 a、b 不动;例 8.3 交换的是 *p1、*p2(便签指向的房间里的东西),所以 a、b 真的换了。
教材 p.228 一句话总结:"本例采取的方法是交换 a 和 b 的值,而 p1 和 p2 的值不变。这恰和例 8.2 相反。"
易错教材 p.224 专门列出的错误写法:
void swap(int *p1, int *p2)
{
int *temp;
*temp = *p1; // ❌ 此语句有问题
*p1 = *p2;
*p2 = *temp;
}为什么错:*p1 是 int(a),而 *temp 是 temp 指向的变量。可 temp 从没被赋值,它的值是垃圾,所以 *temp 指向一个未知单元;往里写数据可能覆盖有用数据,破坏系统。正确做法是把 *p1 赋给同类型变量,所以 temp 必须定义成 int。
一句话记:要中转的是"人",用普通变量;要中转的是"号",才用指针变量。
##### 例 8.4:想通过改形参指针去改实参指针 —— 失败
完整代码见 第三节 代码 3,输入 5,9,输出却是 max=5,min=9。
失败原因(教材 p.227–228 的推理链):
- 调用时
pointer_1的值(&a)复制给 p1,pointer_2的值复制给 p2; - swap 里换掉的是 p1、p2 这两张复印件;
- 函数结束,p1、p2 被释放,复印件上的涂改不会传回原件;
- 所以
pointer_1仍指向 a,*pointer_1还是 5。
考点全章最重要的一句话(教材 p.228 原文):
不可能通过执行调用函数来改变实参指针变量的值,但是可以改变实参指针变量所指变量的值。
| 你想改的 | 能不能 | 办法 |
|---|---|---|
| 实参指针变量自己(指向谁) | ❌ 不能 | 值传递,只能改复印件 |
| 实参指针变量所指的变量 | ✅ 能 | 用 *p 去改 |
考点教材 p.228 的另一句:函数的调用可以(而且只可以)得到一个返回值,而使用指针变量作参数,可以得到多个变化了的值。 这是 C 里"一个函数带回多个结果"的唯一标准做法。
##### 例 8.5:三个数由大到小输出
思路:exchange 改变 3 个变量的值,内部再调 swap 交换两个变量的值。完整代码见 第三节 代码 4。
算法跟踪(a=20, b=-54, c=67):
| 步骤 | 判断 | 交换? | 结果 |
|---|---|---|---|
| 1 | *q1(20) < *q2(-54) | 否 | a=20, b=-54, c=67 |
| 2 | *q1(20) < *q3(67) | 是 | a=67, c=20 |
| 3 | *q2(-54) < *q3(20) | 是 | b=20, c=-54 |
【教材 p.229 思考题】main 里的 3 个指针变量的值(指向)改变了没有?答:没有。 变的始终是 a、b、c 的值。
##### 【本节对比表:三种"交换"】
| 写法 | 交换的是 | 结果 | 出处 |
|---|---|---|---|
swap(int x, int y) 里换 x、y | 形参的值(复印件) | a、b 不变 | 第 7 课 |
swap 里换 p1、p2 | 形参指针本身 | 实参指针不变 | 例 8.4 |
swap 里换 *p1、*p2 | 指针所指的变量 | a、b 真的换了 | 例 8.3、8.5 |
8.3 通过指针引用数组
8.3.1 数组元素的指针
【一句话】数组元素的指针,就是数组元素的地址。
【拆名字】array = 排列、阵列 → 数组(一间挨一间的连号房);element = 元素。
【生活比喻】数组就是走廊上一排连号的房间:a[0] 是 2000 号,a[1] 是 2004 号,a[2] 是 2008 号……
【怎么用】(教材 p.230)
int a[10] = {1,3,5,7,9,11,13,15,17,19};
int *p;
p = &a[0]; // 把 a[0] 的地址赋给 p
p = a; // 完全等价!考点教材 p.230 画线句:"数组名(不包括形参数组名)代表数组中首元素(即序号为 0 的元素)的地址。"
易错"p = a; 的作用是把 a 数组首元素的地址赋给 p",不是"把数组 a 各元素的值赋给 p"。数组名不代表整个数组,只代表首元素地址。
易错初始化 vs 赋值:
| 场合 | 正确写法 | 错误写法 |
|---|---|---|
| 定义时初始化 | int *p = &a[0]; | int *p; *p = &a[0]; |
| 定义后赋值 | p = &a[0]; | *p = &a[0];(类型不匹配) |
注意 int *p = a[0]; 是错的——那是把"人"塞进便签。
8.3.2 在引用数组元素时指针的运算
【先问】对地址做乘除有意义吗?教材 p.231:显然没有意义,实际上也无此必要。 但加减在一定条件下允许——条件是:指针指向数组元素的时候。
【拆名字】这叫 指针算术运算(pointer arithmetic),单位不是字节,是"元素"。
规则一:p + 1 不是加 1 字节,是加 1 个元素。 教材原文:执行 p+1 并不是将 p 的值简单加 1,而是加上一个数组元素所占用的字节数。
p + 1 的实际地址 = p + 1 × d (d 是一个元素占的字节数)Visual C++ 里:int、float、long 的 d = 4,char 的 d = 1。若 p 的值是 2000,p + 1 的值不是 2001,而是 2004。
考点系统凭什么把 1 变成 4?靠定义时的基类型:float *p; 时执行 ++p,系统自动加 4 字节。这就是 8.2.2 强调基类型的真正用途。
规则二:p + i 和 a + i 都是 a[i] 的地址。 例如 p + 9 和 a + 9 都等于 &a[9]。
规则三:*(p + i)、*(a + i)、a[i] 三者等价。 教材 p.232 还有一句极重要的话:"在编译时,对数组元素 a[i] 就是按 *(a+i) 处理的。" 也就是说,你写下标,编译器偷偷翻成指针——下标法和指针法本来就是同一种东西。
教材 p.233 举例:若 a 的首元素地址为 1000、数组为 float 型,则 a[3] 的地址是 1000 + 3 × 4 = 1012,再从 1012 取出值。教材还说 [] 实际上是变址运算符。
考点等价公式全家桶:
| 写法 | 等价于 | 含义 |
|---|---|---|
a[i] | *(a + i) | 第 i 个元素的值 |
&a[i] | a + i | 第 i 个元素的地址 |
p[i] | *(p + i) | 当 p 指向 a 时,同 a[i] |
规则四:两个指针相减 = 相隔几个元素。 教材 p.233:p2 指向 a[5](2020),p1 指向 a[3](2012),则 p2 - p1 = (2020-2012)/4 = 2。
易错两个地址不能相加,p1 + p2 没有实际意义。
【本节运算总表】
| 运算 | 写法 | 含义 | 结果类型 | 前提 |
|---|---|---|---|---|
| 加整数 | p + i | 向后 i 个元素 | 地址 | p 指向数组元素 |
| 减整数 | p - i | 向前 i 个元素 | 地址 | 同上 |
| 自加/自减 | p++、--p | 指向下一个/上一个元素 | 地址 | 同上 |
| 指针相减 | p2 - p1 | 相隔几个元素 | 整数 | 同一数组 |
| 指针相加 | p1 + p2 | ❌ 无意义 | — | — |
| 乘、除 | — | ❌ 无意义 | — | — |
8.3.3 通过指针引用数组元素
【一句话】引用数组元素有两条路:下标法(a[i])和指针法(*(a+i) 或 *(p+i))。
【拆名字】subscript = sub(在下)+ script(写)→ 写在下面的小字 → 下标,从 0 开始。
##### 例 8.6:输出数组全部元素(3 种写法)
完整代码见 第三节 代码 5。三种写法的核心差别只有一行:
| 方法 | 输出那一行 |
|---|---|
| (1) 下标法 | printf("%d ", a[i]); |
| (2) 用数组名算地址 | printf("%d ", *(a + i)); |
| (3) 用指针变量 | for (p = a; p < a + 10; p++) printf("%d ", *p); |
教材 p.232 补充:scanf 那行也可写成 scanf("%d", a + i);,因为 a + i 就是 &a[i],而 scanf 要的正是地址。
【三种方法比较】(教材 p.233–234)
| 方法 | 效率 | 直观性 | 适合谁 |
|---|---|---|---|
(1) a[i] | 一般 | ★★★ 一眼看出第几个 | 初学者、考试写题 |
(2) *(a+i) | 与 (1) 相同(编译时 a[i] 也转成 *(a+i)) | ★★ | 理解等价关系 |
(3) p++ | 最快(不必每次算地址) | ★ 要跟着 p 的当前值推 | 有经验者 |
【建议】考试写题用 (1),最不容易错;读别人代码要能看懂 (3)。
##### 易错数组名是常量,a++ 不行
for (p = a; a < (p + 10); a++) // ❌ 不行
printf("%d", *a);教材 p.234:数组名 a 代表数组首元素的地址,它是一个指针型常量,它的值在程序运行期间是固定不变的。 既然 a 是常量,a++ 无法实现。
考点一句话:p 可以自增,a 不行。a 是钉在墙上的门牌,p 是可以带走的便签。
##### 例 8.7:指针越界的经典陷阱
教材 p.234 的第一个版本:
for (i = 0; i < 10; i++)
scanf("%d", p++); // 读完后 p 已经跑到 a+10
for (i = 0; i < 10; i++, p++)
printf("%d ", *p); // ❌ 从 a+10 开始读,全是垃圾为什么错:第一个循环每读一个数,p 就往后挪一间;循环结束时 p 已经在 a + 10,也就是这排房的尽头之外。第二个循环读的是数组后面 10 间"别人的房",值不可预料。
修法:第二个循环前加一句 p = a;。完整正确代码见 第三节 代码 6。
易错教材 p.235 的两条结论:
- C 编译程序不检查数组越界。 写
a[10](不存在这个元素)编译器不认为非法,按*(a+10)处理,照算地址、照取内容。编译不出错,结果却不预期,这是"程序逻辑上的错误,比较隐蔽"。 - 用指针访问数组时,必须自己保证它指向数组中的有效元素。
考点"指针越界"和"数组越界"是同一件事的两种写法,C 都不检查。
##### 易错指针变量也能带下标
教材 p.235:当指针变量指向数组元素时,p[i] 按 *(p+i) 处理。但必须弄清 p 的当前值:如果当前 p 指向 a[3],则 p[2] 不是 a[2],而是 a[3+2],即 a[5]。建议少用。
##### 考点* 与 ++ 的组合
设 p 开始时指向 a[0](教材 p.235 逐条分析):
| 写法 | 等价于 | 先干什么后干什么 | 结果(p 原指向 a[0]) |
|---|---|---|---|
*p++ | *(p++) | 先取 *p,再 p 加 1 | 得到 a[0],p 变 a[1] |
*(++p) | — | 先 p 加 1,再取 *p | 得到 a[1] |
++(*p) | ++a[i] | 元素值加 1,p 不动 | a[0] 变 4(原为 3) |
*(p--) | a[i--] | 先取值,再 p 减 1 | a[0] |
*(--p) | a[--i] | 先 p 减 1,再取值 | 越界读取(p 移到 a[0] 之前,结果未定义) |
为什么 *p++ 等价于 *(p++)?教材原话:"由于 ++ 和 * 同优先级,结合方向为自右而左。"
【速记口诀】*p++ = 先拿后走;*++p = 先走后拿;++*p = 只改人不挪窝。
教材还指出,例 8.7 里的 for (...; i++, p++) printf("%d ", *p); 可以改写成 for (...; i++) printf("%d ", *p++);,作用完全一样。
易错教材 p.236 提醒:"如果不小心,很容易弄错……对初学者不建议多用,但应当知道。"
8.3.4 用数组名作函数参数
【一句话】用数组名作实参时,传递的是数组首元素的地址;形参收到这个地址就指向了实参数组的第一个元素,因此函数里改数组元素,外面的数组跟着变。
【拆名字】关键还是 值传递——只不过这次传的"值"恰好是地址。
先回顾第 7 课那个"神奇现象":用数组名作参数时,形参数组元素变了,实参数组跟着变。当时只能"记住结论",现在能解释了。
| 实参形式 | 传递的东西 | 能否改外面的值 |
|---|---|---|
swap(a[0], a[2]);(数组元素) | 元素的值(复印件) | ❌ 不能 |
fun(a, 10);(数组名) | 首元素地址 | ✅ 能 |
教材 p.236 原话:用数组元素 a[0]、a[2] 作实参,"与用变量作实参时一样,是值传递方式"。
形参为什么写成数组、实际却是指针? 教材 p.236–237:实参数组名代表首元素地址,而形参是用来接收这个地址的,因此形参应该是一个指针变量;"实际上,C 编译都是将形参数组名作为指针变量来处理的。"
所以这两种写法完全等价:
void fun(int arr[], int n) // 写法一:形参数组
void fun(int *arr, int n) // 写法二:形参指针【证明】教材 p.237 的妙招:在 fun 里量一下 sizeof(arr),结果是 4(Visual C++)。如果 arr 真是数组,sizeof 应该是 4×n;结果是 4,说明它就是个指针变量。
注:64 位编译器下指针占 8 字节,结果是 8。结论不变——它是指针,不是数组。
核心等价关系:*(arr + i) ≡ arr[i]。教材原话:"*(arr+i) 和 arr[i] 是无条件等价的。" 所以调用期间 arr[0]、*arr、array[0] 都代表数组 array 序号为 0 的元素。
##### 考点表 8.1:变量名 vs 数组名作函数参数
| 实参类型 | 变量名 | 数组名 |
|---|---|---|
| 要求形参的类型 | 变量名 | 数组名或指针变量 |
| 传递的信息 | 变量的值 | 实参数组首元素的地址 |
| 通过函数调用能否改变实参的值 | 不能改变实参变量的值 | 能改变实参数组的值 |
##### 易错实参数组名是常量,形参数组名不是
教材 p.237:实参数组名代表一个固定的地址,或者说是指针常量;形参数组名并不是一个固定的地址,而是按指针变量处理。
所以形参可以再赋值:
void fun(int arr[], int n)
{
printf("%d\n", *arr); // 输出 array[0]
arr = arr + 3; // ✅ 形参数组名可以被赋值
printf("%d\n", *arr); // 输出 array[3]
}记忆:实参的 a 是钉死的门牌,形参的 arr 是拿到手的便签。
##### 例 8.8:将数组逆序存放
【思路】a[0] 与 a[n-1] 对换,a[1] 与 a[n-2] 对换……直到 a[int((n-1)/2)] 与 a[n-int((n-1)/2)-1] 对换。设 i 从 0 开始、j 从 n-1 开始,交换后 i 加 1、j 减 1,直到 i = (n-1)/2。
完整代码与配对过程见 第三节 代码 7。运行结果:
The original array:
3 7 9 11 0 6 7 5 4 2
The array has been inverted:
2 4 5 7 6 0 11 9 7 3【为什么是 (n-1)/2】n=10 时结果是 4(整数除法),i 走 0~4 共 5 次,正好把前 5 个和后 5 个配对。如果 i 走到 9,就会换两次,等于没换。
教材 p.239 还有一个形参用指针变量的版本,核心四行:
void inv(int *x, int n)
{
int temp, *p, *i, *j, m = (n - 1) / 2;
i = x; j = x + n - 1; p = x + m;
for (; i <= p; i++, j--)
{ temp = *i; *i = *j; *j = temp; }
}运行结果完全相同。
##### 考点实参与形参的 4 种对应关系(教材 p.240)
| # | 实参 | 形参 | 写法 | 出处 |
|---|---|---|---|---|
| 1 | 数组名 | 数组名 | int f(int x[], int n) | 例 8.8 第 1 版 |
| 2 | 数组名 | 指针变量 | void f(int *x, int n) | 例 8.8 第 2 版 |
| 3 | 指针变量 | 指针变量 | void f(int *x, int n) | 例 8.9 |
| 4 | 指针变量 | 数组名 | void f(int x[], int n) | 例 8.9 改写 |
教材结论:这 4 种方法,实质上都是地址的传递。 其中 (3)(4) 只是形式上不同,实际上形参都是指针变量。
##### 易错用指针变量作实参,必须先有确定值
int main()
{
void inv(int *x, int n);
int i, *arr; // ❌ arr 没指向任何数组
// ...(省略读入等若干行)
inv(arr, 10); // 编译能过,运行必错
}教材 p.241 原话:"原因是指针变量 arr 没有确定值,谈不上指向哪个变量。"正确做法:要么 int arr[10];,要么 int arr[10], *p = arr;。
考点一句话:野便签不能拿去用。
##### 例 8.9 与例 8.10(要点)
例 8.9(用指针变量作实参)与例 8.8 的区别只有三处:int arr[10], *p = arr;、读入时 scanf("%d", p);、读完和输出前都要 p = arr; 把便签拨回起点。
易错scanf("%d", p) vs scanf("%d", &p):
| 写法 | 含义 | 对错 |
|---|---|---|
scanf("%d", p); | 把数存到 p 指向的单元 | ✅ 对 |
scanf("%d", &p); | 把数存到 p 自己那 8 个字节里 | ❌ 错,会覆盖指针本身 |
例 8.10(用指针方法对 10 个数排序):main 里 p = a; 读入、p = a; 拨回、sort(p, 10);。sort 用选择排序,形参写 int x[]:
void sort(int x[], int n)
{
int i, j, k, t;
for (i = 0; i < n - 1; i++)
{
k = i;
for (j = i + 1; j < n; j++)
if (x[j] > x[k]) k = j; // 找更大的 → 降序
if (k != i)
{ t = x[i]; x[i] = x[k]; x[k] = t; }
}
}【教材补充】sort(int x[], int n) 完全可以改成 sort(int *x, int n),其他一行都不用改,运行结果不变——因为在函数里 x[i] 就是 *(x+i)。
8.3.5 通过指针引用多维数组选学
教材这一小节标了
*,是选学,期末大概率不直接考。但你必须知道核心概念,第 9 章结构体数组会用。下面只讲"够用"的版本。
【一句话】二维数组是"数组的数组"——每一行本身就是一个一维数组,所以 a 是"指向行的指针",a[i] 是"指向列的指针"(multi- 多 + dimension 维度 = 多维;row 行,column 列)。
【生活比喻】宿舍楼升级成"分层的楼":3 层,每层 4 间。
a(数组名)= 整栋楼的入口地址,走一步跨过一整层(4 间 × 4 字节 = 16 字节)。a[i]= 第 i 层 0 号房的门牌,走一步只跨一间(4 字节)。
这就是教材 p.245–246 的"排长 vs 班长"(排长按"班"走,班长按"战士"走)。记成:楼长跨一层,房号跨一间。
【关键数字】设 int a[3][4],首行起始地址 2000:
| 表达式 | 值 | 意思 |
|---|---|---|
a、a + 1 | 2000、2016 | 第 0/1 行起始地址(指向行,一步 16 字节) |
a[0]、*(a+0)、*a | 2000 | 0 行 0 列元素地址(指向列,一步 4 字节) |
a[1]、*(a+1) | 2016 | 1 行 0 列元素地址 |
a[1] + 2、*(a+1)+2 | 2024 | 1 行 2 列元素地址 |
a[1][2]、*(*(a+1)+2) | 13 | 1 行 2 列元素的值 |
考点教材表 8.2 就是这张表。最容易错的三个:
a + 1和*(a + 1)的"纯地址"可能相同(都是 2016),但基类型不同——一个指向行,一个指向列。*(a+1)+2不能写成*(a+1+2),后者成了*(a+3),越界。a[0]不是元素的值,它是地址。
【地址公式】a[i][j] 的地址 = &a[0][0] + (i × m + j)(m 是列数)。教材 p.249 例:3×4 数组里 a[2][3] 的相对位移是 2×4+3 = 11,地址差 11×4 = 44 字节——因为它前面有 i 整行(每行 m 个),本行内还有 j 个。
【行指针】int (*p)[4]; —— (*p) 锁定"p 是指针",[4] 说明它指向含 4 个 int 的一维数组。
易错括号不能少:int (*p)[4]; 是行指针;int *p[4]; 是"p 是数组,含 4 个 int 指针"(指针数组,第 9 章讲)。
教材例 8.13 核心三行:
int (*p)[4], i, j;
p = a; /* 不能写 p = a[0]; */
printf("a[%d,%d]=%d\n", i, j, *(*(p + i) + j));三、现场写代码
下面 7 段程序全部来自教材 8.1–8.3,都是完整可编译的程序,已在本机(Visual Studio 2022)逐个编译运行验证。
代码 1:例 8.1 —— 指针变量访问整型变量
#include <stdio.h>
int main()
{
int a = 100, b = 10;
int *pointer_1, *pointer_2;
pointer_1 = &a;
pointer_2 = &b;
printf("a=%d,b=%d\n", a, b);
printf("*pointer_1=%d,*pointer_2=%d\n", *pointer_1, *pointer_2);
return 0;
}输出
a=100,b=10
*pointer_1=100,*pointer_2=10逐行
#include <stdio.h>—— 引入 printf。int main()—— 主函数入口。int a = 100, b = 10;—— 建两间 int 房,住进 100、10,名牌 a、b。int *pointer_1, *pointer_2;—— 造两张空白 int 便签。pointer_1 = &a;—— 抄 a 的房号到便签 1。pointer_2 = &b;—— 抄 b 的房号到便签 2。printf(...a, b);—— 直接访问,输出 100、10。printf(...*pointer_1, *pointer_2);—— 间接访问,走到便签指的房里取人。
这道题教你什么:* 读出来的是"人",不是"号"。
代码 2:例 8.3 —— 用指针作参数真正交换
#include <stdio.h>
int main()
{
void swap(int *p1, int *p2);
int a, b;
int *pointer_1, *pointer_2;
printf("please enter a and b:");
scanf("%d,%d", &a, &b);
pointer_1 = &a;
pointer_2 = &b;
if (a < b) swap(pointer_1, pointer_2);
printf("max=%d,min=%d\n", a, b);
return 0;
}
void swap(int *p1, int *p2)
{
int temp;
temp = *p1;
*p1 = *p2;
*p2 = temp;
}输入 5,9,输出
max=9,min=5逐行
void swap(int *p1, int *p2);—— 声明,形参是 int 指针。pointer_1 = &a; pointer_2 = &b;—— 便签分别指向 a、b。if (a < b) swap(...)—— 5 < 9,调用。void swap(int *p1, int *p2)—— 形参 p1 得到&a,p2 得到&b。int temp;—— ⚠️ 是int,不是int *。temp = *p1;—— 把 a 的值 5 抄到 temp。*p1 = *p2;—— 把 b 的值 9 搬进 a。*p2 = temp;—— 把 5 搬进 b。printf("...", a, b);—— 输出 9、5,a、b 真的换了。
这道题教你什么:换"人"用 int temp,换"号"才用 int *temp。
代码 3:例 8.4 —— 反面教材:交换形参指针无效
#include <stdio.h>
int main()
{
void swap(int *p1, int *p2);
int a, b;
int *pointer_1, *pointer_2;
printf("please enter two integer numbers:");
scanf("%d,%d", &a, &b);
pointer_1 = &a;
pointer_2 = &b;
if (a < b) swap(pointer_1, pointer_2);
printf("max=%d,min=%d\n", *pointer_1, *pointer_2);
return 0;
}
void swap(int *p1, int *p2)
{
int *p;
p = p1;
p1 = p2;
p2 = p;
}输入 5,9,输出却是
max=5,min=9为什么"努力了却没用":p1、p2 是 pointer_1、pointer_2 的复印件。在函数里改复印件,原件不跟着变;函数一结束,复印件销毁。
记住:能通过函数改的,永远是"指针所指的变量",不是"指针自己"。
代码 4:例 8.5 —— 三个数排序(嵌套调用)
#include <stdio.h>
int main()
{
void exchange(int *q1, int *q2, int *q3);
int a, b, c, *p1, *p2, *p3;
printf("please enter three numbers:");
scanf("%d,%d,%d", &a, &b, &c);
p1 = &a; p2 = &b; p3 = &c;
exchange(p1, p2, p3);
printf("The order is:%d,%d,%d\n", a, b, c);
return 0;
}
void exchange(int *q1, int *q2, int *q3)
{
void swap(int *pt1, int *pt2);
if (*q1 < *q2) swap(q1, q2);
if (*q1 < *q3) swap(q1, q3);
if (*q2 < *q3) swap(q2, q3);
}
void swap(int *pt1, int *pt2)
{
int temp;
temp = *pt1;
*pt1 = *pt2;
*pt2 = temp;
}输入 20,-54,67,输出
The order is:67,20,-54逐行
int a, b, c, *p1, *p2, *p3;再p1 = &a; ...—— 三张便签分别指向三个变量。exchange(p1, p2, p3);—— 把三个地址传进去。void swap(int *pt1, int *pt2);—— exchange 内部再声明 swap(教材原书写法,因为 swap 定义在后面)。if (*q1 < *q2) swap(q1, q2);—— 比较的是值(*q1),传给 swap 的是地址;后两句同理,把最大顶到 q1、次大顶到 q2。swap里temp是int,交换*pt1和*pt2,也就是真正的变量值。
这道题教你什么:指针作参数可以带回多个结果(这里是 a、b、c 三个)。
代码 5:例 8.6 —— 三种方法遍历数组
#include <stdio.h>
int main()
{
int a[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
int i, *p;
printf("方法1(下标法):");
for (i = 0; i < 10; i++)
printf("%d ", a[i]);
printf("\n");
printf("方法2(数组名算地址):");
for (i = 0; i < 10; i++)
printf("%d ", *(a + i));
printf("\n");
printf("方法3(指针变量):");
for (p = a; p < a + 10; p++)
printf("%d ", *p);
printf("\n");
return 0;
}输出三行完全相同:
方法1(下标法):1 2 3 4 5 6 7 8 9 10
方法2(数组名算地址):1 2 3 4 5 6 7 8 9 10
方法3(指针变量):1 2 3 4 5 6 7 8 9 10为什么一样:a[i] 在编译时就变成 *(a+i),而 p 从 a 开始走,*p 自然是 *(a+i)。
代码 6:例 8.7 —— 越界陷阱与修正
#include <stdio.h>
int main()
{
int i, a[10], *p = a;
printf("please enter 10 integer numbers:");
for (i = 0; i < 10; i++)
scanf("%d", p++); /* 读完后 p 已跑到 a+10 */
p = a; /* ← 把 p 拨回起点,关键的一句 */
for (i = 0; i < 10; i++, p++)
printf("%d ", *p);
printf("\n");
return 0;
}输入 0 1 2 3 4 5 6 7 8 9,输出
0 1 2 3 4 5 6 7 8 9删掉 p = a; 会怎样:第二个循环从 a+10 开始读,输出数组后面 10 个未知单元的内容,看起来像乱码。
这道题教你什么:指针是有"当前位置"的。走完一趟想再走一趟,先把它拨回起点。
代码 7:例 8.8 —— 数组逆序
#include <stdio.h>
void inv(int x[], int n)
{
int temp, i, j, m = (n - 1) / 2;
for (i = 0; i <= m; i++)
{
j = n - 1 - i;
temp = x[i];
x[i] = x[j];
x[j] = temp;
}
}
int main()
{
int i, a[10] = {3, 7, 9, 11, 0, 6, 7, 5, 4, 2};
printf("The original array:\n");
for (i = 0; i < 10; i++)
printf("%d ", a[i]);
printf("\n");
inv(a, 10);
printf("The array has been inverted:\n");
for (i = 0; i < 10; i++)
printf("%d ", a[i]);
printf("\n");
return 0;
}输出
The original array:
3 7 9 11 0 6 7 5 4 2
The array has been inverted:
2 4 5 7 6 0 11 9 7 3逐行
void inv(int x[], int n)—— 形参写数组形式,编译时按指针处理。int temp, i, j, m = (n - 1) / 2;—— m 是 i 的上界;n=10 时 m=4。for (i = 0; i <= m; i++)—— i 从 0 走到 4,共 5 次。j = n - 1 - i;—— j 和 i 对着走:i=0 时 j=9,i=1 时 j=8……temp = x[i]; x[i] = x[j]; x[j] = temp;—— 交换两间房的人。x[i] 就是 a[i],所以真的改到了实参数组。inv(a, 10);—— 传的是数组名 a,即首元素地址;没有返回值,数组却变了。
这道题教你什么:这就是"用地址改实参"的标准样板。
四、书上习题精讲
本课不讲书末习题。 第 8 章书末习题统一放到第 09 课(讲完 8.4–8.7)后集中精讲。
原因:习题里相当一部分要用到 8.4(指针引用字符串)、8.5(指向函数的指针)、8.7(指针数组),现在讲只能硬背;而指针题多是"读程序写输出"和"找错",等 8.4–8.7 学完一起做,一题顶现在三题。第 8 章习题量在全书排前三,拆成两课才不撑破每天 2 小时的节奏。
本课要练的就是下面的随课练习——全是按书末习题的题型仿的,难度和考法一一对应。
🛠 随课练习(只给题不给答案)
🔮 猜输出(4 题)
猜输出 1
#include <stdio.h>
int main()
{
int a = 10, b = 20;
int *p = &a, *q = &b;
*p = *q;
p = q;
*p = 30;
printf("%d %d %d %d\n", a, b, *p, *q);
return 0;
}猜输出 2
#include <stdio.h>
void f(int x, int *y)
{
x = x + 1;
*y = *y + 1;
}
int main()
{
int a = 1, b = 1;
f(a, &b);
printf("%d %d\n", a, b);
return 0;
}猜输出 3
#include <stdio.h>
int main()
{
int a[5] = {1, 2, 3, 4, 5};
int *p = a;
printf("%d ", *p++);
printf("%d ", *(++p));
printf("%d ", ++(*p));
printf("%d %d\n", *p, a[2]);
return 0;
}猜输出 4
#include <stdio.h>
int main()
{
int a[6] = {10, 20, 30, 40, 50, 60};
int *p = &a[1], *q = &a[4];
printf("%d ", (int)(q - p));
printf("%d ", *(p + 2));
printf("%d\n", *(q - 1));
return 0;
}🛠 变式题(4 题)
变式题 1(照猫画虎) 把例 8.3 的 swap 改成能交换两个 double 变量的版本。自己写完整程序,在 main 里读入两个小数、调用、输出交换前后的值。
变式题 2(改需求) 写一个函数 void maxmin(int a[], int n, int *pmax, int *pmin);,一次求出数组的最大值和最小值,通过两个指针参数"带回"给 main。在 main 里打印这两个值。要求:函数里不许用 return 返回值,两个结果必须靠指针带回。
变式题 3(翻译) 下面这段用下标法写的代码,请改成纯指针写法(不出现任何 []),运行结果必须完全一致(答案应为 186)。
#include <stdio.h>
int main()
{
int a[8] = {12, 5, 78, 3, 90, 6, 41, 27};
int i, sum = 0;
for (i = 0; i < 8; i++)
if (a[i] % 2 == 0)
sum += a[i];
printf("%d\n", sum);
return 0;
}变式题 4(补全) 补全 reverse 函数,使数组逆序。要求形参用指针变量,循环用两个指针 i、j 对撞的写法(不许用下标)。
#include <stdio.h>
void reverse(int *x, int n)
{
/* 在这里补全:定义两个指针 i、j,一个指向头,一个指向尾,
交换它们指向的元素,然后向中间靠拢,直到相遇 */
}
int main()
{
int a[6] = {1, 2, 3, 4, 5, 6};
int i;
reverse(a, 6);
for (i = 0; i < 6; i++)
printf("%d ", a[i]);
printf("\n");
return 0;
}正确输出应为:6 5 4 3 2 1
🐛 改错题(3 题)
改错 1
#include <stdio.h>
int main()
{
int *p;
*p = 100;
printf("%d\n", *p);
return 0;
}改错 2
#include <stdio.h>
int main()
{
int a = 5;
int *p;
*p = &a;
printf("%d\n", *p);
return 0;
}改错 3
#include <stdio.h>
void swap(int *p1, int *p2)
{
int *t;
t = p1;
p1 = p2;
p2 = t;
}
int main()
{
int a = 3, b = 8;
swap(&a, &b);
printf("%d %d\n", a, b);
return 0;
}📈 加练题(你随时喊,我随时追加到这里)
(暂无。想加练就说"再出几道 XX 题")
💬 问答区(你问过的问题 + 我的解答)
(暂无。你问的问题会按 Q/A 整理进这里。)
该带走的 6 句话
- 指针就是地址;指针变量就是一张写着地址的便签,
&抄号,*按号找人。 - 定义指针变量必须写基类型,因为它决定"走一步跨几个字节"——
int *跨 4,char *跨 1。 - 函数参数永远是值传递:能改的只有"指针所指的变量",改不了"指针自己"。
- 数组名(非形参)就是首元素地址,而且是常量;
a[i]与*(a+i)无条件等价。 p + 1加的是一个元素,不是一字节;两个指针相减得到"隔几个元素",相加无意义。- C 不检查越界:野指针和越界指针都不报错,只会悄悄给出垃圾结果——用之前必须让它有确定值。
📚 教材覆盖清单(第8章,善于利用指针)
这张表把本课和教材逐节对上:从网页可以回查书页,从书页也能找到网页位置。标「未展开」的,就是教材里有、本课没讲的部分,已明确列出。
| 教材节号 | 标题 | 书页 | 网站对应 | 状态 |
|---|---|---|---|---|
| 8.1 | 指针是什么 | p.217–218 | 8.1 指针是什么 | 精讲 |
| 8.2 | 指针变量 | p.219–228 | 8.2 指针变量 | 精讲 |
| 8.2.1 | 使用指针变量的例子 | p.219 | 8.2 指针变量 | 精讲(并入 8.2) |
| 8.2.2 | 怎样定义指针变量 | p.220–221 | 8.2.2 怎样定义指针变量 | 精讲 |
| 8.2.3 | 怎样引用指针变量 | p.222–223 | 8.2.3 怎样引用指针变量 | 精讲 |
| 8.2.4 | 指针变量作为函数参数 | p.224–228 | 8.2.4 指针变量作为函数参数 | 精讲 |
| 8.3 | 通过指针引用数组 | p.229–253 | 8.3 通过指针引用数组 | 精讲 |
| 8.3.1 | 数组元素的指针 | p.229 | 8.3.1 数组元素的指针 | 精讲 |
| 8.3.2 | 在引用数组元素时指针的运算 | p.229–230 | 8.3.2 在引用数组元素时指针的运算 | 精讲 |
| 8.3.3 | 通过指针引用数组元素 | p.231–235 | 8.3.3 通过指针引用数组元素 | 精讲 |
| 8.3.4 | 用数组名作函数参数 | p.236–242 | 8.3.4 用数组名作函数参数 | 精讲 |
| 8.3.5 | 通过指针引用多维数组 | p.243–253 | 8.3.5 通过指针引用多维数组选学 | 略讲 |
| 习题 | 第8章书末习题 | p.291–292 | 第8章习题统一在第09课 | 见第09课 |
| 插图 | 本课范围内教材插图 | p.217–253 | 用文字/ASCII 重画 | 未逐张复刻 |
| 附录 | 附录 A–E | p.357–370 | 全书对照页 | 未单独成课 |
- 教材插图(流程图、内存示意图等)本课用文字/ASCII 重画,未逐张复刻,页码见上表。
- 附录 A–E(p.357–370)是速查表,未单独成课;全站对照页列出。
- 第8章书末 21 道习题统一放在第09课逐题讲解(p.291–292)。
- 标「本课补充」的小节(先补地基、比喻、随课练习等)不是教材原文,是为理解而加的。