教材《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 通过指针引用字符串
8.4.1 字符串的引用方式
一句话定义:C 语言没有"字符串变量",字符串就是一串以 '\0' 结尾的字符,它一定住在某个字符数组里;引用它只有两种姿势——用数组名,或用字符指针。
拆名字:string = 一串;char * = char(字符)+ *(指针)→ 指向字符的指针。它一次只指一个字符,但字符是连续排队的,顺着地址能走完整串。
生活比喻:字符串是"一排连着的房间,每间住一个字符,最后一间挂着'到此为止'的牌子 '\0'"。数组名是"这一排的起始房间号",字符指针是"便签上抄下的起始房间号"。区别是:数组名那张便签焊死了不能改,你手里那张可以改。
##### (1)用字符数组存放一个字符串
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)用字符指针变量指向一个字符串常量
char *string = "I love China!"; /* 把字符串第 1 个字符的地址赋给指针 */
printf("%s\n", string); /* 从 string 指向处开始输出,直到 '\0' */这一行等价于下面两行:
char *string; /* 定义一个 char * 型变量 */
string = "I love China!"; /* 把首字符地址赋给它 */注意三件事(选择题最爱挖的坑):
- 不是把 13 个字符装进
string。指针变量只有一张便签大,只装得下地址。 - 不是把字符串赋给
*string。*string = "I love China!";是错的——多了一个*,string才是指针变量名。 - C 里只有字符变量,没有字符串变量。
char *string是"字符指针变量"。
指针可以再赋值,改指别处:
string = "I am a student."; /* 便签改写成新字符串首字符的地址 */此后它就找不回原来那串了。
为什么 %s 能输出一整串? 系统拿到地址后:输出这个字符 → 地址加 1 → 输出下一个 → …… 直到遇见 '\0' 停。所以 '\0' 是字符串的"句号";没有它,%s 会一路读下去,这就是"乱码"的常见来源。
顺带一个对比考点:字符数组可以整体输出,数值数组不行。printf("%d\n", a); 对 int a[10] 输出的是 a[0] 的地址,不是 10 个数。
##### (3)字符串复制:地址法 vs 指针法
例 8.18 用地址法:
for (i = 0; *(a + i) != '\0'; i++)
*(b + i) = *(a + i); /* 把 a[i] 复制到 b[i] */
*(b + i) = '\0'; /* 别忘了给 b 补句号 */例 8.19 用指针法:
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 字符指针作函数参数
一句话定义:想把字符串从一个函数"传"到另一个函数,传的是首字符的地址;形参写 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):
void copy_string(char *from, char *to)
{
for (; *from != '\0'; from++, to++)
*to = *from;
*to = '\0'; /* 复制完补句号 */
}教材还给了几个"越改越短"的版本,你至少要能看懂:
while ((*to = *from) != '\0') { to++; from++; } /* 赋值和判断合在一起 */
while ((*to++ = *from++) != '\0'); /* 再合并:先赋值,再各自 ++ */
while (*from) /* '\0' 的 ASCII 码是 0,*from 非 0 就是真 */
*to++ = *from++;拆解 *to++ = *from++(考点,也是易错点):
++是后缀,先用后加:先取*from的值,赋给*to指向的单元;- 赋值完成后,
from、to各自加 1; - 整个赋值表达式的值 = 刚赋进去的字符,所以能直接和
'\0'比较; - 复制到
'\0'时,'\0'也被赋过去了,循环结束——结尾的'\0'顺手就补上了。
##### 考点
- 形参
char a[]与char *a等价,都按指针处理。 - 实参传数组名 = 传首元素地址;函数内改的是原数组。
##### 易错
- 在函数里写
to = from;—— 只改了形参这张便签,主调函数的指针没变。 - 忘了补
'\0'—— 复制出的字符数组不是合法字符串,%s会带出后面的垃圾。
8.4.3 使用字符指针变量和字符数组的比较
教材列了 8 条,一条条过,最后压成一张表。
(1)装的东西不同。 字符数组每个元素放一个字符;字符指针变量里放的是地址(首字符的地址),绝不是把字符串塞进指针。
(2)赋值方式不同。
char *a;
a = "I love China!"; /* 合法:把首字符地址赋给指针 */
char str[14];
str[0] = 'I'; /* 合法:给某一个元素赋值 */
str = "I love China!"; /* 非法:数组名是地址常量 */(3)初始化的含义不同。
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 字节)。
这也带来安全问题:数组没赋值,元素是垃圾值,读出来只是"没意义";指针没赋值,它指向不确定的地方,往里写就可能破坏程序甚至系统。
char *a;
scanf("%s", a); /* 错误!野指针 */
char *a, str[10];
a = str; /* 正确:先让它指向确定的房间 */
scanf("%s", a);(5)指针的值可以变,数组名不能变。
char *a = "I love China!";
a = a + 7; /* 便签改指第 8 个字符 */
printf("%s\n", a); /* 输出 China! */str = str + 7; 是错的,数组名是常量。
(6)数组元素能改,指针指向的字符串常量不能改。
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)用指针指向格式字符串。
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 指向数组或字符串) |
| 未初始化 | 元素是垃圾值,可读但无意义 | 野指针,写入极其危险 |
##### 易错三条高频
char *p = "hello"; p[0] = 'H';—— 语法能过,运行可能崩(只读区)。char s[20]; s = "hello";—— 编译就报错。char *p; strcpy(p, "hello");—— 野指针,最典型的崩溃来源。
8.5 指向函数的指针选学
期末考试通常只考概念和定义形式,但第 10 章链表、很多"通用函数"都要用它。
8.5.1 什么是函数的指针
一句话定义:函数被编译后是一段可执行代码,放在内存里也有起始地址;这个地址叫入口地址,也叫函数的指针。函数名就代表它。
拆名字:function = 函数;pointer = 指针。函数指针 = 指向函数的指针;函数指针变量 = 存放"函数入口地址"的变量。
生活比喻:每个函数是楼里的一个服务窗口,函数名就是窗口号。函数指针是一张写着窗口号的便签:交给谁,谁就能照着号去那个窗口办事。便签上只能写"窗口入口",不能写"窗口里第 3 条指令"。
int (*p)(int, int); /* p 指向"参数为两个 int、返回 int"的函数 */8.5.2 用函数指针变量调用函数
例 8.22 的核心就三行:
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 = max;。 - 赋值时只给函数名,不给实参。
p = max(a, b);错——那是把返回值赋给 p。 - 调用时用
(*p)(实参表)。 - 函数指针不能做算术运算:
p++、p+n毫无意义。 - 用函数名调用只能调固定函数;用函数指针可以运行时换指向。
例 8.23 就是这个思路:输入 1 让 p 指向 max,输入 2 让 p 指向 min,最后统一 c = (*p)(a, b);——一句调用,两种功能。
8.5.4 用指向函数的指针作函数参数
一句话定义:把函数入口地址当实参传给另一个函数,被调函数里就能用这个指针调用"传进来的那个函数"。
生活比喻:你到总服务台说"帮我叫 3 号窗口的师傅"。总服务台不需要事先认识所有师傅,拿到窗口号就能去叫。总服务台 = 通用函数,窗口号 = 函数指针参数。
例 8.24:fun(a, b, max)、fun(a, b, min)、fun(a, b, add) 三次调用,fun 一个字都不用改:
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 返回指针值的函数选学
一句话定义:一个函数的返回值也可以是一个地址,这种函数叫"返回指针值的函数",俗称指针函数。
拆名字:返回 + 指针值 + 的函数 = 函数,它的返回值类型是"指针"。是"返回(指针值)的函数",不是"指针"。
生活比喻:问询处不直接告诉你答案,而是递给你一张便签:"答案在 5 楼 502"。这张便签就是返回值。
类型名 *函数名(参数表列); /* 一般形式 */
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:
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。
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 指针数组和多重指针选学
8.7.1 什么是指针数组
一句话定义:一个数组,如果它的每个元素都是指针,就叫指针数组——"一个装便签的夹子"。
拆名字:指针 + 数组 = 元素为指针的数组。中心词是数组。
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 的选择法排序:
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;
}
}
}教材强调的易错点:不能写成
if (*name[k] > *name[j]) k = j; /* 错!只比较了两个字符串的第 1 个字符 */字符串比较必须用 strcmp。
8.7.2 指向指针数据的指针变量(二级指针)
一句话定义:如果一个指针变量存放的是另一个指针变量的地址,它就是"指向指针数据的指针变量",简称二级指针。
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:
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 把同样思路用在整型:
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 函数的形参
一句话定义:main 也可以有参数,用来接收命令行参数:int main(int argc, char *argv[])。
argc= argument count,参数个数;argv= argument vector,参数向量,字符指针数组,每个元素指向命令行里的一个字符串。
生活比喻:你在楼下喊"帮我把 301、302、305 的快递拿下来",argc 是"一共几个包裹",argv 是"每个包裹单号"。
要点:
- 第一个形参必须是
int,第二个必须是char *[](或char **)。 argv[0]是程序名本身,真正的参数从argv[1]开始。- 命令行参数一律是字符串,要当数字用必须自己转换(如
atoi)。 - 例:
file1 China Beijing中argc是 3,argv[0]指向"file1"。
教材 echo 程序的核心:
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 动态内存分配与指向它的指针变量选学
8.8.1 什么是内存的动态分配
一句话定义:程序运行过程中,临时向系统申请一块内存,用完再还回去,这就是动态内存分配。
生活比喻:数组是"入住前就订好 N 个房间,订多了浪费、订少了不够";动态内存是"到前台说'给我开 5 间房',走的时候退房"。
C 程序的内存大致分三块:
| 区域 | 放什么 | 什么时候分配/释放 |
|---|---|---|
| 静态存储区 | 全局变量、静态局部变量 | 程序开始到结束 |
| 栈(stack) | 非静态局部变量、形参 | 进入函数时分配,返回时自动释放 |
| 堆(heap) | malloc 等申请的空间 | 申请时分配,free 时释放 |
堆上的数据没有变量名、没有数组名,只能通过指针引用——这就是讲动态内存必讲指针的原因。
8.8.2 怎样建立内存的动态分配
四个函数,先拆名字:
| 函数 | 拆开 | 干什么 |
|---|---|---|
malloc | memory(内存)+ allocate(分配) | 分配指定字节数的空间 |
calloc | clear(清零)+ allocate(分配) | 分配 n 个元素,每块清零 |
realloc | re(重新)+ allocate | 把已分配空间改成新大小 |
free | 释放 | 把空间还给系统 |
原型(教材写法):
void *malloc(unsigned int size);
void *calloc(unsigned n, unsigned size);
void *realloc(void *p, unsigned int size);
void free(void *p);(1)malloc
p = malloc(100); /* 开辟 100 字节,返回首字节地址 */- 参数是字节数;返回
void *(纯地址);失败返回NULL。 - 实际用法一定配
sizeof:malloc(5 * sizeof(int))。
(2)calloc
p = calloc(50, 4); /* 50 个元素,每个 4 字节,共 200 字节,且全部清零 */- 两个参数:元素个数和每个元素的字节数。
- 和 malloc 的区别:calloc 自动清零,malloc 不清零。
(3)realloc
p = realloc(p, 50); /* 把 p 原来那块空间改成 50 字节 */- 返回新地址(可能和原来不同),失败返回 NULL。
(4)free
free(p); /* 释放 p 指向的动态空间 */- 参数必须是最近一次 malloc/calloc/realloc 返回的地址;
free没有返回值。 - 释放后要把指针置 NULL:
p = NULL;。
四个函数都在 <stdlib.h> 里,用之前必须 #include <stdlib.h>。
8.8.3 void 指针类型
一句话定义:void * 是"无指向的地址"——只有房间号,没有房型信息。
生活比喻:便签上写着"3 楼 302",但没写这间是单人床还是双人床。住进去之前,必须先确认房型(类型转换)。
教材关键句:不要把"指向 void 类型"理解成"能指向任何类型",而应理解为"不指向确定的类型"。
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 *是"无类型指针",只提供纯地址。
易错动态内存五连坑
- 野指针:
int *p; *p = 5;或scanf("%d", p);。 - 内存泄漏:
malloc了却从不free。 - 重复释放:
free(p); free(p);—— 未定义行为。 - 释放后继续用:
free(p); *p = 10;。 - free 非 malloc 地址:
int a[10]; free(a);。
8.9 有关指针的小结
(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),安全;后者是垃圾值,危险。
指针的三大优点:提高效率;让函数修改主调函数的变量,从而"带回"多个值;实现动态存储分配。 最大缺点:太灵活,错误隐蔽,一错就可能破坏整个程序。所以——用指针要小心,多上机调试。
三、现场写代码
下面 7 个程序都来自教材例题,全部完整可编译。每题按"题目 → 思路 → 代码 → 逐行解释 → 运行结果"走。
例 8.16 用字符数组引用字符串
题目:定义字符数组存放 "I love China!",输出该字符串和第 8 个字符。
思路:初始化时字符个数确定,可不写数组长度;用 %s + 数组名输出整串,用 %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'。
运行结果:
I love China!
C例 8.19 用指针变量复制字符串
题目:将字符串 a 复制为字符串 b,然后输出 b,要求用指针变量。
思路:两个指针分别指向 a、b 的首字符,边复制边同步后移,最后补 '\0'。
#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 的下一个空位。
运行结果:
string a is: I am a boy.
string b is: I am a boy.例 8.20(3)用字符指针变量作形参和实参
题目:用函数调用实现字符串复制,形参和实参都用字符指针变量。
思路:函数接收两个地址,在函数里逐字符复制;传地址才能改到主调函数里的 b。
#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);—— 数组名传首地址,指针传它存的值(也是地址)。
运行结果:
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 用函数指针变量调用函数
题目:求整数 a 和 b 中的大者,要求通过指向函数的指针变量调用。
思路:定义 int (*p)(int, int),令 p = max,再 c = (*p)(a, b)。
#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)当函数名用。
运行结果:
a=34, b=89, max=89例 8.25 返回指针值的函数
题目:3 个学生各 4 门课成绩,输入学生序号,输出该生全部成绩,用返回指针的函数实现。
思路:二维数组 score[3][4],search 接收行指针和学生序号,返回 &score[k][0];主函数用 *(p+i) 输出。
#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):
enter the number of student: 1
56.00 89.00 67.00 88.00例 8.27 指针数组排序字符串
题目:将若干字符串按字母顺序(由小到大)输出。
思路:指针数组保存 5 个字符串的首地址;选择法排序只交换指针,不移动字符串本身。
#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]; ...—— 交换的是两个指针,字符串本身没动。
运行结果:
BASIC
Computer design
FORTRAN
Follow me
Great Wall例 8.30 动态数组 + malloc/free
题目:建立动态数组,输入 5 个学生成绩,用函数检查有无低于 60 分的并输出。
思路:malloc(5 * sizeof(int)) 开空间并转成 int *,输入后传给 check,最后 free。
#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):
67 98 59 78 57
They are fail: 59 57四、书上习题精讲
教材第 8 章习题共 21 题,书页 291–292,全部要求用指针方法处理。上一课留到这里集中讲,逐题补齐,一题不落。
第 1 题
题目:输入 3 个整数,按由小到大的顺序输出。
思路:swap 交换两个变量的值,关键在传地址。
#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 题
题目:输入 3 个字符串,按由小到大的顺序输出。
思路:3 个字符数组 + 3 张名片(指针数组);比较用 strcmp,交换指针用 char **。
#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 题
题目:输入 10 个整数,将最小的数与第一个数对换,最大的数与最后一个数对换。写 3 个函数:①输入;②处理;③输出。
思路:先找最小与 a[0] 换;然后重新找最大(刚才的交换可能把最大值挪走了),再与 a[9] 换。
#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 题
题目:n 个整数,前面各数顺序后移 m 个位置,最后 m 个数变成最前面 m 个数。写一函数实现,主函数输入输出。
思路:整体右移 m 位 = 重复 m 次"存最后一个 → 其余后移一位 → 存的那个放到最前"。
#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 题
题目:n 个人围成一圈,从第 1 人开始 1 到 3 报数,报到 3 的退出,问最后留下的是原来第几号。
思路:数组标记"还在圈里",指针扫描,数到 3 标记退出,直到只剩 1 人。
#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 题
题目:写一函数求一个字符串的长度,在 main 中输入字符串并输出其长度。
思路:指针走到 '\0',两个地址相减。
#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 题
题目:字符串含 n 个字符,写一函数把从第 m 个字符开始的全部字符复制成另一个字符串。
思路:源指针先移到第 m 个字符(from + m - 1),再逐个复制。
#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 题
题目:输入一行文字,找出其中大写字母、小写字母、空格、数字以及其他字符各有多少。
思路:一次遍历,5 个计数器,用 ASCII 范围判断类别。
#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 题
题目:写一函数,将一个 3×3 的整型矩阵转置。
思路:a[i][j] 与 a[j][i] 互换,只遍历上三角。
#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 题
题目:5×5 矩阵中最大元素放中心,4 个角放 4 个最小元素(从左到右、从上到下依次从小到大),写一函数实现,main 调用。
思路:先把最大换到中心;再按"左上 → 右上 → 左下 → 右下"顺序,每次在未锁定的格子里找最小换上。
#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 题
题目:在主函数输入 10 个等长字符串,用另一函数排序,然后在主函数输出。
思路:char s[10][20] 存字符串;strcmp 比较,strcpy 交换内容。
#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 题
题目:用指针数组处理上一题,字符串不等长。
思路:char s[10][100] 存字符串,char *p[10] 存名片;排序只交换指针。
#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 题
题目:写一个用矩形法求定积分的通用函数,分别求 ∫₀¹sin x dx、∫₀¹cos x dx、∫₀¹eˣ dx。sin、cos、exp 在数学库中,开头要 #include <math.h>。
思路:被积函数写成 3 个函数,用函数指针作参数传给通用积分函数。
#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 题
题目:将 n 个数按输入时顺序的逆序排列,用函数实现。
思路:首尾双指针向中间靠拢,边走边交换。
#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 题
题目:一个班 4 个学生 5 门课。①求第 1 门课平均分;②找出有两门以上不及格的学生,输出学号、全部成绩及平均分;③找出平均成绩 90 以上或全部课程 85 以上的学生。分别编 3 个函数。
思路:float score[4][5],三个函数都用行指针 float (*p)[5] 接收。
#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 题
题目:输入一个字符串,内有数字和非数字字符,如 A123x456 17960? 302tab5876。将连续数字作为整数依次存到数组 a,统计个数并输出。
思路:一次扫描,用 in_num 标记"是否正在读一个数",遇到非数字就存。
#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 题
题目:写一函数实现两个字符串的比较,原型 int strcmp(char *p1, char *p2);,s1=s2 返回 0,否则返回第一个不同字符的 ASCII 差。
思路:两指针同步走,字符相同就继续;不同或遇到 '\0' 就返回差值。
#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 题
题目:输入月份号,输出该月英文月名,例如输入 3 输出 "March",要求用指针数组处理。
思路:char *month[12] 存 12 个字符串常量的地址,用下标直接取。
#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 题
题目:(1)编写函数 new,对 n 个字符开辟连续存储空间,返回指向开始空间的指针;(2)写一函数 free,把 new 占用的空间释放。
思路:就是 malloc 和 free 的封装。
#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 题
题目:用指向指针的指针的方法对 5 个字符串排序并输出。
思路:char *name[5] 是指针数组,char **p 是二级指针;排序函数接收 char **。
#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 题
题目:用指向指针的指针的方法对 n 个整数排序并输出,排序单独写成函数,n 个整数在主函数输入输出。
思路:int **p 指向"指针数组",每个元素指向一个 int;排序交换的是这些指针。
#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
#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
#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
#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
#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
// ❌ 错误示例(故意)
#include <stdio.h>
int main(void)
{
char *p;
scanf("%s", p);
printf("%s\n", p);
return 0;
}题 10
// ❌ 错误示例(故意)
#include <stdio.h>
int main(void)
{
char str[20];
str = "hello";
printf("%s\n", str);
return 0;
}题 11
// ❌ 错误示例(故意)
#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 句话
- 字符串就是以
'\0'结尾的字符数组;字符指针只存首字符的地址,不存字符本身。 - 字符数组名是常量地址,不能整体赋值、不能
++;字符指针可以改指向,但它指向的字符串常量不能改。 int (*p)(int, int)是指向函数的指针,int *p(int, int)是返回指针的函数——括号包住谁,谁就是主角。- 指针数组
int *p[4]是装名片的夹子,二级指针int **p是放名片的保险柜;排序只换名片,不搬行李。 malloc开的房必须free退房,退完立刻置NULL;没赋值的指针是野指针,绝对不能往里写。- 想让函数改主调函数里的指针本身,要传指针的地址(二级指针);只传指针,改的只是函数里的副本。
📚 教材覆盖清单(第8章,善于利用指针)
这张表把本课和教材逐节对上:从网页可以回查书页,从书页也能找到网页位置。标「未展开」的,就是教材里有、本课没讲的部分,已明确列出。
| 教材节号 | 标题 | 书页 | 网站对应 | 状态 |
|---|---|---|---|---|
| 8.4 | 通过指针引用字符串 | p.254–264 | 8.4 通过指针引用字符串 | 精讲 |
| 8.4.1 | 字符串的引用方式 | p.254–257 | 8.4.1 字符串的引用方式 | 精讲 |
| 8.4.2 | 字符指针作函数参数 | p.258–261 | 8.4.2 字符指针作函数参数 | 精讲 |
| 8.4.3 | 使用字符指针变量和字符数组的比较 | p.262–264 | 8.4.3 使用字符指针变量和字符数组的比较 | 精讲 |
| 8.5 | 指向函数的指针 | p.265–272 | 8.5 指向函数的指针选学 | 略讲 |
| 8.5.1 | 什么是函数的指针 | p.265 | 8.5.1 什么是函数的指针 | 略讲 |
| 8.5.2 | 用函数指针变量调用函数 | p.265–266 | 8.5.2 用函数指针变量调用函数 | 略讲 |
| 8.5.3 | 怎样定义和使用指向函数的指针变量 | p.267–268 | 8.5.3 怎样定义和使用指向函数的指针变量 | 略讲 |
| 8.5.4 | 用指向函数的指针作函数参数 | p.269–272 | 8.5.4 用指向函数的指针作函数参数 | 略讲 |
| 8.6 | 返回指针值的函数 | p.273–275 | 8.6 返回指针值的函数选学 | 略讲 |
| 8.7 | 指针数组和多重指针 | p.276–283 | 8.7 指针数组和多重指针选学 | 略讲 |
| 8.7.1 | 什么是指针数组 | p.276–278 | 8.7.1 什么是指针数组 | 略讲 |
| 8.7.2 | 指向指针数据的指针变量 | p.279–280 | 8.7.2 指向指针数据的指针变量(二级指针) | 略讲 |
| 8.7.3 | 指针数组作main函数的形参 | p.281–283 | 8.7.3 指针数组作 main 函数的形参 | 略讲 |
| 8.8 | 动态内存分配与指向它的指针变量 | p.284–287 | 8.8 动态内存分配与指向它的指针变量选学 | 略讲 |
| 8.8.1 | 什么是内存的动态分配 | p.284 | 8.8.1 什么是内存的动态分配 | 略讲 |
| 8.8.2 | 怎样建立内存的动态分配 | p.284–285 | 8.8.2 怎样建立内存的动态分配 | 略讲 |
| 8.8.3 | void指针类型 | p.286–287 | 8.8.3 void 指针类型 | 略讲 |
| 8.9 | 有关指针的小结 | p.288–290 | 8.9 有关指针的小结 | 精讲 |
| 习题 | 第8章书末习题 | p.291–292 | 本课「书上习题精讲」 | 逐题精讲(第1–21题) |
| 插图 | 本课范围内教材插图 | p.254–290 | 用文字/ASCII 重画 | 未逐张复刻 |
| 附录 | 附录 A–E | p.357–370 | 全书对照页 | 未单独成课 |
- 教材插图(流程图、内存示意图等)本课用文字/ASCII 重画,未逐张复刻,页码见上表。
- 附录 A–E(p.357–370)是速查表,未单独成课;全站对照页列出。
- 标「本课补充」的小节(先补地基、比喻、随课练习等)不是教材原文,是为理解而加的。