1
第 10章 指针
2
指针是C中的重要概念和内容,也是 C的特色,
但对初学者来说不易掌握。
在C程序设计中指针用得很多,一方面是由于有时候它是表达一种处理的唯一手段;另一方面使用它要比使用其他手段更高效。特别对于系统软件设计而言,该语言功能是必要的。
可以这样说,不能正确、熟练、灵活地使用这种语言功能就没有很好掌握 C!
本章详细讨论C语言中指针的概念和使用 。
3
10.1 地址和指针的概念
1) 地址的概念地址是内存单元的唯一编号。地址是何种形式与所用系统有关。
程序在 TC2.0上运行后的输出为:
FFD6,FFD9,FFDA
main ( )
{
int i ;
char c ;
float f ;
printf (,%p,%p,%p”,&i,&c,&f ) ;
}
一般以字节为单位例如:
4
i 的地址 DS:FFD6H
c的地址 DS:FFD9H
f的地址 DS:FFDAH …
DS,0000
i
DS,FFD6
DS,FFD7
DS,FFD8
DS,FFD9
DS,FFDA
DS,FFDB
DS,FFDC
DS,FFDE
内存( DS段)

c
f
在 TC系统中的每个地址:
用两个字节存储用四位十六进制表示是相对于 DS段 (数据段 )的相对地址表示形式是,
5
记住:
在 C中 地址是一种特别的数据类型 (指针类型 ),
不同于整型量,二者不相容。
& 是单目运算符 ; 优先级仅低于小括号 ;结合方向自右向左 ;
&的操作数只能是变量名和数组元素名,不能是常量、表达式和 register型变量名。
6
2) 指针概念指针即是内存单元的地址,只不过它是 C语言中对地址的称呼。
在 C语言中,指针是 一种数据类型,声明这种类型的变量,将给该变量分配该两个字节的存储空间,用于存放一个地址量。 因此 指针类型对象的值是一个内存地址 。
一个变量的地址称为该 变量的指针,即该变量占用的内存区域的 起始地址 。
7
一个 数组的指针 指的是数组存储空间的起始地址,
也即第一个数组元素的地址,数组名代表这个地址。
因此,数组名是一个指针型量且是指针常量。例如:
程序运行后的输出可能是,
FF8C,FFA0,FFB4
main ( )
{ int c[10] ;
float s[5] ;
long d[10] ;
printf (,%p,%p,%p”,c,s,d ) ;
}
8
函数是一段程序,程序要在内存中才能执行。因此函数也有内存地址。 函数在内存的存储地址称为函数指针 。 函数名代表函数的地址,是一个指针型常量 。例如:
该程序运行后输出的函数地址可能是:
020F /*这就是 f函数的指针 */
void f ( void ){
printf (,hello” ) ;}
main ( ){
f ( ) ; /*调用 f函数 */printf (,%p”,f ) ;
}
9
10.2 指针变量用于存储变量地址的变量称之为 指针变量 。当一个指针变量存储了某个变量的地址后,就称该指针变量 指向 那个变量,
1、指针变量的声明(定义)
指针是一种特殊数据类型,与其他数据类型的变量一样必须先定义后使用。 指针定义的一般形式为:
存储类型 数据类型 * 指针变量名该,*”指出其后的标识符被定义成指针类型,
用于存放地址量。
这是 指针变量本身的存储类型。可指定
auto,register( 仅用于局部量),static、
extern。 因此,指针变量也具有局部与全局、临时和永久特性。
这是指针变量所指向对象的 数据类型。
为方便叙述,不妨将其称为,基类型,。
可以放臵任何数据类型关键字
10
一旦定义了指针变量,便可用它来存放 ( 指向 )
与其 基类型 相同的某个对象的地址。 如下面的指针变量定义:
int *xp ;
xp 总是用于存放 int 型对象的地址。换句话说,
xp中存储的地址所指向的存储区域中的内容总按 int
型数据来解释。
若 int型数据为 16位,并假定分配给一个 int 型变量 k的存储区域的首地址为 2000,如果执行,
xp = &k ;
则有:
11
… …
2000 2001
xp
2000
分配给指针变量 xp的存储单元分配给变量 k
的存储单元
12
C语言允许定义如下形式的指针变量:
void *指针变量名 ;
void型 指针变量称之为,无基类型指针变量”,
它可用来存储任何一种数据类型对象的地址。可把
void型指针变量看作为,通用” 型指针变量但是,如果希望把 void型指针变量中存储的地址值赋给另一个具有确定基类型的指针变量时,则应该用强制类型转换运算符将它先转换成相应的基类型指针 后再赋给那个指针变量。
例如:
13
main ( )
{
void *p ;
int a,*ip ;
p = &a ;
ip = ( int* ) p ;
……
}
/*其中的,*”号表示指针类型不能丢掉 */
14
2、指针变量的初始化与定义普通变量相同,C语言也允许在定义指针变量的同时给指针变量臵初始值:
存储类型 基类型 *指针变量名 =初始化值 ;
给出的,初始化值,必须是与其基类型相同的对象的地址。
若未给全局和 static型指针变量指定初始化值,其初值自动为 0 (NULL),可直接使用 ;
局部与非 static型指针变量的初始化是当每一次进入函数或复合语句时进行的。它们在没有指定初值之前其值无定义,不能直接使用。
例如:
15
main ( )
{
int a ;
int *p=&a,*p1=p ;
……
}
从这个例子可以看出,指针变量的初始化值一定要为前面已定义过的变量的地址,或在这之前已被初始化过的指针变量名,且基类型应该一致。
16
具有 0值的指针变量不指向任何对象,被称之为
,空指针变量,。定义指针变量时可用如下的方法直接把其初始化成,空指针,,
int *p = 0 ;
或 int *p = NULL ;
再次强调,
地址是一种特殊的数据类型,并非整数,它们是与整数不相容。 因此,除数值 0之外,不能简单地把一个整数当作地址,作为指针 变量 的初始化值。
在 stdio.h 中定义的 值为
0 的符号常量
17
空指针变量与无定义指针变量有区别:
空指针变量指的是指针变量的值为 0,表示指针变量未指向任何对象,虽然能够直接使用它但仍然有破坏系统的危险性;
无定义指针变量的值是不确定的,它可能指向一个不可预知的存储单元,直接使用无定义指针变量要比使用空指针变量的危险性大得多。
18
3、指针变量的使用若 x是初值为 1085 的 int型变量,且 px是基类型为
int的指针变量,那么当执行如下的操作:
px = &x ;
main ( )
{
int val,x = 1085,*px ;
px = &x ;
……
}
19
x 的地址赋给指针变量 px 之后,px,指向,x
。 如下图所示:
20
这以后便可以通过指针变量 px,间接,地存取变量 x 。 而这种,间接,存取是用间接引用运算符
,*” 来实现的。
间接引用运算符,*” 的优先级同其他所有单目运算符的优先级和结合性相同。 由它构成的表达式的一般形式是:
*p
p 是操作数,可以是 指针常量,指针变量名,指针数组元素名,及值为指针的表达式 等。 这些操作数的值都必须是地址量。
21
例如:
先取出 px中存储的地址值 2000,再从地址 2000
开始的存储区中取出 1085,把 1085 存入变量 val的存储区中。
等价val=*px ; val=x ;
*p 是一个左值表达式,既可出现在赋值运算符的左边向它赋值,也可出现在赋值运算符的右边作为运算分量参与表达式的运算。例如:
*px=123;
val = *px ;
表示把 123
存入 px 指向的地址的存储空间中去。
表示取出 px指向的地址中存储的内容,存入变量 val 。
22
假定有 int a[10],i,j,*ip=&i,*jp=&j; 声明,则:
*ip=1; i=1;等价
*jp=*ip; j=i;等价
*&i=1; i=1;等价
(*jp)++; j++;等价
*a=1; a[0]=1; 等价
printf (“%d”,*ip+*jp ) ; printf (,%d”,i+j );等价
23
为了说明这个问题,请看下面的例子,
main ( )
{
int *xp ;
float f1=15.3,f2 ;
xp=&f1 ;
f2=*xp ;
printf (,%f\n”,f2 ) ;
}
从理论上讲指针变量可以用来存放任何内存地址。
那么,为什么要规定指针变量指向一种特定类型的对象呢?
程序输出,-13107.000000
按定义,xp只能指向 int 型对象,
但这里却把一个浮点变量 f1的地址赋给了 xp
执行 f2 = *xp;
不能实现 f2=f1;
的目的。 因为
24
指针变量 xp用于指向 int型对象,只作用于两个字节。而 float 型对象占用连续四个字节,程序中虽然将一个浮点变量的地址赋给了 xp,但这并不意味着将 xp的基类型改变为 float型而作用于连续四个字节,xp 仍然作用于两个字节。 因此执行 f2=*xp ;
只是将 f1 这个浮点数的前两个字节的内容作为 int
型数据解释并送给 f2,显然这已不是原来整个 f1的内容了。
从这个例子可以看出,在使用指针变量时必须保证其基类型与指向对象的 数据类型相一致 。
25
main ( )
{
void *p ;
int a=10,*ip ;
p=&a ;
ip=p ;
*p=123 ;
……
}
void型指针无基类型,它所指向对象的类型还没有确定,所以不可以对它使用,*”运算操作; 同样,不可以直接将变量赋给由 void型指针所指向的对象。
例如:
应写成,
ip=(int*)p;
应写成,
*(int*)p=123
26
若有 int x=1,*px=&x; 声明,则对于:
y = *px++ ;
y = (*px)++ ;
这两条语句的执行效果是不同的:
前者等价于执行,y=x ; px++ ;,而后者则等价于执行 y=x; x++; 。 因 (*px)就是 x
注意:语句 y=*(*px)++; 是错误的。这是典型的非法间接访问操作 。
27
main ( )
{
int a=1,*b=&a ;
printf (,%d\n”,*b ) ;
printf (,%p\n”,b ) ;
printf (,%p\n”,&*b ) ;
printf (,%d\n”,*&a ) ;
*&*b=10 ;
printf (,%d\n,,a ) ;
}
/* 相当于输出 a */
/* 输出 a 的地址 */
/* 输出 a的地址 */
/* 相当于输出 a */
/* 相当于执行 a=10; */
/* 输出 a */
程序输出,1
FFDC
FFDC
1
10
例 1:几种间接引用运算的效果,1FFDC a
bFFDCFFDE
10
28
main ( )
{
int a=1,b=2;
swap (a,b) ;
printf (“a=%d,b=%d”,a,b);
}
void swap(int x,int y)
{
int z ;
z=x ;
x=y ;
y=z ;
}
swap的功能是交换两个变量的值。实际上交换的只是 swap函数中两个局部变量 x和 y的值,并不反映到
main函数中,因此 main函数中变量 a和 b的值不变。
例 2:指针变量作函数参数在第八章中曾举过一个函数间传递变量的例子:
程序输出,a=1,b=2
29
void swap(int *x,int *y)
{
int z ;
z=*x ;
*x=*y ;
*y=z ;
}
main ( )
{
int a=1,b=2;
swap( &a,&b );
printf(“a=%d,b=%d”,a,b);
}
程序输出,a=2,b=1
要 通过调用 swap函数,使得 main函数中变量 a和 b
的值被交换,必须在两个函数间传递变量 a和 b的地址。
例如:
30
例 3 使用指针变量进行 输入 /输出假定有声明:
int x,*p=&x ;
则调用 scanf 函数输入 x,通常这样进行:
scanf (,%d”,&x ) ;
因为现在指针变量 p 已经存储了变量 x的地址
(已指向了 x),所以输入 x也可按如下方法进行:
scanf (,%d”,p ) ; /* 不能写成 &p */
同理,要输出 x 的值,下面的两种方式是等价的:
等价printf (,%d”,*p ) ; printf (,%d”,x ) ;
31
4、指针运算
C 语言中允许对指针进行某些运算。 除前面介绍过的求地址运算 &,间接引用运算 * 及赋值 = 运算外,还可以进行算术运算和关系运算。
(1) 指针的算术运算对指针仅能进行加、减算术运算。而且除下面列出的运算外,任何其他的算术运算都是非法的或无意义的。
px+k,px-k、
px++,px--,++px,--px、
px-=k,px+=k,px-py
k为整数 px,py为指针变量
32
px+k 和 px-k的意义是将 px当前指向的对象在内存中的位臵向地址大的方向 ( 对于 + ) / 向地址小的方向 ( 对于 - ) 移动 k个对象的位臵。 操作执行后的结果是一个地址量 。
它们计算后得到的地址将由 px的当前值及 px的基类型确定。C编译系统将按如下公式计算出 px+k
和 px-k:
px+k 相当于计算 px 加上 k*sizeof( px的基类型 )
px–k 相当于计算 px 减去 k*sizeof( px的基类型 )
33
计算式中的 sizeof 是 C语言中的一个单目运算符,
其优先级和结合性与其他单目运算符相同。
sizeof 运算符有如下的基本使用形式,
sizeof (表达式 )
sizeof (类型关键字或类型名 )
sizeof (数组名 )
sizeof 单一常量或单一变量名
sizeof 的运算功能是计算给出对象占用的存储空间大小(以字节为单位)。如给出的是数组名,那么计算结果将是该数组占用的内存空间的总字节数。
sizeof 的运算结果类型是 int,因此可作为运算分量参与表达式的运算。例如:
34
sizeof (1+2) /*结果为 2*/
sizeof 1.5 /*结果为 8*/
sizeof?A? /*结果为 2*/
sizeof (char) /*结果为 1*/
sizeof (float) /*结果为 4*/
sizeof,abcdefg” /*结果为 8*/
若有 char a=?A? ; 则 sizeof a /*结果为 1*/
若有 int b[10] ; 则 sozeof b /*结果为 20*/
若有 float f; 则 sozeof f /*结果为 4 */
sizeof 运算是在编译阶段完成的,不管它的操作数如何(例如,sizeof(sin(1)*a+1.5)),它总被看作为常量表达式 。
35
假定 px 是指向 int
型的指针变量。 现指向分配在 1000 单元的
int变量。
右图说明了,
px、
px+k、
px-k、
*px、
*(px+k)、
*(px-k)
等表达式的实际含义。
36
有了指针 px+k和 px-k的运算概念,便不难理解关于 px++,px--,++px,--px 以及 px-=k,px+=k
等指针运算的意义。
如果 p1,p2 都是指针变量,p1-p2运算并不象数值运算中那样简单的将二者的值相减得出差值,它的运算意义是:
求出 p1与 p2 所指向的对象之间的数组元素个数。
因此,p1-p2的 结果一定为一个非地址量的整型值 。
两个指针相减的操作,当它们都是指向同一数组中的不同元素时才有意义!
37
strlen ( char *s )
{
char *p=s ;
while ( *p!='\0 ' )
p++ ;
return ( p-s ) ;
}
作为 p1-p2指针减法的实际应用,下面利用它来改写求字符串长度函数 strlen。
38
2,指针的关系运算如果指针变量 p1,p2都指向同一数组中的元素,
那么C语言允许对指针变量 p1,p2 进行所有的关系运算。
若 p1<p2为真,表示指针变量 p1指向对象的存储位臵在 p2所指向对象的存储位臵的前面(在地址小的方向);反之,p2在 p1的前面。
若 p1==p2为真,则 p1,p2指向同一个对象。
至于 p1与 p2之间的 >=,<=,!=的比较意义,可以从上面讨论的 <,> 和 == 两种比较的意义中导出 。
39
注意:
不应将指针与其他类型的对象作比较。也不应该对指向不同数组中的元素两个指针变量进行比较,
虽然这类比较是允许的但无实际意义。
因C语言允许将一指针变量初始化为 0 或 NULL
,所以允许将指针变量与 NULL或数值 0进行 ==
或 != 的直接比较,这主要用于判定一个指针变量是否为空指针。
40
10,3 指向数组的指针变量在C语言中,指针与数组间的关系极为密切,
任何能够通过数组下标对数组元素进行的操作也能够通过指针来实现,而且用指针的方法访问数组的元素比用下标的方法访问数组的元素要快速、
方便。所以在程序设计中使用指针来访问数组元素则更为普遍。但利用它要求程序员随时清楚指针变量的当前指向(即它的当前值)。
41
指针与一维数组的关系较易理解。如果使一指针变量指向某个一维数组,那么对于该数组中元素的访问都可以通过这个指针变量而达到。
int a[10],*p,x ;
根据数组的性质,数组 a 中的 10 个元素 a[0]、
a[1],a[2],…… a[9] 被存储在一片连续的存储单元中。 a是该数组占用的全部存储区域的始点。
1,指针与一维数组若有如下声明语句:
42
按指针算术运算的性质,执行 p+1则得到下一个元素的地址,即 &a[1],因此,*(p+1) 即为 a[1]。
依此类推,就可实现用指针变量访问数组的全部元素。
一般而言,若使指针变量 p指向数组 a,那么指针表达式 p+i 就指向数组 a的第 i个元素,则 *(p+i)与
a[i]等价 ; 若 p 当前指向 a[i],则 p-i 指向 a[0]的位臵,
*(p-i) 与 a[0] 等价 。
如果执行:
p=&a[0];
x=*p;
使 p指向数组 a
相当于将
a[0]赋给 x
43
数组名 a是数组存储区域的首地址。而 &a[0] 也为数组的起始地址,所以就有 &a[0]与 a等价,也即:
等价p=&a[0] ; p=a ;
根据地址运算规则有:
a+i &a[0]+i &a[i]等价等价
*(a+i) *(&a[0]+i) *(&a[i]) a[i]等价 等价 等价
44
关于数组名的使用应 牢记,虽然数组名 a是一个地址量,但它和指针变量有区别,
指针变量 是一个变量名,它既可以出现在赋值号的左边向它赋值,也可以出现在赋值号的右边,
作为一个运算分量参于指针表达式的计算。
而 数组名 是一个指针常量。因此作为单一运算分量它不能出现在赋值号的左边,仅能作为指针表达式中的一个运算分量参于运算。
45
若 a 是一个数组名,p是一个指针变量,下面的运算都是错误的:
例如,
/* a是一个常量,无任何地址可言,&的运算分量仅能是变量名、数组元素名 */
a=p; /* 数组名 a出现在赋值号的左边 */
a++;
p=&a;
/* ++运算符的操作数只能是变量名、数组元素名 */
46
以 a为数组的起始地址,根据数组 a中元素的数据类型计算出第 i个元素存储位臵,即计算地址表达式 a+i,最后存取 a+i中的内容,也即计算 *(a+i)

假定 p为指向某个一维数组 a的指针变量,那么有:
C允许采用带下标的指针形式来访问数组元素。
p[i] a[i]等价可以这样来理解这种等价关系:
访问一维数组 a的第 i个元素 a[i],其处理过程是,
这样便证明了 a[i]与 *(a+i) 是等价的;
47
反之,根据 a[i] 的计算过程,也可以得到 p[i]
等价于 *(p+i) 。
进一步,便有 *(a+i) 等价于 *(p+i),因为 *(a+i)
即为 a[i],所以有 *(p+i)等价于 a[i]。
由此可以得出结论:
p[i] *(p+i) a[i]等价 等价 等价 *(a+i)
在程序中可以根据自己的习惯爱好任意互换使用它们。 例如:
48
main ( )
{
int a[10],i,*p ;
p=a ;
for ( i=0 ; i<10 ; i++ )
scanf ( "%d",p++ ) ;
p=a ;
for ( i=0 ; i<10 ; i++ )
printf ( "*(a+%d)=%d p[%d]=%d\n",i,*(a+i),i,p[i] ) ;
}
/* 请思考:此处为什么把 a再一次赋给 p? */
49
2、指针与多维数组指针与多维数组的关系比指针与一维数组的关系稍许复杂一些,但概念上是类似的。这里仅以二维数组为例说明它们之间的关系。
对于二维数组定义,int a[i][j]; ( i,j均为常量 ),
可理解为定义了 i个一维数组,a[0],a[1],… a[i-1]
分别是每个一维数组的名字,表示二维数组各行上的元素的存储区域的首地址。 而每个一维数组有 j
个元素。
50
a[k]+p ( 0≤k< i,0≤p< j) 表示该二维数组 k 行 p 列上的元素的地址。
按照指针算术运算规则:
则有,
a[k]+p 等价于 &a[k][p]
因此有,
51
进一步,因二维数组名 a 是二维数组存储区域的首地址,它当然与 a[0]和 &a[0][0]表示的地址值相同。
但 a+k 并不与 a[0]+k等值,
因为 a[0]+k 是该二维数组 0行上的 k元素的地址,
它就是 &a[0][k] ;
而 a+k 在二维数组的定义下,它表示的是 k行的一维数组存储空间的起始地址。因此有:
a+0 等价于 a[0],a[0]+0,&a[0][0],*(a+0)
a+1 等价于 a[1],a[1]+0,&a[1][0],*(a+1)
…………..
a+(i-1) 等价于 a[i-1],a[i-1]+0,&a[i-1][0],*(a+i-1)
52
这种关系的直观表示如下图所示:
53
注意,*(a+k) 与 *a[k] 也是不同的,
前者相当于 a[k],是一个地址值;
后者则相当于 *(a[k]+0)或 *&a[k][0],即相当于数组元素 a[k][0]中存储的值。
按照上面对二维数组的分析应不难理解:
a[k][p] 就是 *(a[k]+p) 也就是 *(*(a+k)+p)
弄清了二维数组中有关地址方面的概念便可用指针变量来存取二维数组中的元素。
这种使用的一个简单例子是:
54
#include <stdio.h>
main ( )
{
int i,*p ;
int a[4][4]={ 1,2,3,4,5,6,7,8,
9,10,11,12,13,14,15,16 } ;
p=a ;
for ( i=0; i<16; i++ )
printf((p-a[0])%4==0?"%3d\n":"%3d",*p++ ) ;
}
1 2 3 4
5 6 7 8
9 10 11 12
13 14 15 16
程序运行输出结果是:
/*注意,这里的 p不具有二维数组名的性质 */
55
3,指向二维数组的指针变量上例对二维数组元素的指针引用方法实际上是把二维数组作为一维数组来处理的,虽然程序中将二维数组名赋给指针变量 p,但 p并不具有二维数组名的特性。
为了能使一个指针变量体现二维数组名的特性,
C语言允许定义如下形式的指针变量,
基类型 (*指针变量名 )[m] ;
当把它指向一个二维数组后,它具有二维数组名的特性。
以这种形式定义的指针变量可以指向有 任意行数,每行有 m 列 ( m为正整常数)的二维数组。
56
例如:
int (*p)[3] ;
定义了一个指向若干行,每行有 3 个元素的 int型二维 数组的指针变量 。
p 为指针变量名,用于指向二维数组中某一行的存储区域的首 地址。 (*p) 两边的括号不能省去,
否则就变成了稍后将要讨论的指针数组定义。
其中,
57
p 指向 0行元素的存储起始地址
p+1 指向 1行元素的存储起始地址
………
p+i 指向 i行元素的存储起始地址若把一个二维数组名赋给 p,则有:
因 p 是指针变量,不同于数组名 (常量 ),所以可改变它的值使它指向二维数组中任一行元素的存储区域的起始地址。
58
用这种 指针 表示每一行中元素的形式为:
第 0 行中的第 j个元素表示为,(*p)[j]
第 1 行中的第 j个元素表示为,(*(p+1))[j]
………
第 i 行中的第 j个元素表示为,(*(p+i))[j]
其中 0≤j< m,m 是一行中元素的个数 。
而当改变它的值,使它指向了某一行时,(*p)[j]
就表示 了该行上的 j 元素。
按照二维数组的性质,二维数组中第 i行、第 j列元素的地址也可表示为 *(p+i)+j,那么 *(*(p+i)+j) 便是访问 p[i][j],也就是 (*(p+i))[j]。
下面通过一个例子来说明这种指针变量的使用。
59
main ( )
{ int (*p1)[4],j ;
int p[4][4]={
{ 1,2,3,4 },
{ 5,6,7,8 },
{ 9,10,11,12 },
{ 13,14,15,16 }
} ;
for ( p1=p; p1<p+4; p1++ ) {
for ( j=0; j<4; j++ )
printf ( "%4d",(*p1)[j] ) ;
printf (,\n”) ;
}
}
这里 p1具有二维 数组名的性质该例也可改写成,
60
main ( )
{
int (*p1)[4],i,j ;
int p[4][4]={ { 1,2,3,4 },
{ 5,6,7,8 },
{ 9,10,11,12 },
{ 13,14,15,16 }
} ;
p1=p ; /*这里 p1具有二维数组名的性质 */
for( i=0; i<4; i++ )
for ( j=0; j<4; j++ )
printf(j==3?"%4d\n":"%4d",*(*(p1+i)+j));
/* 或 (*(p1+i))[j] */
} /* 或干脆 写 成 p1[i][j] */
61
4、指向二维数组的指针变量作函数参数某班有 3个学生,各学 4门课,计算总平均分数,以及序号为 n的学生成绩。函数 average
求总平均成绩,函数 search找出并输出序号为
i 的学生成绩,并使用 指向二维数组的指针变量作函数参数。
例 1:
62
main ( )
{
void average ( float *p,int n ) ;
void search ( float (*p)[4],int n ) ;
float score[3][4]={
{ 65,67,70,60 },
{ 80,87,90,81 },
{ 90,99,100,98 }
} ;
average ( score,12 ) ; /*12为成绩数据个数 */
search ( score,2 ) ; /*序号为 2的学生 */
}
63
void average ( float *p,int n )
{
float *p_end,sum=0,aver ;
p_end=p+n ;
for ( ; p<p_end ; p++ )
sum + =*p ;
aver = sum/n ;
printf (,Average=%5.2f\n”,aver ) ;
}
/* p不具有二维数组名性质 */
score 12
64
void search ( float ( *p)[4],int n )
{
int i ;
printf (,The score of No.%d are,\n”,n ) ;
for ( i=0 ; i<4 ; i++ )
printf (,%5.2f,,*(*(p+n)+i) ) ;
} /* 或 ( *(p+n) )[ i ] */
/* 或干脆写成 p[n][i] */程序运行结果如下:
Average=82.25
The score of No.2 are,
90.00 99.00 100.00 98.00
score 2
/* p具有二维数组名性质 */
65
main ( )
{
void search_f ( float (*)[4],int ) ;
float score[3][4]={ { 65,57,70,60 },
{ 58,87,90,81 },
{ 90,99,100,98 }
} ;
printf (,\n” ) ;
search_f ( score,3 ) ; /* 3为学生个数 */
}
查找有一门以上课程不及格的学生,打印他 /她们的全部成绩。
例 2:
66
void search_f ( float (*p)[4],int n ) /* n为学生个数 */
{ int i,j,flag ;
for ( j=0; j<n; j++ ) {
flag=0 ;
for ( i=0 ; i<4 ; i++ )
if ( *(*(p+j)+i )<60 )
flag=1;
if ( flag==1 ) {
printf (,No.%d is fail,scores are,\n”,j ) ;
for ( i=0 ; i<4 ; i++ )
printf (,%5.1f,,*(*(p+j)+i ) ) ;
printf (“\n”) ;
}
}
}
/* 有不及格成绩时 flag为 1 */
/* 无不及格成绩时 flag为 0 */
/* p[j][i] */
/* 可写成,(*(p+j))[i] */
/* p[j][i] */
/* 可写成,(*(p+j))[i] */
67
No,0 is fail,scores are,
65.0 57.0 70.0 60.0
No,1 is fail,scores are,
58.0 87.0 90.0 81.0
程序运行结果为:
68
10.4 指向字符串的指针变量处理字符串一般都用一维字符数组进行。有了指针概念后,当然可定义字符型指针变量,使它指向字符串,对字符串进行处理。
事实上用指针的方法来处理字符串,要比使用数组更方便灵活。
在讨论指针变量与字符串的关系之前,需要先了解C中字符串常量的某些特性及C编译程序对字符串的处理方法。
69
可以这样来理解字符串常量:
字符串常量的“存储类型”是 static ;
字符串常量本身 隐含表示其存储地址,因此它 是指针类型。
字符串常量的内容是在双引号包含的字符序列,
并在最后自动加上一个 NULL字符的字符序列。
70
因此,程序中的两个形式上完全相同的字符串常量是不同的数据对象,因为存储地址不同。
如对于:
printf (,Hello,world\n" ) ;
这样的函数调用,printf 函数实际接收到的是一个指向这个字符串的指针。 且通过这个指针来存取它。
在 编译源程序时,每遇到 一个字符串常量,便在静态存储区分配存储空间,并将该字符串中的字符依次存入其中,然后在其后加上一个 NULL字符。
71
程序设计时,可以定义一个带初始化值的字符型指针变量,使它直接指向一个字符串;或定义一个字符型指针变量,然后将该字符串赋给该字符型指针变量,同样使它指向指定的字符串。
例如:
main ( )
{
char *s="Initialization of pointer with a string",*s1;
s1="assigns to s1 a pointer to the actual string" ;
………
}
72
其后的程序便可通过指针变量 s和 s1来访问这两个字符串中的个别字符。 如可用 *(s1+3) 或 s1[3]
来访问字符串,assigns to s1 a pointer to the
actual string”中序号为 3 的字符 i 等等。
上例不是将两个字符串常量的内容赋给指针变量
s 和 s1,而是把两个字符串在内存中的存储地址赋给
s 和 s1。
73
其实,如果要使一个字符指针变量指向某个字符串,还可以按如下方法进行:
main ( )
{
char array[ ]="Initialization string",*s= array ;
………
}
或者:
main ( )
{
char array[ ]="Initialization string",*s ;
s=array;
………
}
74
上例中的 s相当于数组名 array,它们都指向同一个 字符串:,Initialization string”。 但它们又有区别:
一方面 s 是指针变量,改变它的值可以指向不同的字符串 ;而数组名 array是地址常量,它们仅表示一个固定不变的存储区域的首地址;
另一方面,因 s是指针变量,它的使用存在初值问题,而数组名是一个常数不存在初值问题 。
75
main ( )
{
char array[80],*p ;
scanf (,%s”,array ) ; /* 正确 */
scanf (,%s”,p ) ; /* 错误 */
………
}
如在下面例子中,对数组名的使用是正确的,但对指针变量的使用却是错误的。
因 p没有赋初值!
76
strcpy ( char *s,char *t )
{
while ( *s++=*t++ ) ; /* 空循环体 */
}
作为字符型指针变量的实际应用,下面的例子用字符型指针变量来实现 strcpy和 strcmp函数 。
strcmp ( char *s,char *t )
{
for ( ; *s==*t; s++,t++ )
if ( *s=='\0' )
return (0) ;
return ( *s -*t ) ;
}
77
#include <string.h>
main ( )
{
char s[50],*p=s ;
do {
gets ( p ) ;
while ( *p )
printf ( "%c",*p++ ) ;
} while ( strcmp( s,"done" ) ) ;
}
下例企图用来每次从键盘上读入一个字符串,并将读入的字符串显示出来。当读入的字符串是
,done”时停止程序的执行。
但这是一个错误的程序,请研究并改正之。
/*? */
/*? */
/*? */
78
10.5 指向 函数 的指针变量函数的源代码被编译、连接、装入后,函数在内存中便占有了实际的存储区域,其存储区域的首地址便称为,函数 的指针,,而 函数名 便代表了这个入口地址。因此,函数名是一个地址量且是一个地址常量 。
调用函数实际上是把程序转移到由函数名代表的内存地址处执行。如果把函数名赋给一个指针变量,
那么该指针变量也就指向了这个函数,我们把这样的指针变量 称之为,指向函数的指针 变量,,简称
,函数指针 变量,。
79
1,函数指针变量的定义形式存储类型 数据类型 ( *函数指针变量名 ) ( ) ;
(*函数指针变量名 )两边的小括号一定要有,否则就变成了稍后将要讨论的返回指针类型的函数定义或引用函数的函数说明。
存储类型 指出该函数指针变量本身的存储类型;
数据类型 指的是指向函数的返值类型。
80
例如:
int ( *p ) ( ) ;
定义了一个指向函数的指针变量 p,用于指向返值数据类型为 int型的函数。
函数指针变量的性质与一般指针变量的性质是类似的,但不能对函数指针变量进行算术运算。 不能企图用,p+i”的手段来实现函数的多个入口。
有了函数指针变量,便可以通过它来调用由函数指针变量指向的函数,实现 函数间函数的传递 。
81
#include <stdio.h>
main ( )
{
int f1 ( int x ),f2 ( int x ) ;
int sum_square ( int (*f) ( ),int m,int n ) ;
printf ( "result=%d\n",sum_square ( f1,1,2 ) +
sum_square ( f2,1,2 ) ) ;
}
例:
82
int sum_square ( int (*f) ( ),int m,int n )
{ int k,sum=0 ;
for ( k= m ; k<=n ; k++ )
sum+= (*f) (k)*(*f) (k) ;
return (sum) ;
}
int f1 ( int x )
{
return x+1 ;
}
int f2 ( int x )
{
return x-1 ;
} 程序的输出,result=14
该例中先后将函数名 f1
和 f2 作为实参传递给函数
sum_square中声明为函数指针变量的形式参数 f,这样 f 便先后代表了函数 f1
,f2。
83
main ( )
{ int (*p) ( ) ;
int f1 ( int x ),f2 ( int x ) ;
int sum_square ( int (*f) ( ),int m,int n );
int result=0 ;
p=f1 ;
result+=sum_square ( p,1,2 ) ;
p=f2 ;
result+=sum_square ( p,1,2 ) ;
printf ( "result=%d\n",result ) ;
}
该程序的 主函数完全可以改写成如下形式:
84
main( )
{
char (*p)(char *)=getchar;
……
}
下列程序有错吗?
#include <stdio.h>
main( )
{
char (*p)(char *)=getchar;
……
}
若改成下列形式,程序还有错吗?
85
10.6 返回指针值的函数利用函数的返值除可返回具体的数值外还可以返回各种数据类型对象的地址。
能进行这类数据返回的前提是被调用函数必须定义成返值为地址量的指针型函数:
存储类型 数据类型 *函数名 (形式参数定义 )
{
函数体
}
86
main( )
{
char *p(char *),*k ;
k=p( "ABC" ) ;
printf("%s",k) ;
}
char *p( char *p )
{
printf ( "aaaaaaaaaaaaa\n" ) ;
return "abc" ;
}
返回指针的函数例,引用返回指针的函数 p的引用说明
/*返回地址的指针型函数 */
87
与之相应,在调用一个返回指针型函数之前必须对被调用函数进行如下形式的说明:
存储类型 数据类型 *函数名 (形式参数说明 );
例如,char *p(char *) ;
用于接收这种返值的对象只能是与返值同类型的指针变量名不应将局部于被调用函数的指针变量作为返值返回给调用者。正确做法应该把所返回的对象定义成全局对象或 static型的对象。 原因是当函数返回后,
局部对象已释放(不再可靠了)。 例如,
88
char *cut ( char *str,int location,int number )
{
static char substring[80] ;
int i ;
if ( strlen (str) <= location )
return substring ;
for ( i=0 ; i<number ; i++ )
if ( str[ i+location ] !='\0' )
substring[ i ]=str[ i+location ] ;
else {
substring[ i ]='\0' ;
break ;
}
return ( substring ) ;
}
如果去掉 substring 前的
static,则 substring 就成为非静态的局部数组,程序的目的就可能达不到。
89
#include <string.h>
main ( )
{
char *cut ( char *,int,int ),*sp,string[80] ;
gets ( string ) ;
sp= cut ( string,3,5 ) ;
printf ( "%s\n",sp ) ;
}
程序的运行的情况:
abcdefhhyjk /*这是输入给程序的字符串 */
defhh /*这是程序的输出结果 */
从读入的字符串的序号为 3 的字符位臵开始截取 5个字符
90
10.7 指针数组和指向指针变量的指针变量
1、指针数组如果定义了一个数组,数组的所有元素都是指针,那么该数组便是一个指针数组。
由于数组中的一个元素相当于一个变量,所以指针数组中的每一个元素便相当于一个指针变量。
指针数组的定义形式为:
存储类型 数据类型 *指针数组名 [数组大小 ];
,*” 规定了其后的数组是指针类型
,数据类型,指的是数组中每一个元素的 基类型。
其他项的规定及意义与普通数组中的解释相同
91
例如:
static char *p[4] ;
定义了一个具有 4 个元素的静态指针数组 p
,数组中每个元素 p[0],p[1],p[2],p[3]都用于存储指针,并且它们都指向 char型对象。
定义指针数组时也可以对其指定初始化值,但作为初始化值的地址必须存在。 如下面的初始化是正确的。
92
main ( )
{
static char ch[ ][20]={,Beijing”,
“Nanjing”,
“Shanghai”,
“Guangzhou”
} ;
char *p[ ]={ ch[0],ch[1],ch[2],ch[3] } ;
………
}
注意,指针数组的初始化值 只能是 static对象的地址,
否则将导致非法的初始化错误。
该例也可以直接定义为:
93
main ( )
{
char *p[ ]={ "Beijing",
"Nanjing",
"Shanghai",
"Guangzhou"
} ;………
}
上述定义的效果是:
数组 p占用多少存储空间?
94
不要把一个二维数组与一个指针数组相混淆。
虽然它们在使用上是类似的,但它们之间也有 一些差别 。如对于,
int a[10][10] ;
int *b[10] ;
这两个数组定义中的 a 和 b,在使用上它们可以达到相同的目的。例如要访问 a[5][5],对于 b也可以直接使用 b[5][5]。
但是 a 是数组,已经对它分配了 100 个用于存储
int 型对象的存储区 ;
而对于数组 b,仅仅分配了用于存放 10个指针的存储空间。
95
假定数组 b的每个元素都分别指向有 10个 int型元素的数组,b占用的存储空间要比 a所占用的存储空间多一些,且每一个 b[i]都必须先臵初值才能使用。但使用 b有它的优越性:
首先,存取 b[i]指向的数组中的元素可以通过指针间接地达到,而不需要进行复杂的下标计算。
其次,b[0],b[1],……b[9] 所指向的一维数组中的元素的个数可以不同 。 可以是 1个,2个,3个 …
等,甚至可以不指向任何数组。但在 a 中,每一行上的元素个数必须相等。
96
最后,对 b而言各个 b[i]指向的存储区域之间不必连续。但对 a 而言,其存储区域必须是连续的。
下面是用指针数组处理二维字符数组的例子。
该例对给出的若干字符串按字典顺序排序。使用指针数组则无需对两个字符串的实际存储内容进行交换,只要交换两个字符串的首地址就可以了。
97
#include <string.h>
main ( )
{
int i,min,j ;
char *temp,*p[ ]={,Beijing”,“Nanjing”,
“Shanghai”,“Guangzhou” } ;
for ( i=0 ; i<3 ; i++ ) {
min=i ;
for ( j=i+1 ; j<4 ; j++ )
if ( strcmp( p[j],p[min] )<0 ) min=j ;
temp=p[i] ;
p[i]=p[min] ;
p[min]=temp ;
}
for ( i=0 ; i<4 ; i++ )
printf ( "%s\n",p[i] ) ;
}
98
程序的输出是:
Beijing
Guangzhou
Nanjing
Shanghai
下图指出了程序执行后指针数组中的诸元素的指向 。
99
2、指向指针变量的指针变量指 针 变量 与普通变量一样也分配确定大小的内存空间,因此,指针 变量 本身也对应一个存储单元地址。
若定义了指针变量 p,并使它指向另一个指针 变量 q,那么指针变量 p就是一个指向指针 变量 的指针变量 。其直观表示如下图所示:
指针变量 p
指针变量 q
变量 a
&q
&a
数据
100
利用指向指针变量的指针变量去存取所指向的对象是 二重间接访问过程 。
指向指针变量的指针变量的定义形式如下:
存储类型 数据类型 ** 指针变量名 ;
这两个星号表示其后的指针变量名被定义为指向指针变量的指针变量。
其实,可以定义更多重的指针变量,不过二重以上的间接访问用得较少。
下面是利用指向指针变量的指针变量来实现上面的例子:
101
#include <string.h>
main ( )
{ int i,j,min ;
char *temp,*p[ ]={"Beijing","Nanjing",
"Shanghai","Guangzhou" };
char **pp ;
pp=p ;
for ( i=0 ; i<3 ; i++ ) {
min=i ;
for ( j=i+1 ; j<4 ; j++ )
if ( strcmp(*(pp+j),*(pp+min))<0 )
min=j ;
temp=*(pp+i) ;
*(pp+i)=*(pp+min) ;
*(pp+min)=temp ;
}
for ( i=0 ; i<4 ; i++ )
printf ( "%s\n",*pp++ ) ;
}
102
main ( )
{
int i=10,*p1,**p2,***p3 ;
p1=&i ;
p2=&p1 ;
p3=&p2 ;
printf (,%d,%d,%d”,*p1,**p2,***p3 ) ;
}
程序输出,10,10,10
p3 p2 p1
10
i
多级指针变量使用例:
103
3,向主函数 main传递数据与命令行参数假定用 Turbo C编写了一个实现文件拷贝功能的程序 cp.c,经编译、连接生成可执行程序 cp,exe

当执行 cp时至少需要给 cp提供两个参数,被拷贝的文件名和存放拷贝的目标文件名 。 它们可从键盘上直接读入,但这不自然,最好的方法是在启动
cp的同时在系统命令行上直接给出。例如:
c:\tc> cp filename1 filename2 ↙为了满足这种需要,C允许编写带形参的 main函数,将命令行上的内容作为实参传递给主函数的形参,
使 main函数直接获得程序运行时所需的数据。
104
带形式参数的主函数的定义形式是:
main ( int argc,char *argv[ ] )
{
………
}
带形参的主函数只允许两个形参,按传统习惯这两个形参的名字为 argc 和 argv,当然可使用定其他的名字 。
argc 为 int 型数据,它用于接收命令行中实参的个数( 可执行程序文件名本身也算一个参数 )
argv的数据类型一般为指向字符串的指针数组,
它用于接收命令行上的每一个实参。
105
假定上述 cp程序的主函数被定义成带形式参数的主函数,如在命令行上打入,
cp filename1 fileneme2 ↙
则 argc 中为 3,argv[0],argc[1],argv[2]中分别为,cp”,“fliename1”,“fliename2”的存放地址 。
注意,在命令行中给出的参数之间只能用空格或制表字符作分隔符。
注意,在命令行中给出的参数之间只能用空格或制表字符作分隔符。
106
由于可执行程序名也按一个参数,因此 argc的值总是大于等于 1。当 argc为 1时说明命令行上的程序名后没有任何参数;
而 argv[0]中存储的总是可执行程序名字的存放区域的地址。 argv[1]是第二个参数地址,……
下面是一个带命令行参数的例子。
程序的功能是将命令行上除程序名之外的所有给出的其他参数都回显到显示器上。假定该程序的源文件名为 echo.c。
107
main ( int argc,char *argv[ ] )
{
while ( --argc>0 )
printf ( (argc>1)? "%s","%s\n",*++argv ) ;
}
假定这个程序经编译、连接、装配成 echo.exe。
则在命令行上打入
C:\tc20> echo Hello,world! ↙
程序将输出:
Hello,world!
108
若一个 main函数定义成:
main ( int argc,char **argv )
{
……
}
它的可执行程序文件名为 first.exe。 若打入的命令行是:
first second third ↙
那么,argv[0] argv[1] argv[2] 中的值分别为字符串 first,second,third的存储区域的首地址。
第二个参数是二重间接级指针
109
5、用二级指针变量实现二维动态数组数组一旦被定义其大小也就确定了,不能动态改变数组的大小对程序设计不利。在事先无法估计数组的大小的情况下,一般都定义充分大的数组来满足问题的需要,但这常常导致内存的浪费。
有了指针变量概念后问题就好办了,可以利用指针变量及标准函数库提供的标准函数,如,
malloc,calloc,realloc,free等函数来实现动态数组。
110
malloc 函数
malloc函数用来分配指定数量的主存空间,并返回分配到的存储空间的首地址。
malloc函数的调用形式是:
malloc ( size )
因调用函数只要求分配多少空间,并没有指明在分配的空间中放何种数据,因此 malloc函数总是返回一个 void型的指针。
要求分配的内存量(字节为单位)
111
调用者必须把 malloc函数返回的 void型指针强制类型转换成要求的基类型。
当 malloc函数执行时,若发现当前没有足够的内存空间可分配,将返回一个 空指针 。
因此,调用者必须检查 malloc 函数的返值情况,
以判定是否成功分配。
若程序中要调用 malloc函数,则必须在程序中包含 头文件 stdlib.h 。
注意,有些机器要求使用的是 malloc.h头文件。
112
#include <stdio.h>
#include <stdlib.h>
main ( )
{
float *p ;
p = (float*) malloc ( 1000 ) ;
if ( !p ) { /*检查是否成功分配 */
printf (,Memory allocation failure\n" ) ;
exit(1);
}
……
}
113
calloc 函数
calloc 函数也用来申请指定量的内存空间。它的调用形式是:
calloc ( num,size )
其中,num” 是一个无符号整型量,表示申请 num个对象的存储空间,而每个对象占用的存储空间大小由第二个参数,size”来决定,,size”也必须是一个无符号整型量。
calloc函数使用的其他说明均与 malloc函数相同。
下面是这个函数调用的示例:
114
#include <stdio.h>
#include <stdlib.h>
main ( )
{
float *p ;
p=(float*) calloc ( 250,sizeof ( float ) ) ;
if ( !p ) {
printf (,Memory allocation failure\n" ) ;
exit (1) ;
}
……
}
115
free 函数
free 函数的功能与 malloc 函数的相反。它用来归还先前已经分配的存储空间。以便再分配给其他程序或对象使用。它的调用形式如下:
free ( p )
其中 p 是一个指针变量名,而且它一定指向在这之前用 malloc或 calloc函数分配获得的存储区域的首地址。
调用 free 函数给出的参数 p 不能是一个空指针或无定义指针,否则将会破坏操作系统的存储管理机构。
这个函数的调用通常总是成功的,无需去检查它的执行情况,该函数总是返回 NULL值。
116
realloc的函数
realloc函数用来 动态扩大 /归还 已分配的内存空间 。
realloc 函数的调用形式是:
realloc ( p,size )
其中 p 是指针变量名,且指向先前用 malloc 分配获得的存储区。
size 是无符号整型量,则用来指出把 p指向的内存区域的大小 扩大 /缩小 成 |size-原区域大小 |。
117
使用 realloc函数的,要注意以下两点:
第一,当 size 比原存储区大时,若原区域后面已无空闲区,或现有空闲区不足以满足分配要求,
但内存中另有一块足够的空闲区可满足分配,这会引起内存中数据的移动,把原存储区中的内容搬到新区中,收回原存储区。在这种情况下,realloc函数返回的是一个新存储区的首地址。因此在调用程序中的所有指向原区域的指针的值都要改成新的地址值,否则如果继续使用将会产生错误。这一点往往被程序设计者忽视。
118
第二,对于扩充内存的情况,如果当前内存空间的使用比较紧张,找不到一块连续的足够大的空间满足要求的扩充,此时 realloc函数将把原来的存储区域收回,并返回一个空指针。这样原存储空间当然不再属于调用者,因此程序不能再继续进行下去,
应该停止程序的执行。
下面是一个调用 realloc函数的简单例子。
119
#include <stdlib.h>
#include <string.h>
main( )
{ char *p;
p=(char*)malloc(12);
if(!p){
printf(“Memory allocation failure\n");
exit(1);
}
gets(p); /* 假定输入字符串 Hello,world */
p=(char*)realloc( p,13 ) ;
if(!p){
printf(“Reallocation aborting\n");
exit(1);
}
strcat( p,"!" ) ;
printf(p);
free(p);
}
120
#include <stdlib.h>
main ( )
{ int **b,row,column,i,j ;
printf("Enter rows and column of the array in
n*m:");
scanf ( "%d*%d ",&row,&column ) ;
b=( int** )malloc ( row*sizeof(int*) ) ;
for ( i=0 ; i<row ; i++ ) {
b[i]=( int* ) malloc ( column*sizeof(int) ) ;
if ( !b[i] ) exit ( 1 ) ;
}
有了上述函数,便可用指针手段来实现动态数组。
下面是用二级指针变量实现二维动态数组的例子。
121
printf ( "Enter datum of the array,\n" ) ;
for ( i=0 ; i<row ; i++ )
for ( j=0 ; j<column ; j++ )
scanf ( "%d ",&b[i][j] ) ;
for ( i=0 ; i<row ; i++ )
for ( j=0 ; j<column ; j++ )
printf(j==column-1?"%4d\n":"%4d",b[i][j]);
}
122
程序运行情况:
Enter rows and column of the array in n*m:
3*4
Enter datum of the array,
12 23 34 45 56 78 89
11 22 33 44 55
12 23 34 45
56 78 89 11
22 33 44 55
123