CC语言通关
第09课 · 指针(下)
教材对照
第8章 · 书页 254–292

第09课 · 指针(下)

字符指针、函数指针、指针数组、动态内存 malloc/free、第8章习题

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

教材《C程序设计》第五版(谭浩强)第8章,书页 254–292,共 39 页|建议 2h
上一课把"指针 = 房间号"讲透了;这一课拿着这张便签,去处理一串字符、一段代码、一排便签、一块临时开的房。


课前一句话

📖 本课补充 · 非教材原文

这一课解决 5 件上一课还办不到的事:用指针引用字符串、把函数当参数传、让函数返回一个地址、用指针数组管理一堆字符串、在运行时才申请内存。学完你能独立写出:字符串复制函数、菜单式函数指针程序、按学号查成绩并返回地址的函数、5 个字符串排序、动态数组求平均。第 8 章后半本是期末选择题的"重灾区",也是编程题的好素材——看懂 + 能默写,这章就算通关。


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

📖 本课补充 · 非教材原文

1. 上一课的三句话(复习)

说法含义
指针就是地址内存每个字节都有编号,编号就是地址
指针变量是存放地址的变量一张写着房间号的便签
& 取地址,* 按地址访问&a 抄下 a 的房间号;*p 照着号去敲门

再补三条本课天天要用的:

写法意思
int *p;p 是"指向 int 的指针变量"
int a[5]; p = a;p 指向 a[0];p+i 指向 a[i];*(p+i) 就是 a[i]
int (*p)[4];p 是"行指针",p+1 跳过一整行(4 个 int),不是跳 4 个字节

2. 这一课要解决的五个新问题

上一课我们只会"指着一个变量"或"指着一个数组元素"。可真实程序还有这些麻烦:

问题小节一句话答案
① 一整串字符,怎么用指针领着走?8.4字符指针存首字符的地址,顺着往后数到 '\0' 为止
② 想让"调哪个函数"在运行时才决定?8.5函数名就是入口地址,用函数指针存它
③ 函数想告诉调用者"东西在哪",而不是"东西是什么"?8.6让函数返回指针
④ 有 5 个长短不一的字符串,怎么整整齐齐管起来?8.7用指针数组:只排"名片",不搬"行李"
⑤ 要存多少个数,运行时才知道,怎么办?8.8用 malloc 向堆区临时开房,用完 free 退房

3. 接着上一课的比喻:宿舍楼

第 08 课说定了,这一章只用一个比喻:内存 = 一整栋超长宿舍楼,一个字节 = 一间房,房间号 = 地址,指针变量 = 一张写着房间号的便签。 这一课继续住在这栋楼里:

  • 字符数组 = 你自己包下一整排房,把字一个个搬进去;房是你的,字可以改。
  • 字符指针指向字符串常量 = 便签上写着"样板间"的号;样板间的展品只能看不能动。
  • 函数 = 楼里的服务窗口,函数名就是窗口号;函数指针就是写着窗口号的便签。
  • 指针数组 = 一个名片夹,每张名片一个房间号;排序只换名片,不搬行李。
  • 二级指针 = 一个保险柜(本身也是一间房),柜里放着一张便签,便签上才是真正的房间号。
  • malloc = 向前台临时开房,钥匙(地址)给你;free = 退房,房卡交回后不能再进。
  • void * = 便签上只写"3 楼 302",没写房型(类型),用之前得先问清楚(强制转换)。
  • 野指针 = 一张写着乱码或旧房间号的便签,照着它进门,轻则走错屋,重则把别人家砸了。

二、知识点讲解

8.4 通过指针引用字符串

📖 教材 p.254–264 · 正文总结

8.4.1 字符串的引用方式

📖 教材 p.254–257 · 正文总结

一句话定义:C 语言没有"字符串变量",字符串就是一串以 '\0' 结尾的字符,它一定住在某个字符数组里;引用它只有两种姿势——用数组名,或用字符指针。

拆名字:string = 一串;char * = char(字符)+ *(指针)→ 指向字符的指针。它一次只指一个字符,但字符是连续排队的,顺着地址能走完整串。

生活比喻:字符串是"一排连着的房间,每间住一个字符,最后一间挂着'到此为止'的牌子 '\0'"。数组名是"这一排的起始房间号",字符指针是"便签上抄下的起始房间号"。区别是:数组名那张便签焊死了不能改,你手里那张可以改。

##### (1)用字符数组存放一个字符串

c
char string[] = "I love China!";   /* 长度自动定为 13 个字符 + 1 个 '\0' = 14 */
printf("%s\n", string);            /* 数组名 + %s,输出整串 */
printf("%c\n", string[7]);         /* 数组名 + 下标,输出第 8 个字符 'C' */
  • "I love China!" 共 13 个字符,系统在末尾自动补 '\0',所以数组长度是 14。
  • 下标从 0 开始,所以"第 8 个字符"是 string[7]。
  • string[7] 和 *(string + 7) 完全等价:前者是"按门牌号找第 8 间",后者是"从起始号走 7 步再敲门"。

##### (2)用字符指针变量指向一个字符串常量

c
char *string = "I love China!";    /* 把字符串第 1 个字符的地址赋给指针 */
printf("%s\n", string);            /* 从 string 指向处开始输出,直到 '\0' */

这一行等价于下面两行:

c
char *string;                      /* 定义一个 char * 型变量 */
string = "I love China!";          /* 把首字符地址赋给它 */

注意三件事(选择题最爱挖的坑):

  1. 不是把 13 个字符装进 string。指针变量只有一张便签大,只装得下地址。
  2. 不是把字符串赋给 *string。*string = "I love China!"; 是错的——多了一个 *,string 才是指针变量名。
  3. C 里只有字符变量,没有字符串变量。char *string 是"字符指针变量"。

指针可以再赋值,改指别处:

c
string = "I am a student.";        /* 便签改写成新字符串首字符的地址 */

此后它就找不回原来那串了。

为什么 %s 能输出一整串? 系统拿到地址后:输出这个字符 → 地址加 1 → 输出下一个 → …… 直到遇见 '\0' 停。所以 '\0' 是字符串的"句号";没有它,%s 会一路读下去,这就是"乱码"的常见来源。

顺带一个对比考点:字符数组可以整体输出,数值数组不行。printf("%d\n", a); 对 int a[10] 输出的是 a[0] 的地址,不是 10 个数。

##### (3)字符串复制:地址法 vs 指针法

例 8.18 用地址法:

c
for (i = 0; *(a + i) != '\0'; i++)
    *(b + i) = *(a + i);        /* 把 a[i] 复制到 b[i] */
*(b + i) = '\0';                /* 别忘了给 b 补句号 */

例 8.19 用指针法:

c
for (; *p1 != '\0'; p1++, p2++)
    *p2 = *p1;                  /* p1 指到哪,就把字符搬到 p2 指的地方 */
*p2 = '\0';
写法长什么样特点
下标法a[i]、b[i]直观,需要下标变量
地址法*(a + i)本质同下标法
指针法*p1、*p2,配合 p1++最简洁,指针一走就回不来

教材笔误:例 8.18 分析里印的是 *(b+1)='\0';,按上下文应为 *(b+i)='\0';。

##### 考点8.4.1 必记

  • char s[] = "abc"; 占 4 个字节。
  • s[1]、*(s+1)、*(1+s) 三者等价。
  • char *p = "abc"; 中 p 占一个指针变量的大小,字符串本体在别处(只读区)。
  • printf("%s", p+1) 从第 2 个字符开始输出。

##### 易错

  • char *p; scanf("%s", p); —— 野指针,直接往未知地址写。
  • char s[10]; s = "abc"; —— 数组名是常量地址,不能整体赋值。
  • char *p = "abc"; p[0] = 'A'; —— 字符串常量只读,运行时可能崩。

8.4.2 字符指针作函数参数

📖 教材 p.258–261 · 正文总结

一句话定义:想把字符串从一个函数"传"到另一个函数,传的是首字符的地址;形参写 char from[] 还是 char *from,本质是同一件事。

拆名字:参数 = 函数接收的数据;"字符指针作参数" = 传进来的不是一串字符,而是一个地址。

生活比喻:你不是把整排房间搬过去,而是把"起始房间号"抄给对方。对方拿着号走过去,改的是同一排房间——所以主调函数里能看到改动。

教材例 8.20 给了三种写法,对着看:

写法实参形参
(1)字符数组名char from[]、char to[]
(2)字符指针变量char from[]、char to[]
(3)字符指针变量 / 数组名char *from、char *to

表 8.3 把这四种组合列全了,全都合法:

实参形参
字符数组名字符数组名
字符数组名字符指针变量
字符指针变量字符数组名
字符指针变量字符指针变量

为什么? 编译系统本来就把"字符数组名作形参"按指针变量处理,char from[] 只是写法好看,并不真的在函数里新建数组。

核心函数体(写法 3):

c
void copy_string(char *from, char *to)
{
    for (; *from != '\0'; from++, to++)
        *to = *from;
    *to = '\0';                 /* 复制完补句号 */
}

教材还给了几个"越改越短"的版本,你至少要能看懂:

c
while ((*to = *from) != '\0') { to++; from++; }   /* 赋值和判断合在一起 */
while ((*to++ = *from++) != '\0');                /* 再合并:先赋值,再各自 ++ */
while (*from)                                      /* '\0' 的 ASCII 码是 0,*from 非 0 就是真 */
    *to++ = *from++;

拆解 *to++ = *from++(考点,也是易错点):

  1. ++ 是后缀,先用后加:先取 *from 的值,赋给 *to 指向的单元;
  2. 赋值完成后,from、to 各自加 1;
  3. 整个赋值表达式的值 = 刚赋进去的字符,所以能直接和 '\0' 比较;
  4. 复制到 '\0' 时,'\0' 也被赋过去了,循环结束——结尾的 '\0' 顺手就补上了。

##### 考点

  • 形参 char a[] 与 char *a 等价,都按指针处理。
  • 实参传数组名 = 传首元素地址;函数内改的是原数组。

##### 易错

  • 在函数里写 to = from; —— 只改了形参这张便签,主调函数的指针没变。
  • 忘了补 '\0' —— 复制出的字符数组不是合法字符串,%s 会带出后面的垃圾。

8.4.3 使用字符指针变量和字符数组的比较

📖 教材 p.262–264 · 正文总结

教材列了 8 条,一条条过,最后压成一张表。

(1)装的东西不同。 字符数组每个元素放一个字符;字符指针变量里放的是地址(首字符的地址),绝不是把字符串塞进指针。

(2)赋值方式不同。

c
char *a;
a = "I love China!";      /* 合法:把首字符地址赋给指针 */
char str[14];
str[0] = 'I';             /* 合法:给某一个元素赋值 */
str = "I love China!";    /* 非法:数组名是地址常量 */

(3)初始化的含义不同。

c
char *a = "I love China!";      /* 等价于 char *a; a = "I love China!"; */
char str[14] = "I love China!"; /* 把字符逐个复制进数组 */
char str[14];
str = "I love China!";          /* 错误:不能用赋值语句整体赋值 */

(4)占的存储单元不同。 字符数组分配若干字节;字符指针只分配一个存储单元(Visual C++ 里 4 字节,64 位编译器通常 8 字节)。

这也带来安全问题:数组没赋值,元素是垃圾值,读出来只是"没意义";指针没赋值,它指向不确定的地方,往里写就可能破坏程序甚至系统。

c
char *a;
scanf("%s", a);      /* 错误!野指针 */
char *a, str[10];
a = str;             /* 正确:先让它指向确定的房间 */
scanf("%s", a);

(5)指针的值可以变,数组名不能变。

c
char *a = "I love China!";
a = a + 7;           /* 便签改指第 8 个字符 */
printf("%s\n", a);   /* 输出 China! */

str = str + 7; 是错的,数组名是常量。

(6)数组元素能改,指针指向的字符串常量不能改。

c
char a[2][6] = {"House", "House"};   /* 5 个字符 + '\0',第二维至少 6 */
char *b = "House";
a[0][2] = 'u';       /* 合法:改数组元素 */
b[2] = 'u';          /* 非法:字符串常量不能改变 */

(7)引用数组元素。 char a[10] 可用 a[i]、*(a+i);char *p = a; 可用 p[i]、*(p+i)。但p 没指向数组时,p[5]、*(p+5) 毫无意义,必须避免。p 指向字符串常量时,p[5] 是合法的读操作。

(8)用指针指向格式字符串。

c
char *format;
format = "a=%d, b=%f\n";
printf(format, a, b);     /* 等价于 printf("a=%d, b=%f\n", a, b); */

这就是"可变格式输出"。用字符数组也能做,但数组不能整体重新赋值,反而没指针方便。

##### 一张表压死考点

比较项字符数组 char s[]字符指针 char *p
存什么一串字符本身一个地址(首字符的地址)
赋值只能定义时整体初始化,之后逐元素改可以 p = "..." 随时改指向
初始化含义把字符逐个复制进数组把首字符地址存进指针
占空间字符个数 + 1 字节一个指针变量(4 或 8 字节)
内容能否改数组元素可以改指向字符串常量时不能改
名字能否变数组名是常量指针变量可以 ++、可以改
引用元素s[i]、*(s+i)p[i]、*(p+i)(前提:p 指向数组或字符串)
未初始化元素是垃圾值,可读但无意义野指针,写入极其危险

##### 易错三条高频

  1. char *p = "hello"; p[0] = 'H'; —— 语法能过,运行可能崩(只读区)。
  2. char s[20]; s = "hello"; —— 编译就报错。
  3. char *p; strcpy(p, "hello"); —— 野指针,最典型的崩溃来源。

8.5 指向函数的指针选学

📖 教材 p.265–272 · 正文总结

期末考试通常只考概念和定义形式,但第 10 章链表、很多"通用函数"都要用它。

8.5.1 什么是函数的指针

📖 教材 p.265 · 正文总结

一句话定义:函数被编译后是一段可执行代码,放在内存里也有起始地址;这个地址叫入口地址,也叫函数的指针。函数名就代表它。

拆名字:function = 函数;pointer = 指针。函数指针 = 指向函数的指针;函数指针变量 = 存放"函数入口地址"的变量。

生活比喻:每个函数是楼里的一个服务窗口,函数名就是窗口号。函数指针是一张写着窗口号的便签:交给谁,谁就能照着号去那个窗口办事。便签上只能写"窗口入口",不能写"窗口里第 3 条指令"。

c
int (*p)(int, int);     /* p 指向"参数为两个 int、返回 int"的函数 */

8.5.2 用函数指针变量调用函数

📖 教材 p.265–266 · 正文总结

例 8.22 的核心就三行:

c
int (*p)(int, int);     /* 定义指向函数的指针变量 */
p = max;                /* 把 max 的入口地址赋给 p */
c = (*p)(a, b);         /* 通过 p 调用,等价于 c = max(a, b); */
  • p = max; 只写函数名,不写括号、不写实参。
  • (*p)(a, b) 中 *p 代表那个函数,后面的括号才是实参表。
  • p(a, b) 在 C 里也可以,但教材统一写 (*p)(a, b),你跟着教材走。

为什么 *p 两侧的括号不能省?

写法含义
int (*p)(int, int);p 是指向函数的指针变量
int *p(int, int);这是函数声明:p 是函数名,返回 int *

原因:() 优先级高于 *。不写括号,p 先和后面 () 结合成"函数 p",前面的 * 就退去修饰返回值了。

8.5.3 怎样定义和使用指向函数的指针变量

📖 教材 p.267–268 · 正文总结

一般形式:

c
类型名 (*指针变量名)(函数参数表列);

判断口诀:① 看变量名前有无 *——有,就是指针变量;② 看变量名后有无圆括号(内为形参类型)——有,就是指向函数;③ *指针变量名 必须用圆括号括起来。

六条说明,划重点:

  1. 函数指针只能指向定义时指定类型的函数。
  2. 用之前必须先赋值:p = max;。
  3. 赋值时只给函数名,不给实参。p = max(a, b); 错——那是把返回值赋给 p。
  4. 调用时用 (*p)(实参表)。
  5. 函数指针不能做算术运算:p++、p+n 毫无意义。
  6. 用函数名调用只能调固定函数;用函数指针可以运行时换指向。

例 8.23 就是这个思路:输入 1 让 p 指向 max,输入 2 让 p 指向 min,最后统一 c = (*p)(a, b);——一句调用,两种功能。

8.5.4 用指向函数的指针作函数参数

📖 教材 p.269–272 · 正文总结

一句话定义:把函数入口地址当实参传给另一个函数,被调函数里就能用这个指针调用"传进来的那个函数"。

生活比喻:你到总服务台说"帮我叫 3 号窗口的师傅"。总服务台不需要事先认识所有师傅,拿到窗口号就能去叫。总服务台 = 通用函数,窗口号 = 函数指针参数。

例 8.24:fun(a, b, max)、fun(a, b, min)、fun(a, b, add) 三次调用,fun 一个字都不用改:

c
int fun(int x, int y, int (*p)(int, int))
{
    int result;
    result = (*p)(x, y);      /* 调谁?看实参传的是哪个函数名 */
    printf("%d\n", result);
    return 0;
}

这就是"回调函数"思想的雏形:通用逻辑写在 fun 里,具体做什么由调用者通过函数指针决定。习题 13 的"通用定积分函数"就是它的正经应用。

##### 考点

  • int (*p)(int, int); 与 int *p(int, int); 的区别(必考)。
  • p = max; 对,p = max(a, b); 错。
  • 函数指针作形参的写法。

##### 易错

  • 忘了括号,把"函数指针"写成"返回指针的函数"。
  • 对函数指针做 ++。
  • 函数指针类型和实际函数不匹配。

8.6 返回指针值的函数选学

📖 教材 p.273–275 · 正文总结

一句话定义:一个函数的返回值也可以是一个地址,这种函数叫"返回指针值的函数",俗称指针函数。

拆名字:返回 + 指针值 + 的函数 = 函数,它的返回值类型是"指针"。是"返回(指针值)的函数",不是"指针"。

生活比喻:问询处不直接告诉你答案,而是递给你一张便签:"答案在 5 楼 502"。这张便签就是返回值。

c
类型名 *函数名(参数表列);      /* 一般形式 */
int *a(int x, int y);          /* a 是函数,调用后得到一个 int * 型的值 */

怎么读:*a 两侧没有括号;a 左边是 *,右边是 ()。因为 () 优先级高,a 先和 () 结合 —— 所以 a 是函数;函数前面有个 *,说明函数值是指针;最前面的 int 说明它指向 int。

和函数指针放在一起对比考点:

写法是什么
int *a(int, int);函数,返回 int *(返回指针值的函数)
int (*a)(int, int);指针,指向返回 int 的函数(函数指针)

一句话记忆:括号包住 *a 的是指针,括号包住参数的是函数。

例 8.25 的 search:

c
float *search(float (*pointer)[4], int n)
{
    float *pt;
    pt = *(pointer + n);      /* 先移行,再取该行第 0 个元素地址 */
    return pt;                /* 返回 &score[n][0] */
}

教材特别提醒的坑:把 pt = *(pointer + n); 写成 pt = (*pointer + n); 结果就完全变了——

  • *(pointer + n):先移动行,再取该行首元素地址 → 第 n 个学生第 0 门课;
  • (*pointer + n):*pointer 是第 0 行首元素地址,再 +n → 第 0 行第 n 个元素。

例 8.26 展示了返回指针的另一个常见用法:找不到就返回 NULL。

c
float *search(float (*pointer)[4])
{
    int i = 0;
    float *pt = NULL;                              /* NULL 表示"没找到" */
    for (i = 0; i < 4; i++)
        if (*(*pointer + i) < 60) pt = *pointer;   /* 发现不及格,指向本行第 0 个元素 */
    return pt;
}

主函数用 if (p == *(score + i)) 判断是不是真的返回了地址,返回 NULL 就不输出。NULL 在 stdio.h 里定义为 #define NULL 0。返回指针的函数用 NULL 报"没找到",是 C 的标准套路。

考点

  • 返回指针的函数的定义形式和读法。
  • *(pointer+n) 与 (*pointer+n) 的区别。
  • NULL 表示"空指针",不是"没定义"。

易错

  • 绝不能返回局部变量的地址。局部变量在函数结束时就被回收了,返回它的地址 = 给了一张写着"已退房"的房卡。
  • 返回的指针类型要和函数声明一致。
  • 调用者拿到指针后不判断 NULL 就用。

8.7 指针数组和多重指针选学

📖 教材 p.276–283 · 正文总结

8.7.1 什么是指针数组

📖 教材 p.276–278 · 正文总结

一句话定义:一个数组,如果它的每个元素都是指针,就叫指针数组——"一个装便签的夹子"。

拆名字:指针 + 数组 = 元素为指针的数组。中心词是数组。

c
int *p[4];        /* p 是数组,有 4 个元素,每个元素都是 int * */

怎么读:[] 优先级高于 *,p 先和 [4] 结合 → p 是数组;再和前面的 * 结合 → 元素是指针。

对比考点:

写法是什么
int *p[4];指针数组:4 个元素,每个是 int *
int (*p)[4];指向一维数组的指针(行指针)

生活比喻:指针数组是名片夹——夹子里每张名片一个房间号;房间里的东西一件没动。

为什么需要它? 5 本书名字长短不一:

  • char name[5][20]:每行都按最长书名开 20 个格子,短书名后面全浪费。
  • char *name[5]:5 张名片,各指各的字符串,一点不浪费;排序只换名片。

例 8.27 的选择法排序:

c
void sort(char *name[], int n)
{
    char *temp;
    int i, j, k;
    for (i = 0; i < n - 1; i++) {
        k = i;                                        /* 先假定第 i 个最小 */
        for (j = i + 1; j < n; j++)
            if (strcmp(name[k], name[j]) > 0) k = j;  /* 谁小就把谁的下标记在 k */
        if (k != i) {                                 /* 换名片 */
            temp = name[i];
            name[i] = name[k];
            name[k] = temp;
        }
    }
}

教材强调的易错点:不能写成

c
if (*name[k] > *name[j]) k = j;     /* 错!只比较了两个字符串的第 1 个字符 */

字符串比较必须用 strcmp。

8.7.2 指向指针数据的指针变量(二级指针)

📖 教材 p.279–280 · 正文总结

一句话定义:如果一个指针变量存放的是另一个指针变量的地址,它就是"指向指针数据的指针变量",简称二级指针。

c
char **p;          /* p 指向一个 char * 型数据 */

拆名字:char **p = char *(指向字符的指针类型)+ (*p)(p 是指针)。所以 p 指向"一个 char * 型的数据"。

生活比喻:保险柜(一间房)里放着一张便签,便签上才写着真正的房间号。p 是保险柜号,*p 是柜里的便签,**p 才是最终房间里的东西。

name 是指针数组时:

  • name + i 是 name[i] 的地址(指向"便签"的地址);
  • *(name + i) 就是 name[i],一个字符串首字符地址;
  • **(name + i) 才是字符串的第 1 个字符。

例 8.28:

c
char *name[] = {"Follow me", "BASIC", "Great Wall", "FORTRAN", "Computer design"};
char **p;
for (i = 0; i < 5; i++) {
    p = name + i;        /* p 指向第 i 张名片 */
    printf("%s\n", *p);  /* *p 就是 name[i],一个字符串地址 */
}

例 8.29 把同样思路用在整型:

c
int a[5] = {1, 3, 5, 7, 9};
int *num[5] = {&a[0], &a[1], &a[2], &a[3], &a[4]};
int **p;
p = num;                 /* p 指向 num[0] */
printf("%d\n", **p);     /* **p = *(num[0]) = a[0] = 1 */

注意:int *num[5] = {1,3,5,7,9}; 是错的——指针数组元素只能存地址,不能存整数。

教材还画了"单级 / 二级 / 多级间址"(图 8.40)。理论上可以无限套娃,但实际程序里超过二级就极少见。

8.7.3 指针数组作 main 函数的形参

📖 教材 p.281–283 · 正文总结

一句话定义:main 也可以有参数,用来接收命令行参数:int main(int argc, char *argv[])。

  • argc = argument count,参数个数;
  • argv = argument vector,参数向量,字符指针数组,每个元素指向命令行里的一个字符串。

生活比喻:你在楼下喊"帮我把 301、302、305 的快递拿下来",argc 是"一共几个包裹",argv 是"每个包裹单号"。

要点:

  1. 第一个形参必须是 int,第二个必须是 char *[](或 char **)。
  2. argv[0] 是程序名本身,真正的参数从 argv[1] 开始。
  3. 命令行参数一律是字符串,要当数字用必须自己转换(如 atoi)。
  4. 例:file1 China Beijing 中 argc 是 3,argv[0] 指向 "file1"。

教材 echo 程序的核心:

c
while (--argc > 0)
    printf("%s%c", *++argv, (argc > 1) ? ' ' : '\n');

*++argv:先让 argv 指向下一个元素,再取它指向的字符串。

考点

  • int *p[4] 与 int (*p)[4] 的区分(选择题常客)。
  • char **p 的含义与 *p、**p 各是什么。
  • argc 包含程序名;argv[0] 是程序名。

易错

  • 指针数组元素存整数。
  • 把 char **p 当字符串数组直接 printf("%s", p)——输出的是地址。

8.8 动态内存分配与指向它的指针变量选学

📖 教材 p.284–287 · 正文总结

8.8.1 什么是内存的动态分配

📖 教材 p.284 · 正文总结

一句话定义:程序运行过程中,临时向系统申请一块内存,用完再还回去,这就是动态内存分配。

生活比喻:数组是"入住前就订好 N 个房间,订多了浪费、订少了不够";动态内存是"到前台说'给我开 5 间房',走的时候退房"。

C 程序的内存大致分三块:

区域放什么什么时候分配/释放
静态存储区全局变量、静态局部变量程序开始到结束
栈(stack)非静态局部变量、形参进入函数时分配,返回时自动释放
堆(heap)malloc 等申请的空间申请时分配,free 时释放

堆上的数据没有变量名、没有数组名,只能通过指针引用——这就是讲动态内存必讲指针的原因。

8.8.2 怎样建立内存的动态分配

📖 教材 p.284–285 · 正文总结

四个函数,先拆名字:

函数拆开干什么
mallocmemory(内存)+ allocate(分配)分配指定字节数的空间
callocclear(清零)+ allocate(分配)分配 n 个元素,每块清零
reallocre(重新)+ allocate把已分配空间改成新大小
free释放把空间还给系统

原型(教材写法):

c
void *malloc(unsigned int size);
void *calloc(unsigned n, unsigned size);
void *realloc(void *p, unsigned int size);
void free(void *p);

(1)malloc

c
p = malloc(100);          /* 开辟 100 字节,返回首字节地址 */
  • 参数是字节数;返回 void *(纯地址);失败返回 NULL。
  • 实际用法一定配 sizeof:malloc(5 * sizeof(int))。

(2)calloc

c
p = calloc(50, 4);        /* 50 个元素,每个 4 字节,共 200 字节,且全部清零 */
  • 两个参数:元素个数和每个元素的字节数。
  • 和 malloc 的区别:calloc 自动清零,malloc 不清零。

(3)realloc

c
p = realloc(p, 50);       /* 把 p 原来那块空间改成 50 字节 */
  • 返回新地址(可能和原来不同),失败返回 NULL。

(4)free

c
free(p);                  /* 释放 p 指向的动态空间 */
  • 参数必须是最近一次 malloc/calloc/realloc 返回的地址;free 没有返回值。
  • 释放后要把指针置 NULL:p = NULL;。

四个函数都在 <stdlib.h> 里,用之前必须 #include <stdlib.h>。

8.8.3 void 指针类型

📖 教材 p.286–287 · 正文总结

一句话定义:void * 是"无指向的地址"——只有房间号,没有房型信息。

生活比喻:便签上写着"3 楼 302",但没写这间是单人床还是双人床。住进去之前,必须先确认房型(类型转换)。

教材关键句:不要把"指向 void 类型"理解成"能指向任何类型",而应理解为"不指向确定的类型"。

c
int a = 3;
int *p1 = &a;
void *p3;
p3 = (void *)p1;          /* int * 转成 void * */
p1 = (int *)p3;           /* void * 转回 int * */
  • void * 不能直接 *p3 取值——不知道取几个字节、按什么格式解释。
  • 赋值给别的指针时要做类型转换(C99 起很多编译器会自动转)。
  • malloc 返回 void *,所以常写 (int *)malloc(...);C99 后也可不写。

例 8.30 是本章唯一的动态内存完整例子:malloc 开 5 个 int 的空间当数组用,输入 5 个成绩,check 函数找出不及格的输出,最后 free。

考点

  • malloc 参数是字节数,返回 void *,失败返回 NULL。
  • calloc(n, size) 两个参数,且自动清零。
  • free(p) 后 p = NULL。
  • void * 是"无类型指针",只提供纯地址。

易错动态内存五连坑

  1. 野指针:int *p; *p = 5; 或 scanf("%d", p);。
  2. 内存泄漏:malloc 了却从不 free。
  3. 重复释放:free(p); free(p); —— 未定义行为。
  4. 释放后继续用:free(p); *p = 10;。
  5. free 非 malloc 地址:int a[10]; free(a);。

8.9 有关指针的小结

📖 教材 p.288–290 · 正文总结

(1)指针就是地址。 &a 是 a 的地址,也叫 a 的指针;指针变量是存放地址的变量;数组名是首元素地址,也是指针;函数名是函数代码区首字节的地址,也是指针;实参传数组名,传的是地址,也是指针。

(2)地址是有类型的。 一个地址型数据包含三个信息:纯地址(内存编号)、它本身的类型(指针类型)、基类型(指向的存储单元里放什么类型)。后两条可合成"指向某某类型的指针类型",如 int *。

(3)指针 ≠ 指针变量。 指针就是地址;指针变量是存放地址的变量。类型没有值,变量才有值。

(4)什么叫"指向"。 把谁的地址放进指针变量,就说它指向谁。但类型要匹配:int *p 只能放 int 的地址。

(5)数组与指针。 int *p, a[10]; p = a; 中,p 指向的是 a 数组的首元素,不是"整个数组"。

(6)指针变量的类型对照表(表 8.4,必背)

变量定义类型表示含义
int i;int整型变量 i
int *p;int *p 是指向整型数据的指针变量
int a[5];int [5]整型数组 a,5 个元素
int *p[4];int *[4]指针数组 p,4 个指向 int 的指针元素
int (*p)[4];int (*)[4]p 是指向含 4 个元素的一维数组的指针变量
int f();int ()f 是返回整型值的函数
int *p();int *()p 是返回指针的函数,该指针指向整型数据
int (*p)();int (*)()p 是指向函数的指针,该函数返回整型值
int **p;int **p 是指针,指向一个"指向整型数据的指针变量"
void *p;void *p 是基类型为 void 的指针,不指向具体对象

(7)指针运算

运算例子说明
加/减整数p++、p+i、p-i按所指类型的字节数跳,不是跳 1 个字节
赋值p = &a;、p = array;、p = max;、p1 = p2;类型要匹配;不能把整数赋给指针
两个指针相减p2 - p1前提:指向同一数组;结果是相差的元素个数
两个指针比较p1 < p2前提:同一数组;指向前面元素的"小"

(8)指针可以为空。 p = NULL; 表示不指向任何变量。NULL 是符号常量 0,stdio.h 里 #define NULL 0。 p 为 NULL 和"没给 p 赋值"是两回事:前者有值(0),安全;后者是垃圾值,危险。

指针的三大优点:提高效率;让函数修改主调函数的变量,从而"带回"多个值;实现动态存储分配。 最大缺点:太灵活,错误隐蔽,一错就可能破坏整个程序。所以——用指针要小心,多上机调试。


三、现场写代码

📖 教材例题 · 本课范围 p.254–290

下面 7 个程序都来自教材例题,全部完整可编译。每题按"题目 → 思路 → 代码 → 逐行解释 → 运行结果"走。

例 8.16 用字符数组引用字符串

📖 教材 p.254 · 例题 8.16

题目:定义字符数组存放 "I love China!",输出该字符串和第 8 个字符。

思路:初始化时字符个数确定,可不写数组长度;用 %s + 数组名输出整串,用 %c + 下标输出单个字符。

c
#include <stdio.h>

int main(void)
{
    char string[] = "I love China!";      /* 长度自动定为 14 */
    printf("%s\n", string);               /* 输出整个字符串 */
    printf("%c\n", string[7]);            /* 输出下标为 7 的字符 */
    return 0;
}

逐行解释:

  • char string[] = "I love China!"; —— 方括号空着,编译器数出 13 个字符,再留 1 个给 '\0'。
  • printf("%s\n", string); —— 数组名传给 %s,等价于传首元素地址。
  • printf("%c\n", string[7]); —— 下标从 0 起,string[7] 是 'C'。

运行结果:

c
I love China!
C

例 8.19 用指针变量复制字符串

📖 教材 p.257 · 例题 8.19

题目:将字符串 a 复制为字符串 b,然后输出 b,要求用指针变量。

思路:两个指针分别指向 a、b 的首字符,边复制边同步后移,最后补 '\0'。

c
#include <stdio.h>

int main(void)
{
    char a[] = "I am a boy.", b[20], *p1, *p2;
    p1 = a;                                      /* p1 指向 a 的首字符 */
    p2 = b;                                      /* p2 指向 b 的首元素 */
    for (; *p1 != '\0'; p1++, p2++)              /* 只要没到 '\0' 就继续 */
        *p2 = *p1;                               /* 把字符抄到 p2 指的位置 */
    *p2 = '\0';                                  /* 补结束标志 */
    printf("string a is: %s\n", a);
    printf("string b is: %s\n", b);
    return 0;
}

逐行解释:

  • char a[] = "I am a boy.", b[20], *p1, *p2; —— 一条语句定义两个数组、两个指针。
  • p1 = a; —— 数组名 a 就是 &a[0]。
  • for (; *p1 != '\0'; p1++, p2++) —— 初始化部分空着;每轮结束两个指针都加 1。
  • *p2 = *p1; —— 取一个字符,写进另一间房。
  • *p2 = '\0'; —— 循环结束时 p2 正好停在 b 的下一个空位。

运行结果:

c
string a is: I am a boy.
string b is: I am a boy.

例 8.20(3)用字符指针变量作形参和实参

📖 教材 p.258 · 例题 8.20

题目:用函数调用实现字符串复制,形参和实参都用字符指针变量。

思路:函数接收两个地址,在函数里逐字符复制;传地址才能改到主调函数里的 b。

c
#include <stdio.h>

void copy_string(char *from, char *to)
{
    for (; *from != '\0'; from++, to++)
        *to = *from;
    *to = '\0';
}

int main(void)
{
    char a[] = "I am a teacher.";
    char b[] = "You are a student.";
    char *p = b;
    printf("string a = %s\nstring b = %s\n", a, b);
    copy_string(a, p);                     /* 实参:数组名、指针,传的都是地址 */
    printf("string a = %s\nstring b = %s\n", a, b);
    return 0;
}

逐行解释:

  • void copy_string(char *from, char *to) —— 形参是字符指针,接收地址。
  • for (; *from != '\0'; from++, to++) —— 判断的是 *from(字符),不是 from(地址)。
  • *to = '\0'; —— 补句号,否则 b 后面残留的 "student." 会被 %s 一起输出。
  • char *p = b; —— p 指向 b[0]。
  • copy_string(a, p); —— 数组名传首地址,指针传它存的值(也是地址)。

运行结果:

c
string a = I am a teacher.
string b = You are a student.
string a = I am a teacher.
string b = I am a teacher.

例 8.22 用函数指针变量调用函数

📖 教材 p.265 · 例题 8.22

题目:求整数 a 和 b 中的大者,要求通过指向函数的指针变量调用。

思路:定义 int (*p)(int, int),令 p = max,再 c = (*p)(a, b)。

c
#include <stdio.h>

int max(int x, int y)
{
    return x > y ? x : y;
}

int main(void)
{
    int (*p)(int, int);        /* 指向函数的指针变量 */
    int a = 34, b = 89, c;
    p = max;                   /* 只写函数名,不写括号和实参 */
    c = (*p)(a, b);            /* 等价于 c = max(a, b); */
    printf("a=%d, b=%d, max=%d\n", a, b, c);
    return 0;
}

逐行解释:

  • int (*p)(int, int); —— 括号包住 *p 说明 p 是指针;后面的 (int,int) 是被指函数的参数表。
  • p = max; —— 函数名就是入口地址。
  • c = (*p)(a, b); —— (*p) 当函数名用。

运行结果:

c
a=34, b=89, max=89

例 8.25 返回指针值的函数

📖 教材 p.273 · 例题 8.25

题目:3 个学生各 4 门课成绩,输入学生序号,输出该生全部成绩,用返回指针的函数实现。

思路:二维数组 score[3][4],search 接收行指针和学生序号,返回 &score[k][0];主函数用 *(p+i) 输出。

c
#include <stdio.h>

float *search(float (*pointer)[4], int n)
{
    float *pt;
    pt = *(pointer + n);                    /* 先移行,再取该行首元素地址 */
    return pt;                              /* 返回 &score[n][0] */
}

int main(void)
{
    float score[][4] = {{60, 70, 80, 90},
                        {56, 89, 67, 88},
                        {34, 78, 90, 66}};
    float *p;
    int i, k;
    printf("enter the number of student: ");
    scanf("%d", &k);
    p = search(score, k);
    for (i = 0; i < 4; i++)
        printf("%5.2f\t", *(p + i));
    printf("\n");
    return 0;
}

逐行解释:

  • float *search(float (*pointer)[4], int n) —— 返回值是 float *;形参是行指针。
  • pt = *(pointer + n); —— pointer+n 指向第 n 行;*(...) 变成该行第 0 个元素的地址。
  • p = search(score, k); —— 数组名 score 是行指针,正好匹配形参。
  • *(p + i) —— 从 &score[k][0] 开始,第 i 门课。

运行结果(输入 1):

c
enter the number of student: 1
56.00   89.00   67.00   88.00

例 8.27 指针数组排序字符串

📖 教材 p.276 · 例题 8.27

题目:将若干字符串按字母顺序(由小到大)输出。

思路:指针数组保存 5 个字符串的首地址;选择法排序只交换指针,不移动字符串本身。

c
#include <stdio.h>
#include <string.h>

void sort(char *name[], int n)
{
    char *temp;
    int i, j, k;
    for (i = 0; i < n - 1; i++) {
        k = i;                                        /* 先假定第 i 个最小 */
        for (j = i + 1; j < n; j++)
            if (strcmp(name[k], name[j]) > 0) k = j;  /* 谁小就记谁的下标 */
        if (k != i) {                                 /* 换名片 */
            temp = name[i];
            name[i] = name[k];
            name[k] = temp;
        }
    }
}

void print(char *name[], int n)
{
    int i;
    for (i = 0; i < n; i++)
        printf("%s\n", name[i]);
}

int main(void)
{
    char *name[] = {"Follow me", "BASIC", "Great Wall", "FORTRAN", "Computer design"};
    sort(name, 5);
    print(name, 5);
    return 0;
}

逐行解释:

  • char *name[] = {...} —— 指针数组,每个元素指向一个字符串常量的首字符。
  • void sort(char *name[], int n) —— 形参本质是 char **。
  • strcmp(name[k], name[j]) > 0 —— 字典序比较,前者大返回正数。
  • temp = name[i]; ... —— 交换的是两个指针,字符串本身没动。

运行结果:

c
BASIC
Computer design
FORTRAN
Follow me
Great Wall

例 8.30 动态数组 + malloc/free

📖 教材 p.287 · 例题 8.30

题目:建立动态数组,输入 5 个学生成绩,用函数检查有无低于 60 分的并输出。

思路:malloc(5 * sizeof(int)) 开空间并转成 int *,输入后传给 check,最后 free。

c
#include <stdio.h>
#include <stdlib.h>

void check(int *p, int n)
{
    int i;
    printf("They are fail: ");
    for (i = 0; i < n; i++)
        if (p[i] < 60) printf("%d ", p[i]);
    printf("\n");
}

int main(void)
{
    int *p1, i;
    p1 = (int *)malloc(5 * sizeof(int));      /* 开 5 个 int 的空间 */
    if (p1 == NULL) {                         /* 必须检查是否成功 */
        printf("malloc failed\n");
        return 1;
    }
    for (i = 0; i < 5; i++)
        scanf("%d", p1 + i);                  /* 按地址法输入 */
    check(p1, 5);
    free(p1);                                 /* 退房 */
    p1 = NULL;                                /* 防止野指针 */
    return 0;
}

逐行解释:

  • #include <stdlib.h> —— 不写这个,malloc/free 没有声明。
  • p1 = (int *)malloc(5 * sizeof(int)); —— 参数是字节数;(int *) 把 void * 转成 int *。
  • if (p1 == NULL) —— 申请失败必须处理。
  • scanf("%d", p1 + i); —— p1+i 就是 &p1[i]。
  • free(p1); p1 = NULL; —— 教材代码里漏了这两句(分析文字里却说要 free),我们补上。

运行结果(输入 67 98 59 78 57):

c
67 98 59 78 57
They are fail: 59 57

四、书上习题精讲

📖 教材 p.291–292 · 习题

教材第 8 章习题共 21 题,书页 291–292,全部要求用指针方法处理。上一课留到这里集中讲,逐题补齐,一题不落。

第 1 题

📖 教材 p.291 · 习题 1

题目:输入 3 个整数,按由小到大的顺序输出。

思路:swap 交换两个变量的值,关键在传地址。

c
#include <stdio.h>

void swap(int *p1, int *p2)
{
    int t = *p1; *p1 = *p2; *p2 = t;
}

int main(void)
{
    int a, b, c;
    scanf("%d%d%d", &a, &b, &c);
    if (a > b) swap(&a, &b);
    if (a > c) swap(&a, &c);
    if (b > c) swap(&b, &c);
    printf("%d %d %d\n", a, b, c);
    return 0;
}

解析:三次比较即可。易错:若写成 int *t; t=p1; p1=p2; p2=t;,交换的只是函数里的形参副本,main 里的变量纹丝不动——想改主调函数的指针本身,得传"指针的地址"(二级指针)。

第 2 题

📖 教材 p.291 · 习题 2

题目:输入 3 个字符串,按由小到大的顺序输出。

思路:3 个字符数组 + 3 张名片(指针数组);比较用 strcmp,交换指针用 char **。

c
#include <stdio.h>
#include <string.h>

void swap(char **p1, char **p2)
{
    char *t = *p1; *p1 = *p2; *p2 = t;
}

int main(void)
{
    char s1[50], s2[50], s3[50];
    char *p[3];
    scanf("%49s%49s%49s", s1, s2, s3);
    p[0] = s1; p[1] = s2; p[2] = s3;
    if (strcmp(p[0], p[1]) > 0) swap(&p[0], &p[1]);
    if (strcmp(p[0], p[2]) > 0) swap(&p[0], &p[2]);
    if (strcmp(p[1], p[2]) > 0) swap(&p[1], &p[2]);
    printf("%s\n%s\n%s\n", p[0], p[1], p[2]);
    return 0;
}

解析:排序逻辑同第 1 题,只是"比较什么、交换什么"变了。swap(&p[0], &p[1]) 传的是指针元素的地址,类型 char **。

第 3 题

📖 教材 p.291 · 习题 3

题目:输入 10 个整数,将最小的数与第一个数对换,最大的数与最后一个数对换。写 3 个函数:①输入;②处理;③输出。

思路:先找最小与 a[0] 换;然后重新找最大(刚才的交换可能把最大值挪走了),再与 a[9] 换。

c
#include <stdio.h>

void input(int *p, int n)
{
    int i;
    for (i = 0; i < n; i++) scanf("%d", p + i);
}

void process(int *p, int n)
{
    int *min = p, *max, i, t;
    for (i = 1; i < n; i++)
        if (*(p + i) < *min) min = p + i;
    t = *p; *p = *min; *min = t;                 /* 最小数与第一个对换 */

    max = p;                                     /* 重新找最大 */
    for (i = 1; i < n; i++)
        if (*(p + i) > *max) max = p + i;
    t = *(p + n - 1); *(p + n - 1) = *max; *max = t;
}

void output(int *p, int n)
{
    int i;
    for (i = 0; i < n; i++) printf("%d ", *(p + i));
    printf("\n");
}

int main(void)
{
    int a[10];
    input(a, 10);
    process(a, 10);
    output(a, 10);
    return 0;
}

解析:最大的坑是最大值恰好在 a[0]——第一次交换会把它换到 min 原来的位置,原来的 max 指针就失效了。所以交换完 min 必须重新找 max。

第 4 题

📖 教材 p.291 · 习题 4

题目:n 个整数,前面各数顺序后移 m 个位置,最后 m 个数变成最前面 m 个数。写一函数实现,主函数输入输出。

思路:整体右移 m 位 = 重复 m 次"存最后一个 → 其余后移一位 → 存的那个放到最前"。

c
#include <stdio.h>

void move(int *p, int n, int m)
{
    int i, k, t;
    m = m % n;                        /* m 可能大于 n */
    for (k = 0; k < m; k++) {
        t = *(p + n - 1);             /* 保存最后一个 */
        for (i = n - 1; i > 0; i--)
            *(p + i) = *(p + i - 1);  /* 从后往前依次后移 */
        *p = t;
    }
}

int main(void)
{
    int a[100], n, m, i;
    scanf("%d%d", &n, &m);
    for (i = 0; i < n; i++) scanf("%d", a + i);
    move(a, n, m);
    for (i = 0; i < n; i++) printf("%d ", *(a + i));
    printf("\n");
    return 0;
}

解析:内层循环必须从后往前,否则前面的数会被覆盖。m = m % n 处理 m 比 n 大的情况。

第 5 题

📖 教材 p.291 · 习题 5

题目:n 个人围成一圈,从第 1 人开始 1 到 3 报数,报到 3 的退出,问最后留下的是原来第几号。

思路:数组标记"还在圈里",指针扫描,数到 3 标记退出,直到只剩 1 人。

c
#include <stdio.h>

int main(void)
{
    int a[100], n, i, count = 0, left;
    int *p = a;
    scanf("%d", &n);
    for (i = 0; i < n; i++) *(p + i) = 1;    /* 1 表示还在圈里 */
    left = n;
    i = 0;
    while (left > 1) {
        if (*(p + i)) {                      /* 还在圈里的人才报数 */
            count++;
            if (count == 3) {
                *(p + i) = 0;                /* 报到 3,退出 */
                left--;
                count = 0;
            }
        }
        i = (i + 1) % n;                     /* 走到末尾绕回开头 */
    }
    for (i = 0; i < n; i++)
        if (*(p + i)) printf("最后留下的是原来第%d号\n", i + 1);
    return 0;
}

解析:i = (i + 1) % n 是"围成圈"的关键;输出 i+1 因为人是 1 号起。n=5 时答案是 4 号。

第 6 题

📖 教材 p.292 · 习题 6

题目:写一函数求一个字符串的长度,在 main 中输入字符串并输出其长度。

思路:指针走到 '\0',两个地址相减。

c
#include <stdio.h>

int my_strlen(char *s)
{
    char *p = s;
    while (*p != '\0') p++;
    return (int)(p - s);              /* 两指针相减 = 元素个数 */
}

int main(void)
{
    char str[100];
    scanf("%99s", str);
    printf("%d\n", my_strlen(str));
    return 0;
}

解析:指针相减的结果是中间隔的元素个数;str 是数组名,本身就是地址,不用加 &。

第 7 题

📖 教材 p.292 · 习题 7

题目:字符串含 n 个字符,写一函数把从第 m 个字符开始的全部字符复制成另一个字符串。

思路:源指针先移到第 m 个字符(from + m - 1),再逐个复制。

c
#include <stdio.h>

void copy_from(char *to, char *from, int m)
{
    from = from + m - 1;              /* 指向第 m 个字符(下标 m-1) */
    while (*from != '\0') {
        *to = *from;
        to++; from++;
    }
    *to = '\0';
}

int main(void)
{
    char a[100], b[100];
    int m;
    scanf("%99s%d", a, &m);
    copy_from(b, a, m);
    printf("%s\n", b);
    return 0;
}

解析:"第 m 个字符"是人类数法,下标是 m-1,别忘减 1。输入 abcdef 3,输出 cdef。

第 8 题

📖 教材 p.292 · 习题 8

题目:输入一行文字,找出其中大写字母、小写字母、空格、数字以及其他字符各有多少。

思路:一次遍历,5 个计数器,用 ASCII 范围判断类别。

c
#include <stdio.h>

void count(char *s, int *upper, int *lower, int *space, int *digit, int *other)
{
    *upper = *lower = *space = *digit = *other = 0;
    while (*s != '\0') {
        if (*s >= 'A' && *s <= 'Z') (*upper)++;
        else if (*s >= 'a' && *s <= 'z') (*lower)++;
        else if (*s == ' ') (*space)++;
        else if (*s >= '0' && *s <= '9') (*digit)++;
        else (*other)++;
        s++;
    }
}

int main(void)
{
    char str[200];
    int up, low, sp, dig, oth;
    scanf("%199[^\n]", str);          /* 读一整行 */
    count(str, &up, &low, &sp, &dig, &oth);
    printf("大写:%d 小写:%d 空格:%d 数字:%d 其他:%d\n", up, low, sp, dig, oth);
    return 0;
}

解析:通过指针"带回" 5 个结果,是指针作参数的典型用途(return 只能带回一个)。判断顺序先字母、再空格/数字,剩下算"其他"。

第 9 题

📖 教材 p.292 · 习题 9

题目:写一函数,将一个 3×3 的整型矩阵转置。

思路:a[i][j] 与 a[j][i] 互换,只遍历上三角。

c
#include <stdio.h>

void transpose(int (*p)[3])
{
    int i, j, t;
    for (i = 0; i < 3; i++)
        for (j = i + 1; j < 3; j++) {   /* 只处理 j > i */
            t = p[i][j]; p[i][j] = p[j][i]; p[j][i] = t;
        }
}

int main(void)
{
    int a[3][3], i, j;
    for (i = 0; i < 3; i++)
        for (j = 0; j < 3; j++) scanf("%d", &a[i][j]);
    transpose(a);
    for (i = 0; i < 3; i++) {
        for (j = 0; j < 3; j++) printf("%d ", a[i][j]);
        printf("\n");
    }
    return 0;
}

解析:形参必须写成 int (*p)[3] 才能接二维数组名;j 从 i+1 开始,保证每对元素只换一次。

第 10 题

📖 教材 p.292 · 习题 10

题目:5×5 矩阵中最大元素放中心,4 个角放 4 个最小元素(从左到右、从上到下依次从小到大),写一函数实现,main 调用。

思路:先把最大换到中心;再按"左上 → 右上 → 左下 → 右下"顺序,每次在未锁定的格子里找最小换上。

c
#include <stdio.h>

void change(int a[5][5])
{
    int i, j, k, t;
    int ci[4] = {0, 0, 4, 4};         /* 4 个角的行号 */
    int cj[4] = {0, 4, 0, 4};         /* 4 个角的列号 */
    int locked[5][5] = {0};           /* 放好的格子不再参与 */
    int mi = 0, mj = 0;

    for (i = 0; i < 5; i++)           /* 找最大 */
        for (j = 0; j < 5; j++)
            if (a[i][j] > a[mi][mj]) { mi = i; mj = j; }
    t = a[2][2]; a[2][2] = a[mi][mj]; a[mi][mj] = t;
    locked[2][2] = 1;

    for (k = 0; k < 4; k++) {         /* 依次处理 4 个角 */
        int pi = -1, pj = -1;
        for (i = 0; i < 5; i++)
            for (j = 0; j < 5; j++) {
                if (locked[i][j]) continue;
                if (pi == -1 || a[i][j] < a[pi][pj]) { pi = i; pj = j; }
            }
        t = a[ci[k]][cj[k]]; a[ci[k]][cj[k]] = a[pi][pj]; a[pi][pj] = t;
        locked[ci[k]][cj[k]] = 1;     /* 这个角已放好,锁定 */
    }
}

int main(void)
{
    int a[5][5], i, j;
    for (i = 0; i < 5; i++)
        for (j = 0; j < 5; j++) scanf("%d", &a[i][j]);
    change(a);
    for (i = 0; i < 5; i++) {
        for (j = 0; j < 5; j++) printf("%4d", a[i][j]);
        printf("\n");
    }
    return 0;
}

解析:locked 是关键——放好的角已"定稿",后面找最小值必须排除,否则会被换走。先放最大再找最小,中心是最大值,不会干扰。

第 11 题

📖 教材 p.292 · 习题 11

题目:在主函数输入 10 个等长字符串,用另一函数排序,然后在主函数输出。

思路:char s[10][20] 存字符串;strcmp 比较,strcpy 交换内容。

c
#include <stdio.h>
#include <string.h>

void sort(char (*p)[20], int n)
{
    int i, j;
    char temp[20];
    for (i = 0; i < n - 1; i++)
        for (j = i + 1; j < n; j++)
            if (strcmp(p[i], p[j]) > 0) {
                strcpy(temp, p[i]);
                strcpy(p[i], p[j]);
                strcpy(p[j], temp);
            }
}

int main(void)
{
    char s[10][20];
    int i;
    for (i = 0; i < 10; i++) scanf("%19s", s[i]);
    sort(s, 10);
    for (i = 0; i < 10; i++) printf("%s\n", s[i]);
    return 0;
}

解析:二维数组传参必须用行指针 char (*p)[20],第二维长度不能省;等长字符串交换要 strcpy 三次,不能 p[i] = p[j]。

第 12 题

📖 教材 p.292 · 习题 12

题目:用指针数组处理上一题,字符串不等长。

思路:char s[10][100] 存字符串,char *p[10] 存名片;排序只交换指针。

c
#include <stdio.h>
#include <string.h>

void sort(char *p[], int n)
{
    int i, j;
    char *t;
    for (i = 0; i < n - 1; i++)
        for (j = i + 1; j < n; j++)
            if (strcmp(p[i], p[j]) > 0) {
                t = p[i]; p[i] = p[j]; p[j] = t;
            }
}

int main(void)
{
    char s[10][100];
    char *p[10];
    int i;
    for (i = 0; i < 10; i++) {
        scanf("%99s", s[i]);
        p[i] = s[i];                  /* 名片指向第 i 个字符串 */
    }
    sort(p, 10);
    for (i = 0; i < 10; i++) printf("%s\n", p[i]);
    return 0;
}

解析:与第 11 题相比,交换从"三次 strcpy"变成"三个指针赋值",快得多;字符串本身的位置没动。

第 13 题

📖 教材 p.292 · 习题 13

题目:写一个用矩形法求定积分的通用函数,分别求 ∫₀¹sin x dx、∫₀¹cos x dx、∫₀¹eˣ dx。sin、cos、exp 在数学库中,开头要 #include <math.h>。

思路:被积函数写成 3 个函数,用函数指针作参数传给通用积分函数。

c
#include <stdio.h>
#include <math.h>

double integral(double a, double b, double (*fun)(double))
{
    int n = 10000, i;
    double h = (b - a) / n;             /* 小矩形宽 */
    double s = 0.0;
    for (i = 0; i < n; i++)
        s += fun(a + (i + 0.5) * h) * h;  /* 中点矩形法:高 × 宽 */
    return s;
}

double f1(double x) { return sin(x); }
double f2(double x) { return cos(x); }
double f3(double x) { return exp(x); }

int main(void)
{
    printf("sin: %f\n", integral(0.0, 1.0, f1));
    printf("cos: %f\n", integral(0.0, 1.0, f2));
    printf("exp: %f\n", integral(0.0, 1.0, f3));
    return 0;
}

解析:同一个积分函数能算任意函数,靠的就是函数指针参数。取小区间中点算高度,精度更好。结果:sin ≈ 0.459698,cos ≈ 0.841471,exp ≈ 1.718282。

第 14 题

📖 教材 p.292 · 习题 14

题目:将 n 个数按输入时顺序的逆序排列,用函数实现。

思路:首尾双指针向中间靠拢,边走边交换。

c
#include <stdio.h>

void reverse(int *p, int n)
{
    int *q = p + n - 1, t;        /* q 指向最后一个 */
    while (p < q) {
        t = *p; *p = *q; *q = t;
        p++; q--;
    }
}

int main(void)
{
    int a[100], n, i;
    scanf("%d", &n);
    for (i = 0; i < n; i++) scanf("%d", a + i);
    reverse(a, n);
    for (i = 0; i < n; i++) printf("%d ", *(a + i));
    printf("\n");
    return 0;
}

解析:p < q 比较两个同数组指针是合法的;奇数个时中间那个自己和自己换,不影响。

第 15 题

📖 教材 p.292 · 习题 15

题目:一个班 4 个学生 5 门课。①求第 1 门课平均分;②找出有两门以上不及格的学生,输出学号、全部成绩及平均分;③找出平均成绩 90 以上或全部课程 85 以上的学生。分别编 3 个函数。

思路:float score[4][5],三个函数都用行指针 float (*p)[5] 接收。

c
#include <stdio.h>
#define STU 4
#define COUR 5

void average1(float (*p)[COUR])
{
    int i;
    float sum = 0;
    for (i = 0; i < STU; i++) sum += p[i][0];
    printf("第1门课平均分: %.2f\n", sum / STU);
}

void fail2(float (*p)[COUR])
{
    int i, j, cnt;
    for (i = 0; i < STU; i++) {
        cnt = 0;
        for (j = 0; j < COUR; j++)
            if (p[i][j] < 60) cnt++;
        if (cnt >= 2) {
            float sum = 0;
            printf("学号 %d 不及格 %d 门: ", i + 1, cnt);
            for (j = 0; j < COUR; j++) {
                printf("%.2f ", p[i][j]);
                sum += p[i][j];
            }
            printf("平均 %.2f\n", sum / COUR);
        }
    }
}

void excellent(float (*p)[COUR])
{
    int i, j, all85;
    for (i = 0; i < STU; i++) {
        float sum = 0;
        all85 = 1;
        for (j = 0; j < COUR; j++) {
            sum += p[i][j];
            if (p[i][j] <= 85) all85 = 0;
        }
        if (sum / COUR > 90 || all85)
            printf("学号 %d 平均 %.2f%s\n", i + 1, sum / COUR,
                   all85 ? " 全部85以上" : "");
    }
}

int main(void)
{
    float score[STU][COUR] = {
        {85, 90, 92, 88, 95},
        {50, 55, 70, 60, 65},
        {95, 96, 92, 98, 90},
        {70, 80, 60, 90, 85}
    };
    average1(score);
    fail2(score);
    excellent(score);
    return 0;
}

解析:三个形参都是 float (*p)[COUR],同一份数据被三个函数反复使用。②的平均是该生 5 门课平均;③的"85 分以上"按严格大于处理。

第 16 题

📖 教材 p.292 · 习题 16

题目:输入一个字符串,内有数字和非数字字符,如 A123x456 17960? 302tab5876。将连续数字作为整数依次存到数组 a,统计个数并输出。

思路:一次扫描,用 in_num 标记"是否正在读一个数",遇到非数字就存。

c
#include <stdio.h>

int main(void)
{
    char str[200];
    int a[100], n = 0, num = 0, in_num = 0, i;
    char *p;
    scanf("%199[^\n]", str);
    for (p = str; ; p++) {
        if (*p >= '0' && *p <= '9') {
            num = num * 10 + (*p - '0');   /* 拼到末尾 */
            in_num = 1;
        } else {
            if (in_num) {                  /* 一段数字刚结束 */
                a[n++] = num;
                num = 0;
                in_num = 0;
            }
            if (*p == '\0') break;
        }
    }
    printf("共%d个整数: ", n);
    for (i = 0; i < n; i++) printf("%d ", a[i]);
    printf("\n");
    return 0;
}

解析:num = num*10 + (*p-'0') 是"字符转整数"的标准写法;in_num 避免把空段存成 0。输入示例输出 5 个整数:123、456、17960、302、5876。

第 17 题

📖 教材 p.292 · 习题 17

题目:写一函数实现两个字符串的比较,原型 int strcmp(char *p1, char *p2);,s1=s2 返回 0,否则返回第一个不同字符的 ASCII 差。

思路:两指针同步走,字符相同就继续;不同或遇到 '\0' 就返回差值。

c
#include <stdio.h>

int my_strcmp(char *p1, char *p2)
{
    while (*p1 != '\0' && *p1 == *p2) {
        p1++; p2++;
    }
    return *p1 - *p2;                 /* 第一个不同字符的差 */
}

int main(void)
{
    char s1[100], s2[100];
    scanf("%99s%99s", s1, s2);
    printf("%d\n", my_strcmp(s1, s2));
    return 0;
}

解析:循环结束时只有两种可能:字符不同,或其中一个到了 '\0',两种情况都能直接 *p1 - *p2。教材括号里 "BOY" 与 "BAD" 实际第 3 个字符才不同,差值是 'Y'-'D'=21;它要说明的是"返回第一个不同字符的 ASCII 差"这条规则。

第 18 题

📖 教材 p.292 · 习题 18

题目:输入月份号,输出该月英文月名,例如输入 3 输出 "March",要求用指针数组处理。

思路:char *month[12] 存 12 个字符串常量的地址,用下标直接取。

c
#include <stdio.h>

int main(void)
{
    char *month[] = {"January", "February", "March", "April", "May", "June",
                     "July", "August", "September", "October", "November", "December"};
    int n;
    scanf("%d", &n);
    if (n >= 1 && n <= 12)
        printf("%s\n", month[n - 1]);   /* 月份从 1 起,下标从 0 起 */
    else
        printf("输入错误\n");
    return 0;
}

解析:指针数组最典型的用法——不等长字符串 + 按编号查表;用二维数组要按最长的 "September" 开 10 列,浪费空间。

第 19 题

📖 教材 p.292 · 习题 19

题目:(1)编写函数 new,对 n 个字符开辟连续存储空间,返回指向开始空间的指针;(2)写一函数 free,把 new 占用的空间释放。

思路:就是 malloc 和 free 的封装。

c
#include <stdio.h>
#include <stdlib.h>

void *new(int n)
{
    return malloc((unsigned)n);   /* 借用标准库的 malloc */
}

void my_free(void *p)             /* 不能叫 free,会和标准库重名 */
{
    free(p);
}

int main(void)
{
    char *p = (char *)new(20);
    if (p == NULL) return 1;
    scanf("%19s", p);
    printf("%s\n", p);
    my_free(p);
    p = NULL;
    return 0;
}

解析:返回类型必须能接住 malloc 的 void *;释放函数参数用 void * 可接收任何指针。易错:真起名 free 会与 <stdlib.h> 冲突。

第 20 题

📖 教材 p.292 · 习题 20

题目:用指向指针的指针的方法对 5 个字符串排序并输出。

思路:char *name[5] 是指针数组,char **p 是二级指针;排序函数接收 char **。

c
#include <stdio.h>
#include <string.h>

void sort(char **p, int n)
{
    int i, j;
    char *t;
    for (i = 0; i < n - 1; i++)
        for (j = i + 1; j < n; j++)
            if (strcmp(*(p + i), *(p + j)) > 0) {
                t = *(p + i);
                *(p + i) = *(p + j);
                *(p + j) = t;
            }
}

int main(void)
{
    char *name[] = {"Follow me", "BASIC", "Great Wall", "FORTRAN", "Computer design"};
    int i;
    sort(name, 5);
    for (i = 0; i < 5; i++) printf("%s\n", *(name + i));
    return 0;
}

解析:*(p+i) 是第 i 张名片(char *),**(p+i) 才是字符;和第 12 题只是写法不同。

第 21 题

📖 教材 p.292 · 习题 21

题目:用指向指针的指针的方法对 n 个整数排序并输出,排序单独写成函数,n 个整数在主函数输入输出。

思路:int **p 指向"指针数组",每个元素指向一个 int;排序交换的是这些指针。

c
#include <stdio.h>
#include <stdlib.h>

void sort(int **p, int n)
{
    int i, j, *t;
    for (i = 0; i < n - 1; i++)
        for (j = i + 1; j < n; j++)
            if (**(p + i) > **(p + j)) {   /* 比较最终指向的整数 */
                t = *(p + i);
                *(p + i) = *(p + j);
                *(p + j) = t;              /* 交换指针 */
            }
}

int main(void)
{
    int n, i;
    int *a, **p;
    scanf("%d", &n);
    a = (int *)malloc(n * sizeof(int));        /* 存数据 */
    p = (int **)malloc(n * sizeof(int *));     /* 存指针 */
    if (a == NULL || p == NULL) return 1;
    for (i = 0; i < n; i++) {
        scanf("%d", a + i);
        *(p + i) = a + i;                      /* 第 i 个指针指向第 i 个数 */
    }
    sort(p, n);
    for (i = 0; i < n; i++) printf("%d ", **(p + i));
    printf("\n");
    free(p);
    free(a);
    return 0;
}

解析:**(p+i) 先拿第 i 个指针,再取整数。排序后 a 数组本身顺序没变,变的是 p 里指针的顺序。这题同时用上二级指针和动态内存,是本章综合题。


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

📖 本课补充 · 非教材原文

🔮 猜输出

题 1

c
#include <stdio.h>
int main(void)
{
    char s[] = "abcdef";
    char *p = s;
    printf("%c\n", *(p + 2));
    printf("%s\n", p + 3);
    printf("%c\n", p[5]);
    p += 2;
    printf("%s\n", p);
    return 0;
}

题 2

c
#include <stdio.h>
int add(int x, int y) { return x + y; }
int mul(int x, int y) { return x * y; }
int main(void)
{
    int (*p)(int, int) = add;
    printf("%d\n", (*p)(3, 4));
    p = mul;
    printf("%d\n", p(3, 4));
    return 0;
}

题 3

c
#include <stdio.h>
int main(void)
{
    char *name[] = {"Follow me", "BASIC", "Great Wall"};
    char **p = name;
    printf("%s\n", *(p + 1));
    printf("%c\n", **(p + 2));
    printf("%s\n", *p + 7);
    return 0;
}

题 4

c
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
    int *p = (int *)malloc(3 * sizeof(int));
    int *q;
    if (p == NULL) return 1;
    p[0] = 10; p[1] = 20; p[2] = 30;
    q = p;
    q++;
    printf("%d %d %d\n", *p, *q, *(p + 2));
    printf("%d\n", (int)(q - p));
    free(p);
    p = NULL;
    return 0;
}

🛠 变式题

题 5:把例 8.19 的复制程序改成"只复制大写字母",其余字符跳过。提示:循环里加 if (*p1 >= 'A' && *p1 <= 'Z')。

题 6:用函数指针写一个"计算器":定义 add、sub、mul、div 四个函数,用 int (*p[4])(int,int) 指针数组存它们,输入两个数和运算符序号,调用对应函数。提示:这是"指针数组 + 函数指针"的组合。

题 7:用指针数组 char *p[5] 管理 5 个字符串,按字符串长度从小到大排序输出。提示:比较用 strlen(p[i]),交换的是指针。

题 8:用 malloc 建立长度为 n 的动态数组,输入 n 个数求平均值,最后 free 并置 NULL。提示:int *a = (int *)malloc(n * sizeof(int));。

🐛 改错题

题 9

c
// ❌ 错误示例(故意)
#include <stdio.h>
int main(void)
{
    char *p;
    scanf("%s", p);
    printf("%s\n", p);
    return 0;
}

题 10

c
// ❌ 错误示例(故意)
#include <stdio.h>
int main(void)
{
    char str[20];
    str = "hello";
    printf("%s\n", str);
    return 0;
}

题 11

c
// ❌ 错误示例(故意)
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
    int *p = (int *)malloc(5 * sizeof(int));
    int i;
    if (p == NULL) return 1;
    for (i = 0; i < 5; i++) p[i] = i;
    free(p);
    printf("%d\n", p[0]);
    return 0;
}

📈 加练题(你随时喊,我随时追加到这里)

📖 本课补充 · 非教材原文

(暂无。想加练就说"再出几道 XX 题",我出的题会标上日期追加在这块。)


💬 问答区(你问过的问题 + 我的解答)

📖 本课补充 · 非教材原文

(暂无。你问的问题和我的解答会按 Q/A 整理进这里。)


该带走的 6 句话

📖 本课补充 · 非教材原文
  1. 字符串就是以 '\0' 结尾的字符数组;字符指针只存首字符的地址,不存字符本身。
  2. 字符数组名是常量地址,不能整体赋值、不能 ++;字符指针可以改指向,但它指向的字符串常量不能改。
  3. int (*p)(int, int) 是指向函数的指针,int *p(int, int) 是返回指针的函数——括号包住谁,谁就是主角。
  4. 指针数组 int *p[4] 是装名片的夹子,二级指针 int **p 是放名片的保险柜;排序只换名片,不搬行李。
  5. malloc 开的房必须 free 退房,退完立刻置 NULL;没赋值的指针是野指针,绝对不能往里写。
  6. 想让函数改主调函数里的指针本身,要传指针的地址(二级指针);只传指针,改的只是函数里的副本。

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

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

教材节号标题书页网站对应状态
8.4通过指针引用字符串p.254–2648.4 通过指针引用字符串精讲
8.4.1字符串的引用方式p.254–2578.4.1 字符串的引用方式精讲
8.4.2字符指针作函数参数p.258–2618.4.2 字符指针作函数参数精讲
8.4.3使用字符指针变量和字符数组的比较p.262–2648.4.3 使用字符指针变量和字符数组的比较精讲
8.5指向函数的指针p.265–2728.5 指向函数的指针选学略讲
8.5.1什么是函数的指针p.2658.5.1 什么是函数的指针略讲
8.5.2用函数指针变量调用函数p.265–2668.5.2 用函数指针变量调用函数略讲
8.5.3怎样定义和使用指向函数的指针变量p.267–2688.5.3 怎样定义和使用指向函数的指针变量略讲
8.5.4用指向函数的指针作函数参数p.269–2728.5.4 用指向函数的指针作函数参数略讲
8.6返回指针值的函数p.273–2758.6 返回指针值的函数选学略讲
8.7指针数组和多重指针p.276–2838.7 指针数组和多重指针选学略讲
8.7.1什么是指针数组p.276–2788.7.1 什么是指针数组略讲
8.7.2指向指针数据的指针变量p.279–2808.7.2 指向指针数据的指针变量(二级指针)略讲
8.7.3指针数组作main函数的形参p.281–2838.7.3 指针数组作 main 函数的形参略讲
8.8动态内存分配与指向它的指针变量p.284–2878.8 动态内存分配与指向它的指针变量选学略讲
8.8.1什么是内存的动态分配p.2848.8.1 什么是内存的动态分配略讲
8.8.2怎样建立内存的动态分配p.284–2858.8.2 怎样建立内存的动态分配略讲
8.8.3void指针类型p.286–2878.8.3 void 指针类型略讲
8.9有关指针的小结p.288–2908.9 有关指针的小结精讲
习题第8章书末习题p.291–292本课「书上习题精讲」逐题精讲(第1–21题)
插图本课范围内教材插图p.254–290用文字/ASCII 重画未逐张复刻
附录附录 A–Ep.357–370全书对照页未单独成课
  • 教材插图(流程图、内存示意图等)本课用文字/ASCII 重画,未逐张复刻,页码见上表。
  • 附录 A–E(p.357–370)是速查表,未单独成课;全站对照页列出。
  • 标「本课补充」的小节(先补地基、比喻、随课练习等)不是教材原文,是为理解而加的。