今天我们来讲一下c语言中的精华--指针。可能来理解起来会有点抽象,既然下来就让我给大家详细的讲一下~
指针的含义
什么是指针?在内存中,我们把内存划分为一个个的内存单元,而每一个内存单元为1个字节,每个内存单元都会给一个编号,而这个编号,又称为地址。在c语言中,把这一个个编号叫做指针。指针==地址==内存编号。
指针变量的定义
指针的定义,其实就是在定义普通变量前加“*”号,这样一个指针就创建好啦。看以下的代码,我们可以看到对指针变量定义的两种方式,这两种方式没什么区别,不过我们一般使用第一种,可读性更强。(也许有小伙伴不清楚为什么要这样定义,*代表什么,指针的类型是什么?*号其实就是表示了这是一个指针变量,而前面的int呢,是说明指针指向的对象是int类型。而指针的类型呢,就是int* ,说明这是一个整型指针变量)
可能有人会问指针有什么用?
这里来道题:利用指针来交换两个数的值
传值调用和传址调用
这里讲之前,先来说下函数调用有两种方式:传值调用和传址调用
传值调用:其实就是对实参的一份拷贝,传到函数去,但并不会对实参的值造成影响(从传递方向看,是从实参到形参单向传递的。
在调用函数的时候,只是传值给形参,传的只是实参的值,但形参由于也是一个变量,所以会占内存空间,不与实参使用同一个地址。
传址调用:这样就是把实参的地址传到形参(此时形参就是一个指向实参地址的指针),通过对形参解引用的方式来改变实参的值,对实参的值造成变化。在此过程中,实参和形参使用的是同一个地址,因此形参不会另占空间。
我这里自定义一个swap函数来交换x和y的值,当然你也可以直接在main函数数内通过中间变量来交换这两个的值。在代码中,调用swap函数来改变x,y的值,这里把x,y的地址传到swap函数里,再用两个指针来接受两者的地址。在函数体内通过一个中间变量来改变x,y的值。(传址调用)
当然,我们也可以来看下传值调用的例子
可以看到,这里只是拷贝了一份数据给swap,但并没有对x,y,造成影响.
指针变量的大小
在32位机器下是4个字节;在64位机器下是8的字节。
注意:指针变量的大小跟类型无关,任何指针的大小都是相同的。
&取地址操作符
我们在创建玩一个指针变量后,如何去获取目标变量的地址呢?
这里就需要用到我们的&取地址操作符。
在下面的代码中,我们可以看到想要取得a的地址,就需要用到&操作符,取出a的地址放到指针变量去,这个时候,指针变量p就是放的a的地址,我们想要获取对应的值,就需要*解引用操作符。
我们在上面指针的含义中讲了一个字节就是一个地址,而在内存中,一个内存单元是一个字节,而int占4个字节,但是我们看到指针变量其实是存了第一个字节的地址(地址较小的字节的地址)。
那么我们就可以通过解引用的方式往下访问4个字节。可能这里就有点疑惑了,但是其实我们知道,指针变量的大小是4/8个字节,且其大小跟类型无关。但指针类型决定了指针能一次性访问多少个字节。
指针类型的作用:
1.指针类型决定了指针解引用后一次性能够访问多少个字节。
2.指针类型决定了指针一次能跳过几个字节。
在图中,我们可以看到,对于int*类型的指针,+1一次性能跳过4个字节,而对于char*类型的指针,一次性只能跳过1个字节。
*解引用操作符
在前面,我们已经知道了指针变量存放的是地址,那么我们向要通过指针找到对应地址指向的对象。就需要用到*解引用操作符。
当然,我么们可以看下面代码,这里的int* p的*是说明这个指针。而在输出函数中,想要获取指针所指向地址的对象,那么就需要*解引来得到。
指针运算
指针运算有三种:指针+-整数,指针-指针,指针的关系运算
指针+整数
这里我们用数组的例子来解释一下,我们知道,数组的元素在内存中是连续存放的,所以,如果我们知道了首元素的地址,那么我们就可以顺着找到后面的元素。
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
int* p = &arr;//取出的是首元素地址--等价于&arr[0]
int sz = sizeof(arr) / sizeof(arr[0]);//求出数组元素个数
for (int i = 0; i < sz; i++)
printf("%d ", *(p + i));//(p+i)使指针指向下一个元素的地址
printf("\n");
//此时指针指向了最后一个元素的地址,我们想反过来打印,可以先让p重新指向数组首元素地址,再(p+i)再解引用
p = &arr;
for (int i = sz-1; i >=0 ; i--)
printf("%d ", *(p + i));
return 0;
}
指针-指针
指针-指针最后得到的是两个指针间的元素个数。
当然,它这个也有它的局限性,两个指针必须是指向同一块空间的。
来看两个例子:
1.我们可以看到两个指针,一个指针存放第10个元素的地址,一个存放第1个元素,那么它们相减自然就可以的出它们之间有9个元素。
2.strlen函数:用来计算字符串中字符的个数。
我们可以自定义一个函数来实现strlen函数
int my_strlen(char* s)//用字符指针接受数组a的首元素地址
{
char* p = s;
while (*p != '\0')//使p指向最后一个字符
{
p++;
}
return p - s; //计算两个指针之间的字符个数
}
int main()
{
char a[] = "abcdef";
int ret=my_strlen(a);
printf("%d", ret); //计算出字符个数为6
return 0;
}
指针关系运算
其实就是指针之间比较大小。(指针也是有大小的)
这里我们可以看个例子:
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
int sz = sizeof(arr) / sizeof(arr[0]);
int* p = &arr[0];//取数组首元素地址
//打印数组元素:用指针打印(while循环)
while (p < arr + sz)
{
printf("%d ", *p);
p++;
}
return 0;
}
多级指针
我们在写代码时经常用的就是一级指针
二级指针:存放一级指针的地址。
以此类推,三级指针就是存放二级指针的地址。。。(当然,我们一般很少用到二级指针以上的指针)
那么多级指针应该能理解了叭?就像无限套娃一样hh。
我们来看下下面的代码,我们创建一个一级指针来存放变量a的地址,再创建一个二级指针存放一级指针的地址。
那么此时,我们其实可以知道,二级指针其实时存放了变量a的地址。
为什么呢?我们上面讲了,一级指针存放了变量a的地址,那么我们再用二级指针存放一级指针的地址,其时就是存放变量a的地址。
int main()
{
int a = 10;
int* p = &a; //存放变量的地址
int** pp = &p;//存放一级指针的地址
printf("%d", **pp);//**pp==*(*pp)==*p==a
return 0;
}
void*指针
相信小伙伴们都知道void是无类型,那么void*就是无类型指针(又叫泛型指针)。
作用:void*指针能够接受任意类型的地址。
缺点:但是void*指针不能够进行+-运算和解引用运算(因为不知道指针是哪种类型的指针,+-该跳过几个字节是不知道的,解引用后指针能访问多少个字节也是不清楚的,所以不能精选+-,解引用运算。
既然void*指针这也不行,那也不行,那他能干嘛呢?
这里给小伙伴们说个例子:
我们知道,想要对数字进行排序,我们可以用冒泡排序,但是冒泡排序也有他的局限性,他只能对int型的数进行排序,对于浮点数,结构体之类的,他并不能排。但是我们可以用快速排序。
快速排序(qsort):
含义:
我们在cplusplus可以看到快速排序(qsort)所需的参数有4个,大家也可以去看看。qsort - C++ 参考 (cplusplus.com)
所需头文件<stdlib.h>
我们可以看到,第一个参数:其实就是传所需排数组的首元素的地址
第二个参数:所需排元素个数
第三个参数:每个元素所占字节大小
第四个参数:这是个函数指针,专门用来比较元素大小的
我们可以看到,在比较函数中,如果第一个元素>第二个元素,则返回>0的值
如果第一个元素=第二个元素,则返回0
如果第一个元素<第二个元素,则返回<0的值
代码实现
ok,既然了解了快速排序的基本含义及所需要的函数,那么我们就来写一个对整型数组的升序排序吧
比较函数是需要我们自己去写的,当然,我们在前面看到,比较函数的参数其实是(void*类型),所以不能直接进行比较,需要我们将其强制转换为(int*类型)再解引用进行比较。
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
#include<stdlib.h>
void print(int* ptr,int sz)
{
for (int i = 0; i < sz; i++)
printf("%d ", *(ptr + i));//打印数组每个元素
printf("\n");
}
int compa(void* p1, void* p2) //比较函数
{
return *(int*)p1 - *(int*)p2; //升序
//return *(int*)p2 - *(int*)p1; //降序
}
test()
{
int arr[] = { 12,65,11,45,78,24,38,2,30,57 };
int sz = sizeof(arr) / sizeof(arr[0]);
printf("排序前:\n");
print(arr, sz);
qsort(arr, sz, sizeof(arr[0]), compa);//参数1传数组首元素地址;2:传个数;3:元素所占字节大小;4:比较函数
printf("排序后:\n");
print(arr, sz);
}
int main()
{
test();
return 0;
}
程序运行结果
如果想要实现降序操作,只需要把比较函数中的参数颠倒下位置即可
数组指针
欧克,我们既然已经初步了解了什么是指针,那我们来认识一下数组指针。
数组指针的含义
数组指针就是指向数组的指针(数组指针需取出整个数组,所以要用&数组名)
形式举例:int (*p)[n];
什么意思呢?其实就是说(*p)说明这是一个指针,int (*p)【n】,说明指针指向一个有n个整型元素的数组。
我们来看下,其实数组指针也挺容易理解的。
先用普通指针打印数组:
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
int *p = arr;//这就是一个普通指针
int i = 0;
int sz = sizeof(arr) / sizeof(arr[0]);
for (i = 0; i < sz; i++)
{
//printf("%d ", arr[i]); //arr[i]==*(arr+i)
//printf("%d ", *(p + i));//*(p+1)==p[i]
printf("%d ", i[p]);//由交换律我们可以知道*(arr+i)==*(i+arr)==arr[i]==p[i] 所以我们也可以写成i[arr]/i[p]
}
}
用数组指针打印数组
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
int (*p)[10] = &arr;//这就是一个数组指针
int i = 0;
int sz = sizeof(arr) / sizeof(arr[0]);
for (i = 0; i < sz; i++)
{
printf("%d ",(*p)[i]);
}
}
sizeof+数组名 and &数组名
这里我们来说下什么时候数组名是取整个数组的情况:
1.sizeof+数组名,此时取得是整个数组。
2.&+数组名
看个代码,我们可以看到,当用&+数组名的时候,我们看到的虽然是首元素地址,但是其实它取出的是整个数组,所以当&+数组名+1的时候,会跳过8+2*16=40个字节(地址显示出来时是16进制的,但实际在内存中是以二进制存的)。我们看直接打印arr数组名,地址与&arr相同,但是arr其实是首元素地址,所以arr+1只会跳过4个字节。
指针数组
存放指针的数组叫做指针数组
我们来看个例子:用指针数组模拟实现二维数组(但是本质上不是二维数组,三个一维数组的地址不是连续存放的),只是实现二维数组的功能。
int main()
{
int a[5] = { 1,2,3,4,5 };
int b[5] = { 2,3,4,5,6 };
int c[5] = { 3,4,5,6,7 };
int* t[3] = { a,b,c };//此时这就是一个指针数组,存放三个数组的地址
for (int i = 0; i < 3; i++)//相当于二维数组的行
{
for (int j = 0; j < 5; j++)//相当于二维数组的列
{
printf("%d ",t[i][j]);
}
printf("\n");
}
}
运行结果
字符指针(char*)
存放字符的指针
例子:大家先来思考一下,下面代码输出的是什么?是不是感觉很奇葩?
int main()
{
char a[] = "abcdef";
char* p = a;
printf("%c\n", p[1])
printf("%c", "abcdef"[1]);
}
程序运行结果
为什么是b呢?
我们创建了一个字符数组,那么,我们在这里创建的指针,肯定是指向字符数组的首元素地址。
为了更直观点,我们直接在把常量字符串的地址放到指针中。那么其实这里面存放的就是常量字符串‘abcdef'中的a字符的地址。
我们可以把字符串想象成一个字符数组,但是在这个数组内,他的元素是不能被改的,因为是一个常量字符串。当常量字符串出现在表达式中时,存的是第一个字符的地址。
如果还是不太了解的话,可以看一下数组指针含义代码里我所讲的交换律,与之类似。
函数指针
函数指针:存放函数的地址,未来能通过地址调用函数的指针。
形式:int(*p)(int,int) : int 说明这个函数指针的返回类型是int型,(*p)说明这是个指针,(int,int)说明函数的参数是int类型。
在下面代码中,我们创建了一个函数指针来存放函数test的地址。那么后续我们就可以通过指针来调用test函数。对于指针,我们可以解引用也可以不解引用,二者都可以写。
函数也是有地址的,函数名就是函数的地址,当然也可以直接&函数名来存放函数地址
例子:
函数指针数组
用来存放函数指针的数组
形式:int(*ptr[n]()
ptr和[]先结合,说明这是个数组,类型就是int(*)(),说明是函数指针类型
函数指针数组有什么呢?
利用函数指针数组我们可以实现回调函数。
回调函数
回调函数就是利用函数指针调用的函数。
我们如果把一个函数的指针(地址)作为参数传给另一个函数,当这个指针被用来调用其所指向的函数时,被调用函数就是回调函数。
回调函数不是有该函数直接调用的,是在特定的条件或事件发生时由另外的一方调用的,用于对该事件或条件的响应。
(其实我们可以简单的理解为把索要实现的函数的地址传给一个中间商,通过中间商来调用所需的函数,实现对应的功能)
这里我们来说个例子:实现一个简单的计算器(+,-,*,/)
怎么实现呢?
法一:利用if语句和函数指针数组
#include<stdio.h>
void menu()
{
printf("*********************************\n");
printf("**** 1.加法 2.减法 ****\n");
printf("**** 3.乘法 4.除法 ****\n");
printf("**** 5.按位或 6.按位与 ****\n");
printf("**** 7.按位或与 0.退出 ****\n");
printf("*********************************\n");
}
int add(int x,int y)
{
return x+y;
}
int sub(int x,int y)
{
return x-y;
}
int mul(int x,int y)
{
return x*y;
}
int div(int x,int y)
{
return x/y;
}
int anor(int x,int y)
{
return x|y;
}
int anand(int x,int y)
{
return x&y;
}
int orand(int x,int y)
{
return x^y;
}
int main()
{
int input=0;
int x=0,y=0;
do
{
menu();
int (*ptr[8])(int,int)={NULL,add,sub,mul,div,anor,anand,orand};
printf("请输入您的选择:");
scanf("%d",&input);
if(input==0)
{
printf("退出计算器\n");
}
else if(input>=1&&input<=7)
{
printf("请输入两个数:");
scanf("%d %d",&x,&y);
int ret=ptr[input](x,y);
printf("%d\n",ret);
}
else
{
printf("输入有误,请重新选择\n");
}
}while(input);
return 0;
}
法二:利用switch语句和函数指针(回调函数)
如果我们不利用回调函数,那么我们每次不就需要多写点下面这样的代码。那岂不是很麻烦,有点冗余了,
int x, y;
printf("请输入两个数:>");
scanf("%d %d", &x,&y);
int ret=add(x,y);
printf("%d",ret);
void menu()
{
printf("***********************************\n");
printf("******* 1.add 2.sub **********\n");
printf("******* 3.mul 4.div **********\n");
printf("******* 0. exit **********\n");
}
int add(int x, int y)
{
return x + y;
}
int sub(int x, int y)
{
return x - y;
}
int mul(int x, int y)
{
return x * y;
}
int div(int x, int y)
{
return x / y;
}
void molc(int (*pc)(int x, int y))
{
int x, y;
printf("请输入两个数:>");
scanf("%d %d", &x,&y);
printf("%d\n", pc(x, y));
}
int main()
{
int input = 0;
do
{
menu();
scanf("%d", &input);
switch (input)
{
case 0:
printf("退出\n");
break;
case 1:
molc(add);
break;
case 2:
molc(sub);
break;
case 3:
molc(mul);
break;
case 4:
molc(div);
break;
default :
printf("输入错误\n");
break;
}
} while (input);
}
指针篇就先到这里~