第 8 章 指针
8.1 教学要点
本章主要介绍指针的基本概念、指针变量的定义、指针变量的基本运算、指针作为函数 的参数、指针和数组、指针和字符串、动态内存分配和使用等知识。首先要把基本概念讲清,
一定要结合图例来说明变量、内存单元和地址之间的关系;本章重点难点较多,讲解时注意 趣味性激发学生兴趣,通过对比演示来分析指针和普通变量的区别。
8.1 节通过示例程序“密码开锁”引出指针的概念和主要知识点,教师在讲授时,重点 分析密码开锁的过程,通过过程分析来说明变量、内存单元和地址之间的关系,从而引出指 针的基本概念,通过指针与所指向变量的示意图,加深学生对指针的理解。接着介绍指针的 基本运算以及指针变量的初始化。
8.2 节主要介绍指针作为函数的参数。该小节通过例子“角色互换”引入,演示使用指 针作为函数参数的用途和用法。在本小节中要提示学生注意观察函数的形参和实参,区分按
值调用和引用调用。在具体讲解时,通过详细分析3 个 swap()函数进行对比,讲解过程一定
要配合画出指针指向图来说明参数变量值是如何改变的。最后通过一个例子来讲解如何使用
指针作为函数参数返回多个值,解决以前一个函数最多只能return 一个值的问题。
8.3 节通过示例程序“冒泡排序”引入数组名作为函数参数。首先重点介绍指针、数组 和地址之间的关系,分析数组下标运算的含义和指针偏移的对应关系。通过例子来说明指针 和数组可以实现相同的操作。然后重点讲述如何使用数组名作为函数的参数,最后对冒泡排 序进行详细分析,总结回顾本节知识点。
8.4 节中通过示例程序“电码加密”引入字符指针与字符串处理。首先重点介绍字符指 针和字符串的关联和区别。然后对常用字符串处理函数进行讲解,通过对比示例强调字符串 运算的特殊性并分析原因。
8.5 节中通过示例程序“任意个整数求和”引入用指针实现内存动态分配。首先要分析 为什么要进行内存的动态分配管理,然后介绍动态内存分配的一般步骤和具体的分配函数。
最后可以通过和学生一起使用动态内存分配的方法来改写“冒泡排序”,做到融会贯通。
讲授学时:6 学时 实验学时:4 学时 本章的知识能力结构图见图8.1。
图 8.1 知识能力结构图 指针概念与
定义
指 针
指针作为函 数的参数
指针与数组
指针与字符 串
形参和实参 按值调用和 引用调用
通过指针参数使函数返回多个值 指针、数组与地址的关系
数组名作为函数参数 字符指针与字符 串的关联和区别 常用字符串处理
函数
能 够 掌 握 指针概念,
定 义 指 针 变 量 和 指 针 基 本 运 算
能 够 使 用 数 组 名 作 为 函 数 参 数 进 行熟练编程,并能 利 用 指 针 进 行 数 组相关操作
掌 握 通 过 指 针 实 现动态内存分配,
并能进行编程 指针与动态
内存分配*
变量、内存单元 与地址的关系
指针变量
定义方式 初始化 变量使用
动态内存分配的 概念和基本步骤 常用动态内存分
配函数
能 够 使 用 字 符 串 常 用 处 理 函 数 进 行编程,并能使用 字 符 指 针 进 行 字 符串相关操作
能 够 掌 握 指 针 作 为 函 数 的 参 数 进 行熟练编程,通函 数 调 用 改 变 主 调 函数变量的值
8.2 讲稿
1
Chap 8 指针
8.1 密码开锁 8.2 角色互换 8.3 冒泡排序 8.4 电码加密 8.5 任意个整数求和*
介绍主要章节安排。通过这些小节的名字 引起学生对本章学习的兴趣。
2
本章要点
n变量、内存单元和地址之间是什么关系?
n如何定义指针变量,怎样才能使用指针变量?
n什么是指针变量的初始化?
n指针变量的基本运算有哪些?如何使用指针 操作所指向的变量?
n指针作为函数参数的作用是什么?
n如何使用指针实现函数调用返回多个值?
n如何利用指针实现内存的动态分配?
要点可快速过一遍,不用展开。可以在本 章知识点讲完后再回顾。
3
8.1 密码开锁
一个密室逃脱游戏中的密码开锁:26个寄存箱,每个寄存箱 上按顺序都有一个英文字母和一个编号,字母从A到Z,编号 从01到26
关键点分析
n 得到线索:找到一把钥匙,打开p寄存箱(编号为16)
n 提示地址:里面是一把刻着数字24的钥匙 n 找到目标:打开编号为24的X寄存箱 n 取出内容:“5342”
介绍一下游戏过程,重点放在关键点分 析。
4
8.1 寻找密码的途径分析
n 密码存放需要一定的存储空间作为存放地,每 个存放地都会有地址
n 如果知道了存放地的名字,当然能够找到密码
n 如果不知道存放地的名字,知道该存放地的地 址也能够取出密码
n 如果有另外一个地方存放了该密码存放地的地 址,那么就能顺藤摸瓜,间接找到密码
分析寻找密码的两种可能途径
5
8.1 密码存放示意图
5342 24
p
24 16
名字 p x
地址 16 24 内容 24 5342
x
通过示意图说明地址为 16 的 P 变量内容 存放的 24 是另外一个变量 x 的地址,通
过这个地址可以顺藤摸瓜得到变量x 的内
容:5342 强调箭头的指向
6
例8-1 利用指针模拟密码开锁游戏
获取密码的两种方法:
#include <stdio.h>
int main(void) {
int x = 5342; /* 变量x用于存放密码值5342 */
int *p = NULL; /* 定义整型指针变量p,NULL值为0,代表空指针 */
p = &x; /*将变量x的地址存储在p中 */
/* 通过变量名x输出密码值*/
printf("If I know the name of the variable, I can get it’s value by name: %d\n ", x);
/* 通过变量x的地址输出密码值 */
printf("If I know the address of the variable is: %x, then I also can get it’s value by address: %d\n",p, *p);
return 0;
}
If I know the name of the variable, I can get it’s value by name: 5342
If I know the address of the variable is:12ff7c, then I also can get it’s value by address: 5342
简单描述模拟程序并运行
让学生留意 int *p,一种新的指针类型变 量定义
7
8.1.2 地址和指针-指针的概念
内存单元 地址 内容 变量
int x = 20, y = 1, z = 155;
printf("%d", x;) 直接访问:通过变量名访问
间接访问:通过另一个变量访问 把变量的地址放到另一变量中 使用时先找到后者 再从中取出前者的地址 1000 20 x
1002 1 y 1004 155 z
2000 1000 p 2002
地址 指针变量
通过对图的分析,详细说明直接访问和间 接访问。
8
指针
内存单元 地址 内容 变量
int x = 20, y = 1, z = 155;
printf("%d", x;) 1000 20 x
1002 1 y 1004 155 z
2000 1000 p 2002
地址 指针变量
指针变量:存放地址的变量 某个变量的地址
指向
通过例子说明指针变量的概念 提出 “指向”关系
9
指针变量所指向的变量的类型 int *p;
p 是整型指针,指向整型变量 float *fp;
fp 是浮点型指针,指向浮点型变量 char *cp;
cp 是字符型指针,指向字符型变量 类型名 *指针变量名
指针声明符
8.1.3 指针变量的定义
如何定义指针变量
强调定义时类型名是指针变量所指向的 变量类型
10
指针变量的定义
类型名 *指针变量名 int *p;
¨指针变量名是p,不是*p
¨*是指针声明符 int k, *p1, *p2;
等价于:
int k;
int *p1;
int *p2;
¨定义多个指针变量时,每一个指针变量 前面都必须加上*
注意:指针变量名是p,不是 *p
定义指针变量可以和与它所指变量类型 具有相同类型的变量放在同一条语句中 定义。
11
8.1.4 指针的基本运算
* 间接访问运算符,访问指针所指向的变量 printf("%d", *p); 输出p指向的变量a的值
a 3
&a p
*p
如果指针的值是某个变量的地址,通过指针就 能间接访问那个变量。
1、取地址运算和间接访问运算
& 取地址运算符 int *p, a = 3;
p = &a; 把 a 的地址赋给 p,即 p 指向 a 指针变量的类型和它所指向变量的类型相同
详细介绍取地址符&
一定要在黑板上画出如左图那样的示意 图,强调p 的内容是 a 的地址,然后画出 指向的箭头。
建议学生在初学指针时都必须要画出指 向关系图
12
# include <stdio.h>
int main (void) { int a = 3, *p;
p = &a;
printf (“a=%d, *p=%d\n”, a, *p);
*p = 10;
printf("a=%d, *p=%d\n", a, *p);
printf("Enter a: ");
scanf("%d", &a);
printf("a=%d, *p=%d\n", a, *p);
(*p)++;
printf("a=%d, *p=%d\n", a, *p);
return 0;
}
例8-2指针取地址运算和间接访问运算
a 3
&a p
*p
a = 3, *p = 3 a = 10, *p = 10 Enter a: 5 a = 5, *p = 5 a = 6, *p = 6
演示程序的运行
提示指针赋值语句 p=&a 然后画出指向关系图
13
a 3
&a p
*p (1) 当 p = &a 后,*p 与 a 相同 (2) int *p; 定义指针变量 p
*p=10; 指针p所指向的变量,即a (3) &*p 与 &a 相同,是地址
*&a 与 a 相同,是变量 (4) (*p)++ 等价于 a++
将p 所指向的变量值加1
*p++ 等价于 *(p++)
先取*p,然后 p 自加,此时p不再指向a
说明
int a = 1, x, *p;
p = &a;
printf(“%x\n”, p);
x = *p++;
printf(“%d,%x”, x, p);
难点:
*p++等价于*(p++)
*p++的值为 a 的值,因为 p++先返回 p 的
值作为表达式的值,然后才是 p=p+1,这
时p 就不再指向 a 了
14
2、赋值运算
a 3
&a p1
&a p2
*p1
*p2 int a = 3, *p1, *p2;
p1 = &a;
把a 的地址赋给 p1,即 p1 指向 a p2 = p1;
p2 也指向 a 相同类型的指针才能相互赋值
指针赋值会建立指向关系
但只有相同类型的指针才能互相赋值
15
1) 指针变量在定义后也要先赋值再引用 2) 在定义指针变量时,可以同时对它赋初值
int a;
int *p1 = &a;
int *p2 = p1;
3) 不能用数值作为指针变量的初值,但可以将一 个指针变量初始化为一个空指针
int *p=1000;
p = 0;
p = NULL;
p = (int*)1732;
8.1.5 指针变量的初始化
使用强制类型转换 (int*) 来避免编译错误,不提倡
注意:
(1)在指针变量定义或者初始化时变量
名前面的“*”只表示该变量是个指针变
量,它既不是乘法运算符也不是间接访问 符。
(2)把一个变量的地址作为初始化值赋
给指针变量时,该变量必须在此之前已经 定义。因为变量只有在定义后才被分配存 储单元,它的地址才能赋给指针变量。
(3)可以用初始化了的指针变量给另一
个指针变量作初始化值。
(4)不能用数值作为指针变量的初值,
但可以将一个指针变量初始化为一个空 指针。例如
int *p=1000;
是不对的,而 int *p=0;
是将指针变量初始化为空指针。这里0 是
ASCII 字符 NULL 的值。
(5)指针变量定义时的数据类型和它所
指向的目标变量的数据类型必须一致,因 为不同的数据类型所占用的存储单元的 字节数不同。
16 提出问题:怎样通过函数调用实现2 个变 量的互换?
有三套交换方案,哪套会成功?可以让学 生猜测那个swap()可以交换变量 a 和 b 的 值
讲这节时还可以联系历史故事狸猫换太 子,怎么做狸猫和太子的互换。
17
例8-3 指针作为函数参数模拟角色互换
int main (void) { int a = 1, b = 2;
int *pa = &a, *pb = &b;
void swap1(int x, int y), swap2( int *px, int *py ), swap3 (int *px, int *py);
swap1 (a, b);
printf (“After calling swap1: a=%d b=%d\n”, a, b);
a = 1; b = 2;
swap2(pa, pb);
printf (“After calling swap2: a=%d b=%d\n”, a, b);
a = 1; b = 2;
swap3(pa, pb);
printf (“After calling swap3: a=%d b=%d\n”, a, b);
return 0;
}
调用哪个函数,可以交换main () 中变量a和b的值?
1、 介绍程序框架,注意 3 个 swap()函数 2、 让同学观察 3 个 swap()函数的不同
18
例8-3 swap1()
swap1 (a, b);
void swap1 (int x, int y) { int t;
t = x;
x = y;
y = t;
}
swap1()的形参是普通整型变量
19
例8-3 swap2()
swap2 (&a, &b);
void swap2 (int *px, int *py) { int t;
t = *px;
*px = *py;
*py = t;
}
swap2()的形参是指针变量 8.2
…
! swap1()
! swap2()
! swap3()
20
例8-3 swap3()
swap3 (&a, &b);
void swap3 (int *px, int *py) { int *pt;
pt = px;
px = py;
py = pt;
}
After calling swap1: a=1, b=2 After calling swap2: a=2, b=1 After calling swap3: a=1, b=2
swap3()的形参也是指针变量
看运行结果,swap2()实现了两个变量的互 换,而其他两个没有
21
8.2.2 指针作为函数参数
n函数参数包括实参和形参,两者的类型要 一致, 可以是指针类型
n如果实参是某个变量的地址,相应的形参 就是指针
n在C语言中实参和形参之间的数据传递是单 向的“值传递”方式
1、 回顾形参和实参
2、 按值调用和引用调用的区别
3、 在函数定义时,将指针做为函数的参 数,在函数调用时,要把同类型指针 或者变量的地址作为实参
4、 提示学生注意区分 3 个 swap()的形参 区别
22
例8-3 swap1()
a
1 2
b
x
1 2
y
2 1
swap1 (a, b);
void swap1 (int x, int y) { int t;
t = x;
x = y;
y = t;
}
在swap1()函数中改变了形参x,y的值 但不会反过来影响到实参的值
swap1()不能改变main()函数中实参a和b 的值
1、 提示实参是 a 和 b,形参是 x 和 y 2、 函数 swap1()使用的是变量调用,也就
是值调用
3、 分析函数调用过程:
a) 实参 a 和 b 分别把自己的值赋给 了可看作函数局部变量的x 和 y b) 函数局部变量 x 和 y 在函数中进
行了交换
c) 当函数调用结束,x 和 y 也被销
毁,不再存在;但原来的变量 a
和b 仍然保持原值,没有发生任
何改变
4、 小结:参数的传递是从实参变量到形 参变量的单方向上值的传递,即使在 swap1()函数中改变了形参的值,也不 会反过来影响到实参的值。因此,调 用swap1()不能改变 main()函数中实参 a 和 b 的值。
23
例8-3 swap2()
swap2 (&a, &b);
void swap2 (int *px, int *py) { int t;
t = *px;
*px = *py;
*py = t;
}
a b
px py
1 2
值传递,地址未变,
但存放的变量值改变了
2 1
在swap2()函数中交换*px和*py的值,主 调函数中a和b的值也相应交换了
1、 让学生注意观察形参和实参 2、 分析函数调用过程:
a) 在函数 swap2()被调用时,将实参 a 的地址和 b 的地址传递给形参 px 和 py。这样,px 和 py 中分别 存放了a 和 b 的地址,px 指向 a,
py 指向 b
b) swap2()执行过程中把 px 所指向
的单元内容和 py 所指向的单元
内容作了互换
c) 函数结束,px 和 py 被销毁,但 a 和 b 的值已经发生改变 3、 小结:在函数 swap2()中交换*px 和*py
的值,主调函数中 a 和 b 的值也相应 交换了,即达到了交换数据的目的 24
例8-3 swap3()
swap3 (&a, &b);
void swap3 (int *px, int *py) { int *pt;
pt = px;
px = py;
py = pt;
}
a b
px py
1 2
值传递,形参指针的改变 不会影响实参
swap3()中直接交换了形参指针px和py的值
1、 让学生注意观察形参和实参,和前面 两个函数比较
2、 分析函数调用过程:
a) 在函数 swap3()被调用时,将实参 a 的地址和 b 的地址传递给形参 px 和 py。这样,px 和 py 中分别 存放了a 和 b 的地址,px 指向 a,
py 指向 b
b) Swap3()执行过程中把 px 和 py 的值作了互换,但a 和 b 的值然 后保持不变
c) 函数结束,px 和 py 被销毁,但 a 和 b 的值已经发生改变 3、 小结:形参 px 和 py 的改变不会影响
实参,因此调用该函数并不能改变主 调函数main()中变量 a 和 b 值 25
指针作为函数参数的应用
swap2 (&a, &b);
void swap2 (int *px, int *py) { int t;
t = *px;
*px = *py;
*py = t;
}
要通过函数调用来改变主调函数中某个变量的值:
(1) 在主调函数中,将该变量的地址或者指向该变量的 指针作为实参
(2) 在被调函数中,用指针类型形参接受该变量的地址 (3) 在被调函数中,改变形参所指向变量的值
a b
px py
12 21
分析什么情况下可以通过函数调用来改 变主调函数中变量的值:
可以把指针作为函数的参数。在主调函数 中,将该变量的地址或者指向该变量的指 针作为实参。在被调函数中,用指针类型 形参接受该变量的地址,并改变形参所指 向变量的值。
26
通过指针实现函数调用返回多个值
例8-4 输入年和天数,输出对应的年、月、日。
例如:输入2000和61,输出2000-3-1。
定义函数month_day(year, yearday, *pmonth, *pday) 用2个指针作为函数的参数,带回2个结果 int main (void)
{
int day, month, year, yearday;
void month_day(int year,int yearday, int *pmonth,int *pday);
printf(“input year and yearday: ”);
scanf ("%d%d", &year, &yearday );
month_day (year, yearday, &month, &day );
printf ("%d-%d-%d \n", year, month, day );
return 0;
}
1、 先 提 出 背 景 问 题 : 函 数 只 能 通 过 return 语句返回 1 个值。如果希望函数 调用能将多个计算结果带回主调函数,
用return 语句是无法实现的
2、 解决方案:将指针作为函数的参数就 能使函数返回多个值
3、 演示运行例子
27 例8-4
void month_day ( int year, int yearday, int * pmonth, int * pday) { int k, leap;
int tab [2][13] = {
{0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 }, {0, 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 }, };
/* 建立闰年判别条件leap */
leap = (year%4 == 0 && year%100 != 0) || year%400 == 0;
for ( k = 1; yearday > tab[leap][k]; k++) yearday = yearday-tab [leap][k];
*pmonth = k;
*pday = yearday;
}
input year and yearday: 2000 61 2000-3-1
month day
pmonth pday
3 1
1、 分析算法设计,特别是 2 维数组的用 途
2、 让学生回顾怎么判断闰年
3、 分析程序执行:在 main()函数中调用 函数month_day()时,将变量 month 和 day 的地址作为实参,在被调函数中,
用形参指针 pmonth 和 pday 分别接收 地址,并改变了形参所指向变量的值。
因此,main()函数中 month 和 day 的值 也随之改变
28 8.3 冒泡排序-
程序解析
void bubble(int a[ ], int n);
int main(void) { int n, a[8]; int i;
printf("Enter n (n<=8): ");
scanf("%d", &n);
printf("Enter a[%d] : ",n);
for (i=0; i<n;i++) scanf("%d",&a[i]);
bubble(a, n);
printf("After sorted, a[%d] = ", n);
for (i=0; i<n; i++) printf("%3d",a[i]);
return 0;
}
void bubble(int a[ ], int n) { int i, j;
for( i = 1; i < n; i++ ) for (j = 0; j < n-i; j++ )
if (a[j] > a[j+1]){
t=a[j]; a[j]=a[j+1]; a[j+1]=t;
} }
Enter n (n<=8): 8 Enter a[8] : 7 3 66 3 -5 22 -77 2 After sorted, a[8] = -77 -5 2 3 3 7 22 66
1、 简要回顾选择排序
2、 运行冒泡排序程序,不用详细讲解 3、 引入数组作为函数形参
29
8.3.2 数组和地址间的关系
int a[100], *p;
数组名代表一个地 址,它的值是数 组首元素的地址
(基地址)
a+i是数组a的基 地址的第i个偏移 量
3000 a[0]
地址 内容 数组元素 3002 a[1]
3198 a[99]
a[i]
a a+1
a+99 a+i
&a[i] *(a+i)
sum = 0;
for(i = 0; i < 100; i++) sum = sum + a[i] ;*(a+i)
下标运算符[ ]的含义
1、在声明数组时,编译器必须分配基地 址和足够的存储空间,以存储数组的 所有元素
2、数组的基地址是在内存中存储数组的
起始位置,它是数组中第1 个元素(下
标为0)的地址,因此数组名本身是一
个地址即指针值
3、指针是以地址作为值的变量,而数组 名的值是一个特殊的固定地址,可以 把它看作是常量指针
4、数组下标运算符的含义和指针偏移的 对应关系
30
指针和数组的关系
任何由数组下标来实现的操作都能用指针来 完成
int a[100], *p;
p = a;
或 p = &a[0];
p p+1
p+99 p+i
3000 a[0]
地址 内容 数组元素 3002 a[1]
3198 a[99]
a[i]
a a+1
a+99 a+i
&a[i] a[i]
a+i *(a+i)
p+i *(p+i)
&p[i] p[i]
p = a;
sum = 0;
for(i = 0; i < 100; i++) sum = sum + p[i];
等价 等价
通过示意图来详细说明地址、数组和指针 的关系
31
用指针完成对数组的操作
int a[100], *p;
移动指针 p 3000 a[0]
地址 内容 数组元素 3002 a[1]
3198 a[99]
a[i]
a a+1
a+99 a+i
sum = 0;
for(p = a; p <= &a[99]; p++) sum = sum + *p;
p p p
把上例从数组操作改为对指针操作
32
# include <stdio.h>
int main (void) { double a[2], *p, *q;
p = &a[0];
q = p + 1;
printf ("%d\n", q - p);
printf ("%d\n", (int) q - (int) p);
return 0;
}
例8-6 使用指针计算数组元素个数和 数组元素的存储单元数
1 指针p和q之间元素的个数 8
指针p和q之间的字节数 地址值
p q
3000 a[0]
地址 内容 数组元素 3008 a[1]
a a+1
注意 q-p 和(int)q-(int)p 的含义上的差别和 结果的不同
33
double *p, *q;
nq - p
两个相同类型的指针相减,表示它们之间相隔的存储单元的 数目
np + 1 / p-1
指向下一个存储单元/ 指向上一个存储单元 n其他操作都是非法的
指针相加、相乘和相除,或指针加上和减去一个浮点数 np < q
两个相同类型指针可以用关系运算符比较大小
指针的算术运算和比较运算
p q
3000 a[0]
地址 内容 数组元素 3008 a[1]
a a+1
强度非法的指针操作类型,让学生思考两 个相同类型的指针变量能不能相加?为 什么?
34
int main(void) { int i, a[10], *p;
long sum = 0;
printf("Enter 10 integers: ");
for(i = 0; i < 10; i++) scanf("%d", &a[i]);
for ( i = 0; i < 10; i++) sum = sum + a[i];
printf("calculated by array, sum=%ld \n", sum);
sum=0;
for(p = a; p <= a+9; p++) sum = sum + *p;
printf("calculated by pointer, sum=%ld \n", sum);
return 0;
}
例8-7 分别使用数组和指针计算数组元素之和
Enter 10 integers:10 9 8 7 6 5 4 3 2 1
calculated by array, sum=55 calculated by pointer, sum=55 p 3000 a[0]
地址 内容 数组元素 3002 a[1]
3018 a[9]
a[i]
a a+1
a+9 a+i p p p
通过该例子来总结使用数组和指针可以 实现相同的操作
35
数组元素作为函数实参时,函数形参为变量 与变量作为函数实参相同,值传递
8.3.3 数组名作为函数的参数
double fact (int n);
int main(void ) {
int i, n = 5;
double sum;
sum = 0;
for(i = 1; i <= n; i++ ) sum = sum + fact (i);
printf("sum = %e\n", sum);
return 0;
}
double fact (int n) { int i;
double result = 1;
for (i = 1; i <= n; i++) result = result * i ; return result ; }
int a[5]={1, 4, 5, 7, 9};
fact(a[i-1]);
1!+4!+5!+7!+9!
数组元素作为参数实参时,形参为普通 变量
36
n数组名是指针常量,相当于指针作为函数的参数
n数组名做为实参,形参是指针变量(数组)
数组名作为函数的参数
int sum (int *a, int n) { int i, s = 0;
for(i=0; i<n; i++) s += a[i];
return(s);
}
例
int main(void ) { int i;
int b[5] = {1, 4, 5, 7, 9};
printf("%d\n", sum(b, 5));
return 0; } ( 1) 实参是数组名 ( 2) 形参是指针变量
可以写成数组形式 inta[ ]
*(a+i)
1、数组名作为函数实参时,形参是指针 变量(或者数组)
2、在函数定义中,被声明为数组的形参 实际上是一个指针
3、当传递数组时,按值调用传递它的基 地址,数组元素本身不被复制
4、作为一种表示习惯,编译器允许在作 为参数声明的指针中使用数组方括号
37
int sum (int *a, int n) { int i, s = 0;
for(i=0; i<n; i++) s += a[i];
return(s);
}
int main(void ) { int i;
int b[5] = {1, 4, 5, 7, 9};
printf("%d\n", sum(b, 5));
return 0;
}
b b[0]
b[5] a
sum(b, 5) b[0]+b[1]+...+b[4]
sum(b, 3) b[0]+b[1]+b[2]
sum(b+1, 3) b[1]+b[2]+b[3]
sum(&b[2],3) b[2]+b[3]+b[4]
结合图来分析sum 的几个不同调用
38
例8-8二分查找
mid=(low+high)/2
x==a[mid]
high=mid-1 假(0) x<a[mid]
真(非0)
真(非0) 假(0)
low=0;high=n-1
low<=high 假(0) 真(非0)
low=mid+1
low<=high 找到x,输出mid
真(非0) 假(0)
Not Found
介绍二分查找法的基本算法思路
39
int Bsearch(int *p, int n, int x) /* 二分查找函数 */
{ int low, high, mid;
low = 0; high = n - 1; /* 开始时查找区间为整个数组 */
while (low <= high) { /* 循环条件 */
mid = (low + high) / 2; /* 中间位置 */
if (x == p[mid])
break; /* 查找成功,中止循环 */
else if (x < p[mid]) high = mid - 1; /* 前半段,high前移 */
else low = mid + 1; /* 后半段,low后移 */
} if(low <= high)
return mid; /* 找到返回下标 */
else
return -1; /* 找不到返回-1 */
}
例8-8二分查找
以指针作为函数参数形式实现二分查找,
演示程序
40
n数组名做为函数的参数,在函数调用时,将实参数组 首元素的地址传给形参(指针变量),因此,形参也 指向实参数组的首元素。如果改变形参所指向单元的 值,就是改变实参数组首元素的值。
n或:形参数组和实参数组共用同一段存贮空间,如果 形参数组中元素的值发生变化,实参数组中元素的值 也同时发生变化。
a a[0]
a[5]
p
强调:形参数组和实参数组共用同一段存 储空间,可类别为同一地址的大楼的两个 名称。
41
8.3.4 冒泡排序算法分析
相邻两个数比较,小的调到前面,大的调到后面 9 8 8 8 8 8 5 4 4 0 8 9 5 5 5 5 4 5 0 4 5 5 9 4 4 4 6 0 5 4 4 4 9 6 6 0 6 6 6 6 6 9 0 8 0 0 0 0 0 9
i=1
i=2 i=3
i=4 i=5
详细分析冒泡排序的过程
42
9 8 5 4 6 0 i=1
j=0: 8 9 5 4 6 0 j=1: 8 5 9 4 6 0 j=2: 8 5 4 9 6 0 j=3: 8 5 4 6 9 0 j=4: 8 5 4 6 0 9 int main(void )
{ int i, j, n, t, a[10];
n = 6;
for(i = 0; i < n; i++) scanf("%d", &a[i]);
for(i = 1; i < n; i++) for(j = 0; j < n-i; j++)
if(a[j] > a[j+1]) { t = a[j];
a[j] = a[j+1];
a[j+1] = t;
} return 0;
}
没调用排序函数的程序写法,主要为验证 前面分析的算法思想
43
int main(void ) { int i, a[10];
for(i=0; i<10; i++) scanf("%d", &a[i]);
sort(a, 10);
for(i=0; i<10; i++) printf("%d ", a[i]);
printf("\n");
return 0;
} void sort(int *array, int n) {
int i, j, t;
for(i=1; i<n; i++) for(j=0; j<n-i; j++)
if(array[j]>array[j+1]){
t = array[j];
array[j] = array[j+1];
array[j+1] = t;
} }
为了实现算法可重用性,把排序功能提炼 为一个通用函数sort,注意形参
44
字符串:字符数组 字符指针
8.4.1 程序解析
8.4.2 字符串和字符指针 8.4.3 常用的字符串处理函数
8.4 电码加密
字符串是一种常用的特殊字符数组
45
8.4.1 程序解析-加密
# define MAXLINE 100 void encrypt(char *);
int main (void) { char line [MAXLINE];
printf ("Input the string: ");
gets(line);
encrypt (line);
printf (“%s %s\n”,“After being encrypted: ”, line);
return 0;
}
void encrypt( char *s) {
for ( ; *s != '\0'; s++) if (*s == 'z')
*s = 'a';
else
*s = *s+1;
}
Input the string:hello hangzhou After being encrypted: ifmmp!ibohaipv
通过例子来演示加密字符串,此处不用细 讲实现细节
46
8.4.2 字符串和字符指针
n字符串常量
"array"
"point"
¨用一对双引号括起来的字符序列
¨被看做一个特殊的一维字符数组,在内存 中连续存放
¨实质上是一个指向该字符串首字符的指针 常量
char sa[ ] = "array";
char *sp = "point";
字符串常量的特点:
1、是一对双引号括起来的字符序列 2、由'\0'标识结束
3、实质上是一个指向该字符串首字符 的指针常量
字符数组和字符指针都可以用来处理字 符串:
1、如果定义一个字符指针接收字符串常 量的值,该指针就指向字符串的首字 符
2、注意给出的两种初始化方法
47
char sa[ ] = "array";
char *sp = "point";
printf("%s ", sa);
printf("%s ", sp);
printf("%s\n", "string");
array point string
printf("%s ", sa+2);
printf("%s ", sp+3);
printf("%s\n", string"+1);
ray nt tring
数组名sa、指针sp和字符串 "string" 的值都是 地址
明确数组名、指针和字符串常量的值都是 地址
48
字符数组与字符指针的重要区别
char sa[ ] = "This is a string";
char *sp = "This is a string";
sa Thi s i s a st r i ng\0
sp Thi s i s a st r i ng\0 如果要改变数组sa所代表的字符串,只能改变
数组元素的内容
如果要改变指针sp所代表的字符串,通常直接 改变指针的值,让它指向新的字符串
看图讲述字符数组与字符指针的重要区 别
49
示例
char sa[ ] = "This is a string";
char *sp = "This is a string";
strcpy (sa, "Hello");
sp = "Hello";
sa = “Hello”; 非法
数组名是常量,不能对它赋值
给数组名直接赋值是非法的
50
字符指针-先赋值,后引用
定义字符指针后,如果没有对它赋值,指针的值不 确定。
char *s ; scanf(“%s”, s);
char *s, str[20];
s = str;
scanf(“%s”, s);
定义指针时,先将它的初值置为空 char *s = NULL
不要引用未赋值的指针
先定义,再使用 先赋值,再使用
良好的习惯:把指针初始化为NULL
51
加密函数的两种实现
void encrypt (char s[ ]) {
int i;
for(i = 0; s[i] != '\0'; i++) if (s[i] == 'z')
s[i] = 'a';
else s[i] = s[i]+1;
} void encrypt( char *s) {
for ( ; *s != '\0'; s++) if (*s == 'z')
*s = 'a';
else
*s = *s+1;
}
改写和比较加密函数的不同实现方式
52
8.4.3 常用的字符串处理函数
n函数原型在stdio.h 或 string.h 中给出
1、字符串的输入和输出
¨输入字符串:scanf( )或gets( )
¨输出字符串:printf( )或puts( )
¨stdio.h
使用这些字符串处理函数别忘记包含相 应的头文件
53
char str[80];
i = 0;
while((str[i] = getchar( ))!= '\n') i++;
str[i] = '\0';
(1) scanf("%s", str)
输入参数:字符数组名,不加地址符
遇回车或空格输入结束,并自动将输入的一串字符和‘\0’
送入数组中 (2) gets(str)
遇回车输入结束,自动将输入的一串字符和‘\0’送入数 组中
字符串的输入
gets()可以输入含空格的字符串,而 scanf() 则不能
54
char str[80];
for(i = 0; str[i] != ‘\0 ’; i++) putchar(str[i]);
(3) printf("%s", str) printf("%s", "hello");
(4) puts(str) puts("hello");
输出字符串后自动换行
输出参数可以是字符数组名或字符串常量,输出遇'\0' 结束
字符串的输出
puts()输出字符串后会自动换行
55
#include <stdio.h>
int main( ) { char str[80];
scanf("%s", str);
printf("%s", str);
printf("%s", "Hello");
return 0;
}
例8-10 字符串输入输出函数示例
#include <stdio.h>
int main( ) { char str[80];
gets(str);
puts(str);
puts("Hello");
return 0;
Programming } ProgrammingHello Programming is fun!
ProgrammingHello
Programming Programming
Hello Programming is fun!
Programming is fun!
Hello
先让学生回答会有什么样的输出结果,在 给出答案
56
2、字符串的复制、连接、比较、
求字符串长度
¨字符串复制:strcpy(str1, str2)
¨字符串连接:strcat(str1, str2)
¨字符串比较:strcmp(str1, str2)
¨求字符串长度:strlen(str)
¨string.h
几个常用的字符串处理函数
57
strcpy(str1, str2);
将字符串str2 复制到 str1 中
static char str1[20];
static char str2[20] = “happy”;
字符串复制函数strcpy()
h a p p y \0
\0
strcpy(str1, str2); str1中 h a p p y \0 strcpy(str1, “world”); str1中:w o r l d \0
可 以 让 学 生 考 虑 如 果 自 己 编 写 一 个 strcpy()函数,应该如何实现
58
# include "stdio.h"
# include "string.h"
int main(void ) {
char str1[20], str2[20];
gets(str2);
strcpy(str1,str2);
puts(str1);
return 0;
}
strcpy() 示例
1234 1234
演示例子,可以看出strcpy()相当于字符串 的赋值
提问为什么不能直接写作:str=str2
59
strcat(str1, str2);
连接两个字符串str1和str2, 并将结果放入str1中
字符串连接函数strcat
# include "stdio.h"
# include "string.h"
int main(void) {
char str1[80], str2[20];
gets(str1);
gets(str2);
strcat(str1, str2);
puts(str1);
return 0;
}
str1中: Let us \0 str2中:go.\0 str1中: Let us go.\0 str2中:go.\0
Let us go.
Let us go.
str1=str1+str2 非法!
通过例子说明strcat()的作用 相当于字符串的加法
60
strcmp(str1, str2)
比较 两个字符串str1和str2的大小。
规则:按字典序(ASCII码序)
¨如果str1 和 str2 相等,返回 0;
¨如果str1 大于 str2 ,返回一个正整数;
¨如果str1 小于 str2 ,返回一个负整数;
static char s1[20] = "sea";
字符串比较函数strcmp
strcmp(s1, "Sea");
strcmp("Sea", "Sea ");
strcmp("Sea", "Sea");
正整数 负整数 0
字符串比较大小,注意规则
61
# include "stdio.h"
# include "string.h"
int main(void ) { int res;
char s1[20], s2[20];
gets(s1);
gets(s2);
res = strcmp(s1, s2);
printf("%d", res);
return 0;
}
strcmp() 示例
1234 2 -1
深入学习提示:如何实现strcmp()?
关键代码片段:
char *s1 = str1;
char *s2 = str2;
char c1, c2;
do{
c1 = *s1++;
c2 = *s2++;
if (c1 == '\0') return c1 - c2;
} while (c1 == c2);
return c1 - c2;
62
利用字符串比较函数比较字符串的大小 strcmp(str1, str2);
为什么定义这样的函数?
用strcmp()比较字符串
strcmp(str1, str2) > 0 strcmp(str1, "hello") < 0 strcmp(str1, str2) == 0 str1 > str2
str1 < "hello"
str1 == str2
比较字符串首元素的地址 比较字符串的内容
对比说明含义
63
nstrlen(str)
计算字符串的有效长度,不包括‘\0’。
static char str[20]="How are you?"
strlen ("hello") 的值是:
strlen(str) 的值是:
字符串长度函数strlen
5 12
注意:长度不包括’\0’,但数组大小要能存 放
64
函数 功能 头文件
puts(str) 输出字符串 stdio.h gets(str) 输入字符串(回车间隔)
strcpy(s1,s2) s2 ==> s1 strcat(s1,s2) s1 “+” s2 ==> s1
若s1“==”s2, 函数值为0 strcmp(s1,s2) 若 s1 “>” s2, 函数值 >0 string.h
若s1 “<” s2, 函数值<0 计算字符串的有效长度,
strlen(str) 不包括 ‘\0’
字符串处理函数小结
小结
65
int main( ) { int i;
int x, min;
scanf("%d", &x);
min = x;
for(i = 1; i < 5; i++){
scanf("%d", &x);
if(x < min) min = x;
}
printf("min is %d\n", min);
return 0;
}
例8-11 求最小字符串#include <string.h>
int main( ) { int i;
char sx[80], smin[80];
scanf("%s", sx);
strcpy(smin,sx);
for(i = 1; i < 5; i++){
scanf("%s", sx);
if(strcmp(sx, smin)<0) strcpy(smin,sx);
}
printf("min is %s\n", smin);
return 0;
} 2 8 -1 99 0 min is –1
tool key about zoo sea min is about
先回顾求最小值的程序 再对比求最小字符串
重点描述字符串处理函数的应用
66
8.5 任意个整数求和 *
例8-12 先输入一个正整数n,再输入任意n个 整数,计算并输出这n个整数的和。
要求使用动态内存分配方法为这n个整数分 配空间。
此节为选学部分
首先引入背景问题:为什么需要动态内存 分配?
然后给出例子,说明和以往数组程序的区 别,这里没有要求n 要小于多少
67 8.5.1 程序解析
int main ( ) { int n, sum, i, *p;
printf("Enter n: ");
scanf("%d", &n);
if ((p = (int *) calloc (n, sizeof(int))) == NULL) { printf("Not able to allocate memory. \n");
exit(1);
}
printf("Enter %d integers: ", n);
for (i = 0; i < n; i++) scanf("%d", p+i);
sum = 0;
for (i = 0; i < n; i++) sum = sum + *(p+i);
printf("The sum is %d \n",sum);
free(p);
return 0;
}
Enter n:10
Enter 10 integers:3 7 12 54 2 –19 8 –1 0 15 The sum is 81
演示程序,简要说明一下,让学生注意新 的内存分配相关函数
68
8.5.2 用指针实现内存动态分配
n变量在使用前必须被定义且安排好存储空间
n全局变量、静态局部变量的存储是在编译时 确定,在程序开始执行前完成。
n自动变量,在执行进入变量定义所在的复合 语句时为它们分配存储,变量的大小也是静 态确定的。
n一般情况下,运行中的很多存储要求在写程 序时无法确定。
分析不同种类变量的内存分配机制的差 别
69
动态存储管理
n不是由编译系统分配的,而是由用户在程 序中通过动态分配获取。
n使用动态内存分配能有效地使用内存
¨使用时申请
¨用完就释放
同一段内存可以有不同的用途
强调动态内存管理的高效性,一定要用完 释放,否则将造成内存泄漏
70
动态内存分配的步骤
(1)了解需要多少内存空间
(2)利用C语言提供的动态分配函数来分配 所需要的存储空间。
(3)使指针指向获得的内存空间,以便用指 针在该空间内实施运算或操作。
(4)当使用完毕内存后,释放这一空间。
内存分配的典型步骤
71 强调要检查内存分配不成功时候的出错
处理
难点:讲透(void *)通用指针类型
malloc 只负责申请一块大小为 size 的连续 内存,但该段内存空间如何划分需要通过
强制转化实现。比如100 字节的内存块可
以划分为 50 个整形单元(假设每个整形
占 2 个字节),也可以划分为 25 个 float 型单元(假设每个float 型占 4 个字节)
72
malloc()示例
/* 动态分配n个整数类型大小的空间 */
if ((p = (int *)malloc(n*sizeof(int)))== NULL) { printf(“Not able to allocate memory. \n”);
exit(1);
}
n调用malloc时,用 sizeof 计算存储块大小
n每次动态分配都要检查是否成功,考虑例外情况处理
n虽然存储块是动态分配的,但它的大小在分配后也是 确定的,不要越界使用。
强调用 sizeof 来计算存储块的大小,不要 直接填字节数,因为操作系统平台不一样 可能同一类型占的字节数不同
强调不要越界使用
73
计数动态存储分配函数calloc()
void *calloc( unsigned n, unsigned size) 在内存的动态存储区中分配n个连续空间,每一存储空间的
长度为size,并且分配后还把存储块里全部初始化为0
¨若申请成功,则返回一个指向被分配内存空间的起始地 址的指针
¨若申请内存空间不成功,则返回NULL
¨malloc对所分配的存储块不做任何事情
¨calloc对整个区域进行初始化
对比malloc()
calloc()分配后会初始化为 0
可以类比宾馆房间分配,malloc()是只负责 分配,但里面不打扫的,原来怎样就保持 原样;但calloc()会打扫干净,像新的一样 malloc()
void *malloc(unsigned size)
size
!
! NULL
! void (*)
"
" malloc
74
动态存储释放函数free()
void free(void *ptr)
释放由动态存储分配函数申请到的整块内存空间,
ptr为指向要释放空间的首地址。
当某个动态分配的存储块不再用时,要及时 将它释放
强调要及时释放,但不能早释放。释放后 就不能再使用了
75
分配调整函数realloc()
void *realloc(void *ptr, unsigned size) 更改以前的存储分配
¨ptr必须是以前通过动态存储分配得到的指针
¨参数size为现在需要的空间大小
¨如果调整失败,返回NULL,同时原来ptr指向存储块的 内容不变。
¨如果调整成功,返回一片能存放大小为size的区块,并 保证该块的内容与原块的一致。如果size小于原块的大 小,则内容为原块前size范围内的数据;如果新块更大,
则原有数据存在新块的前一部分。
¨如果分配成功,原存储块的内容就可能改变了,因此不 允许再通过ptr去使用它。
分析各种可能性,讲述realloc()执行策略
76
本章小结
n指针的概念与定义
¨变量、内存单元与地址的关系
¨指针变量的定义与初始化 n指针作为函数参数
¨通过指针参数使函数返回多个值 n指针与数组
¨指针、数组与地址的关系
¨数组名作为函数参数 n指针与字符串
¨常用字符串处理函数
n*指针实现内存动态分配
•能够掌握指针概念,定义 指针变量和指针基本运算
•能够掌握指针作为函数的 参数进行熟练编程,通函 数调用改变主调函数变量 的值
•作为函数参数进行熟练编 程,并能利用指针进行数 组相关操作
•能够使用字符串常用处理 函数进行编程,并能使用 字符指针进行字符串相关 操作
•了解通过指针实现动态内 存分配,并能进行编程
回顾本章,总结要点
8.3 练习与习题答案
8.3.1 练习参考答案
8-1 如果有定义:int m, n = 5, *p = &m; 与 m = n 等价的语句是 B 。 A.m = *p; B. *p = *&n; C. m = &n; D. m = **p;
8-2 调用函数求两个数的和与差:计算输入的两个数的和与差,要求自定义一个函数 sum_diff(float op1, float op2, float *psum, float *pdiff),其中 op1 和 op2 是输入的两个数,
*psum 和*pdiff 是计算得出的和与差。
解答:
#include <stdio.h>
int main (void) {
float op1, op2, sum, diff;
void sum_diff(float op1, float op2, float *psum, float *pdiff);
printf(“input op1 and op2: “);
scanf(“%f%f”, &op1, &op2);
sum_diff(op1, op2, &sum, &diff);
printf(“%f+%f=%f; %f-%f=%f \n”,op1,op2,sum,op1,op2,diff);
return 0;
}
void sum_diff(float op1, float op2, float *psum, float *pdiff) {
*psum = op1 + op2;
*pdiff = op1 – op2;
}
8-3 两个相同类型的指针变量能不能相加?为什么?
解答:不能。因为指针变量是一种特殊的变量,指针变量的值存放的是所指向变量的地址,
两个地址相加并不能保证结果为一个有效的地址值,因而在 C 语言中指针变量相加是非法
的。
8-4 根据表 8.2 所示,这组数据的冒泡排序其实循环到第 6 遍(即 n-2)时就已经排好序了,
说明有时候并不一定需要n-1 次循环。请思考如何改进冒泡排序算法并编程实现(提示:当
发现一遍循环后没有数据发生交换,说明已经排好序了)。
解答:设置一个标志变量flag,进入一轮循环前设置为 0,在循环中有发生数据交换就改写
flag 值为 1。当该轮循环结束后检查 flag 值,如果变为 1 说明发生了数据交换,还没有排好
序,如果为0 说明没有发生交换,已经排好序。
#include <stdio.h>
void bubble (int a[ ], int n);
int main(void) {
int n, a[8];
int i;
printf("Enter n (n<=8): ");
scanf("%d", &n);
printf("Enter a[%d] : ",n);
for (i=0; i<n;i++) scanf("%d",&a[i]);
bubble(a,n);
printf("After sorted, a[%d] = ", n);
for (i=0; i<n; i++) printf("%3d",a[i]);
return 0;
}
void bubble (int a[ ], int n) /* n 是数组 a 中待排序元素的数量 */
{
int i, j, t, flag;
for( i = 1; i < n; i++ ) { /* 外部循环 */
flag=0;
for (j = 0; j < n-i; j++ ) /* 内部循环 */
if (a[j] > a[j+1]){ /* 比较两个元素的大小 */
t=a[j]; a[j]=a[j+1]; a[j+1]=t; /* 如果前一个元素大,则交换 */
flag=1; /* 发生交换,flag 置为 1 */
}
if (flag==0) /* 如果一轮循环没有发生数据交换,排序结束*/
break;
} }
8-5 重做例 8-5,要求使用选择排序算法。
解答:
#include <stdio.h>
void bubble (int a[ ], int n);
int main(void) {
int n, a[8];
int i;
printf("Enter n (n<=8): ");
scanf("%d", &n);
printf("Enter a[%d] : ",n);
for (i=0; i<n;i++) scanf("%d",&a[i]);
bubble(a,n);
printf("After sorted, a[%d] = ", n);
for (i=0; i<n; i++) printf("%3d",a[i]);
return 0;
}
void bubble (int a[ ], int n) /* n 是数组 a 中待排序元素的数量 */
{
int i, j, t, index;
for( i = 0; i < n-1; i++ ) { /* 外部循环 */
index=i;