指针是c语言的重点.docx
《指针是c语言的重点.docx》由会员分享,可在线阅读,更多相关《指针是c语言的重点.docx(32页珍藏版)》请在冰豆网上搜索。
![指针是c语言的重点.docx](https://file1.bdocx.com/fileroot1/2023-1/7/ba851b9d-ea9f-4984-a5db-9ec3c9738b91/ba851b9d-ea9f-4984-a5db-9ec3c9738b911.gif)
指针是c语言的重点
第七章指针
指针是c语言的重点,也是难点,没有掌握指针也就没有掌握c语言的精华。
正确而熟练地掌握了指针的概念和指针的使用方法就能设计出复杂的数据结构和高效的程序。
本章主要讲述指针变量、指针与数组、指针与字符串、指针与函数等内容。
7.1指针的概念
计算机的内存是以字节为单位的一片连续的存储空间,每一个字节都有一个编号,这个编号就称为内存地址。
内存的存储空间是连续的,内存的地址号也是连续的。
一个变量实质上代表了“内存中的某个存储单元”。
若我们在程序中定义了一个变量,C编译系统就会根据定义的变量的类型为其分配一定字节数的内存空间(如:
整型占2字节,实型占4字节,双精度占8字节,字符型占1字节
)。
每个变量所占的存储单元都有确定的地址。
具体的地址是在编译时分配的。
若有定义
inta,b;floatx;
则系统为变量a,b,x分配内存空间情况如图7_1所示
a
b
x
1011
1012
1014
1015
1204
1205
1206
1207
图7_1
例如:
inta=3;
printf(“%d”,a);
要访问内存中的变量a,在程序中是通过变量名a来引用变量的值。
在编译时将变量名a对应一个地址,在内存中不再出现变量名而只是地址。
程序中引用变量a,系统找到其对应的地址,然后从该地址中取出其中的值。
程序中我们对变量进行存取操作实际上是对某个地址的存储单元进行操作,这种直接按变量的地址存取变量值的方式称为“直接访问”。
在C语言中还可以定义一种特殊的变量,该变量是用来存放内存地址的,称为指针变量。
如图7_2所示,
假设变量p它有自己的地址(2002),若变量a的内存地址(1002)存放在变量p中,这时要访问变量a所代表的存储单元,可以先找到变量p的地址(2002),从中取出a的地址(1002),然后再去访问以1002为首地址的存储单元。
这种通过变量p间接得到变量a的地址,然后取出变量a的值的方式称为“间接访问”。
打个比方来解释说明“直接访问”和“间接访问”这两种方式,假设我们用房间代表存储单元,房间的钥匙代表存储单元的地址编号,房间里的物品代表存储内容。
前一种可以理解为用A房间的钥匙直接打开A房间,取出所需的物品;后一种方法可以理解为A房间的钥匙放在B房间里,要想打开A房间,首先必须用B房间的钥匙打开B房间取出A房间的钥匙,然后用A房间的钥匙打开A房间,这样才能取出所需的物品。
所谓“指针”就是地址,指针是地址的形象化名称,“指针”是人们形象地表示访问变量的指引关系,实际上它只是存放了一个变量的地址而已。
在C语言中,指针被广泛使用,它和数组、字符串、函数间数据的传递等有着密不可分的联系。
在某些场合,指针是使运算得以进行的唯一途径,同时指针的运用可以使得程序代码更简洁、效率更高。
但是,若对指针的概念不清,以至滥用,将大大降低程序的可读性,使用不当,将使指针指向意料不到的地方,致使程序失控,严重的将导致系统崩溃。
因此,正确掌握指针的概念、正确使用指针是十分重要的。
7.2指针变量的定义和引用
7.2.1指针变量的定义
定义指针变量的一般形式:
类型名*指针变量名1,*指针变量名2,
;
例如:
int*p;
p是用户定义的标识符,变量前的星号(*)是一个说明符,用来说明该变量是指针变量,其中变量前面的星号不可省略。
上式表示定义了一个指针变量p,它指向一个整型变量。
也就是说,p当中存放一个整型变量的地址且只能存放整型变量的地址,此时称int是指针变量p的“基类型”。
应特别说明,定义一个指针变量必须用符号“*”,它表明其后的变量是指针变量,但不要认为“*p”是指针变量,指针变量是p而不是*p。
要想使一个指针变量指向一个整型变量,必须将整型变量的地址赋给该指针变量。
例如:
int*p,i=3;
p=&i;
上面首先定义了一个指针变量p和一个整型变量i,i的初值为3,此时p与i之间无任何联系,当执行了赋值语句“p=&i;”,则p中存放了变量i的地址,此时p就指向i。
指针变量也可以指向实型、字符型以及其它类型的变量,例如:
float*p,f=3.14;
char*q,ch=’A’;
p=&f;q=&ch;
7.2.2指针变量的引用
必须记住,指针变量只能存放地址(指针),而不要将任何非地址类型的数据赋给一个指针变量。
如:
p=1000;(p为指针变量,1000为整数)是非法的。
再举个例子:
inta,*p;
p=&a;
*p=5;
需要注意一点的是,此处的*p与定义指针变量时用的*p含义是不同的。
定义“int*p”中的“*”不是运算符,它只是表示其后的变量是一个指针类型的变量,在程序的执行语句中引用的“*p”,其中的“*”是一个指针运算符,*p表示“p指向的变量”。
所以当执行了语句“*p=5;”,相当于把5赋给了p所指向的变量a,即把5赋给了a变量。
若有以下语句:
int*p;
*p=5;
虽然有时以上语句可以执行,但这种方法是危险的。
因为编译时虽然分配给指针变量p一个单元,p的地址是已指定了,但p的值未指定,在p单元中是一个不可预料的值。
很可能会指向已存放指令或数据的内存段,造成严重后果。
在C语言中有两个有关指针的运算符:
1)&运算符:
取地址运算符,&x的值为x的地址。
2)*运算符:
指针运算符或指向运算符,也称间接运算符,*p代表p所指向的变量。
“&”和“*”两个运算符的优先级别相同,按从右到左的方向结合。
如果已执行了“p=&a;”语句,若有&*p,则&*p与&a相同。
*&a与a等价。
(*p)++相当于a++,如果没有括号,就成为*(p++),这时p不再指向a了。
例7.1
main()
{int*p1,*p2,a,b;
scanf(“%d,%d”,&a,&b);
p1=&a;p2=&b;
printf(“%d,%d\n”,a,b);
printf(“%d,%d\n”,*p1,*p2);
p2=p1;
printf(“%d,%d\n”,*p1,*p2);
printf(“%d,%d\n”,a,b);
}
运行结果:
10,20
↙
10,20
10,20
10,10
10,20
程序中定义了两个指针变量和两个整型变量。
在输入整型变量a和b的值之后,使指针变量p1指向a,p2指向b,然后输出a和b的值,(如图7_3(a)所示)。
再输出*p1和*p2的值,也就是a和b的值。
接着把p1的值(即a的地址)赋给p2,这样p1==p2==&a,即p1和p2都指向a,(如图7_3(b)所示)。
此时*p1和*p2都代表a,*p2不再代表b了。
例7.2交换两个指针变量
main()
{int*p1,*p2,*p;inti1=10,i2=20;
p1=&i1;p2=&i2;
printf(“%d,%d\n”,i1,i2);
printf(“%d,%d\n”,*p1,*p2);
p=p1;p1=p2;p2=p;
printf(“%d,%d\n”,i1,i2);
printf(“%d,%d\n”,*p1,*p2);
}
运行结果:
10,20
10,20
10,20
20,10
第一个printf函数语句执行时,*p1就是i1,*p2就是i2。
将p1与p2互换,则p1的值为&i2,p2的值为&i1,即p1指向i2,p2指向i1。
*p1是i2而*p2是i1了。
i1和i2的值始终没有改变。
程序中的指针变量p的作用是交换p1和p2的值所需的临时变量。
(如图7_4所示)
例7.3交换两个指针变量所指向的变量的值。
main()
{int*p1,*p2,,i1,i2,i;
i1=10;i2=20;
p1=&i1;p2=&i2;
i=*p1;*p1=*p2;*p2=i;
printf(“i1=%d,i2=%d\n”,i1,i2);
}
运行结果:
i1=20,i2=10
*p1和*p2的值互换,也就是i1和i2的值互换,而p1和p2始终指向i1和i2。
(如图7_5所示)
7.3指针变量作为函数参数
函数的参数不仅可以是整型、实型、字符型等数据,还可以是指针类型。
下面通过例子来说明。
例7.4
main()
{voidmov(int*,int*);
intx,y;
mov(&x,&y);
printf(“%d,%d\n”,x,y);
}
voidmov(int*px,int*py)
{*px=10;
*py=20;
}
运行情况:
10,20
在main函数中没有对x和y赋值。
把x与y的地址作为实参传给mov函数。
mov函数的形参是指针变量,可以存放地址,因而能够接受从main函数传来的实参&x和&y。
在执行mov函数时,将10和20分别送给px和py所指向的变量,即x和y。
这样使得x和y得到了值。
于是在main函数中就可以输出x和y的值。
例7.5对输入的两个整数按大小顺序输出。
main()
{inta,b,*p1,*p2;
voidswap(int*,int*);
scanf(“%d,%d”,&a,&b);
p1=&a;p2=&b;
if(a
printf(“%d,%d\n”,a,b);
}
voidswap(int*pointer1,int*pointer2)
{intp;
p=*pointer1;*pointer1=*pointer2;*pointer2=p;
}
运行情况:
10,20↙
20,10
对程序的说明:
swap是用户定义的函数,它的作用是交换两个变量的值。
swap函数的两个形参pointer1和pointer2是指针变量。
程序开始执行时,先输入a和b的值,然后使指针变量p1和p2分别指向变量a和b。
当调用swap函数时,实参p1、p2的值分别传递给形参pointer1和pointer2,即使形参pointer1和pointer2分别指向变量a和b。
通过执行swap函数,使pointer1和pointer2所指向的变量交换,即a和b交换。
通过上面的介绍,可以知道,使用指针变量作为函数参数可以在调用一个函数时得到多个由被调函数改变了的值。
如果想通过函数调用改变n个变量的值,可采用如下方法:
在主函数中设n个变量,用n个指针变量指向它们,然后将指针变量作为实参将n个变量的地址传给所调用的函数的形参;通过形参指针变量改变该n个变量的值,则主调函数就可以得到改变了值的变量。
不能企图通过改变指针形参的值而使得指针实参的值也改变。
如对例7.5的程序的子函数swap改写如下
voidswap(int*p1,int*p2)
{int*p;
p=p1;
p1=p2;
p2=p;
}
则主函数调用之后,主函数中的变量a和b并没有发生改变。
7.4指向一维数组的指针变量
7.4.1一维数组指针的概念
变量有地址,数组包含若干元素,每个数组元素都在内存中占用存储单元,且是连续的存储单元,也有相应的地址。
指针变量可以指向变量,自然也可以指向数组和数组元素。
数组元素的指针就是数组元素的地址。
如:
inta[10],*p;
其中定义a为包含10个整型数据的数组,p为指向整型变量的指针变量。
下面对该指针赋值:
p=&a[0];
把a[0]的地址赋给指针变量p。
在C语言函数体中或函数体外部定义的数组名可以认为是一个存放地址值的指针变量名,其中的地址值是数组第一个元素的地址,也就是数组所占一串连续存储单元的起始地址;定义数组时的类型即是此指针变量的基类型;重要的是这个指针变量中的地址值不可改变,也就是说不可以给数组名重新赋值,因此可以认为数组名是一个地址常量。
若在函数中有以下定义:
inta[10],*p,x;
下面两个语句等价:
p=&a[0];
p=a;/*这里“p=a;”的作用是把a数组的首地址赋给指针变量p。
*/
如果有语句a=&x;或a++;都是非法的,不能给a重新赋地址值,一旦定义,a永远指向a数组的首地址。
引用一个数组元素可以有两种方法。
一种是下标法,即指出数组名和下标值,系统就会找到该元素,如a[3]就是用下标法表示的数组元素;另一种方法是地址法,即通过给出地址访问某一元素。
如通过a+3地址可以找到a[3]元素,*(a+3)就是a[3]。
即
(1)下标法,如a[i]形式;
(2)指针法,如*(a+i)或*(p+i).其中a是数组名,p是指向数组的指针变量,其初值p=a。
例7.6分别用下标法、地址法访问数组元素。
main()
{inta[5],i,*p;
for(i=0;i<5;i++)
scanf(“%d”,&a[i]);
for(i=0;i<5;i++)
printf(“%d”,a[i]);
printf(“\n”);
for(i=0;i<5;i++)
printf(“%d”,*(a+i));
printf(“\n”);
for(p=a;pprintf(“%d”,*p);
printf(“\n”);
}
运行结果:
24579↙
24579
24579
24579
可以看到用三种方法都能得到a数组各个元素的值。
但第3种方法比前两种快,用指针变量指向元素,不必每次都重新计算地址,这种有规律地改变地址值(p++)能大大提高执行效率。
现在着重分析第三种方法:
p的初值等于a,此时p指向a数组中的第一个元素a[0],*p就是a[0]。
在输出*p的值后,p++使p指向下一个元素,此时p指向a[1],在输出a[1]的值之后,p++又使p指向下一个元素,直到p=a+5为止,此时已输出5个元素。
注意,p是变量,p的值不断在变化。
7.4.2数组元素地址作实参
当调用函数时,数组元素可以作为实参传给形参,和普通变量一样,对应的形参必须是类型相同的变量。
数组元素的值可以传送给该变量,在函数中只能对该变量进行操作,而不能直接引用对应的数组元素。
当数组元素的地址作为实参时,因为是地址值,所以对应的形参也应当是基类型相同的指针变量。
例7.7编写函数,对具有10个元素的字符类型数组,从下标为4的元素开始,全部设置‘*’,保持前四个元素中的内容不变。
#defineM10
#defineB4
voidsetstar(char*,int);
voidarrout(char*,int);
main()
{charc[M]={‘A’,’B’,’C’,’D’,’E’,’F’,’G’,’H’,’I’,’J’};
setstar(&c[4],M-B);
arrout(c,M);
}
voidsetstar(char*p,intn)
{inti;
for(i=0;i}
voidarrout(char*p,intn)
{inti;
for(i=0;iprintf(“\n”);
}
输出结果:
ABCD******
在main函数中已对c数组的全体元素置初值,调用setstar函数时,把数组元素c[4]的地址传送给指针变量p,这时在setstar函数中的指针变量p指向数组元素c[4]、*(p+1)或p[1]代表数组元素c[5]、
、*(p+5)或p[5]代表数组c[9]。
注意,在setstar函数中,通过指针p引用主函数中的c数组时,不可以越过c数组最后一个元素的位置。
setstar函数的首部还可以写成以下形式:
setstar(charp[],intn)或setstar(charp[M-B],intn)这和数组名作实参的情况相同,实质上setstar函数把c[4]作为一串连续存储单元的首地址。
7.4.3数组名作函数参数
指针可以作参数,数组名表示数组的首地址,所以数组名可以作函数参数。
归纳起来,如果有一个实参数组,要在函数中改变此数组的元素的值,实参与形参的对应关系有以下4种情况:
(1)形参和实参都用数组名;
(2)实参用数组名,形参用指针变量;(3)实参形参都用指针变量;(4)实参为指针变量,形参为数组名。
应注意,如果用指针变量作实参,必须先使指针变量有确定值,指向一个已定义的数组。
例7.7编写程序,通过函数数组输入若干大于等于0的整数,用负数作为输入结束标志;调用函数输出该数组中的数据。
#defineM100
voidarrout(int*,int);
intarrin(int*);
main()
{ints[M],k;
k=arrin(s);
arrout(s,k);
}
arrin(int*p)
{inti,x;
i=0;
scanf(“%d”,&x);
while(x>=0)
{*(p+i)=x;
i++;
scanf(“%d”,&x);
}
returni;
}
voidarrout(int*p,intn)
{inti;
for(i=0;iprintf(((i+1)%5==0)?
“%d\n”,*(p+i));
printf(“\n”);
}
在arrin和arrout两个函数中,都用名为p的指针变量作为形参,与主函数中的实参数组s相对应。
当调用这两个函数时,指针变量p指向s数组的首地址。
在函数中,表达式*(p+1)就代表了主函数中的数组元素s[1],表达式*(p+i)就代表了主函数中的数组元素s[i],当i的值由0变化到9时,*(p+i)就表示引用了数组元素s[0]到s[9]。
当然在arrin和arrout两个函数中,形参可以用数组名,如两个函数定义可以改为:
voidarrin(intp[])和voidarrout(intp[],intn)。
实参也可以使用指针形式,但该指针变量必须事先指向已定义的数组。
如上面程序的main函数可以改写成指针变量作实参的形式:
main()
{ints[M],k,*p;
p=s;
k=arrin(p);
p=s;
arrout(p,k);
}
7.4.4函数的指针形参和函数体中数组的区别:
若有以下程序,程序中定义了fun函数,形参a指向w数组,函数体内定义了一个b数组,函数把b数组的起始地址值作为函数值返回,企图使指针p指向函数体内b数组的开头。
#defineN10
int*fun(inta[N],intn)
{intb[N];
returnb;
}
main()
{intw[N],*p;
p=fun(w,N);
}
以上程序涉及几个概念:
(1)函数fun中,形参a在形式上写作了a[N],实际上它也可以写作a[]或*a。
但无论写成上述哪种形式,C编译程序都将其作为一个指针变量处理。
在调用fun函数时,系统只为形参a开辟一个存储单元,并把main函数中的起始地址存入其中,使它指向w数组的首地址;因此,在fun函数中,凡是指针变量可以参与的运算,形参指针a同样可以参与,如:
可以进行a++等操作,使它移动去指向w数组的其它元素,甚至可以通过赋值使它不再指向w数组中的元素。
(2)函数fun的函数体中定义了一个b数组,在调用fun函数时,系统为它开辟一串连续的存储单元,b是一个地址常量,不可以对它重新赋值;虽然对a和b,有相同的说明形式,但它们一个是作为形参的指针变量,一个是函数体内定义的数组,具有完全不同的含义。
(3)在函数fun执行完毕,返回主函数时,系统将释放a和b所占存储单元,指针变量a和数组b将不再存在。
因此,函数fun不应把b的值作为函数值返回,这样做,主函数中的指针变量p将不指向任何对象而成为“无向指针”。
7.5指向二维数组的指针变量
7.5.1二维数组地址的概念
在C语言中定义的二维数组实际上是一个一维数组,这个一维数组的每个成员又是一个一维数组。
如有下面定义:
int*p,a[3][4];
则可把a数组看成由a[0]、a[1]、a[2]三个元素组成,而a[0]、a[1]、a[2]每个元素又分别是由4个整型元素组成的一维数组。
可用a[0][0]、a[0][1]等来引用a[0]中的每个元素,可用a[1][0]、a[1][1]等来引用a[1]中的每个元素,其它依此类推。
我们知道,C语言中,在函数体中或在函数体外部定义的一维数组名是一个地址常量,其值为数组第一个元素的地址,此地址的基类型就是数组元素的类型。
在以上二维数组中,a[0]、a[1]、a[2]都是一维数组名,同样也代表一个不可变的地址常量,其值依次为二维数组每行第一个元素的地址,其基类型就是数组元素的类型。
因此,对于二维数组,象a[0]++这样的表达式是非法的。
若有表达式a[0]+1,表达式1的单位应当是2个字节。
二维数组名同样也是一个存放地址常量的指针,其值为二维数组的首地址,也就是第0行的首地址。
以上a数组,数组名a的值与a[0]的值相同,只是其基类型为具有4个整型元素的数组类型。
即a+0的值与a[0]的值相同,a+1的值与a[1]的值相同,a+2的值与a[2]的值相同。
它们分别表示a数组中第0行、第1行、第2行的首地址。
二维数组名应理解为一个行指针。
在表达式a+1中,数值1的单位应当是4
2个字节,而不是2个字节。
赋值语句p=a;是不合法的,因为p和a的基类型不同。
同样,对于二维数组名a,也不可以进行a++,a=a+i等运算。
a[i]和*(a+i)等价,&a[i]或a+i指向行,而a[i]或*(a+i)指向列。
这里不要把&a[i]理解为a[i]的物理地址,因为并不存在a[i]这样的变量,它只是一种地址的计算方法,能得到第i行的首地址。
&a[i]和a[i]的值是一样的,但它们的含义是不同的。
如下表所示
表示形式
含义
地址
a
二维数组名,数组首地址
2000
a[0],*(a+0),*a
第0行第0列元素地址
2000
a+1
第1行首地址
2008
a[1],*(a+1)
第1行第0列元素地址
2008
a[1]+2,*(a+1)+2,&a[1][2]
第1行第2列元素地址
2012
*(a[1]+2),*(*(a+1)+2),a[1][2]
第1行第2列元素的值
元素值为13
第i行第j列元素地址的表示方式a[i]+j,*(a+i)+j,&a[i][j],第i行第j列元素值的表示方式有*(a[i]+j),*(*(a+i)+j),a[i][j]。
例7.8
#defineFORMAT“%d,%d\n”
main()
{inta[3][4]={1,3,5,7,9,11,13,15,17,19,21,23};
printf(FORMAT,a,*a);
printf(