第 8 章 指 针
8.1 指针概念
8.2 变量的指针和指向变量的指针变量
8.3 指针变量的定义
8.5 指针与数组
8.4 指针变量的引用
8.6 字符串的指针和指向字符串的指针变量
8.7 指针与函数
8.8 指向结构体类型数据的指针
8.9 动态数据结构
第 8 章 指 针
8.1 指针概念
指针是 C的一个重要概念。其特点是:
能有效地表示复杂的数据结构; 能动态地分配内存,直
接处理内存地址; 能方便、有效地使用字符串和数组;
能在调用函数后获得多个值。
C中定义的变量,在编译时按变量的类型来分配一
定长度的内存单元。在运行时,根据变量名与地址的对
应关系,相应地存取变量的数据。这种按变量地址存取
变量值的方法称为,直接访问,方式。
如果变量的地址也存放在某一个内存单元, 则存取
方式为先找到存放变量的地址单元, 再取变量的值, 这
种方式称为, 间接访问, 方式 。如,
p=&i; 将变量 i的地址存放到变量 p“指向, 的内存单元中
8.2 变量的指针和指向变量的指针变量
变量的指针就是变量的地址,即 p
的值。也就是说,指针变量的值就是该
指针变量所指变量的内存首地址 。
在 p的前面加上一个,*,,即 *p就
表示指针变量 p所指向的变量。
从右图可知,
*p也是一个变量,且与变量 i是同
一回事。
i = 3;
p=&i;
*p=3; 将 3赋给指针变量 p所指向的变量,与 i=3等价
用户数据区
?
2000 3 变量 i
2002 6 变量 j
2004 9 变量 k
? ?
? ?
3010 2000 变量 p
8.3 指针变量的定义
定义形式:
类型标识符 *标识符 ;
指针变量必须定义为“指针类型”。 如:
int i,j;
i
jint *p1,*p2;
p1
p2
p1=&i; p2=&j;
p1,p2 为指向整型变量 i,j 的指针变量。
说明,?类型可以是整型、实型、字符型等。
?标识符前的, *” 表示该变量为指针变量。
?指针变量只能指向同一类型的变量。
?上述的指针变量是 p1,p2,而不是 *p1,*p2。
*p1 和 *p2 是 p1 和 p2 所指向的变量 i和 j。
*p1
*p2
?指针变量中只能存放地址。 p1=100; 是非法的
8.4 指针变量的引用
与指针有关的运算符:
& 取地址运算符 如,&a 为变量 a 的地址
? 指针运算符 如,*p为指针变量 p所指向的变量
?? 运算和 & 运算互为逆运算。
?指针变量 p的内容为地址量;
p *p
x
? *p=i; 是将变量 i 的值赋给目标变量 *p。
i
? &p为指针变量 p 的地址。
&p
? &(*p)的结果为 p,即变量 *p也就是变量 x的地址。
? *(&i) 表示访问变量 i 的
地址,结果就是 i 本身。
至此,你应该明白了
p,*p和 &p的意义。
&x
*p为指针 p的目标量,内
容为数据,即指针变量 p所指向的变量的值。
例 8.1
main( )
{int a,b;
int *p1,*p2;
a=100; b=10;
p1=&a; p2=&b;
printf(“%d,%d\n”,a,b);
printf(“%d,%d\n”,*p1,*p2);
}
将变量 a和 b的地址赋给指针变量 p1和 p2
*p1和 *p2就是变量 a和 b
运行结果,
100,10
100,10
说明:
?若先执行 p1=&a; 再执行 &*p1; 是什么意思? 这里
,&”和,*”的优先级相同,但按“自右至左”结合,即
&*p1与 &a相同。
p1
a
&*p1
而 p2=&*p1; 的作用是将 &a赋给 p2。
p2
?若先执行 p1=&a; 则 *&a和 *p1的作用相同。即:
*&a与 a是等价的
? (*p1)++ 等价于 a++。 *p1
*&a
括号是必须的,否则就成为
*(p1++)。这时先按 p1的原值进行
* 运算得到 a 的值。然后使 p1的值
改变,则 p1不再指向 a 了。
注意:
例 8.2
main( )
{int *p1,*p2,*p,a,b;
scanf(“%d%d”,&a,&b);
p1=&a; p2=&b;
if (a<b) { p=p1; p1=p2; p2=p; }
printf(“a=%d,b=%d\n”,a,b);
printf(“max=%d,min=%d\n”,*p1,*p2);
}
a
b
p1
p2
&a
&b
5
9
p
运行情况:
5 9 ?
注意,此例中 a和 b并未交
换,而 p1和 p2的值改变。
a=5,b=9
max=9,min=5
8.5 指针与数组
1,指向数组元素的指针变量的定义与赋值
例,int a[10],*p;
p=&a[0]; 将 a[0]元素的地址赋给指针变量 p
在 C中,数组名代表数组的首地址。因此 p=a;与 p=&a[0];是等价的。
注,p=a; 是将 a数组的首地址赋给指针变量 p。但这里的 a并不
代表整个数组。
2,通过指针引用数组元素
?若 p=a; 是将整型数组 a的首地址赋给整型指针变量 p,则 *p=5;
是将整数 5赋给 a数组的第一个元素 a[0],等价于 a[0]=5 。
?p+1表示指针变量 p当前所指的数组元素的下一个元素。但 p+1
不是 p的值加 1。若有 p=&a[0]; 则 p+i(或 a+i) 就是 a[i]的地址
(因为 a代表数组 a的首地址 )。
?若有 p=&a[0]; 则 *(p+i)或 *(a+i)就是 p+i或 a+i所指向的数组元素,
即 a[i]。也就是说 *(p+i) = *(a+i) = a[i]。
?指向数组的指针变量可以带下标,如,p[i] 与 *( p+i)是等价的。
例 8.3 输出有 10个元素的整型数组 a 的元素值。
main( )
{ int a[10],*p,i;
for (i=0; i<10; i++) scanf(“%d”,&a[i]);
printf(“\n”);
for (p=a; p<(a+10); p++) printf(“%d”,*p);
}
也可写成如下:
main( )
{ int a[10],*p=a,i;
for (i=0; i<10; i++) scanf(“%d”,p++);
printf(“\n”);
p=a;
for (i=0; i<10; i++,p++) printf(“%d”,*p);
}
此语句必不可少
?要注意指向数组的指针变量的当前值,因为指针可以
指向数组最后一个元素以后的内存单元。
?要注意指针变量的运算。如果 p 指向数组 a(即 p=a),
则:
? p++(或 p+=1)表示 p指向下一个元素; *p 表示取得当
前所指元素之值。
? *p++等价于 *(p++),相当于 a[i++],表示先 *p,再 p+1?p。
? *p––等价于 *(p––),相当于 a[i––],表示先 *p,再 p–1?p。
? *(++p) 相当于 a[++i],表示 p+1?p,再 *p。
? *(––p) 相当于 a[––i],表示 p–1?p,再 *p。
? (*p)++ 表示 p 所指之元素的值加 1,不是指针值加 1。
即 a[i]+1。
3,指向多维数组的指针和指针变量
设二维数组 a 定义如下:
static int a[3][4]={{1,3,5,7},{9,11,13,15},{17,19,21,23}};
? a代表整个二维数组的首地址,也就是 0行的首地址。
a+1,a+2分别代表 1行和 2行的首地址。同样,a[0]或
&a[0][0],a[1]或 &a[1][0],a[2]或 &a[2][0] 也分别代
表 0行,1行,2行的首地址。
? a[0]等价于 *(a+0),a[1]等价于 *(a+1),?, a[i]等价于
*(a+i)。因此,a[0]+1和 *(a+0)+1的值都是 &a[0][1];
a[1]+2和 *(a+1)+2的值为 &a[1][2]。
*(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+1)是 a[0][1]的
值,*(a[i]+ j)或 *(*(a+i)+j )是 a[i][ j]的值。
务必记住,*(a+i) 和 a[i] 是等价的。
?如果 a是一维数组名,则 a[i]代表第 i+1个元素所占的
内存单元。但如果 a是二维数组,则 a[i]代表一维数组
名,a[i]本身是不占用内存单元的,也不存放元素值,
而只是一个地址。
a,a+i,a[i],*(a+i),*(a+i)+j,a[ i]+j都是地址,
而 *(a[i]+j),*(*(a+i)+j)是二维数组元素 a[i][ j]的值。
表示形式
a
a[0],*(a+0),*a
a+1
a[1],*(a+1)
a[1]+2,*(a+1)+2,&a[1][2]
*(a[1]+2),*(*(a+1)+2),a[1][2]
含义
二维数组名,数组首地址
0行 0列元素地址
1行首地址
1行 0列元素地址
1行 2列元素地址
1行 2列元素的值
地址
2000
2000
2008
2008
2012
元素值为 13
static int a[3][4]={{1,3,5,7},{9,11,13,15},{17,19,21,23}};
?在二维数组中 &a[i]并不是 a[i]单元的物理地址,
但能得到 i行的首地址。虽然 &a[i]和 a[i]的值是
相同,但含义不同。 &a[i]或 a+i表示行,而 a[i]
或 *(a+i)表示列。
?在一维数组中 a+i 所指的是第 i 个数组元素的存
储单元;在二维数组中:
a+i = a[i] = *(a+i) = &a[i] = &a[i][0]
都表示 i 行 0 列元素的地址值。
例 8.4 输出二维数组中任一行任一列的元素。
main( )
{static int a[3][4]={1,3,5,7,9,11,13,15,17,19,21,23};
int (*p)[4],i,j;
p=a;
scanf(“i=%d,j=%d”,&i,&j);
printf(“a[%d,%d]=%d\n”,i,j,*(*(p+i)+j));
}
运行情况:
i=1,j=2 ? 注意这里的输入格式
a[1,2]=13
程序中的, int (*p)[4]”表示 p是一个指向包含 4个元素的
一维数组的指针变量,即 p是一个行指针,指向一维数
组的首地址。由于运算符优先级的关系,这里的 (*p)[4]
不能写成 *p[4]。
定义 p为只能指向有 4个元
素的一维数组的指针变量
4,指针数组和指向指针的指针
1) 指针数组的概念
指针数组的定义形式:
类型标识符 *数组名 [数组长度 ]
定义了有 4个元素的指针数组,每个数组元素都可以指向一个
整型变量。
例如,int *p[4];
例 8.5
main( )
{static char *name[ ]={“Follow me”,“BASIC”,“Great Wall”,
“FORTRAN”,“Computer design”};
name
name数组
name[0]
name[1]
name[2]
name[3]
name[4]
字符串
Follow me
Basic
Great Wall
Fortrasn
Computer design
p
char **p ;
int i;
for (i=0; i<5; i+ +)
{p= name+i;
printf(“%s\n”,*p ); }
}
指向指针的指针变量
输出字符串
2) 指向指针的指针
指向指针数据的指针变量称为指向指针的指针。 如,
char **p;
*运算符的结合性 从右至左,因此,**p相当于 *(*p)。
例 8.6
main( )
{static int a[5]={1,3,5,7,9};
static int *num[5]={&a[0],&a[1],&a[2],&a[3],&a[4]};
int **p,i;
p=num;
for (i=0; i<5; i++)
{ printf(“%d\t”,**p); p++; }
}
此处 **p改为 *p行吗?
注意,不得将第 2,3行错写为如下的一行,
static int *num[5]={1,3,5,7,9};
8.6 字符串的指针和指向字符串的指针变量
1,字符串的表示形式
? 用字符数组实现
static char str[ ] =,I love China !”;
str[i]表示字符数组中第 i+1个字符。如, str[8]为字符‘ h?
? 用字符指针实现
Char *a =,I love China!”;
尽管没有定义为字符数组,但实际在内存开辟了一个字
符数组用来存放字符串常量。同样,a[8]为字符‘ h?
注意, a不是字符串变量,只是将,I love China!”的首
地址赋给指针变量 a 。
2,字符串指针作函数参数
可以用地址传递的方式,即用字符数组名作参数或
用指向字符串的指针变量作参数,将一个字符串从一个
函数传递到另一个函数。
3,用字符指针变量作形参
void copy_string(from,to)
char *from,*to ;
{ for ( ; *from != ?\0?; from++; to++)
*to = *from ;
*to = ?\0?;
}
main( )
{char *a =,I am a teacher.”;
char *b =,you are a student.”;
copy_string(a,b);
printf(,string_a = %s\nstring_b = %s\n”,a,b) ;
}
4,字符指针变量与字符数组
用字符数组和字符指针变量都能实现字符串的存储
和运算,但二者之间是有区别的,不要混为一谈,必须
注意以下几点,
?字符数组由若干个元素组成,每个元素可存放 1个字
符,而字符指针变量存放的是字符串的首地址,绝不
是将字符串存放到字符指针变量中。
?对字符数组赋初值要用 static存储类别。
static str[ ]={“I love China!”};
而对字符指针变量不必加 static。
如,
如,
char *a=“I love China!”;
这是因为它只是对指针变量而不是对数组初始化。
?对字符数组只能对各个元素赋值,下列方式是错误的,
char str[15];
str=“I love China!”;
对于字符指针变量,可采用如下方法:
char *a;
a =,I love China!”;
?对字符指针变量赋初值
char *a=“I love China!”; 等价于:
char *a;
a=“I love China!”;
注意,对数组初始化时:
static char str[14]={“I love China!”};
不等价于,char str[14];
str[ ]=“I love China!”;
即数组可以在变量定义时整体赋初值,但不能在赋
值语句中整体赋值。
这里赋给 a 的是字符串的首地址, 而不是字符串 。
?数组在编译时被分配内存单元,有确定的地址。而指
针变量必须赋给一个确定的地址值,否则,在程序运
行时会发生意想不到的后果。 如:
char str[14];
scanf(“%s”,str);
是可以的 。 但如果写成如下形式:
char *a;
scanf(“%s”,a);
是非常危险的 。 因为指针变量没有确定的地址而指
向程序区或其它数据区, 从而会造成系统, 冲突, 。
应当写成如下形式:
char *a,str[10];
a = str;
scanf(“%s”,a);
?指针变量的地址值是可以改变的,而数组名的地址值
是不能改变的。 如:
main( )
{char *a=“I love China!”;
a = a+7;
printf(“%s”,a );
}
运行结果如下:
China!
从第 (7+1) 个字符开
始输出至字符串结尾
下面的写法是错误的:
char str[ ]={“I love China!”};
str = str+7;
printf(“%s”,str);
例 8.7
main( )
{char *a=“I love China.”;
int i=5;
printf(“%c\n”,a[i]);
for (i=7; a[i]!=?\0?; i++)
printf(“%c”,a[i]);
}
运行结果如下,
e
China.
?若定义一个指针变量使它指向一个字符串后,可以
用下标形式引用指针变量所指字符串中的字符。
输出第 6个字符
从第 (7+1) 个字符开始输出至字符串结尾
例 8.8 顺读和倒读都一样的字符串称为,回文”,如,LEVEL。
试编写一个判断输入的字符串是否为回文的程序。
#include“stdio.h”
#include“string.h”
main( )
{char s[81],*pi,*pj;
int i,j,n;
gets(s); n=strlen(s);
pi=s; pj=s+n–1;
while(*pi==??) pi++;
while(*pj==??) pj––;
while( (pi<pj) && (*pi==*pj) )
{ pi++; pj––; }
if (pi<pj) printf(“NO\n”);
else printf(“YES\n”);
}
从键盘输入字符串
得到字符串的长度
pi指针指向字符串的开头
pj指针指向字符串的末尾
跳过输入时的前导空格
跳过输入时的尾随空格
只要 pi和 pj指针没有碰
头且其所指字符相同。
则 pi指针向后移动
而 pj指针向前移动
while循环结束后若 pi<pj,则表示比较没
有完成,可知该字符串肯定不是“回文”
8.7 指针与函数
1,指针变量作为函数参数
指针变量作为函数参数的作用是将一个变量的地址
传送至另一个函数中。
例 8.9
swap(p1,p2)
int *p1,*p2;
{int p;
p=*p1; *p1=*p2; *p2=p;
}
main( )
{int a,b;
int *s1,*s2;
scanf(“%d%d”,&a,&b);
s1=&a; s2=&b;
if(a<b) swap(s1,s2);
printf(“\n%d,%d\n”,a,b);
}
运行情况:
5 9 ?
9,5
此例中 a 和 b 的值交换,而 s1 和 s2 的值不变。
用指针变量作参数调用函数能获得多个值 。
2,用函数指针变量调用函数
例 8.10 求 a,b 两数中较大者。
main( )
{ int max( );
int (*p)( );
int a,b,c;
p=max; scanf(“%d,%d”,&a,&b);
c=(*p)(a,b); printf(“a=%d,b=%d,max=%d”,a,b,c);
}
max(x,y)
int x,y;
{ int z;
if (x>y) z=x;
else z=y;
return(z);
}
说明 p是指向函数的指针变量
3,将指向函数的指针变量作函数参数
函数的参数可以是变量、指向变量的指针变量、数
组名、指向数组的指针变量等。而函数指针变量也可以
作为参数,以实现函数地址的传递,即将函数名传递给
形参。
4,返回指针值的函数
返回指针值的函数也称为指针函数。调用指针函数
后能得到一个指向指定类型数据的指针 (地址 )。
指针函数的定义形式如下:
类型标识符 *函数名 (参数表 )
例如,int *fa(x,y);
定义了一个指针函数 fa 。 x,y 为函数 fa 的形参 。
注意 指针函数与指向函数的指针变量之间的区别 。 例如,
int *a(x,y); 定义 a为整型指针函数。
int (*a)( ); 定义 a为指向函数的指针变量。
课外练习:习题册中习题 八 全部
8.8 指向结构体类型数据的指针
同普通变量一样,也可以定义一个
指针变量指向一个结构体变量,则此时
该指针变量的值是结构体变量的起始地
址。指针变量也可以用来指向结构体数
组中的元素,同样也可以用指向结构体
的指针作函数参数。
例 8.11 用指向结构体的指针作函数参数
#include“string.h”
main( )
{ struct student
{long int num;
char name[20];
char sex;
float score;
};
struct student stu;
struct student *p;
p=&stu; stu.num=89101;
strcpy(stu.name,“LiLin” );
stu.sex=?M?; stu.score=89.5;
printf(“No.:%d\nname:%s\nsex:%c\nscore:%f \n”,
stu.num,stu.name,stu.sex,sru.score);
printf(“\nNo.:%d\nname:%s\nsex:%c\nscore:%f \n”,
(*p).num,(*p).name,(*p).sex,(*p).score);
}
注意这里的引用格式,也可写成
p–>uum,?。若写成 p.num,?
或 *p.num,? 则是错误的
注意,
? (*p) 表示 p 指向的结构体变量,不得省去括号。而
*p.num 等价于 *(p.num)。
称为指向运算符。
? (*p).num可写成 p─>num,使之直观,余类推。,–>”
? 结构体变量 ?分量名,(*p)?分量名,p–>分量名,三
者是等价的。
? p–>n 得到 p 指向的结构体变量中的分量 n 的值。
? p–>n++ 得到 p 指向的结构体变量中的分量 n 的值,
用完该值后加 1。
? ++p–>n 得到 p 指向的结构体变量中的分量 n 的值,
并在用该值前先加 1。
8.9 动态数据结构
静态数据结构 (例如数组 ) 占据内存空间的位置和大
小是在它们被说明的同时由系统分配的,在程序运行期间
是不变的,因此可以有效地访问它们的任何一个元素。但
要删除和插入一个元素则比较困难,往往要引起大量的数
据移动 。 而且数据量的扩充更受到它们所占用的有限内存
空间的限制 。 C 中的动态数据结构有效地解决了这一问题
动态数据结构中最基本的形式是 链表 和 二叉树,它们
在数据处理中起着十分重要的作用。
动态数据结构中的每个组成数据在逻辑上是连续排列的,
但在物理上即在内存中存储时并不占用连续的内存空间,
它们可以根据需要随机地增加或减少其元素,相应地占用
或释放内存空间 。
1,动态存储分配
C语言实现动态存储分配的函数:
? malloc(size)
在内存的动态存储区中分配一个结点长度为 size的
连续存储空间,并返回一个指向其起始地址的指针,若
分配不成功,则返回值为 0。 size为整型。
? calloc(n,size)
在内存的动态存储区中分配 n个结点长度为 size的
连续存储空间,并返回一个指向其起始地址的指针,若
分配不成功,则返回值为 0。 n,size为整型。
? free(ptr)
释放由指针 ptr所指向的存储空间。 ptr是最近一次
调用 malloc 或 calloc函数或链表指针返回的值。 ptr 为
字符型指针。
2,链表
1) 链表概念
单向链表是按照输入数据的顺序建立的。它有一
个,头指针” (图中为 head),指向第一个元素;每一
个元素称为“结点”,每个结点包括两个域,数据域 和
指向下一个结点的 指针域 ;最后一个元素的指针域为
,NULL”(“空地址” ),表示链表的结束,称为“表尾” 。
head A B B C E
NULL
链表是一种常见的动态地进行存储分配的数据结
构。链表有,单向链表”、“双向链表”、“循环链表”、
“双向循环链表”之分。下图是一个“单向链表”的示例
2) 建立链表
例 8.12 链表的建立和遍历 (队列 )
#define NULL 0
#define LEN sizeof(struct node)
struct node
{ int data;
struct node *next;
}
main( )
{ struct node *head,*rear,*p;
int n=0;
p=(struct node *)malloc(LEN);
p–>data=n+1;
head=rear=p;
for (n=1; n<10; n++)
{p=(struct node *)malloc(LEN);
p–>data=n+1;
rear–>next=p; rear=p;
}
rear–>next=NULL;
p=head;
while (p != NULL)
{printf(“%3d”,p–>data);
p=p–>next;
}
}
恢复指针
head 1 2 3 4 5 6 7 8 9 10
p p p p p p p p p
0
rear
p
rear rear rearrear rear rear rear rear rear
例 8.13 链表的建立和遍历 (栈 )
#define NULL 0
#define LEN sizeof(struct node)
struct node
{int data;
struct node *next;
};
main( )
{struct node *base,*p;
int n;
base = NULL ;
for (n = 0; n<10; n+ +)
{p=(struct node *)malloc(LEN);
p?>data = n+1;
p?>next = base ;
base = p;
}
p = base;
while ( p != NULL )
{printf(“%3d”,p?>data);
p = p?>next;
}
}
1
0
2 3 4 5 6 7 8 9 10
p
base
输出结果:
10 9 8 7 6 5 4 3 2 1
恢复指针
3) 删除链表元素
例 8.14 删除链表中指定的结点。
struct node *delete(head,data)
struct node *head;
int data;
{struct node *p1,*p2;
if (head != NULL)
{p1 = head ;
while (p1?>data != data && p1?>next != NULL)
{ p2 = p1; p1 = p1?>next; }
if (p1?>data == data)
{if (p1= = head) head = p1?>next;
else
{p2?>next = p1?>next; printf(“deleted,%d\n”,data); n = n?1; }
else printf(“%d not been found!\n”,data);
}
return(head);
}
p1p2 p1p2 p1p2 p1p2 p1
1 2 3 4 5 6 7 8 9 10
0
p
rear
head
4) 链表的插入操作
例 8.15 在链表中插入一个新结点。
struct node *insert(head,new)
struct node *head,*new;
{struct node *p,*p1,*p2;
p=new; p1=head;
if (head == NULL)
{ head=p; p?>next = NULL; }
else
{while(p1?>data != p?>data
&&p1?>next != NULL)
{p2=p1; p1=p1?>next; }
if (p1?>next == NULL)
{ p1?>next=p; p?>next=NULL; }
else
{ p2?>next = p; p?>next = p1; }
}
n = n ? 1 ;
}
1 2 3 4 6 7 8 9 10
0
p
rear
p1p2 p1p2 p1p2 p1p2 p1
p 5
0new
head