CC语言通关
第08课 · 指针(上)
教材对照
第8章 · 书页 217–253

第08课 · 指针(上)

指针就是地址、& 与 *、指针作函数参数、指针与数组

9 个小节 43 段代码 建议 3–4h
教材依据第8章《善于利用指针》书页 p.217–292
本课覆盖:教材第8章正文 + 书末习题(p.291–292)。标「教材 p.X」= 教材段落总结;「例题」= 教材例题;「习题」= 书末题目;「本课补充」= 非教材原文。
在全书对照表中查看 →

教材《C程序设计》第五版(谭浩强)第 8 章,书页 217–253,共 37 页|建议 2h


课前一句话

📖 本课补充 · 非教材原文
  • 前 7 课,你都是"叫变量的名字,让编译器自己去找";这一课你要学会拿着地址,自己走过去。
  • 一句话:指针就是地址;指针变量就是一张写着地址的便签。
  • 学完你能写出:用指针让函数真正交换两个变量、用指针走遍一个数组、用指针一次给函数带回多个结果。
  • 这是全书的分水岭:指针通了,第 9 章结构体、第 10 章文件都是延长线;没通,后面只能靠背。

一、先补地基:本章要解决的问题

📖 本课补充 · 非教材原文

1. 上一课留下的"不服气"

第 7 课你写过这个函数:

c
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 指针是什么

📖 教材 p.217–218 · 正文总结

【一句话定义】

指针就是地址;更准确地说,C 的"指针"是带类型信息的地址——既告诉你房间号,也告诉你这间房住的是什么类型的人。

【拆名字】

  • pointer = point(点、指向)+ -er(东西)→ 用来指向的东西 → 指针。
  • address = ad-(朝向)+ dress(引导、摆正)→ 把你引导到某个位置 → 地址。
  • 二者在 C 里几乎是同义词:指针的值就是一个地址。

【变量是怎么住进楼里的】

教材 p.219:定义变量后,编译时系统就会给它分配内存单元,按类型分配长度。Visual C++ 里:int 4 字节、float 4 字节、char 1 字节。

假设定义了三个 int 变量 i、j、k,编译器可能这样分房(教材图 8.1):

变量占用字节起始地址
i2000 – 20032000
j2004 – 20072004
k2008 – 20112008

注意三件事:每个字节一个编号;"i 的地址"指起始地址 2000;具体分到哪个号每次编译都可能不同,书上只是举例。

【直接访问:叫名字,编译器替你跑腿】

写 printf("%d\n", i); 时,实际发生的是:

  1. 编译器按名字查到 i 的地址 2000;
  2. 从 2000 开始,按 int 的规矩读 4 个字节;
  3. 按整型解释得到 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 门(间接访问)。多绕的这一道,就是指针。

c
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 指针变量

📖 教材 p.219–228 · 正文总结

一句话:指针变量是专门用来装地址的变量,它指向另一个对象(变量、数组、函数……)。

8.2.1 使用指针变量的例子(例 8.1)

📖 教材 p.219 · 例题 8.1

【题目】通过指针变量访问整型变量。【思路】定义 2 个整型变量,再定义 2 个指针变量分别指向它们,通过 * 访问。

完整代码与逐行解释见 第三节 代码 1。这里只记结果与三条要点:

  1. 定义 pointer_1、pointer_2 时,它们并未指向任何变量,只是"规定它们可以指向整型变量";指向谁要靠后面的赋值语句。
  2. * 在两处出现,含义完全不同(见下表)。
  3. 输出结果与直接输出 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 怎样定义指针变量

📖 教材 p.220–221 · 正文总结

【一句话】一般形式:类型名 *指针变量名;

【拆名字】这个"类型名"有专名叫基类型(base type)——"底座"的类型,规定这张便签只能写这种类型房间的号。

【怎么用】

c
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。

基类型写法走一步跨几字节能指向谁
intint *p;4int 变量、int 数组元素
floatfloat *p;4float 变量、float 数组元素
charchar *p;1char 变量、char 数组元素
doubledouble *p;8double 变量、double 数组元素

易错一个指针变量只能指向同一种类型的变量,不能忽而指向整型、忽而指向实型。

易错类型不匹配(教材 p.224):

c
int a, *p;
p = &a;          // ✅ 匹配
float *q;
q = &a;          // ⚠️ 警告:"把一个 int * 型数据转换为 float * 数据"

Visual C++ 6.0 里这是警告不是错误:编译器把 &a 的基类型自动改成 float 再赋给 q,但 q 不能用这个地址正确指向整型变量。

易错"指针变量中只能存放地址,不要把一个整数赋给指针变量。"

c
*pointer_1 = 100;   // ❌ pointer_1 是指针变量,100 是整数
p = 100;            // ❌ 系统无法辨别它是地址
p = &a;             // ✅ 地址只能用 & 得到

【类型怎么写、怎么读】:int * 读作"指向 int 的指针"(简称 int 指针),char * 读作 char 指针。int *、char *、float * 是三种不同类型,不能混。

易错描述时不能只说"a 是一个指针变量",要完整说:"a 是指向整型数据的指针变量"。

8.2.3 怎样引用指针变量

📖 教材 p.222–223 · 正文总结

考点引用指针变量只有 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 指向的变量

记法:& 和 * 是一对反操作。& 把"人"变成"号",* 把"号"变成"人"。

c
   变量 a  ──&──▶  地址(号)  ──*──▶  变量 a

【教材例 8.2】输入 a、b,按先大后小输出。思路:不交换 a、b 的值,而是交换两个指针变量的值。

c
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 指针变量作为函数参数

📖 教材 p.224–228 · 正文总结

【一句话】函数的参数不仅可以传整型、浮点型、字符型,还可以传指针类型;作用是把一个变量的地址传送到另一个函数中。

【拆名字】这里的机制叫 值传递(pass by value):实参的值复制一份给形参。传指针时,复制的就是"房间号"——拿到号的复印件,照样能走到那间房。

##### 例 8.3:用指针实现真正的 swap

完整代码见 第三节 代码 2。关键在最后三行:

c
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 专门列出的错误写法:

c
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 的推理链):

  1. 调用时 pointer_1 的值(&a)复制给 p1,pointer_2 的值复制给 p2;
  2. swap 里换掉的是 p1、p2 这两张复印件;
  3. 函数结束,p1、p2 被释放,复印件上的涂改不会传回原件;
  4. 所以 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 通过指针引用数组

📖 教材 p.229–253 · 正文总结

8.3.1 数组元素的指针

📖 教材 p.229 · 正文总结

【一句话】数组元素的指针,就是数组元素的地址。

【拆名字】array = 排列、阵列 → 数组(一间挨一间的连号房);element = 元素。

【生活比喻】数组就是走廊上一排连号的房间:a[0] 是 2000 号,a[1] 是 2004 号,a[2] 是 2008 号……

【怎么用】(教材 p.230)

c
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.229–230 · 正文总结

【先问】对地址做乘除有意义吗?教材 p.231:显然没有意义,实际上也无此必要。 但加减在一定条件下允许——条件是:指针指向数组元素的时候。

【拆名字】这叫 指针算术运算(pointer arithmetic),单位不是字节,是"元素"。

规则一:p + 1 不是加 1 字节,是加 1 个元素。 教材原文:执行 p+1 并不是将 p 的值简单加 1,而是加上一个数组元素所占用的字节数。

c
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 通过指针引用数组元素

📖 教材 p.231–235 · 正文总结

【一句话】引用数组元素有两条路:下标法(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++ 不行

c
for (p = a; a < (p + 10); a++)      // ❌ 不行
    printf("%d", *a);

教材 p.234:数组名 a 代表数组首元素的地址,它是一个指针型常量,它的值在程序运行期间是固定不变的。 既然 a 是常量,a++ 无法实现。

考点一句话:p 可以自增,a 不行。a 是钉在墙上的门牌,p 是可以带走的便签。

##### 例 8.7:指针越界的经典陷阱

教材 p.234 的第一个版本:

c
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 的两条结论:

  1. C 编译程序不检查数组越界。 写 a[10](不存在这个元素)编译器不认为非法,按 *(a+10) 处理,照算地址、照取内容。编译不出错,结果却不预期,这是"程序逻辑上的错误,比较隐蔽"。
  2. 用指针访问数组时,必须自己保证它指向数组中的有效元素。

考点"指针越界"和"数组越界"是同一件事的两种写法,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 减 1a[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 用数组名作函数参数

📖 教材 p.236–242 · 正文总结

【一句话】用数组名作实参时,传递的是数组首元素的地址;形参收到这个地址就指向了实参数组的第一个元素,因此函数里改数组元素,外面的数组跟着变。

【拆名字】关键还是 值传递——只不过这次传的"值"恰好是地址。

先回顾第 7 课那个"神奇现象":用数组名作参数时,形参数组元素变了,实参数组跟着变。当时只能"记住结论",现在能解释了。

实参形式传递的东西能否改外面的值
swap(a[0], a[2]);(数组元素)元素的值(复印件)❌ 不能
fun(a, 10);(数组名)首元素地址✅ 能

教材 p.236 原话:用数组元素 a[0]、a[2] 作实参,"与用变量作实参时一样,是值传递方式"。

形参为什么写成数组、实际却是指针? 教材 p.236–237:实参数组名代表首元素地址,而形参是用来接收这个地址的,因此形参应该是一个指针变量;"实际上,C 编译都是将形参数组名作为指针变量来处理的。"

所以这两种写法完全等价:

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:实参数组名代表一个固定的地址,或者说是指针常量;形参数组名并不是一个固定的地址,而是按指针变量处理。

所以形参可以再赋值:

c
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。运行结果:

c
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 还有一个形参用指针变量的版本,核心四行:

c
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) 只是形式上不同,实际上形参都是指针变量。

##### 易错用指针变量作实参,必须先有确定值

c
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[]:

c
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 通过指针引用多维数组选学

📖 教材 p.243–253 · 正文总结

教材这一小节标了 *,是选学,期末大概率不直接考。但你必须知道核心概念,第 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 + 12000、2016第 0/1 行起始地址(指向行,一步 16 字节)
a[0]、*(a+0)、*a20000 行 0 列元素地址(指向列,一步 4 字节)
a[1]、*(a+1)20161 行 0 列元素地址
a[1] + 2、*(a+1)+220241 行 2 列元素地址
a[1][2]、*(*(a+1)+2)131 行 2 列元素的值

考点教材表 8.2 就是这张表。最容易错的三个:

  1. a + 1 和 *(a + 1) 的"纯地址"可能相同(都是 2016),但基类型不同——一个指向行,一个指向列。
  2. *(a+1)+2 不能写成 *(a+1+2),后者成了 *(a+3),越界。
  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 核心三行:

c
int (*p)[4], i, j;
p = a;                              /* 不能写 p = a[0]; */
printf("a[%d,%d]=%d\n", i, j, *(*(p + i) + j));

三、现场写代码

📖 教材例题 · 本课范围 p.217–253

下面 7 段程序全部来自教材 8.1–8.3,都是完整可编译的程序,已在本机(Visual Studio 2022)逐个编译运行验证。

代码 1:例 8.1 —— 指针变量访问整型变量

📖 教材 p.219 · 例题 8.1
c
#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;
}

输出

c
a=100,b=10
*pointer_1=100,*pointer_2=10

逐行

  1. #include <stdio.h> —— 引入 printf。
  2. int main() —— 主函数入口。
  3. int a = 100, b = 10; —— 建两间 int 房,住进 100、10,名牌 a、b。
  4. int *pointer_1, *pointer_2; —— 造两张空白 int 便签。
  5. pointer_1 = &a; —— 抄 a 的房号到便签 1。
  6. pointer_2 = &b; —— 抄 b 的房号到便签 2。
  7. printf(...a, b); —— 直接访问,输出 100、10。
  8. printf(...*pointer_1, *pointer_2); —— 间接访问,走到便签指的房里取人。

这道题教你什么:* 读出来的是"人",不是"号"。

代码 2:例 8.3 —— 用指针作参数真正交换

📖 教材 p.224 · 例题 8.3
c
#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,输出

c
max=9,min=5

逐行

  1. void swap(int *p1, int *p2); —— 声明,形参是 int 指针。
  2. pointer_1 = &a; pointer_2 = &b; —— 便签分别指向 a、b。
  3. if (a < b) swap(...) —— 5 < 9,调用。
  4. void swap(int *p1, int *p2) —— 形参 p1 得到 &a,p2 得到 &b。
  5. int temp; —— ⚠️ 是 int,不是 int *。
  6. temp = *p1; —— 把 a 的值 5 抄到 temp。
  7. *p1 = *p2; —— 把 b 的值 9 搬进 a。
  8. *p2 = temp; —— 把 5 搬进 b。
  9. printf("...", a, b); —— 输出 9、5,a、b 真的换了。

这道题教你什么:换"人"用 int temp,换"号"才用 int *temp。

代码 3:例 8.4 —— 反面教材:交换形参指针无效

📖 教材 p.226 · 例题 8.4
c
#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,输出却是

c
max=5,min=9

为什么"努力了却没用":p1、p2 是 pointer_1、pointer_2 的复印件。在函数里改复印件,原件不跟着变;函数一结束,复印件销毁。

记住:能通过函数改的,永远是"指针所指的变量",不是"指针自己"。

代码 4:例 8.5 —— 三个数排序(嵌套调用)

📖 教材 p.228 · 例题 8.5
c
#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,输出

c
The order is:67,20,-54

逐行

  1. int a, b, c, *p1, *p2, *p3; 再 p1 = &a; ... —— 三张便签分别指向三个变量。
  2. exchange(p1, p2, p3); —— 把三个地址传进去。
  3. void swap(int *pt1, int *pt2); —— exchange 内部再声明 swap(教材原书写法,因为 swap 定义在后面)。
  4. if (*q1 < *q2) swap(q1, q2); —— 比较的是值(*q1),传给 swap 的是地址;后两句同理,把最大顶到 q1、次大顶到 q2。
  5. swap 里 temp 是 int,交换 *pt1 和 *pt2,也就是真正的变量值。

这道题教你什么:指针作参数可以带回多个结果(这里是 a、b、c 三个)。

代码 5:例 8.6 —— 三种方法遍历数组

📖 教材 p.231 · 例题 8.6
c
#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;
}

输出三行完全相同:

c
方法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 —— 越界陷阱与修正

📖 教材 p.233 · 例题 8.7
c
#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,输出

c
0 1 2 3 4 5 6 7 8 9

删掉 p = a; 会怎样:第二个循环从 a+10 开始读,输出数组后面 10 个未知单元的内容,看起来像乱码。

这道题教你什么:指针是有"当前位置"的。走完一趟想再走一趟,先把它拨回起点。

代码 7:例 8.8 —— 数组逆序

📖 教材 p.238 · 例题 8.8
c
#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;
}

输出

c
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

逐行

  1. void inv(int x[], int n) —— 形参写数组形式,编译时按指针处理。
  2. int temp, i, j, m = (n - 1) / 2; —— m 是 i 的上界;n=10 时 m=4。
  3. for (i = 0; i <= m; i++) —— i 从 0 走到 4,共 5 次。
  4. j = n - 1 - i; —— j 和 i 对着走:i=0 时 j=9,i=1 时 j=8……
  5. temp = x[i]; x[i] = x[j]; x[j] = temp; —— 交换两间房的人。x[i] 就是 a[i],所以真的改到了实参数组。
  6. inv(a, 10); —— 传的是数组名 a,即首元素地址;没有返回值,数组却变了。

这道题教你什么:这就是"用地址改实参"的标准样板。


四、书上习题精讲

📖 教材 p.291–292 · 习题

本课不讲书末习题。 第 8 章书末习题统一放到第 09 课(讲完 8.4–8.7)后集中精讲。

原因:习题里相当一部分要用到 8.4(指针引用字符串)、8.5(指向函数的指针)、8.7(指针数组),现在讲只能硬背;而指针题多是"读程序写输出"和"找错",等 8.4–8.7 学完一起做,一题顶现在三题。第 8 章习题量在全书排前三,拆成两课才不撑破每天 2 小时的节奏。

本课要练的就是下面的随课练习——全是按书末习题的题型仿的,难度和考法一一对应。


🛠 随课练习(只给题不给答案)

📖 本课补充 · 非教材原文

🔮 猜输出(4 题)

猜输出 1

c
#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

c
#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

c
#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

c
#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)。

c
#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 对撞的写法(不许用下标)。

c
#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

c
#include <stdio.h>
int main()
{
    int *p;
    *p = 100;
    printf("%d\n", *p);
    return 0;
}

改错 2

c
#include <stdio.h>
int main()
{
    int a = 5;
    int *p;
    *p = &a;
    printf("%d\n", *p);
    return 0;
}

改错 3

c
#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 句话

📖 本课补充 · 非教材原文
  1. 指针就是地址;指针变量就是一张写着地址的便签,& 抄号,* 按号找人。
  2. 定义指针变量必须写基类型,因为它决定"走一步跨几个字节"——int * 跨 4,char * 跨 1。
  3. 函数参数永远是值传递:能改的只有"指针所指的变量",改不了"指针自己"。
  4. 数组名(非形参)就是首元素地址,而且是常量;a[i] 与 *(a+i) 无条件等价。
  5. p + 1 加的是一个元素,不是一字节;两个指针相减得到"隔几个元素",相加无意义。
  6. C 不检查越界:野指针和越界指针都不报错,只会悄悄给出垃圾结果——用之前必须让它有确定值。
🧭 指针可视化点 a 或 p,看地址、指针、解引用到底谁连着谁
p ──▶ a
p 里存的是 a 的地址(0x1000),所以 *p 就是 a 本身。

📚 教材覆盖清单(第8章,善于利用指针)

这张表把本课和教材逐节对上:从网页可以回查书页,从书页也能找到网页位置。标「未展开」的,就是教材里有、本课没讲的部分,已明确列出。

教材节号标题书页网站对应状态
8.1指针是什么p.217–2188.1 指针是什么精讲
8.2指针变量p.219–2288.2 指针变量精讲
8.2.1使用指针变量的例子p.2198.2 指针变量精讲(并入 8.2)
8.2.2怎样定义指针变量p.220–2218.2.2 怎样定义指针变量精讲
8.2.3怎样引用指针变量p.222–2238.2.3 怎样引用指针变量精讲
8.2.4指针变量作为函数参数p.224–2288.2.4 指针变量作为函数参数精讲
8.3通过指针引用数组p.229–2538.3 通过指针引用数组精讲
8.3.1数组元素的指针p.2298.3.1 数组元素的指针精讲
8.3.2在引用数组元素时指针的运算p.229–2308.3.2 在引用数组元素时指针的运算精讲
8.3.3通过指针引用数组元素p.231–2358.3.3 通过指针引用数组元素精讲
8.3.4用数组名作函数参数p.236–2428.3.4 用数组名作函数参数精讲
8.3.5通过指针引用多维数组p.243–2538.3.5 通过指针引用多维数组选学略讲
习题第8章书末习题p.291–292第8章习题统一在第09课见第09课
插图本课范围内教材插图p.217–253用文字/ASCII 重画未逐张复刻
附录附录 A–Ep.357–370全书对照页未单独成课
  • 教材插图(流程图、内存示意图等)本课用文字/ASCII 重画,未逐张复刻,页码见上表。
  • 附录 A–E(p.357–370)是速查表,未单独成课;全站对照页列出。
  • 第8章书末 21 道习题统一放在第09课逐题讲解(p.291–292)。
  • 标「本课补充」的小节(先补地基、比喻、随课练习等)不是教材原文,是为理解而加的。