目录
第十章:指针操作
概述
指针是C语言的重要概念,也是C语言的一个重要特点.
正确灵活地运用指针,可以
- 方便有效地表达复杂的数据结构
- 实现内存空间的动态管理
- 提高程序的编译效率和执行速度
- 方便的使用数组
- 直接处理内存单元地址
掌握指针的应用,可以使程序简洁,紧凑,高效.
本章内容如下:
- 指针和地址
- 指针和指针变量
- 指针和数组
- 指针和函数
指针和地址
计算机中,内存是以字节为单位的连续储存空间.
每一个字节都会有一个编号,这个编号称为地址
由于内存空间是连续的,因此地址也是连续的
数据存放在地址所标志的内存单元
系统会为变量分配内存单元地址
地址是一个无符号的整数.
一个变量占据的所有字节的第一个字节的地址称为该变量的地址
任何变量在生存期内都占据一定数量的字节.
变量占据的字节数量和变量的类型有关
所以我们如果想知道一个变量在内存中的储存,就要知道:
- 该变量在内存中的首地址,即地址
- 该变量所占的字节数,即变量的长度
这点对于后面理解申明指针的类型即指向数组的指针极为重要.
内存单元的地址和内存单元的内容是不相同的.
编译系统通过变量名来对内存单元进行存取操作.但其实经过编译之后,变量名已经变成了变量的地址,对变量值的存取工作都是用过地址进行的.
这种通过直接按变量的地址存取变量值的方式称为直接存取方式
所以在后面会讲到,对于数组元素的访问可以通过下标来访问,或者直接通过元素的地址来访问.
一个变量的内存地址称为该变量的指针
如果一个变量用来储存指针,则称该变量为指针类型的变量.
如果指针变量a的值是变量b的地址,则称指针变量a指向变量b
这种通过变量a得到变量b地址,然后在存取变量b的值的方式称为间接存取方式.
指针和指针变量
前面已经讲过,变量的内存地址就是变量的指针,指针变量就是用来存放内存地址的变量,即存放指针的变量
下面就将讲解指针变量的定义,赋值,引用和指针的运算
指针变量的定义
在C语言中,除了可以定义整型,字符型和实数型变量外,还能定义一种专门用于存放其他变量在内存中所分配的储存单元的地址的变量,称为指针变量
假设变量p是一个指针变量,它的值是变量c的内存单元首地址.那么我们想要通过变量p得到变量c的值的步骤如下:
- 根据变量p的地址和变量p的长度,读取其内存中所存放的数据,即变量c的地址
- 根据变量c的地址和长度,读取变量c的内容.
从上面的步骤中,我们发现想要读取指针指向的变量的值,不仅要知道变量地址,还要知道变量的长度.因此指针变量的定义如下:
类型说明符 *标识符
其中:
- 类型说明符的类型和指向的变量的类型一样,以让编译器知道指向变量的长度
这一点及其重要,在后面指针和数组的关系时,如果不知道这以前就会很难去理解.
此外,指针变量也具有地址,所以我们其实也可以让另一个指针变量来指向这个指针变量.这个在后面讲解
指针变量的赋值
指针变量的赋值和普通变量的赋值相似.
但是由于地址是一个无符号整型,因此指针变量的值也是一个无符号整型,所以不能把整型常量直接赋给指针变量(因为不同类型的数据在内存中的组织结构不一样).
声明时赋值:
int a;
int *a_pointer = &a;
声明后赋值:
float b;
float *b_pointer;
b_pointer=&b;
需要注意的是:
-
指针变量的名字是*后面的部分,即标识符部分.
-
声明时的*只是告诉编译器我们声明的变量是一个地址
-
指针变量可以指向普通类型变量(短型,实数型,字符型),数组,字符串,函数.
指针变量的引用
讲解指针变量的引用之前,需要先了解下有关指针变量的运算符
指针变量的运算符
-
& :取地址符号,作用是获取变量的地址.例如:
&a;
-
* :取内容符号,作用是取指针变量只想的内容.例如
*p;
所以指针变量的引用有如下的方法:
//为指针赋值
int a;
int *a_pointer;
a_pointer=&a;
//访问指针指向变量的值
int a=10;
int *a_pointer=&a;
int a_value;
a_value=*p_pointer;
//改变指针指向变量的值
int a=10;
int *a_pointer=&a;
*a_pointer=100
需要注意的是:
- *在定义指针变量的时候不代表取内容运算,而是通知编译器这是一个指针变量,所以在后面引用的时候只需要用指针变量的变量名即可.
- *在非定义指针变量时候,就代表取内容运算.
- 只要是在内存空间中占据内存的数据,就有其地址,就可以用指针来指向该数据.
指针的运算
指针的运算,主要是赋值运算,取地址运算,取内容运算,加减算术运算,关系运算
赋值运算
即通过赋值表达式为指针变量赋值
int a;
int *a_pointer;
a_pointer=&a;
除此以外,还能将指针变量的值赋给同一类型的指针变量
int a;
int *a_pointer1,*a_pointer2;
a_pointer1=&a;
a_pointer2=pointer1;
取地址和取内容运算
前面已经讲过,略
算数运算
从前面的叙述可以知道,通过一个指针变量可以知道指向变量的首地址和长度(因为我们申明了指针变量的类型)
而对指针变量进行算数运算实际上就是改变指针变量指向的地址.
因此对指针变量进行乘除是没有意义的.
对指针变量进行加减运算表示指针向前或者向后移动n个数据单元
例如:
int a[5];
int *a_pointer;
a_pointer=&a[1];
a_pointer=a_pointer+1;
我们知道一个整数占据四个字节,而指针a_pointer是a[1]的首字节的地址
但是a_pointer+1表示的向后移动四个字节到a[2]的地址而非向后移动一个字节.即移动一个数据单元
关系运算
指针的关系运算和普通变量的关系运算一样
但是需要注意的是关系运算一般用在用指针遍历数组的时候
因为这个时候数组内元素地址是连续的,可以进行比较,而对于单独的两个变量的地址,由于声明变量时内存地址的划分是随机的,一般不具有可比性.
常用的套路如下:
int a[5];
int *a_startPointer;*a_endPointer;
a_endPointer=&a[4];
while(a_startPointer<=a_endPointer)
{
*a_startPointer=1;
a_startPointer++;
}
或者是:
int a[5]={2,1,4,2,5};
int *a_startPointer,*a_endPointer;
int sum=0;
for(a_startPointer=&a[0],a_endPointer=&a[4];a_startrPointer<=a_endPointer;a_startPointer++)
{
sum=sum+*a_startPointer;
}
//同理可以尾指针向前遍历.
指针和数组
一个变量有地址,一个数组也有地址.但是不同的是,一个数组内具有若干个元素,每个元素都要占据内存空间,因此数组内的每个元素都有其地址.
指针变量可以指向变量,也可以指向数组,还可以指向数组中的元素
我们在引用数组元素时候可以使用下标法或指针法.但是,使用指针法能够使程序质量高,占用内存小,运行速度快
原因就是程序在编译时候实际上是把下标法转换成指针.
有一个很小的点需要首先声明
但是对于数组而言,数组是一个构造型数据,是我们自己构造出来的,声明数组时候用的类型标识符只是告诉编译器数组里面的元素的数据类型.所以他的长度就是元素的长度乘以元素个数
而对于数组的元素而言,他的长度就是我们所声明的数据类型的长度.
因此指针指向数组和指向数组元素是不一样的
当指针指向数组元素时候,指针每次移动就是按照数据类型的长度来移动.
当指针指向数组整体时候,指针每次移动都是按照数组的长度来移动.
因此对于数组和指针,需要分为两部分讲解,一个是数组元素的指针,另一个就是数组的指针.
此外当数组的元素都是指针的时候,就称之为指针数组.
我们还可以用指针来指向另外一个指针,这个时候就是二级指针
最后由于字符串的实现方式是字符数组,所以最后还会介绍指向字符串的数组.
数组元素的指针
就像前面所说的,数组元素的指针指向的是数组内的元素
一维数组元素的指针
上面刚讲,一个数组内的元素占据一定的内存地址,可以被赋给指针变量
int a[5];
int *a_pointer;
a_pointer=&a[2];
注意:
-
在数组名表示数组的首地址,等于数组第一个元素的地址,本质上还是指向元素的指针,即指针法a+i中i=0
int a[5]; int *p; p=a等价于p=&a[0]
-
可以对数组元素的指针进行取内容运算
int a[5]={1,2,3,4,5}; int *p; p=&a[2]; printf("%d",*p); >>>3
事实上,我们通过下标法来访问数组的元素的时候,编译器还是先把下标转换为地址符的形式.
例如:将a[2]转换成a[2]的地址,即将a[2]转化为(a+2)再进行操作.
因此直接使用地址来操作比使用下标法更快
前面已经讲过,对数组元素的指针加减表示移动多少个数据单元
二维数组元素的指针
对二维乃至更高维度的数组,和一维数组一样,每个元素都在内存中占据一定的空间,因而每个元素都具有自己的地址,都可以被单独的取地址.
对于二维数组中的元素,其地址按照如下规则:
-
可以对具体的元素取地址,例如:
int a[2][3]; int *a_pointer; a_pointer=&a[1][2];
-
前面讲过,二维数组的实现其实靠的是一维数组的嵌套,例如a[2][3]是由两个含有三个元素的一维数组嵌套形成的.
所以a[0]其实就是a的第一个元素的地址,即第一个数组的地址,例如:
int a[2][3]={1,2,3,4,5,6}; printf("%d",a[0]); >>>14483846,并不是1 >>>因为a[2][3]={1,2,3,4,5,6}的储存方式是{{1,2,3},{4,5,6}} >>>即两个一维数组的嵌套. >>>所以我们这里引用a[0],其实是用的第一个数组{1,2,3}的地址
这个时候,如果我们想引用{1,2,3}的第一个元素,就需要对a[0]取内容(a[0]是地址)
所以*a[0]就等价于b1[3]={1,2,3}(不妨把子数组记为b1[3]),所以我们直接把*a[0]打印出来,就会是b1[3]的第一个元素,(*a[3]相当于b1,即一维数组的数组名).int a[2][3]={1,2,3,4,5,6}; printf("%d",*a[0]); >>>1
-
a+i表示第i+1个元素的地址,即第i+1个列表的地址例如:
int a[2][3]; int *a_pointer; a_pointer=a+1; >>>a_pointer=a+1等价于a_pointer=&a[1][0]等价于a_pointer=a[1]
-
(a+i)+j表示第i+1行的第j+1个元素的地址,即第i+1个列表的第j+1个元素.例如
int a[2][3]; int *a_pointer; a_pointer=(a+1)+2; >>>a_pointe=(a+1)+2等价于a_pointer=&a[2][3]
故a[i][j]等价于*(a+i)+j
指向数组的指针
上面的指针,是指向数组的元素的指针,即指针的内容是数组的元素,每一次对指针的运算,都是进行数据类型的字长的整数倍运算.
例如:
int a[3]={1,2,3};
char b[3]={'a','b','c'};
int *a_pointer=a;
char *b_pointer=b;
a_pointer++;
b_pointer++;
>>>假如原来a_pointer的值是1000,a_pointer自增之后为1004(一个整数占四个字节);
>>>假如原来b_pointer的值是2000,b_pointer自增之后为1001(一个字符变量站一个字节);
但是有的时候我们希望一次能够直接移动整个数组长度的倍数.
例如:
char Word=[3][5]={"this","look","books"}
>>>Word这个二维字符串数组就等价于{{'t','h','i','s','\0'},{'l','o','o','k','\0'}, {'b','o','o','k','\0'}}
>>>我们如果想要将这三个单词按照首字母来排序,那么我们显然就想要指针每次移动5个字节
那么这个时候,我们就需要指向数组的指针.
语法:
类型说明符 (*标识符)[指向的数组长度]
其中,类型说明符和指向的数组的元素的类型一样
例如:
int a[2][3]={1,2,3,4,5,6};
int (*a_pointer);
a_pointer=a[0];
printf("%d",a[1]);
printf("%d",a_pointer++);
如果a_pointer指向的是数组的元素,那么a_pointer++就应该指向2,但是输出之后的结果发现a[1]和a_pointer结果一样,而我们根据上面二维数组的描述,我们知道a[1]是第二个子数组的地址,a_pointer自增实际上增加了12个字节.
故a_pointer是指向数组的指针.
指针数组
前面所讲解的数组,内部的变量类型都是基本变量类型:字符型,整数型,实数型.
由于地址是一个无符号的整型数,因此我们其实可以声明指针数组,即元素都是指针的数组
指向数组的指针的声明如下:
类型说明符 *数组名[数组长度1][数组长度2]...;
需要说明的是:
- 类型说明符不是指针数组中的元素的类型,而是指针数组中的每一个指针元素所指向的数据的类型.
- 除了==*==外,指针数组的声明和普通数组的声明没有区别
- 指针数组的元素的访问和普通数组一样,可以使用下标法和指针法
例如:
int a[5];
int *p[5];
p[0]=a; //指针数组p的第一个元素指向数组a的首元素
p[1]=a+2; //指针数组p的第二个元素指向数组a的第三个元素,即a[2]
*(p+2)=&a; //指针数组p的第三个元素的值取为数组a的首元素地址,即p[0]=a,这里p是p的首元素地址
*(*(p+1))=2; //给数组a的第3个元素赋值为2
*p[2]=3; //将数组a的第一个元素赋值为3
二级指针
指针可以用于指向整形数据,浮点型数据,字符型数据,那么指针能不能指向指针,或者说定义的指针变量的值另一个指针呢?
当然是可以的.我们称指向指针的指针为二级指针.
二级指针的声明如下:
类型说明符 **二级指针变量名;
在使用上和普通指针没有区别,但是要注意,二级指针指向的数据必须是指针.
事实上还能用类似的方法定义更高级的指针,但是这样会越来越难以理解.
指向字符串的指针
C语言中对字符串的处理是通过字符数组实现的,因此我们可以让指针指向字符串,来为我们对字符串的处理提供便捷
让指针指向字符串的方法有两种:
-
声明一个字符数组,让指针指向字符数组
char a[]="Interesting"; char *a_pointer; a_pointer=a;
-
使用指针变量直接指向字符数组
char *pointer="Interesting;
当我们命名一个字符串的时候,编译系统实际上会创建一个字符数组,第二个方法实际上就是直接让指针指向这个字符数组的首地址.
指针和函数
指针作为函数参数
我们如果在函数的形式参数的声明中,将参数声明为指针变量,那么这个时候我们的实际参数就应该是一个地址/指针
例如:
int func(int *p)
{
...
}
void main(){
int a;
a=5;
func(&a);
}
由于我们声明了传入的参数应该是指针,所以我们在主函数调用的时候,实际上需要传入的是a的地址
数组名作为函数参数
前面我们讲过,给函数的参数进制传递的时候,有两种,一个是值传递,还有另外一种是地址传递.
一般我们将某个变量或者值作为实际参数传入函数之后,都是数值传递
如果我们想要改变主调函数中的变量的值的时候,就可以将变量的地址传入.
同样,我们如果直接将数组名作为参数传入,因为数组名就是指针法中i=0的情况,实际上就是传递地址,例如:
int func(int arr[],int n){
....
}
void main(){
int a[5]={1,2,3,4,5};
func(a,10);
}
这样的话func中对数组的操作都是对主函数的数组进行的.
因此对于数组作为函数的参数时候,实参和形参的对应关系如下:
- 都是数组名
- 实参是数组名,形参是指针变量
- 都是指针变量
- 实参用指针变量,形参用数组名
例如:
//都是数组名
int func(int a[], int n){
...
}
void main(){
int a[5];
func(a,10);
}
//实参是数组名,形参是指针变量
int func(int *p,int n){
...
}
void main(){
int a[5];
func(a,10);
}
//都是指针
int func(int *p,int n){
...
}
void main(){
int a[5];
int *p=a;
func(p,10);
}
//实参是指针,形参是数组名
int func(int a[],int n){
...
}
void main(){
int a[5];
int *p=a;
func(p,10);
}
指针作为函数返回值
我们有的时候如果想要函数来返回一个地址的时候,那么我们需要声明函数的类型是指针
语法:
类型说明符 *函数名(参数列表)
并且函数的类型说明应该和指针指向的数据类型是一样的,例如
int *func(){
int *p;
return (p)
}
需要注意的是:
- 由于*func()中()优先级高于*,因此func先于()结合,表示是一个函数,然后在表明函数的返回值是指针,如果用(*func)(),就表示取指针func的内容,如果func指向一个函数,那么()中的实际参数传递进去后开始运行函数.
指向函数的指针
编写的源程序经过编译之后,其中的函数就被编译成一段程序放在内存中,该函数有一个入口地址.当我们调用这个函数的时候,就是转向这个入口地址来执行里面的函数,知道遇到返回语句或者函数以及运行完内部的所有语句.
因此函数的入口地址既然是一个地址,那么我们就可以定义一个指针比那里来指向函数.
指向函数的指针的定义
类型说明符 (*指针变量名)(函数参数列表);
其中:
- 类型说明符应该与指针指向的函数相同
- 函数参数列表要和函数一样
例如:
int func(int a, int b){};
int (*p)=(int a,int b);
指向函数的指针的赋值
赋值语句如下
指针变量=函数名
指向函数的指针的调用
由于指向函数的指针指向函数,因此我们可以通过指针来调用程序
int func(int a,int b){};
int (*P)=(int a,int b);
p=func;
(*P)(2,3)
*func()中()优先级高于*,因此func先于()结合,表示是一个函数,然后在表明函数的返回值是指针,如果用(*func)(),就表示取指针func的内容,如果func指向一个函数,那么()中的实际参数传递进去后开始运行函数.
但是使用指向函数的指针来调用函数很不常用,极度不推荐.