目录
一、内存、地址和指针
(1)、内存
在了解指针前要先知道C语言中的内存和地址是什么,我们的内存分为了存储内存和运行内存,内存就是用来存放一些数据的,将内存细分成很多个内存单元,每个内存单元的大小为一个字节,我们就可以想象一个名为内存的大楼,每个内存单元是一个一个空间大小为一个字节的房间,一个一个的房间搭建出这个内存大楼。然后为了区分每个房间,每个房间像酒店一样都有一个门牌号302、408……这些就是地址了。而指针就是一个指向这块空间地址的道具。
(2)、地址
每次创造变量时,系统会自动为该变量创造相应大小的空间并为其创建门牌号也就是地址,地址是一个由16进制位组成的数字。如图当int a=10时,系统随机分配了地址为0x00000001000FF594的门牌号,并在里面存储数据为0a也就是10;
(3)、指针
取地址符&就是用来获取一个变量的地址的。为了时刻记住这个变量的地址,我们就可以使用指针创建一个指针变量来记录这个a的地址,指针变量的创建如int* pa=&a 。int 用来声明这个指针指向的类型是什么,*声明pa是个指针变量,pa的指针变量的变量名,最后再用地址符将a的地址赋给pa,指针变量pa就指向a了。
(4)、指针的使用
pa有了a的地址就可以直接找到a所在的位置进行修改a的数据,访问一个地址里的数据操作要用到解引用符号(*),在地址前加上*就可以对这个地址解引用,如pa得到的是a的地址,*pa得到的就是10,如果我们要将a的值改为20,就可以*pa=20;此时再去访问a就会发现a已经变成20了。
(5)、指针的类型操作
指针因为也区分了它所指向的类型,所以当你如果对一个int*的指针进行加减操作时,他就会根据指向的类型大小,来跳过几个字节后重新指向新的位置,如我们创建一个int*指针来指向一个数组的首元素地址,此时将指针++,指针就会跳过4个字节刚好指向该数组的第二个元素的地址。指针类型还可以是void*类型,这类指针包罗万象,可以接受任意类型大小的地址,但此时指针自己也不知道自己能访问多大的空间位置,所以在使用时一定要强制类型转换来说明之后操作时遵循的类型操作。
二、指针的深入认识
(1)、const修饰指针变量
为了防止让指针功能更单一,我们只需要这个指针指向这部分空间的地址而不想让指针修改数据,我们可以在指针定义前加上const修饰,此时指针就不能够修改它所指向空间的内容,只可访问不可修改,如果还想让这个指针变得更唯一,再严格限制它只需指向这片空间不准再指向其他空间,我们可以在*后加上const修饰。也就是说*前加const让其不能修改数据,*后加const让其不能指向其他空间。如const int * const pa =&a;那么pa就只能这辈子跟着a了,还不能对a动手动脚。
(2)、野指针和空指针的区别
有些人经常分不清野指针和空指针,空指针指的是这个指针所存储的地址是空,它不指向任何一块空间,所以无法进行访问,而野指针是存储了地址空间的,但是由于它所指向的空间已经不存在了,导致这个指针流离失所,变成野指针。如本来pa是指向整型变量a的,我们将pa++一下跳过一个整型空间,那么pa此时就指向一个我们不知道的空间,谁也不知道里面存储了什么数据,如果此时强行用pa访问这片空间就容易造成不可知的后果。所以野指针的存在比空指针危险多了。
(3)、指针的一些实际用途
有了指针后,我们可以对一些操作进行更细的使用,如用指针模拟strlen函数,因为strlen函数是用来统计在遇到‘\0’前有多少个字符的,我们也可以用一个char*指针指向这个字符串的首元素地址,对这块空间解引用后如果里面存储的值不为‘\0’,那么就对指针++跳过一个元素并计数,反复操作直到遇到'\0',那么最后就能知道这个字符串的长度了。
char str[20]="Hello bits";
char* p = &str[0];
int count = 0; //统计字符串长度
while(*p!='\0')
{
count++;
p++;
}
还有一件事,因为我们在向函数传值时,传的不过是这个变量数据的一份临时拷贝,所以我们对函数里的那个变量进行修改数据时无法改变函数外的那个值啊,为了改变外面的值,让函数里的变量真正成为那个传参的变量,我们可以传递这个变量的地址,函数用一个指针来接收这个地址,我们对这个指针操作时就会直接改变地址里存储的数据,从而改变函数外的数据。
void Addp(int* a)
{
*a=10; //可以改变
}
void Add(int a)
{
a=10; //无法改变
}
int a = 0;
Addp(&a); //传地址
Add(a); //传数据
三、指针对数组的操作
(1)、数组名
我们知道数组名是用来访问数组的,我们平常都会使用arr[0],arr[1][2]来访问数组来进行操作,如果对只有一个arr数组名进行操作会如何呢?首先要知道的是,一般情况下,数组名代表的是这个数组的首元素地址,也就是说我们平时将数组作为参数时传给函数时,其实只是将这个数组的首元素地址传了过去,所以我们函数设置参数时其实也可以用一个指针来接收这个数组。
二维数组的本质其实是有一个一维数组,但它的成员是其他几个一维数组的首元素地址,它们的数组名就是这个数组的下标如arr[3][4],其实本质为arr[3]={arr[0],arr[1],arr[2]}。由于arr[1]也就是第一个数组的数组名,所以arr[1]代表的就是arr[1]这个数组的首元素地址,也就是arr[1][0]了。
现在我们知道一般情况下的数组名代表的是它的首元素地址了,所以以后传数组时可不要再傻傻的对数组取地址了,如果对数组取地址的话,就是我们的二般情况了,对一个数据进行取地址操作取出来的可就不是首元素的地址了,而是取出这一整个数组的地址,此时对这个值进行++操作,就会跳过一整个数组。还有一种情况下数组名不代表它的首元素地址,当我们在sizeof操作符下时,数组名代表的也是一整个数组,sizeof(arr)就是得到整个数组的大小。
(2)、冒泡排序法
冒泡排序是最常见的一个排序法,用来将一个数组按升(降)序来重排数组。冒泡顾名思义就是将一个数的大小视为一个物体的重量,重的就会沉下去,轻的就会浮起来。我们只需要从头开始将这个数与它下一个数进行比较大小,如果比它大就交换两个数,这样子大的数就会慢慢沉下去,轻的就会浮起来。
//冒泡排序法
void Bubble(int* arr,int sz)
{
for(int i=0;i<sz;i++)
{
for(int j=0;j<sz-i;j++)
{
if(arr[j]>arr[j+1])//大的沉下去
{
//交换两个数据
int tmp=arr[j];
arr[j]=arr[j+1];
arr[j+1]=tmp;
}
}
}
}
四、其他指针类型
(1)、二级指针
我们现在知道指针变量是用来存放变量的地址的,那么指针变量也是变量,也理应有地址,那么指针变量的地址又由谁来存储呢?由此我们引入二级指针的概念,二级指针就是用来存储一级指针变量的地址的。
我们示例一个二级指针:int**ppa=&pa;int** 为二级指针类型,取出一级指针的地址存放即可。二级指针的功能和一级指针的功能基本一致,我们平常用的二维数组就可以用二级指针变量来接收它的地址。
(2)、指针数组
指针数组,重点在于数组,它是一个数组,用于存放的类型是指针类型,它的每个元素都是指针变量。示例:int *p[3]={"HELLO","BIT","WELCOME"};首先int说明类型一定是个整型,标识符p首先会与[]结合变成数组,然后再与*结合变成每个成员类型为整型指针的数组。
(3)、数组指针
数组指针,重点在于指针,它容易与上一个混淆,注意区分,它是一个指针,用于指向一个数组,存放的是一个数组的地址。示例int (*p)[5]=arr[5];首先我们可以看到,由于*p被括号括起来了,标识符p只能先和*结合变成指针,再与[]结合,表面指向的是一个类型为int,空间大小为5的数组。记住,一定要加上括号防止p与[]先结合变成指针数组。
(4)、函数指针
没座!函数它也有地址,也是可以由一个指针来接收它的地址的。接收它的指针就是函数指针。我们给出示例:int (*p)(int,int)=&Add;首先Add是一个函数名,它的作用是将两个数相加再返回回去,为了让p变成指针,我们将p与*括起来,否则p与最右边的左括号先结合的话将会被编译器认为这是个函数声明,p就变成函数名了。右边括号里的是你要存放函数的参数类型和数量,表明你要存的函数都是有两个int参数的函数,最右边的int是函数的返回类型,所以函数指针的模板一般为void (*)()。函数的地址可以用&取出来,也可以不用&,只放一个Add也可以代表Add的地址。
int Add(int a,int b)
{
return a+b;
}
int (*p)(int,int)=&Add;
我们来看一个比较阴间的代码:void (*signal(int , void(*)(int)))(int),第一眼你能看出它应该是个函数指针,但你肯很难看出来它的具体构造。首先你应该能确定的是signal是一个函数名才对,因为*和signal并没有被单独括起来,所以会优先与右边的括号结合,变成signal(int,void(*)(int)),这下能看出来了吧,signal是一个函数名,参数为int和一个函数指针,我们再把它屏蔽掉,发现就剩一个int(*)(int)了,这明显是一个函数指针,所以signal函数的返回类型就是一个函数指针。
(5)、函数指针数组
没座!还有高手。我们还可以将函数指针组成集合放在数组里,这个数组就是函数指针数组,每个成员都是函数指针。示例int (*p[4])(int)={Add,Sub,Mul,Div};首先我们还是先看标识符p,p会先与[]结合变成数组,再与int(*)(int)结合变成函数指针。
与此类推我们还能整出更加复杂的指针,下面就不再俄罗斯套娃了。
五、回调函数
回调函数指的是一个函数,它的参数中有一个函数指针,这个函数调用的函数指针指向的函数就叫做回调函数,所以回调函数指的是被调用函数调用的函数,而不是被调用的函数。
我们的函数库有一个函数叫快排函数(qsort)q是Quick,它的用法是qsort(void*base,int num,int size,int (*)(void*,void*))。第一个参数是你要排序的数组首元素地址,void*是为了保证能排序任意类型的数组;第二个参数是num,你要排的函数的长度;第三个参数是你的数组每个元素的类型大小,确定你要排序的类型;最后一个参数是要你提供一个返回类型为int,参数为(void*,void*)的函数,你要写一个函数,确定你要排升序还是降序,或者按哪一种排序法则来排序,函数的内容为比较两个参数的大小,左边的比右边大就返回1,否则返回-1,等于就返回0。我们提供的函数就是sqort后面要调用的回调函数。
int Compare(void* a,void* b)//你提供的比较大小函数
{
return *(int*)a - *(int*)b;
}
int arr[]={9,5,2,7,4,6,3,8,1,0};
int sz = sizeof(arr)/sizeof(arr[0]);
sqort(arr,sz,sizeof(arr[0]),Compare);
看到这了想必你对指针已经有一定的理解了,从陌生到熟悉就要深刻理解指针的含义,指针就是地址,地址就是指针,这样你对指针的理解将会更快适应。
再次感谢你的阅读 OVO!