• 沒有找到結果。

第 8 章 指针

N/A
N/A
Protected

Academic year: 2021

Share "第 8 章 指针"

Copied!
39
0
0

加載中.... (立即查看全文)

全文

(1)

第 8 章 指针

8.1 教学要点

本章主要介绍指针的基本概念、指针变量的定义、指针变量的基本运算、指针作为函数 的参数、指针和数组、指针和字符串、动态内存分配和使用等知识。首先要把基本概念讲清,

一定要结合图例来说明变量、内存单元和地址之间的关系;本章重点难点较多,讲解时注意 趣味性激发学生兴趣,通过对比演示来分析指针和普通变量的区别。

8.1 节通过示例程序“密码开锁”引出指针的概念和主要知识点,教师在讲授时,重点 分析密码开锁的过程,通过过程分析来说明变量、内存单元和地址之间的关系,从而引出指 针的基本概念,通过指针与所指向变量的示意图,加深学生对指针的理解。接着介绍指针的 基本运算以及指针变量的初始化。

8.2 节主要介绍指针作为函数的参数。该小节通过例子“角色互换”引入,演示使用指 针作为函数参数的用途和用法。在本小节中要提示学生注意观察函数的形参和实参,区分按

值调用和引用调用。在具体讲解时,通过详细分析3 个 swap()函数进行对比,讲解过程一定

要配合画出指针指向图来说明参数变量值是如何改变的。最后通过一个例子来讲解如何使用

指针作为函数参数返回多个值,解决以前一个函数最多只能return 一个值的问题。

8.3 节通过示例程序“冒泡排序”引入数组名作为函数参数。首先重点介绍指针、数组 和地址之间的关系,分析数组下标运算的含义和指针偏移的对应关系。通过例子来说明指针 和数组可以实现相同的操作。然后重点讲述如何使用数组名作为函数的参数,最后对冒泡排 序进行详细分析,总结回顾本节知识点。

8.4 节中通过示例程序“电码加密”引入字符指针与字符串处理。首先重点介绍字符指 针和字符串的关联和区别。然后对常用字符串处理函数进行讲解,通过对比示例强调字符串 运算的特殊性并分析原因。

8.5 节中通过示例程序“任意个整数求和”引入用指针实现内存动态分配。首先要分析 为什么要进行内存的动态分配管理,然后介绍动态内存分配的一般步骤和具体的分配函数。

最后可以通过和学生一起使用动态内存分配的方法来改写“冒泡排序”,做到融会贯通。

讲授学时:6 学时 实验学时:4 学时 本章的知识能力结构图见图8.1。

(2)

图 8.1 知识能力结构图 指针概念与

定义

指针作为函 数的参数

指针与数组

指针与字符

形参和实参 按值调用和 引用调用

通过指针参数使函数返回多个值 指针、数组与地址的关系

数组名作为函数参数 字符指针与字符 串的关联和区别 常用字符串处理

函数

能 够 掌 握 指针概念,

定 义 指 针 变 量 和 指 针 基 本 运

能 够 使 用 数 组 名 作 为 函 数 参 数 进 行熟练编程,并能 利 用 指 针 进 行 数 组相关操作

掌 握 通 过 指 针 实 现动态内存分配,

并能进行编程 指针与动态

内存分配*

变量、内存单元 与地址的关系

指针变量

定义方式 初始化 变量使用

动态内存分配的 概念和基本步骤 常用动态内存分

配函数

能 够 使 用 字 符 串 常 用 处 理 函 数 进 行编程,并能使用 字 符 指 针 进 行 字 符串相关操作

能 够 掌 握 指 针 作 为 函 数 的 参 数 进 行熟练编程,通函 数 调 用 改 变 主 调 函数变量的值

(3)

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 如果有另外一个地方存放了该密码存放地的地 址,那么就能顺藤摸瓜,间接找到密码

分析寻找密码的两种可能途径

(4)

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

地址 指针变量

指针变量:存放地址的变量 某个变量的地址

指向

通过例子说明指针变量的概念 提出 “指向”关系

(5)

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 然后画出指向关系图

(6)

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)指针变量定义时的数据类型和它所

指向的目标变量的数据类型必须一致,因 为不同的数据类型所占用的存储单元的 字节数不同。

(7)

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()

(8)

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 的值。

(9)

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

分析什么情况下可以通过函数调用来改 变主调函数中变量的值:

可以把指针作为函数的参数。在主调函数 中,将该变量的地址或者指向该变量的指 针作为实参。在被调函数中,用指针类型 形参接受该变量的地址,并改变形参所指 向变量的值。

(10)

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、数组下标运算符的含义和指针偏移的 对应关系

(11)

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;

nq - p

两个相同类型的指针相减,表示它们之间相隔的存储单元的 数目

np + 1 / p-1

指向下一个存储单元/ 指向上一个存储单元 n其他操作都是非法的

指针相加、相乘和相除,或指针加上和减去一个浮点数 np < q

两个相同类型指针可以用关系运算符比较大小

指针的算术运算和比较运算

p q

3000 a[0]

地址 内容 数组元素 3008 a[1]

a a+1

强度非法的指针操作类型,让学生思考两 个相同类型的指针变量能不能相加?为 什么?

(12)

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 的几个不同调用

(13)

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

详细分析冒泡排序的过程

(14)

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

通过例子来演示加密字符串,此处不用细 讲实现细节

(15)

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”; 非法

数组名是常量,不能对它赋值

给数组名直接赋值是非法的

(16)

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() 则不能

(17)

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()函数,应该如何实现

(18)

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;

(19)

62

利用字符串比较函数比较字符串的大小 strcmp(str1, str2);

为什么定义这样的函数?

strcmp()比较字符串

strcmp(str1, str2) > 0 strcmp(str1, "hello") < 0 strcmp(str1, str2) == 0 str1 > str2

str1 < "hello"

str1 == str2

比较字符串首元素的地址 比较字符串的内容

对比说明含义

63

nstrlen(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

先回顾求最小值的程序 再对比求最小字符串

重点描述字符串处理函数的应用

(20)

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使用动态内存分配能有效地使用内存

¨使用时申请

¨用完就释放

同一段内存可以有不同的用途

强调动态内存管理的高效性,一定要用完 释放,否则将造成内存泄漏

(21)

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

(22)

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 是计算得出的和与差。

(23)

解答:

#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]);

(24)

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;

參考文獻

相關文件

Agent: Okay, I will issue 2 tickets for you, tomorrow 9:00 pm at AMC pacific place 11 theater, Seattle, movie ‘Deadpool’. User:

[r]

平均会期(日) Duração média (dias) Average Duration (day). 活动数目 N o de

但是讀者還是應該可以揣測出 n 重積分的 Fubini 定理...

[r]

[r]

We compare the results of analytical and numerical studies of lattice 2D quantum gravity, where the internal quantum metric is described by random (dynamical)

• 我們通常用 nD/mD 來表示一個狀態 O(N^n) ,轉移 O(N^m) 的 dp 演算法. • 在做每題