当前位置:首页 >> 电脑基础知识 >>

厦门理工学院11级C语言 第07章 指针


第七章 指针
? 指针在C语言中占有重要的地位,是最具有特色

的语言成分,是C语言的精华。正确而灵活地使 用它,可以有效地表示复杂的数据结构;能动态 分配内存;直接处理内存地址;对内存中各种不 同的数据结构进行快速处理,也为函数间各类数 据的传递提供了简捷便利的方法。使用指针,可 以编制出简洁明快、功能强和质量高的程序。 ? 但指针也是最有风险的。譬如,未初始化的指针 可能造成系统的错误。也许夸张了,但指针确实 很容易被误用,使得系统造成难以发现的错误。 造成程序“挂死”的大部分原因都是由于错误地 使用指针或数组越界所造成的。

C程序设计中使用指针可以:
?使程序简洁、紧凑、高效
?有效地表示复杂的数据结构 ?动态分配内存

?得到多于一个的函数返回值

?

7.1 指针的基本概念
?

7.1.1预备知识

1.内存地址
? 在计算机硬件系统的内存储器中,拥有大量的存储单元(以字节为

单位)。为了便于管理,每个存储单元都有惟一的编号,这个编号 就是存储单元的“地址”。例如,对16位机,DOS环境下的应用程 序,其代码段、数据段和堆栈段放在位于内存地址0x0000~0xffff之 间的640KB常规内存中。也就是说,程序中的某一变量,对应 0x0000~0xfff范围内的某些存储单元。 ? 注:TC只能开发DOS下的16位命令行方式的应用程序。

2.变量的存储
? 回顾一下变量的定义:“变量代表内存中具有特定属性的一个存储

单元,它用来存放数据,也就是变量的值,在程序运行期间,这些 值是可以改变的,一个变量应该有一个名字,以便被引用。” 下面以一个简单的程序讨论一下变量在内存中的存储情况。 关键字: 变量名、存放变量的内存单元格、内存单元格的地址、变量值

下面以一个简单的程序讨论一下变量在内存中的存储情 况。

关键字: 变量名、存放变量的内存单元格、内存单元格的地址、变量 值

00000000H 00000001H 00000002H

变量a

1
变量b

一个程序片段
main( ) { int a=1; float b=2; int c[2]={5, 6}; char d=’d’; }

00000003H 00000004H 00000005H 00000006H 00000007H

2

5
6 ‘d’
…...

数组c

00000008H
00000009H 0000000AH 00000009H

变量d

变量的地址 存 变 的 存 元 00000000H 1 变量的值 放 量 内 单

变量名 a

00000001H

图7-2变量a的局部示意图
注:变量的地址是二进制的,为了便于书写而在这里写成对应的十六进 制形式。等读者熟悉后在以后的章节中则会直接用十进制来书写,以便于阅 读。

程序中分别定义了4个变量:int变量a,float变量b,int数 组c,char变量d,它们在内存中分别占据2个、4个、4个、 1个字节。其中变量a的首地址是00000000H(这是假设, 实际情况中程序定义的变量并不是从内存的0字节开始存 放的),则变量b的首地址是00000002H,数组c的首地 址是00000006H,变量d的首地址是0000000AH。首地 址就简称为变量的地址。

要访问变量首先就要知道变量的地址,可是通过数字形 式的地址值访问变量,显然是不方便的(正如使用URL 网址比IP地址要方便):

?不便于书写和记忆,而且数字本身没有什么具体的字面 意义。 ?需要了解硬件细节。比如当前哪些内存空间是空闲的等 等。

这就失去了高级语言容易使用、接近人类语言的优点。

好在C语言提供了变量名,程序员通过变量名来访问变 量,不需要知道变量的存储单元是如何开辟在内存的空 闲区的,也不需要关心变量的实际存放地址。变量名和 变量的地址之间由编译器和操作系统进行联系和转换 (最终当然还是要通过地址对变量进行访问),这个转 换过程对程序员来说是透明的。这样做显然是有好处的: ?变量名比地址好记而且可以表文达意,提高了程序的书 写性和可读性; ?普通程序员可以把更多的精力放在程序的逻辑实现上而 不需要过分关注计算机硬件系统的有关细节。

这些也正是高级语言的优点之表现。
注:常量是没有地址的。(以后通过对汇编语言的学习, 我们可以了解到常量的存储)

?

7.1.2指针就是地址 它们具有双重含义

什么是指针呢?指针其实就是地址!既然变量名 比变量地址使用起来方便,那么为什么还要引入指针 呢?这是因为指针可以给我们的程序带来意想不到的 灵活度,随着本章的深入学习,您一定会体会到这句 话的!

“指针就是地址”,因此对指针的认识要建立在对 地址的深刻理解之上。地址有两个方面的含义。 ?地址值(也就是内存单元的编址)。 ?是什么类型的数据的地址。这就存在着一个跨度也 就是存储空间大小的问题。(我们已经知道,不同的数 据类型其占据内存空间的大小是不同的 。比如对于一 个int变量的地址,应该是内存中某2个连续字节单元的 首地址;如果是一个float变量的地址,那么该指针应 该是内存中某4个连续字节单元的首地址)。

?

7.1.3指针其名

明白指针就是地址,这一点十分重要。多数情况 下,这个地址是内存中另一个变量的位置。如果一个 变量包含了另一个变量的地址,那么第1个变量就是个 指针变量而且说它是“指向”第2个变量的,“指针” 由此而得其名。例如,如果在地址为1000的变量指向 地址为1004的变量,那么也就是说地址为1000的这个 变量的值是1004。 为什么要表达为“指向”呢?下一节中将会看到 如果变量p的值是变量a的地址,则可以利用变量p来 访问和操作变量a(其实这是很自然的事情,有了某变 量的地址当然就可以访问该变量)。所以这样的变量p 和a之间是有种联系的,这种联系就被表达为“指向”。

图7-3解释了这一点,它仅仅用来对地址进行偏移。
内存地址 内存单元

1000 1001 1002 1003 1004

1004

图7-3一个变量指向另一个变量

?

7.1.4变量的指针与指针变量

1.变量的指针 一个变量x的地址就是该变量的指针,记作&x,即在变量 名前加上取地址运算符“&”。例如,变量x的地址是0x2000, 我们就说x的指针是0x2000。显然每个变量的地址或说指针都 是客观存在的,而且是个常量。

2.指针变量 大家都知道整型变量就是存放整数的变量,同理,专门 用来存放地址的变量称为指针变量。当指针变量中存放着某一 个变量的地址时,就称这个指针变量指向那一个变量。由于地 址或指针是常量,因此当我们需要对地址进行操作的时候一般 要用指针变量来保存该地址再做处理。

3.指针变量与它所指向的变量的关系 指针变量和一般变量既有联系又与区别。指针变量也是 变量,具有变量的特征,在内存中也占用一定的存储单元,也 有“地址”和“值”的概念。但指针变量的“值”不同于一般 变量的“值”,指针变量的“值”是另一实体(变量、数组或 函数等)的地址。

指针变量px与它所指向的整型变量x的关系,用指针运算 符“*”表示为:
“*px等价于变量x”

因此,下面四条语句的作用相同,都是将100赋给变量x: 1. x=100; 2. *px=100; /*将100直接赋给变量x*/ /*将100间接赋给变量x*/

3. *(&x)=100; /*将100间接赋给变量x*/ 4. *((int *)100h)=100 /*将100间接赋给变量x*/ (假设x的地址 为100h) 请考虑第4种访问方式:

这里之所以要进行类型转换是因为100h只是个地址值, 并没有关于其代表存储空间大小的描述,不具备我们前面讨论 的地址的双重含义。为了把100放到以100h为起始地址的连续 两个字节中,因此需要进行相应的类型转换,使得100h具有 跨度上的含义后它作为一个地址意义才完整,也才能正确地利 用它把100赋给变量x。

4.指针变量的长度 指针变量的长度可以是2个字节或4个字节,这取决于引 用者和被引用者之间在内存的距离,通常由系统自动决定,程 序员不必理会。(下述内容在学习汇编语言后更好理解,目前 可暂不关注) 编译系统根据设定的内存模式来安排代码段和数据段, 由此确定指针变量的长度。内存模式取决于代码段和数据段的 长度,早期的C系统将内存模式分为六种,如表7-1所示。

表 7-1内存模式
模式 代码段长度 数据段及静态数据长度

微型模式(Tiny Model)

代码段、数据段和数据组的总长度<64KB

代码段、数据段和数据组的总长度<64KB

小型模式(Tiny Model)

<64KB

<64KB

中型模式(Tiny Model)

无限制

<64KB

紧凑型模式(Tiny Model)

<64KB

无限制,但静态数据<64KB

大型模式(Tiny Model)

无限制

无限制,但静态数据<64KB

巨型模式(Tiny Model)

无限制

无限制

当数据段(或代码段)的长度在64KB以内时,其地址长 度不超过16位,因此保存这样地址的指针变量只需要2个字节 即可。当数据段(或代码段)的长度超过64kB时,其地址长 度超过16位,要保存这样的地址,指针变量需要4个字节。通 常指针变量的长度由系统自动决定,程序员只需根据程序中 代码和数据量的多少选用适当的内存模式即可,其他事都由 系统自行处理。 请注意: ? 指针变量中不仅仅只能存放变量的地址,例如还能存放函 数的入口地址等等,这里暂不作讨论。 ? 前面提到,常量是没有地址的。因此变量才有(指向它的) 指针/指针变量,亦即指针/指针变量只能指向变量。因此 “(指向)某类型的指针/指针变量”就是指“(指向)某 类型的变量的指针/指针变量”。比如:“(指向)整型的 指针/指针变量”就是指“(指向)整型变量的指针/指针 变量”等等,以此类推。

? 7.2指针变量的定义和赋值 ? 7.2.1指针变量的定义
指针也是C的一种数据类型,指针变量的定义形式和前 面学过的其他数据类型的变量之定义没有什么大的区别。 C数据类型的变量的一般定义形式 类型名关键字 变量名标识符[=初始值] ; 注:[ ]中的是 可选项 定义指针变量的一般形式为: 类型名关键字 *变量名标识符[=初始值];

注意 ?为了与普通变量区别开来,在变量名前加”*”来说明它是 指针变量。 ?“*”并不属于指针变量名标识符的部分。 ?“类型名关键字”是表示该指针是指向什么类型的变量的。

既然指针有两个方面的含义,那么在指针变量的定义中是 怎么体现这两方面的含义呢?

例如:

int a=1;/*定义了一个变量a,那么变量a在内存的空闲区 占据了2个字节的空间*/
int *p; /*(注意指针变量是p而不是*P)定义了一个指针 变量,这个指针变量是指向int变量的(即是用来盛放int变量 的地址的),所以这个指针变量中盛放的地址一定是内存中 某2个连续的字节单元空间的首地址。这就把跨度含义赋予了 该指针变量。*/ p=&a; /*把int变量a的首地址赋予该指针变量,对指针 的另一个含义“地址值”进行补充。*/ 当然了,上面的三个语句也可以简化为 int a=1; int *p=&a; 这样是在定义指针变量的同时就把地址值给了指针变量, 效果是一样的。 通过图 7-4可以更好地理解什么是指针变量:

int a=10; ……..

00000000H 00000001H ···· ···· a ···· ···· 00000005H 00000006H 10 ···· ···· ···· ····

……..(定义别的变量)
int * p; p=&a;

··· ··· ··

···· ···· P

···· ···· 00000064H 00000065H

00000005H ···· ···· ···· ···· ···· ···· 变量值

···· ···· ···· ···· ···· ···· 程序片段 变量名

···· ···· ···· ···· 40000000H 变量地址

图 7-4 变量在内存中的存储 分配

在上图中可以看到,变量p在内存中占据2个字节。只 不过它里面存放的是变量a的地址,这种关系表现为图7-5中 的箭头,它表示p“指向”a。

指针变量p

整型变量a,它的地址是 00000005H 1

00000005H

图7-5 指针变量p指向变 量a

?

7.2.2指针变量的赋值

指针变量可以通过不同的方法获得一个地址值。不管是 用什么方法,说白了就是用地址表达式给指针变量赋值,这 个地址表达式可以是常量,可以是变量,也可以是函数的返 回值。

注意:指针具有双重含义,所以对指针变量赋值的地址表 达式应该和定义指针变量的时候赋予的两方面的含义相符合, 否则就会出错。

1.通过取地址运算符“&”赋值
地址运算符号“&”是单目运算符,运算对象放在地址运 算符“&”的右边,用于求出运算对象的地址。通过地址运算 “&”可以把一个变量的地址赋给指针变量。 float f,*p; p=&f;

执行后把变量f的地址赋给指针变量p,指针变量p就指 向了变量f。
2.指针变量的初始化

可以在定义变量时给指针变量赋初值,如float f , *p=&f; , 则把变量f的地址赋值给指针变量p,此语句相当于float f , *p; p=&f;这两条语句。

3.通过其他指针变量赋值
可以通过赋值运算符,把一个指针变量的地址值赋给另 一个指针变量,这样两个指针变量均指向同一变量。例如, 有如下程序段: int i, *p1=&i, *p2; p2=p1;

执行后指针变量p1与p2都指向整型变量i。
注意,当把一个指针变量的地址值赋给另一个指针变量 时,赋值号两边指针变量所指的数据类型必须相同。例如:

int i , *pi=&i;
float * pf; 则语句pf=pi;是非法的,因为pf只能指向实型变量,而 不能指向整型变量。

4.用NULL给指针变量赋空值
除了给指针变量赋地址值外,还可以给指针变量赋空值, 如:

p=NULL;
NULL是在stdio.h头文件中定义的预定义标识符,因此在 使用NULL的时候,应该在程序中加上文件包含“stdio.h”。 在stdio.h头文件中NULL被定义成符号常量,与整数0对应。 执行以上的赋值语句后,p为空指针(和指向void空类型的指 针是不同的),在C语言中当指针值为NULL时,指针不指向 任何有效数据,因此在程序中为了防止错误地使用指针来存 取数据,常常在指针未使用之前,先赋初值为NULL。由于 NULL与整数0想对应,所以下面三条语句等价: p=NULL;或p=0 ;或p=’\0’; 但通常都使用p=NULL;的形式,因为这条语句的可读 性好。NULL可以赋值给指向任何类型的指针变量。

5.通过调用标准库函数赋值 可以调用库函数malloc、calloc等在内存中开辟动态存 储单元,并把所开辟的动态存储单元的地址赋给指针变量。 由于这两个函数返回的是“void *”无类型指针类型,因此将 它们的返回值赋值给指针变量的时候要进行强制类型转换。 注:这个动态存储单元到底如何“动态”以及malloc、 calloc函数的用法将在链表部分进行讲述。

?

7.2.3 void指针

前面讲过指针有两个方面的含义,两个含义必须同时存 在指针才可以正常工作。ANSI C中有void类型,如果指针指 向void类型,那么该指针就不指向任何实际的数据类型了。 因此在实际需要的时候通过给它补充该含义,可以使它可以 重新指向实际的数据类型。 void *p; 这个p是个指针变量,但是并不明确它要指向 什么类型的变量。 int a=1; p=(int *)&a; 现在希望p存放int变量a的地址,也就是说 想用p指向int变量a,那么把地址值和p要跨度两个方面的含 义都赋予指针才完整。

那么这样的void指针究竟有什么利用价值呢? 它不指向任何实际的数据类型,也就是说它蜕化掉了指 针“跨度”的含义,也就是说它指向何种类型的数据类型是 不明确的,但是可以把它想象成指向一个抽象的数据类型。 在将它的值赋给另一个指针变量的时候,要进行强制类型转 换使之适合于被赋值的变量之数据类型。ANSI C标准规定用 动态存储分配函数时返回void 指针。这部分的内容在讲述动 态建立链表的时候将深入学习,这里暂不作讨论。抽象数据 类型在《数据结构》中也是一种重要的概念。

?

7.3指针变量的使用
?

指针变量的使用包括通过指针变量访问变量和移动指针。

7.3.1指针运算符

指针变量有两种运算:“&”、“*”。 1.取地址运算符“&” 这个运算符前面已经用到。 例如赋值语句: px=&x; 就是通过取地址运算符“&”,把变量x的地址 赋给指针变量的,也就是使px指向x。

2.指针运算符“*”(根据地址取变量值) 对于图 7-4中所示的程序。px指向x后,就可以通过 px间接访问它所指向的变量x了。*px就等价于x,所以, 以下两条赋值语句: *px=10;

x=10;
是等价的,都是将10赋给x。同样,下两条语句: printf(“x=%d”,x); printf(“x=%d”,*px); 直接和间接方式输出变量x的值,因此,输出结果都 是10。

3.指针的算术运算 指针可以加上或减去一个整数(常量或变量),达到 改变地址值也就是对指针进行移动的目的。 例如: y是个整数,p是一个指针变量,如果它所指向的数据 类型在内存中占据x个字节的存储空间,则p+y表示在p 的地址值基础上前进x*y个字节。减法运算原理相似, 是在p的地址值基础上后退。

?

7.3.2变量的存取方式

(1)直接访问 C语言作为一种高级语言为了方便程序员使用,使用变量名 访问变量,用变量名对变量进行访问叫做直接访问。 (2)间接访问 变量名本质上还是要转换为地址而后访问变量。指针变量的 值就是地址,所以可以通过指针变量来“间接”访问它所指 向的变量。用指针变量对其所指向的变量进行访问的方式叫 做间接访问。

?

7.3.3停下来思考一下

论语有云:“学而不思则罔,思而不学则殆”。一路学到 这里可能有多人会问,前面说指针好,那指针到底好在哪里 呢?下面我们就停下来思考一下,对前面的知识梳理一下, 对后边的内容也做一个交代,以期起到承上启下的作用。

首先,在7.1.1预备知识中已经分析过变量的直接访问方式 的诸多优点,那么通过指针变量来实现对变量的间接访问不 就是画蛇添足了吗?其实我们在函数一章中学习过变量的性 质后很容易回答这个问题。下面我们先来看一个以前考虑过 的实际问题: 背景:王老师任教计算机a班和b班,a班有一个同学叫做 小明,而b班没有。今天王老师去b班上课,在b班的课堂上 王老师突然想找小明。(这在C程序中好比在一个函数中却 想访问或操纵另一个函数中的局部变量)

思考:
?王老师是不能用“小明”这个名字来访问该同学的,这是因为小明这个 名字根本不存在于b班。这就好比小明是定义在a班的范围内的一个局部 变量,而现在是在b班的范围内,当然不能通过小王来访问所对应的同学 了,因为我们已经知道局部变量在此函数之外是无效或说是不可见的。 更术语地讲可以说小明这个名字的“名字空间”是a班,在a班的范围内, 王老师引用小明,会找到对应的同学,在b班则不行。所以王老师对班长 说“帮我把小明同学找来”,是达不到目的的。 ?可以想到全校的同学都有唯一的学号,如果王老师此时用小明的学号, 是可以访问小明的,比如王老师对班长说“帮我把9941083同学找来一 下”是可以达到访问小明的目的的。因为该学号客观存在而且每个人都 不一样。 ?通过指针变量对它所指向的变量进行间接访问事实上和王老师通过学号 达到访问另一个范围内的同学之情况是相似的。换句话说,利用指针变 量对变量进行间接访问,可以跨越名字空间访问变量。这听起来好象显 的很神秘,其实是很自然的事情,因为在内存中变量的地址是唯一的, 正如学生的学号一样,只要该变量仍然客观存在着就行。

在下一节我们就可以看到这个特性能给我们带来什么便捷。 当然了,指针的灵活之处还远不止于此,我们将会慢慢学来。 到现在您还跟得上进度吗?

?

7.3.4指针变量作为函数参数

大家已经知道,C语言中函数参数的传递方式是“值传 递”。指针变量作为函数的实参时,由于指针变量的值实际 上是它所指向的变量的地址,所以传递的是指针所指向的变 量的地址,与此相对应函数的形参也应是指针变量。

前面的程序例6-18中的change函数并不能交换a和b的值, 下面我们通过一个程序看看使用指针变量来间接访问变量能 带来什么好处。 例7-1 编制函数change,交换两个变量的值。
void change(int *m, int *n) { int temp; temp=*m; *m=*n; *n=temp; }

void main()
{ int a=1, b=2; change(&a, &b); printf("a=%d, b=%d", a, b); }

100 int a=1,b=2; a 1 204 b

102 2 206 102

地址 值 208

调用 change(&a,&b);m temp=*m; *m=*n; *n=temp; m a change返 回 a

100 100
2 204 100 100 2 204 100 释放

n
b 指向 n b

102 1
206 102 102 1 206

tem p

随机值

tem p

208 1

208

m

n

102 释放

tem p

2 释放

图7-6程序的内 存示意图

运行后观察显示结果可以发现变量a和b被成功地交换了。 图7.6展示了程序运行过程中各个变量存储单元中的变化。

其实原因很简单,因为在程序6_3.c中对变量直接访问, main函数中的a和b与change函数中的m和n只不过值相同罢 了,除了值的单向传递他们之间没有任何关系,因此对m和n 的改变不能反映在a和b上。而程序7_1.c中,变量m和n中存 放的是变量a和b的地址,它们有“指向”的关系,那么通过 *m和*n就找到并可以访问变量a和b了。

从宏观上来看,变量a、b是定义在main函数中的局部变量, 只有在main函数中可以访问它们,在change函数中则不能通 过变量名a、b达到访问它们的目的,正如我们学过的变量之 有效性范围。可是main函数在调用change函数的时候把变量 a、b在内存中的地址以函数实参的形式“告诉”给了change 函数,因此在change函数中就可以通过指针变量来“指向” 变量a和b从而达到访问它们的目的了。

?

7.4指针与数组

C语言中指针与数组有着极为密切的联系。引用数组 元素可以用下标法,也可以用指针法。两者相比而言,下 标法易于理解,适合于初学者;而指针表示法有利于提高 程序的执行效率。

?

7.4.1数组和数组元素的指针

数组的指针是指数组在内存中的地址,数组元素 的指针是数组元素在内存中的地址。 我们已经知道数组名就是数组的起始地址,其实C 语言规定它是下标为0的元素的起始地址,而不是数组 的指针。尽管如果仅从首地址值的角度考虑,数组名、 数组的指针,以及下标为0的数组元素的指针都相同。 但是如果考虑到指针的完整定义,则其实数组名仅仅是 下标为0的元素的指针。另外,请注意它们都是常量。

1.数组元素的指针 例如,int data[6];

则C语言规定:
a) 数组名data是指针常量,它代表的是数组第一个元素 data[0]的指针。

b) 前一节我们已经讨论过指针的算术运算,现在容易知道 data+i其实也就是data[i]的首地址(i=0,1,2,·· ·,5), 即data+i与&data[i]等价。与简单变量类似,数组元素 data[i]的首地址&data[i]就称为data[i]的指针。所以 data+i也是data[i]的指针,简称为data+i指向data[i]。因 而引用数组元素时,可用*data、 *(data+1)、··*(data+5)的方式,如图7-7(a)所示。 ··

data

p data[0]
data[0] p +1 data[1] data[1] p +2 data[2] data[2] p +3 data[3] data[3] p +4 data[4] data[4] p +5 data[5] (a) 图7-7用指针引用数组元素 (b) data[5]

data+1 data+2

data+3
data+4 data+5

所以,以下两个循环输出语句完全等价: for(i=0;i<6;i++)printf(“%4d”,data[i]);

for(i=0;i<6;i++)printf(“%4d”,*(data+i));

2.数组的指针 那么我们怎么获得数组的指针呢?其实在下标为0的数组元 素的指针上(也就是数组名)作个类型转换即可。

例如对上述的数组int data[6]来说,((int *)[6])data就 是数组data的指针。
一般来说在二维和多维数组中,我们经常会用到数组 的指针和指向数组的指针变量,对于它们来说,可能其 值和某数组元素的首地址相等,但是其含义是不同的, 例如((int *)[6])data+1则表示指针前进6*2=12个字节, 而不是前进2个字节,很明显它和数组元素的指针在跨 度含义上是不同的。 分析:对于强制类型转换((int *)[6])data,考虑到运算 符的优先级和结合性,容易知道data被转换成了指向拥有6 个元素的整型数组的指针。

?

7.4.2指向数组和数组元素的指针变量

1.指向数组元素的指针变量

指向某数组元素的指针变量就是指向该数组元素的指 针变量。定义一个指向数组元素的指针变量的方法,与以 前介绍的指向变量的指针变量相同。而且由于数组的元素 就是某种类型的变量,因此指向数组元素的指针变量在定 义形式上就是指向该类型变量的指针变量,只不过需要利 用数组元素的地址对其赋值罢了。

例如: int data[6]; int *p; 则语句: /*定义data为包含6个整型数据的数组*/ /*定义p为指向整型变量的指针变量*/

p=&data[0];(或p=data;)
就把data[0]元素的地址赋给指针变量p,即p指向data数组 的第0号元素(也可说p指向data数组)。

同样,语句
p=&data[i]; 就使p指向data数组的第i号元素。 ? 如果p的初值为&data[0],则p+i就是data[i]的地址 &data[i](i=0,1,2, ·· ·,5)。 ? 如果p指向数组中的一个元素,则p+1就指向同一数组 的下一个元素。p+1所代表的地址实际上是p+1×d,d 是一个数组元素所占字节数(对整数型,d=2;对实型, d=4;对字符型,d=1)如图7-7(b)所示。

2.指向一维数组的指针变量

指向数组的指针变量要略显复杂一些,但如果我们把 握住指针的本质,很容易想到它具有的两个属性分别是: a) 其值是数组的首地址,亦即下标为0的数组元素的地 址。

b) 它是指向数组的,而不是数组元素。因此它的跨度 是整个数组而不是一个数组元素。

下面定义一个指向数组的指针变量。 int (*p)[6] int data[5][6]; p=data[2]; p++ 应该记住,此时p只能指向一个包含6个元素的一维数 组。p的值就是该一维数组的起始地址(亦即下标为0的 数组元素的地址),p不能指向一维数组中的某一元素。 指向一维数组的指针变量一般用于对二维数组或多维数 组的操作,请务必主意指针变量的类型。

3.指向数组元素的指针变量,在使用中应注意的问题 (1)指针变量可以通过本身值的改变(如p++)来指向数组中的 不同元素。但是数组名是地址常量不能改变本身的值,如 果写data++则是错误的;

(2)应注意下面的几种指针运算形式:

? *p++等价于*(p++),作用是先得到p所指向的变量的值(即 *p),然后再使p加1。
? *(p++)与*(++p)不同,前者是先取得*p的值,后使p加1;后 者是先使p加1,再取*p的值。若p初值为&data[0],输出 *(p++)时,得data[0]的值,而输出*(++p),则得到data[1]的 值。

总结以上两点可知,如果p当前指向data数组中第i个 元素,则: *(p++)相当于data[i++],先对p进行“*”运算,再使p自 增。 *(p--)相当于data[i--],先对p进行“*”运算,再使p自减。 *(++p)相当于data[++i],先使p自增,再对p进行“*”运 算。 data[0]p1data[1]data[2]data[3]data[4]data[5]p2data数 组图7-8指针算术、关系运算10021010

*(--p)相当于data[--i],先使p自减,再对p进行“*”运算。

(3)(*p)++表示p所指向的元素值加1,注意是元素值加1, 而不是指针值加1。比如,如果p所指向的元素为data[3], 且data[3]的值为9,则(*p)++表示将data[3]单元中的值加1, 变成10,而p仍指向元素data[3],也就是说,p中的地址 值并没有改变。

(4)p+n和p-n:将指针从当前位置前进或回退n个元素,而 不是n个字节。显然,p++、p--(或++p、--p)是p+n(p-n) 的特例(n=1)。
(5)p2-p1表示两指针之间的数组元素个数,而不是指针 的地址之差,即图11-8中,p2-p1为4。

(6)两指针之间可以进行关系运算,如果p1指向data[i], p2指向data[j],并且i<j,则p1<p2为“真”,反之亦然, 即图11-8中,p1<p2为真。

data数组 p1 data[0]

1002

data[1]
data[2] data[3]

p2 1010

data[4] data[5]

图7-8指针算术、关系运算

?

7.4.3数组元素的引用

数组元素的引用,既可用下标法,也可以用指针法。 下标法简单直观;而指针法能使得目标程序简短(特别是 采用指针变量的自增自减运算时)、运算速度快。

例如,若有如下定义 int data[6];

int *p=data;
则指针和数组之间有如下恒等式: data+i==&data[i]==p+i (i=0,1, ·· ·,5)

data[i]==*(data+i)==*(p+i)==p[i] (i=0,1, ·· ·,5)

所以,引用数组第i个元素,有以下访问方式:
(1)下标法 ? 数组名下标法:data[i] ? 指针变量下标法:p[i]

(2)指针法
? 数组名指针法:*(data+i) ? 指针变量指针法:*(p+i)

例如,下面4条语句的作用都是将20赋给data[5]元素。
data[5]=20; *(data+5)=20;

*(p+5)=20;
p[5]=20;

例7-2用下标法和指针法引用数组元素。
#include <stdio.h> main()

运行结果为:

{
int data[6]={0, 3, 6, 9, 12, 15}; int *p=data, i; for(i=0; i<6; i++) printf(i==5?"%d\n":"%d ", data[i]); /*数组名下标法*/ for(i=0;i<6;i++) printf(i==5?"%d\n":"%d ", *(data+i)); /*数组名指针法*/

0 3 6 9 12 15
0 3 6 9 12 15 0 3 6 9 12 15 0 3 6 9 12 15

for(i=0;i<6;i++)
printf(i==5?"%d\n":"%d ", p[i]); /*指针变量下标法*/ for(i=0;i<6;i++) printf(i==5?"%d\n":"%d ", *(p+i)); /*指针变量指针法*/ }

说明:

(1)程序中用下标法和指针法的四种方式引用数组元素,结 果完全一样,说明它们是完全等价的。printf()语句中的格 式字符串是一个条件表达式,选择两种输出格式之一,输 出最后一个元素时同时输出回车换行,输出其他元素时同 时输出空格而不是回车换行。 (2)这4种方法,前面两种的执行效率是一样的。编译程序 是将data[i]转换为*(data+i)处理的;而后两种方法比前两种 效率高,用指针变量直接指向元素,不必每次都重新计算 地址,并且象p++这样的自加操作是比较快的。

?

7.4.4数组名作为函数参数

当数组名作为函数参数时,在函数调用时,实际传递 给函数的是该数组的起始地址,即指针值。所以,实参可 以是数组名或指向数组的指针变量。而被调用函数的形参, 既可以说明为数组也可以说明为指针。 由于数组名就是数组的首地址,因此,函数的实参和形 参都可以使用指向数组的指针变量或数组名,于是函数实 参和形参的配合上有四种等价形式,这4种等价形式在本质 上是一种,即指针变量做函数参数。

(1)实参和形参都用数组名 void data_put(int str[], int n) { int i; for(i=0; i<n; i++) printf("\n %d", str[i]);

(2)实参用数组名,形参用指针 void data_put(int *str,int n) { int i; for(i=0; i<n; i++) printf("\n %d",*(str+i));

}
main() { int a[6]={1, 2, 3, 4, 5, 6}; data_put(a, 6); }

}
main() { int a[6]={1, 2, 3, 4, 5, 6}; data_put(a, 6); }

(3)实参用指针,形参用数组 名 利用下标引用指针变 量所指数组元素 void data_put(int str[], int n) { int i; for(i=0; i<n; i++) printf("\n %d", str[i]); } main() { int a[6]={1, 2, 3, 4, 5 ,6}; Int *p=a; data_put(p, 6); }

(4)实参和形参都用指针 void data_put(int *str,int n) { int i; for(i=0; i<n; i++) printf("\n %d", *(str+i));

}
main() { int a[6]={1, 2, 3, 4, 5, 6}; int *p=a; data_put(p, 6); }

说明
当数组名作为函数参数时,形参无论用数组名还是用 指针,在函数体中对数组元素的存取操作,既可以用指针 法也可以用下标发。这留给读者验证。

?

7.4.5字符串的指针和指向字符串的指针变量

C语言中没有字符串类型,字符串是以字符数组的形 式给出的,而数组可以用指针进行访问,所以,字符串也 可以用指针进行访问。

1.字符串的表示和引用 (1)用字符数组存放一个字符串
#include <stdio.h> void main() { char string[ ]="This is a string"; /*字符数组存放字符串*/ printf("\n %s", string); /*整体引用输出*/ printf("\n"); for(i=0; *(string+i)!='\0'; i++) printf("%c", *(string+i)); /*逐个引用*/ }

程序的执行结果如下:

This is a string
This is a string

(2) 用字符指针指向一个字符串 例7-3:用字符指针指向一个字符串
#include <stdio.h> void main() { int i; char *p="This is a string"; /*字符数组存放字符串*/ printf("%s \n", p); /*整体引用输出*/ for(i=0;p[i]!='\0'; i++) printf("%c", p[i]); /*逐个引用*/ printf("\n"); for(;*p!='\0'; p++) printf("%c", *p); }

程序的执行结果如下:

This is a string
This is a string This is a string
说明: 语句: char *p=”This is a string”; 等价于下面两行: char *p;

p= “This is a string”;

C语言对字符串常量是按字符数组处理的,在定义 字符串常量 “This is a string”时,在内存开辟了一个字 符数组来存放它,并把首地址赋给字符指针p,如图7-9 所示。
T h i s i s a s t r i n g \0

p

图7-9字符串常量在 内存中的存放

例7_4用指针方法,求字符串长度。
main() {

char *p,str[80];
int n; printf("输入字符串:\n");

gets(str);
p=str; while(*p!='\0')p++;

n=p-str;
printf("字符串:%s的长度= %d \n", str,n); }

2.字符串指针作函数参数 当数组名作为函数参数时,在函数调用时,实际传递给 函数的是该数组的起始地址,即指针值。这样,在函数形参 说明中,就可以将数组形参说明为指针,并可以在函数体中 通过指针存取或改变数组中的元素。 例7_5 编写一个合并两个字符串的函数

下面给出三种方法。

(1)将形参说明为数组,利用下标引用数组元素

void mystrcat(char str1[],char str2[]) {

int i=0, j=0;
while(str1[i]!='\0')i++; while(str2[j]!='\0') {str1[i]=str2[j]; i++; j++;} /*把字符串str2的内容复制到字符串str1中*/ str1[i]='\0'; } /*添加字符串str1的结束符*/ /*找到字符串str1的结束符*/

(2)将形参说明为指针,利用指针引用数组元素

void mystrcat(char *str1, char *str2) {

while(*str1!='\0')str1++;
while(*str2!='\0') {*str1=*str2; str1++; str2++;} *str1='\0'; }

(3)将形参说明为指针,利用下标引用指针变量所指数组元素

void mystrcat(char *str1,char *str2) {

int i=0, j=0;
while(str1[i]!='\0')i++; while(str2[j]!='\0') {str1[i]=str2[j]; i++; j++;} /*把字符串str2的内容复制到字符串str1中*/ str1[i]='\0'; } /*添加字符串str1的结束符*/ /*找到字符串str1的结束符*/

主函数
#include "stdio.h"

main()
{ char source[80]="String One", object[20]="string two";

mystrcat(source, object);
printf("str1+str2=%s",s ource); }

程序运行结果是: str1+str2=String One string two

?

7.4.6指针数组

1.指针数组的定义与应用 如果一个数组的每个元素都是指针类型的数据,则这 种数组称为指针数组。指针数组定义的一般形式为:

类型标识符 *数组名[常量表达式];
例如: char *p[10]; 表示p是一个指针数组,包括10个元素,每个元素都是字符 型指针。

在定义指针数组的同时也可以为其初始化。如: char *name[ ]={“zhang shan”, “Li shi ”, “Wangwu”}; 由初始表中的初值个数可以看出,name[ ]指针数组中 共有3个元素,每个元素都是一个指针,如图7-10所示。
name[0] name[1] name[2] Zhang shan Li shi Wnagsu

图7-10指针数组

其中name[0]指向字符串 “Zhan shan”,name[1]指 向字符串 “Li shi”,name[2]指向字符串 “Wangwu”。因 此,语句: printf(“%s, %s, %s\n”,name[0], name[1], name[2]); 将显示字符串: Zhan shan,Li shi ,Wangwu 在程序设计中,经常使用指针数组显示菜单信息。下 面的例子就是这方面的实际应用。

[例7-5]利用指针数组显示菜单信息File Edit Search Option

/*7_5.c*/ #include "stdio.h" main()

{
char *menu[]={"File", "Edit", "Search", "Option"}; int i;

for(i=0; i<4; i++) printf("%s", menu[i]);
}

如果利用C语言库函数中的光标定位函数,则上述菜单 信息可以显示在屏幕的任意位置上,有关这方面的内容, 请查阅图形和用户界面方面的技术。

2.指针数组作为mian( )函数的形参

指针数组的一个重要应用就是作为main( )函数的形参,在前面 的程序中,main( )函数是无参函数。实际上,main( )可带两个参数, 其一般形式为: main(int argc,char *argv[ ]) /*这里是形参罢了,名字是可以随便取的,比如int x,char *y[ ] */

第一个参数是整型,第二个参数是作为指向字符的指针数组 来处理的,那么这两个参数如何得到具体的值呢?我们知道, 在DOS命令提示符下,可以键入一个可执行文件名,还可 以 带 参 数 。 如 DOS 命 令 : copy oldfile newfile , 将 实 现 oldfile复制成newfile的功能。假如copy是用C语言实现的, 则相应的argc表示是参数的个数(含可执行程序名),故 argc的值为3,而指针数组argv中的元素argv[0]、argv[1]、 argv[2] 分 别 指 向 三 个 字 符 串 “ copy” 、 “ oldfile” 和 “newfile”,如图11-11所示。

argv

argv[0] argv[1] argv[2]

copy\0 wldfile\0 newfile\0

图7-11指针数组的应用:命令行参数

例如,下面程序echo.c将其所带的参数显示出来,如在 DOS状态下键入echo oldfile newfile,将回显oldfile newfile。

/*echo.c*/ #include <stdio.h> void main(int argc, char *argv[])

注意,若参数(含执行 { 文件名)共有n个,则最 后一个参数由指针argv[] int i; for(i=1; i<argc; i++) printf("%s", argv[i]); 指向。
printf("\n");
}

例7-6 编写带有帮助说明的程序,也就是要求当输入执行 文件名,后跟 “/?”时,将提示命令行的操作方法。
void main(int argc, char *argv[]) #include <stdio.h> void User(void) { · · · /*提示用户正确的操作信息语句*/ { int i;

if(argc==2)
if(strcmp(argv[1],"/?")==0) {user(); return;}

·
· · /*其他代码*/

} }

?

7.4.7指针与二维数组

前面讲过,数组的指针是数组在内存中的起始地址,数 组元素的指针是数组元素在内存中的起始地址;指向的数 组的指针变量是指向该数组的指针变量,指向某数组元素 的指针变量就是指向该数组元素的指针变量。对于多维数 组虽然也是如此,但情况要复杂一些。实际上在程序中很 少有用到超过二维的数组,因此在这里只讨论二维数组与 指针的关系。

1.二维数组和数组元素的地址

C语言的二维数组由若干个一维数组构成,即二维数组 的每一个元素是一个一维数组。例如定义以下二维数组:
int a[3][4]={{1, 3, 5, 7,}, { 9, 11, 13, 15}, {17, 19, 21, 23, }};

a是一个数组名。a数组包含3行,即3个元素:a[0]、 a[1]、a[2]。而每一个元素又是一个一维数组,每个一维数组 又包含4个元素(即4个列元素),例如,a[0]所代表的一维数组 又包含4个元素:a[0][0]、a[0][1]、a[0][2]、a[0][3],见图712。可以认为二维数组是“数组的数组”,即二维数组a是 由3个一维数组所组成的。

a[0] a[1] a[2]

1 9 17

3 11 19

5 13 21

7 15 23

图7-12二维数组是“数组的数组”

从二维数组的角度来看,a代表二维数组首元素的地址, 现在的首元素不是一个简单的整型元素,而是由4个整型元素 所组成的一维数组,因此a代表的是首行(即第0行)的首地址。 a+1代表第1行的首地址。如果二维数组的首行的首地址为 2000,则在Turbo C中,a+1为2008,因为第0行有4个整型 数据,因此a+1的含义是a[1]的地址,即a+4×2=2008。a+2 代表a[2]的首地址,它的值是2016,见图7-13。
a a数组 a[0]

(2000) a+1
(2008) a+2 (2016)

a[1]
a[2]

图7-13二维数组各行

a[0]、a[1]、a[2]既然是一维数组名,而C语言又规定了 数组名代表数组首元素地址,因此a[0]代表一维数组a[0]中第 0列元素的地址,即&a[0][0]。a[1]的值是&a[1][0],a[2]的值 是&a[2][0]。 请考虑0行1列元素的地址怎么表示?a[0]为一维数组名, 该一维数组中序号为1的元素的地址显然应该用a[0]+1来表示, 见图7-14。此时“a[0]+1”中的1代表1个列元素的字节数,即 2个字节。今a[0]的值是2000,a[0]+1的值是2002(而不是 2008)。这是因为现在是在一维数组范围内讨论问题的,正 如有一个一维数组x,x+1是其第1个元素x[1]的地址一样。 a[0]+0、a[0]+1、a[0]+2、a[0]+3分别是a[0][0]、a[0][1]、 a[0][2]、a[0][3]元素的地址(即使&a[0][0]、&a[0][1]、 &a[0][2]、&a[0][3])。

a[0]

a[0]+1

a[0]+2

a[0]+3

a
2000 a+1 1 2008 a+2 9 2016 17 2002 3 2010 11 2018 19 2004 5 2012 13 2020 21 2006 7 2014 15 2022 23

图7-14二维数组各元素

前面已经学过,a[0]和*(a+0)等价,a[1]和*(a+1)等价, a[i]和*(a+i)等价。因此,a[0]+1和*(a+0)+1都是&a[0][1](即 图7-14中的2002)。a[1]+2和*(a+1)+2的值都是&a[1][2](即 图7-14中的2012)。请注意不要将*(a+1)+2错写成*(a+1+2), 后者变成*(a+3)了,相当于a[3]。

进一步分析,欲得到a[0][1]的值,用地址法怎么表示呢? 既然a[0]+1和*(a+0)+1是a[0][1]的地址,那么,*(a[0]+1)就是 a[0][1]的值。同理,*(*(a+0)+1)或*(*a+1)也是a[0][1]的值。 *(a[i]+j)或*(*(a+i)+j)是a[i][j]的值。请务必记住*(a+i)和a[i]是等 价的。

2.指向二维数组及其元素的指针变量

在了解上面的概念后,可以用指针变量指向二维数组或二 维数组的元素。

例7-7 用指针变量输出二维数组元素的值。
/*7-7.c*/ #include <stdio.h>

void main()
{ int a[3][4]={1, 3, 5, 7, 9, 11, 13, 15, 17, 19, 21, 23}; int *p=a; int i; for(i=0;i<=11;i++) {

程序运行结果是:

1
if(i%4==0) printf(“\n”);
printf("%4d", *(p+i));

3

5

7

9 11 13 15 17 19 21 23

} printf("\n"); }

这个p是指向数组元素的指针变量,数组元素是 的类型是int,因此p的定义形式是int *p=a;而且p+1 是向后移动2个字节。

例 7-8用指针变量输出二维数组元素的值。
#include <stdio.h> void main()

{
int a[3][4]={1, 3, 5, 7, 9, 11, 13, 15, 17, 19, 21, 23}; int i=0,j=0; int (*p)[4]=a; for(i=0;i<3;i++) for(j=0;j<4;j++) {

程序运行结果是:

1

3

5

7

9 11 13 15 17 19 21 23

printf("%4d",*(*(p+i)+j)); /*这里用p[i][j]也可以*/
} printf("\n"); }

这个p是指向数组元素的指针变量,数组元素是 的类型是int,因此p的定义形式是int *p=a;而且p+1 是向后移动2个字节。

请注意这里变量p的定义“int (*p)[4]=a;”,由于p先和运算 符“*”结合,因此p是指向数组的指针变量。而如果是“int *p[4]”则由于p先与方括号结合,则p是个包含两个某种元素的 数组,然后再与*结合,表示p是由两个指针构成的数组,而 且指针是指向int类型的数据的,这样的p就成了一个指针数组 了。要区分这两种形式,避免混淆。

注:对于例7-8,为什么要把p定义成int (*p)[4]=a;而不能定 义为int **p(见下节7.5.3的 “3.思考”部分)在这里不作讨论。

?

7.4.8数组的存储结构之本质

前面我们对数组所作的讨论是针对数组的逻辑结构,而 事实上,如果我们能从数组的存储结构上进行把握,那么 多维数组可以彻底简化成简单的一维数组(而不是前面说过 的“把二维数组看作一维数组”)。 逻辑结构是针对逻辑、概念上的讨论;存储结构是针对 其在内存中的储存方式进行的讨论。

例 7-9对二维数组元素的一种简单访问形式。

/*7-9.c*/

void main()
{ int a[3][3]={1,2,3,4,5,6,7,8,9}; int *p=&a[0][0]; printf("%d",*(p+8)); }

运行结果为:

9

可见该程序能够通过*(p+8)访问二维数组的元素a[2][2]。 很明显,此时的访问更象对一维数组元素的引用方式。那 么为什么可以这样呢?

事实上,无论是一维数组、二维数组还是多维数组,其 数组元素事实上在内存中都是线性、连续地存放的,所谓 的“维度”不过是逻辑上的概念,不会应为存在着“维度” 而改变这种存储结构上的特征。因此,指针变量p指向了数 组元素a[0][0],则只需通过对p进行简单的移动即可指向数 组元素a[2][2],进而达到访问该数组元素的效果。在这个 过程中,任何维度的数组没有任何本质上的区别。

存 储 结 构

1

2

3

4

5

6

7

8

9

a[0][0] a[0][1] a[0][2] a[1][0] a[1][1] a[1][2] a[2][0] a[2][1] a[2][2] 逻 辑 结 构 第一行 a[0] 第二行 a[1] 第三行 a[2]

图 7-15 二维数组的存储结构 和逻辑结构

例 7-10对二维数组元素的简单访问。

/*7-10.c*/

void main()
{ int a[3][3]={1,2,3,4,5,6,7,8,9}; int j; int *p=&a[1][1]; for(j=0;j<4;j+=2) printf("%d",p[j]); }

运行结果为:

57

思考:对于m行n列的二维数组a[m][n],如果知道了其某 元素a[i][j]的地址,那么该数组任意元素a[x][y]的地址能用 一个什么样的通用表达式来代表呢?(注意:m和n是整常 量,且0≤i≤m-1, 0≤x≤m-1, 0≤j≤n-1, 0≤y≤n-1)请大家自己思 考!

总结:在这里我们把握住了数组的存储结构而避开其逻 辑结构,这使得我们可以用一种简单的方式方便地访问多 维数组的元素。 事实上,逻辑结构和存储结构统称为数据结构。《数据 结构》作为计算机专业的核心课程我们以后将会学到,在 这里只是简单地提出,使得大家能有个提前的认识。通过 本节的学习大家也应该看到:针对同一个问题,抓住其不同 方面的特性,有时能取得一些意想不到的便利和效果。

?

7.5指向指针的指针

指向指针变量的指针,简称为指向指针的指针;指向 指针变量的指针变量简称为指向指针的指针变量。

?

7.5.1指向指针的指针

指向指针变量的指针,简称为指向指针的指针。

在本章开头已经提到了“间接访问”变量的方式。利用 指针变量访问另一个变量就是“间接访问”。如果在一个 指针变量中存放一个目标变量的地址,这就是“单级间 址”,见图7-16(a)。指向指针的指针用的是“二级间址” 方法,见图7-16(b)。从理论上说,间址方法可以延伸到更 多的级,见图7-16(c)。但实际上在程序中很少有超过二级 间址的。级数越多,越难理解,容易产生混乱,出错机会 也多。

指针变量 地址

变量 值

(a)一级间址 指针变量1 指针变量2 地址1 地址2

变量 值

(b) 二级间址
指针变量 地址1 指针变量2 地址2 … 指针变量n 地址n 变量 值

(c) n级间址
图 7-16 通过指针变量存取变量的值

?

7.5.2定义指向指针变量的指针变量
指向指针变量的指针变量又简称为指向指针的指针变

量。 指向指针的指针变量定义的形式为: 类型名 **指针变量明; 此处,指针变量名是“指向指针的指针变量”的变量 名,类型名是该指针变量经过二级间址后所存取变量的数 据类型。由于运算符“*”的结合性是“从右到左”,因此 “**指针变量名”等价于“*(*指针变量名)”,表示该指针变 量的值存放的是另一个指针变量的地址,要经过两次间接 存取后才能存取到变量的值。例如语句: char **p; 定义p为指向指针的指针变量,它要经过两次间接存 取后才能存取到变量的值,该变量的数据类型为double。

?

7.5.3指向指针的指针变量的应用
1.指向一个指针变量,间接存取变量的值

可以把一个指针变量的地址赋给指向指针的指针变量, 然后通过二级间址方法存取变量的值。

例7-11 通过二级间址方法存取变量的值。
/*7-11.c*/ main() { double d=123.456, *p, **pp; pp=&p; p=&d;

运行结果为 d=123.456, d=666.666

printf("d=%8.3f, ", **pp);
**pp+=543.21; printf("d=%8.3f\n", d);

}

上述指针变量pp指向指针变量p,而指针变量p又指向 双精度实型变量d,如图7-17所示,图中假设指针变量p的 地址是1500,变量d的地址是3500。此时*pp表示指针变量 p的值(即变量d的地址),因此表达式**pp与变量d等价。

指向指针的指针变量pp 1500

指针变量p 3500 1500(地址)

变量d 123.45 3500(地址)

图7-17指向指针指针指向指针变量

2.指向指针数组,存取指针数组元素所指内容

可以把一个指针数组的首地址赋给指向指针的指针变量, 例如: 例7-12 有三个等级分,由键盘输入1,屏幕显示“pass”, 输入2显示“good”,输入3显示“excellent”。

/*7_12.c*/

main()
{ int grade; char *ps[]={"pass","good","excellent"},**pp; pp=ps; printf("请输入等级分(1~3):"); scanf("%d",&grade); printf("%s\n",*(pp+grade-1)); }

运行结果: 请输入等级(1~3):2↙ good

上述程序中pp指向数组ps的第一个元素ps[0],pp+1则指 向ps的下一个元素ps[1],pp+2指向ps[2],如图7-18所示。 因 此 * pp 就 是 字 符 串 ” pass” 的 首 地 址 , * (pp+1) 则 是 字 符 串”good”的首地址,*(pp+2)是字符串”excellent”的首地址。
pp ps[0] p a s s \0

pp+1

ps[1]

g

0

0

d

\0

pp+2

ps[2]

e

x

c

e

l

l

e

n

t

\n

图7-18 指向指针的指针指向指针数组

3.思考 很多人会提出这样的问题:二维数组是一维数组的数组, 而数组名是个地址,因此二维数组本质上应该和二级指针 相同,所以可以写出如下程序:
void main()

{
(1)int b[2][2]={1,2,3,4}; /* 注意这里(1)是行号,程序中是 没有的。*/ (2)int **a; (3)a=b; (4)printf("%d",*(*(a+1)+1));

} /*实验证明这个程序输出随机值,不能准确地输出我们预 期的数组元素b[1][1]的值。要想得到正确的输出结果,则 要把a定义成int (*a)[2] */

其实利用我们前面掌握的有关指针的基础知识不难解 释这个语法现象。

1. 如果把a定义成int **a,则a是一个指向指针的指针变量, 他指向的仍然是一个指针变量,后者是一个指向int变量 的指针变量。大家已经知道,指向int变量的指针变量占 2个字节、int变量也占2个字节,根据指针的基本运算容 易知道: a) a+1表示在a的基础上后移了2个字节, a+1便指向 了b[0][1],因此*(a+1)即为b[0][1]的值,也就是2;

b) 则*(a+1)+1表示在2的基础上后移2个字节,得到4。 *(*(a+1)+1)表示起始地址地址为4的连续两个内存单 元中存储的变量值,显然这个值并不是b[1][1]。

2. 如果把a定义成int (*a)[2],则a是指向具有两个int元素的 一维数组的指针变量,则a+1表示跨越2个int 元素,即 a+1指向二维数组b[2][2]的第1行(注意是从0开始记 数),而*(a+1)+1很显然是指向了b[1][1]。

下面以存储示意图7-19来解释这个程序,以便读者 更容易理解:(假设数组b的起始地址是100h,变量a的 地址是200h)

0000h

0001h

*(*(a+1)+1)的值显然不 是b[1][1]的值,而应该 是由地址为0004h和 0005h的两个内存单元中 的值所决定的。

0002h

0003h *(a+1)+1 0004h 0005h ? ?

………
b的起始地址即0100h

………
a 1 2 a+1

3

注:(3)a=b,则a指向了 b[0][0],而a+1则指向了 b[0][1],因此*(a+1)等于 b[0][1]也就是2。而 *(a+1)+1则为4,即 *(a+1)+1指向地址为4的 单元格

4

0109h

?

………
a的地址即0200H

………

………

………

图7-19程序的存储示意图

?

7.6指针与结构

一个结构体类型变量在内存中占有一段连续存储单元, 这段内存单元的首地址,就是该结构体变量的指针。可以 用一个指针变量指向一个结构体变量,或指向结构体数组 中的元素。这样的指针变量称为结构体的指针变量。。

?

7.6.1指向结构变量的指针

同其他变量一样,结构变量的首地址就是该结构变量的 指针。用地址运算符&,就可以获得结构变量的指针。
例7-13 利用结构指针变量访问结构中的成员。

/*7_13.c*/
#include "stdio.h" struct time

{
int hour; int minute; int second; }; struct time t={2, 34, 56};/*定义结构变量t,并初始化*/

void main(void) {

/*类似简单变量,定义结构指针变量pt*/
struct time *pt; /*使结构指针变量pt指向结构变量t*/ pt=&t; /*用结构指针变量pt间接访问方法,输出结构各成员的值*/ printf("\n Hour=%d, Minute=%d, Second=%d", (*pt).hour, (*pt).minute, (*pt).second); }

程序运行结果: Hour=2,Minute=34 , Second=56 显然,输出结果与初始化值相同。

指向结构的指针变量的定义,与普通指针变量的定义完 全一样。

通过指针来访问结构成员,与直接使用结构变量的效 果一样。在C语言中,为了便于使用和直观,通常使用结 构指针运算符“->”访问结构中的成员,可以把 (*pt).hour改用pt->hour来代替。一般来说,如果指针变 量pt已指向了结构变量t,则以下三种形式等价: t.成员 (结构变量名.成员名)

(*pt).成员 量名.成员名)
pt->成员

((*结构指针变量名)结构变
(结构指针变量名->成员名)

?

7.6.2指向结构体数组的指针

类似于用指向结构体变量的指针,间接访问结构体成员 一样,也可以用指向结构体数组及其元素的指针来处理结 构体数组。

/*7_14.c*/ struct student { char name[13]; char sex; float score;

main()

{
struct student *p; for(p=stu; p<stu+3; p++) printf("\n%-10s%2c, %4f", p->name, p->sex, p->score); }

};
struct student stu[3]= {

{"zhan", 'M', 110},
{"Li", 'F', 67}, {"wang", 'M', 911}

运行结果如下:

zhan
Li

M 110
F 67

};

Wang M 911

p->

Zhan Li Wang

M F M

110 67 911

Stu[0] Stu[1] Stu[2]

图7-20指向结构体数组的指针

说明:
a) 因为p的初值为stu,所以,第一次循环输出stu[0]的 各个成员值。执行p++后,p的值等于stu+1,也就是 p指向stu[1]的起始地址&stu[1]。第二次循环输出 stu[1]的各个成员值。以此类推。 b) 如果p指向结构体数组中的第一元素,则p+1就指向 同一结构体数组的下一个元素。p+1所代表的地址实 际上是p+1×d,d是一个结构体元素所占字节数,即 d等于sizeof(struct student)。

?

7.6.3指向结构体的指针作为函数参数

类似于普通指针变量作为函数参数一样,用指向结构体 的指针变量作实参时,是属于“地址传递”方式。

?

7.6.1指向结构变量的指针

同其他变量一样,结构变量的首地址就是该结构变量的 指针。用地址运算符&,就可以获得结构变量的指针。 例7-15用函数调用方式,改写例,编写一个专门的显示 结构成员函数,主函数调用时,用指向结构的指针变量作实 参。
/*7_15.c*/ void printf_data(struct student *p) { printf("\n%-10s%2c, %4d", p->name, p->sex, p->score); } void main(void) { struct student *p; for(p=stu; p<stu+3; p++) print_data(p); }

?

7.7指针与函数

指针与函数的关系主要包括两方面的内容:
1. 函数的返回值可以是指针类型; 2. 函数指针和指向函数的指针变量。

?

7.7.1指针变量作为函数返回值

一个函数不仅可以返回int型、float型、char型和结构类 型等数据类型,也可以返回指针类型的数据。返回指针类 型的函数定义格式为:
类型名 *函数名([参数表])

{
函数体; } 如: int *func() { int *p;

………..
return (p); }

它说明了func()函数的返回值是一个指向整型变量 的指针。 在C语言中,一个函数可以返回任何类型的指针。下面 例子中定义的函数,将返回整型的指针。
例7-16输出整型变量的地址。 /*7_16.c*/

int *getaddress(int x)
{ return &x; } void main() { int a=10;

printf("%d",getaddress(a));
}

?

7.7.2函数的指针和指向函数的指针变量

函数在内存中也占据一定的存储空间并有一个入口地 址(函数开始运行的地址,但不一定是存储区的首地址), 这个地址就称为该函数的指针。可以用一个指针变量来存 放函数的入口地址,这时称该指针指向这个函数,并成该 指针变量为“指向函数的指针变量”,简称为“函数的指 针变量”或“函数指针”,可以通过函数指针来调用函数, 这是函数指针的主要用途。函数指针定义的一般形式如下:

类型标识符 (*指针变量名)();

其中,类型类型标识符是指针变量所指向的函数的类 型,参数表是所指向的函数的形式参数的列表。注意: “* 指针变量名”外的括号不能少,否则就成了指针函数。 例如: int(*fp)();

定义了一个指向无参整型函数的指针变量fp。
与其他指针的定义一样函数指针定义后,应该它赋一 个函数的入口地址,即只有使它指向一个函数,才能使用 这个指针。C语言中,函数名代表该函数的入口地址。因 此,可用函数名给指向函数的指针变量赋值。

指向函数的指针变量=函数名; 注意:函数名后不能带括号和参数。

函数指针主要用于函数的参数和用它来调用函数,通 过函数指针来调用函数的一般格式是:
(*函数指针)(实参表)

注意:对指向函数的指针变量,进行诸如p+I、p—或 p++等运算是没有意义的。

例7-17求a、b中的最大者。

int max(int x,int y) { if(x>=y) return x; else return y; }

void main() { int a,b,c; int (*p)(); p=max; c=(*p)(a, b); } /*定义p是一个函数指针*/ /*使p指向函数max*/ /*等价于c=max(a,b)*/

scanf("%d%d", &a, &b); printf("\n a=%d, b=%d, max=%d",a, b, c);

?

7.8总结与提高
?

7.8.1本章小结

本章中我们重点学习了指针这种C语言提供的特殊数 据类型,并且详细介绍了用指针作为函数参数与用简单变 量作为函数参数时的不同之处,以及指针与数组之间的关 系;然后介绍了指针数组、指向指针的指针等概念及其应 用。

初学者通常会对指针望而生畏,其实只要从原理上掌 握了指针的概念,它就会变得如此简单而容易使用。首先, 指针不过是C语言提供的一种比较特殊的数据类型而已。 定义为指针类型的变量与其他类型的变量相比,主要差别 在于指针变量的值是一个内存地址。其次,在C语言中, 指针和数组之间有着密不可分的关系,数组名就是一个指 针,它代表数组元素的首地址。只要让声明为相同类型的 指针变量指向数组元素的首地址,那么对数组元素的引用, 既可以用下标法,也可以用指针法。用指针法存取数组比 用数组下标存取数组速度快一些。反之,任何指针变量也 可以取下标,可以象对待数组一样来使用。虽然多位数组 的地址概念稍微麻烦些,但只要知道它的元素在内存中是 如何存放的,使用也就不难了。

指针的一个重要应用是用指针作为函数参数,为函数 提供修改调用变元的手段。当指针作为函数参数使用时, 需要将函数外的某个变量的地址传给函数相应的指针变元。 这时,函数内的代码可以通过指针变元改变函数外的这个 变量的值。
在下一章中将会看到,指针的另一个重要应用是与动 态内存分配函数联用,使得定义动态数组成为可能。

?

7.8.2指针的优点

指针是C语言中重要的概念,是C的一个特色。使用 指针的优点: ?提高程序效率。访问变量、数组、函数等内存中的C语言实体 时,变量名、数组名、函数名等最终要转换成地址才可以进行访 问,因此表面上看起来使用指针是一种间接的访问方式,而事实 上使用指针却正是是最直接、最快速的访问方式。因为,指针就 是地址。 ?在调用函数时变量改变了的值能够为主调函数使用,即可以从 函数调用得到多个可改变的值。 ?利用void *指针可以实现动态存储分配。 ?指针可以和数组、函数、结构体等相互结合以实现更为复杂的 功能。例如,函数间可以利用数组与指针传递数据,函数与指针 相结合的函数指针与指针函数;指针与数组相结合的数组指针与 指针数组,比如主函数main的带参形式在命令行参数中的使用等 等。下一章中的链表则是指针与结构相结合的一种简单应用,是 《数据结构》课程的基础。

?

7.8.3使用指针要注意的一些问题

应该看到,指针使用实在太灵活,对熟练的程序人员 来说,可以利用它编写出很有特色的、质量优良的程序, 实现许多用其他高级语言难以实现的功能,但也十分容易 出错,而且这些错误往往比较隐藏。由于指针运用的错误 可能会使整个程序遭受破坏。下面列出两种使用指针时常 犯的错误。

1. 非法内存访问错误 一种常见的内存异常错误是非法内存访问错误,即代 码访问了不该访问的内存地址。 例如未对指针变量p赋值就向*p赋值,就可能破坏了有 用的单元的内容。 程序段: int *p; int a=12 *p=a; /*注意p并没有指向变量a*/ 这里的p是个动态变量而且没有赋初值,所以p的内容 是个随机值,而p的内容是地址,亦即p的内容是个随机地址。 那么语句*p=12就会把12放到不可预期的任意地址对应的内 存单元中去,如果该单元中存放的数据是有用的或对程序来 说是关键的,那么就出现不可预期的错误了。解决的办法也 是有的:

?在定义指针的时候同时赋以初值NULL。这样即使程序员 范了糊涂,把程序写成写成: int *p=NULL; /*写成int p=0;也是等价的*/

int a=12;
*p=a; /*注意p并没有指向变量a*/ 也不会出致命的错误。

?当已经知道p要指向什么类型的变量(比如int变量),但 是还不指向一个具体的变量的时候。可以先用int p=NULL 对p进行赋初值,这样p就不指向任何内存单元;当需要使 用p指向某内存单元格的时候,再对p赋需要指向的变量的 地址。如:
int *p=NULL; int a=12 p=&a;

?当p还不需要使用的时候,即甚至还不知道以后p应该指 向什么类型的变量。则此时的p应该定义成void *类型,这 样p不指向任何内存单元,也不指向任何数据类型。当需 要使用p指向某种类型的变量的时候再对p进行强制类型转 换后再赋以需要指向的变量的地址。 如: void *p=NULL; int a=12; p=(int *)&a

使用指针时,只要恪守以下2条原则,就不会出现类似 上述的指针未初始化、数组下标越界这类非法内存访问错 误。

a) 永远清除每个指针指向什么位置。

b) 永远清除每个指针指向的位置中的内容是什么。

2.内存泄漏和“野指针”错误

另一类常见的内存异常错误是使用动态内存访问 时容易出现的错误。例如,向系统动态串申请了一 块内存,使用结束后,忘记了释放内存,造成了内 存泄漏,或者释放了内存但却仍然继续使用它,导 致产生“野指针”。养成良好的程序设计风格可以 避免这类错误。

当然了,指针使用过程中遇到的麻烦和容易出现 错误的地方还远不只于此,因此有人说指针是有利 有弊的工具,如果使用指针不当,(例如赋予它一 个错误的值),会出现隐蔽的、难以发现和排除的 故障。因此,使用时针要十分小心谨慎,要上机多 调试程序,以弄清一些细节,并积累经验。

?

7.8.4思考与提高

背景1:

有个名叫《苍蝇》的科幻片,片中讲述了有个科学家 发明了一种装置,可以把一个容器中的任何东西转化成基 本粒子后通过管道传送到另一个容器中,然后按照原样进 行组合,这样就可以把任何东西从这个容器传送到另一个 容器。通过指针我们也可以做类似这样的事情,只不过我 们的操作对象是数据。

思考: 在我们的C语言世界中,字节是最基本的数据存储单元 (当然位比字节小,但是我们仍然说字节是数据最基本的 存储单元,正如原子比分子小,但是仍然说分子是物质的 基本组成单元一样)。现在我们有了char类型和指针的知 识,我们也可以把任何一个数据进行分割和重组,事实以 后在文件一章中大家会发现利用fputc( )和fgetc( )函数可以 实现各种文件读写功能,其道理是一样的。

背景2: 走进书店放眼望去满是诸如C++、JAVA、.NET等面 向对象的书籍;任何一个计算机专业的学生也都会说上几 句:“C语言是面向过程的语言,C++是面向对象的语 言”。打开招聘网站发现招聘信息上要求掌握的也大多都 是.NET、JAVA、VC等技能。有的同学会问,那么我们为 什么还要学习C语言呢,它不过一种面向过程的语言嘛! 为什么还要学习它呢?

思考: 首先面向过程编程是学习面向对象编程的基础。其次 我们还可以在更深的层次上进行如下思考:试想如果一个 结构体的成员变量不是普通变量,而是指向函数的指针变 量,该结构体就可以通过该成员变量去操作函数了,那么 该结构体就相当于既有成员变量也有成员函数,也就是个 数据和方法的封装体了,而这正是面向对象程序设计语言 中“类”的雏形了,虽然它还不具备面向对象的其他重要 特征。其实到底是面向过程抑或是面向对象更在于程序员 的编程思想,而不单纯决定于使用何种编程语言。使用C 语言也能做出来面向对象的程序(win api方式的vc++下的 开发就是纯粹C语言实现的,但是也有“类”、“超类 化”、“子类化”的概念)。只不过使用C++比使用C语言 在实现面向对象思想上来得更加自然和方便些。


相关文章:
厦门理工第07章 指针(简易)_图文.ppt
厦门理工第07章 指针(简易) - 厦门理工学院高级语言程序设计教学课件 第7章 指针 教师: 施华 E-Mail: shihua2002@sohu.com 厦门理工学院计算机科学与技...
厦门理工学院 C语言报告实验7_指针.doc
厦门理工学院 C语言报告实验7_指针_理学_高等教育_教育专区。《C 语言程序设计》实验报告实验序号:7 学号 315 姓名陈 林仙丽 实验项目:指针 专业、班级 实验时间...
厦门理工学院11级C语言 第05章 结构体与共用体_图文.ppt
厦门理工学院11级C语言 第05章 结构体与共用体_电脑基础知识_IT/计算机_专业...结构体数组 ?结构体变量与函数* ?结构体变量与指针* ?共用体 ?枚举类型 ?...
厦门理工学院11级C语言 第01章 C语言程序设计概述_图文.ppt
厦门理工学院11级C语言 第01章 C语言程序设计概述_理学_高等教育_教育专区。...枚举类型、指针类型、文件等,以及由 上述类型构造的类型,如数组、结构体、共用...
厦门理工学院2011年C语言期末考试试卷与答案.doc
厦门理工学院期末考试卷 20-20 学年 第 1 学期 课程名称 C 语言程序设
厦门理工学院 C语言 实验7.pdf
《C 语言程序设计》实验报告实验序号:7 学号姓名 DEBUG 实验项目:指针 专业、...厦门理工学院11级C语言 ... 3页 免费 厦门理工学院11级C语言 ... 4...
厦门理工学院11级C语言 实验8_结构体.doc
厦门理工学院11级C语言 实验8_结构体_电脑基础知识...了解结构指针的定义和
厦门理工学院+C语言+实验7.doc
指针 专业、班级 实验时间 实验地点 一、实验目的及...文档贡献者 463979397 贡献于2014-07-06 ...厦门理工学院11级C语言 ... 3页 免费 厦门...
厦门理工学院11级C语言 实验2_数据类型、运算符和表达式.doc
厦门理工学院11级C语言 实验2_数据类型、运算符和表达式_电脑基础知识_IT/
厦门理工学院11级C语言 实验3_顺序、选择结构.doc
厦门理工学院11级C语言 实验3_顺序、选择结构_电脑基础知识_IT/计算机_专
厦门理工学院11级C语言C语言程序设计课程设计报告汇总.doc
厦门理工学院11级C语言C语言程序设计课程设计报告汇总_幼儿读物_幼儿教育_教育
厦门理工学院11级C语言 实验5_数组.doc
厦门理工学院11级C语言 实验5_数组_电脑基础知识_IT/计算机_专业资料。《
厦门理工学院+C语言+实验8.doc
了解结构指针的定义和使用 二、实验设备(环境)及...文档贡献者 463979397 贡献于2014-07-06 ...厦门理工学院11级C语言 ... 3页 免费 厦门理工...
厦门理工学院c语言程序设计期末试题ABC(含答案) 考试卷子.pdf
厦门理工学院c语言程序设计期末试题ABC(含答案) 考试...(2)指针法(9 分) #include <string.h> #...printf(“%s”,c) 10. 0 11. 全局变量、局部...
厦门理工学院2013级C语言程序期末试卷_B.pdf
厦门理工学院2013级C语言程序期末试卷_B_管理学_...B) for(i=1;i<11;i++) a[i-1]=i; D) ...若有 double *p,x[10];int i=5;使指针变量 p...
厦门理工学院试卷-C语言程序设计-2011-2012学年-第1学期.doc
厦门理工学院试卷-C语言程序设计-2011-2012学年-第1学期_工学_高等教育_教育...10. 以下指针变量的定义和赋值语句正确的是 (A)int a,*p;p=a; (C) int...
厦门理工学院C语言课程设计报告.pdf
总之,这次课程设计学到了两点:一是把 C 语言简单的编程在学一遍;二是,指针、...厦门理工学院11级C语言C... 24页 5下载券 厦门理工学院11级C语言C... ...
厦门理工学院C语言期末考试_图文.pdf
厦门理工学院C语言期末考试_研究生入学考试_高等教育_教育专区。期末复习 ---C.../*** 把第一个得分作为最 高分与最低分***/ for(i=0;i<11;i++) {...
厦门理工学院+C语言+课程设计.doc
厦门理工学院+C语言+课程设计 - 《C 语言程序设计》 课程设计报告 (201
厦门理工学院2013级C语言程序期末试卷_B.doc
厦门理工学院试卷 2013-2014 学年 第 1 学期 课程名称 C 语言程序设计专业 ...若有 double *p,x[10];int i=5;使指针变量 p 指向元素 x[5]的语句为(...
更多相关标签: