Hello大家好!很高兴我们又见面啦!给生活添点passion,开始今天的编程之路!
今天我们来继续学习指针。
目录
1、二维数组传参本质
下面我们来运行这个的代码:
cpp
#include<stdio.h>
void test(int (*p)[5], int r, int c)
{
int i = 0;
int j = 0;
for (i = 0;i < r;i++)
{
for (j = 0;j < c;j++)
{
printf("%2d", *(*(p + i) + j));
}
printf("\n");
}
}
int main()
{
int arr[3][5] = { {1,2,3,4,5},{2,3,4,5,6},{3,4,5,6,7} };
test(arr, 3, 5);
return 0;
}
还记得上节课我们讲的数组指针吗?**数组指针是一个指针,指向了一个有五个元素的数组。那么指针+1,就跳过了这个数组(5个元素)。现在我们在*(p+1)的基础上在加1,解引用,就相当于访问了我i们第二个五个元素里的第二个,**如图:
我们把它放到循环里,就能打印数组啦。
实际上,在指针这一块,最快的学习方法是动手实践,实践出真知,现在留给大家一个疑问,*(*(p + 1) + 5)的值是多少呢?快去亲自测试测试吧!
2、函数指针变量
函数指针变量,首先,他是个指针。他指向了一个函数。
现在我们先来定义一个函数指针变量:
cpp
#include<stdio.h>
int add(int a,int b)
{
return a + b;
}
int main()
{
int a = 3;
int b = 5;
int (*test)(int x, int y) = &add;//指向函数地址。
//int (*test)(int,int) = &add;
//也可以不写x,y,因为形参不是我们在定义指针时定义的,我们只需要定义传入形参类型就可以。
int c=(*test)(a,b);//使用指针函数
printf("%d", c);
return 0;
}
使用时,我们把函数名代替为*(指针),因为我们指针指向了函数的地址 ,解引用之后就相当于函数。
3、函数指针数组
到这里,事情变得有趣起来了。首先函数指针数组肯定是数组,那他里面存放的啥,函数指针吗?答对了,还真是函数指针。
函数指针数组定义格式:
返回值类型 (*指针数组名[数组大小])(参数列表);
现在我们就来定义一个函数指针数组:
cpp
#include<stdio.h>
int add(int a,int b)
{
return a + b;
}
int sub(int a, int b)
{
return a - b;
}
int mul(int a, int b)
{
return a * b;
}
int div(int a, int b)
{
return a / b;
}
int main()
{
int (*p[4])(int x, int y) = { add,sub,mul,div };
return 0;
}
我们发现,他和函数指针的区别就是,在指针名后面加了一个[4],那说白了,我们是不是可以把他理解成4个函数指针呢? 我们把这4个函数指针存放进了一个数组里,我想用第一个 函数我们就引用下标为0 的函数指针,第二个 函数我们就引用下标为1的函数指针。好,那说了半天这玩意到底有什么用呢?怎么用呢?别急,我们接着往下看
4、转移表
转移表通常是一个包含函数指针的数组,数组中的每个元素包含指向对应函数的指针。举例,计算器的实现:
(1)计算器的一般实现
cpp
#include<stdio.h>
int add(int a,int b)
{
return a + b;
}
int sub(int a, int b)
{
return a - b;
}
int mul(int a, int b)
{
return a * b;
}
int div(int a, int b)
{
return a / b;
}
int main()
{
int x, y;
int input = 1;
int ret = 0;
do
{
printf("*************************\n");
printf("*****1、add 2、sub*****\n");
printf("*****3、mul 4、div*****\n");
printf("*****0、sxit *****\n");
printf("请选择:");
scanf_s("%d", &input);
switch (input)
{
case 1:printf("请输入操作数:");
scanf_s("%d %d", &x, &y);
ret = add(x, y);
printf("ret=%d", ret);
break;
case 2:printf("请输入操作数:");
scanf_s("%d %d", &x, &y);
ret = sub(x, y);
printf("ret=%d", ret);
break;
case 3:printf("请输入操作数:");
scanf_s("%d %d", &x, &y);
ret = mul(x, y);
printf("ret=%d", ret);
break;
case 4:printf("请输入操作数:");
scanf_s("%d %d", &x, &y);
ret = div(x, y);
printf("ret=%d", ret);
break;
}
input = 0;
} while (input);
return 0;
}
很好的代码,但是这未免也太长了吧?我们有什么办法简化一下呢?
(2)计算器函数指针数组的实现
cpp
#include<stdio.h>
int add(int a,int b)
{
return a + b;
}
int sub(int a, int b)
{
return a - b;
}
int mul(int a, int b)
{
return a * b;
}
int div(int a, int b)
{
return a / b;
}
int main()
{
int x, y;
int input = 1;
int ret = 0;
do
{
printf("*************************\n");
printf("*****1、add 2、sub*****\n");
printf("*****3、mul 4、div*****\n");
printf("*****0、sxit *****\n");
printf("请选择:");
scanf_s("%d", &input);
int (*p[5])(int x, int y) = { 0,add,sub,mul,div };//我们希望输入1时访问add,所以用0把这四个函数的下标都向前顶一个。
if (input > 0 && input < 5)
{
printf("请输入操作数:");
scanf_s("%d %d", &x, &y);
ret = (*p[input])(x, y);
printf("ret=%d", ret);
}
input = 0;
} while (input);
return 0;
}
我们讲写好的加减乘除函数放到了一个函数指针数组里,通过函数指针数组访问,访问我们想要的数组,这时或许你有个疑问,你不是说函数指针数组里存的是函数指针吗?这咋是函数名?其实,数组名就是地址,而地址和指针是一样的。
5、回调函数
回调函数就是一个通过函数指针调用的函数。简单说就是我自己定义了一个函数,我把这个函数的地址传给咱们C语言自带的一个函数作为参数,系统调用咱们的函数,就叫回调函数。
那么那些系统函数支持传入函数地址呢?这里推荐一个网站: https://www.cplusplus.com 在这个网站我们可以查看各个函数的定义,看他们支持传入的参数类型是什么。比如,我们看接下来我们要讲的qsort函数:
他需要传入的第四个参数就是一个函数指针,这个指针指向了一个需要用两个const void*型(const修饰代表这个指针是个常量,是不可被修改的)变量作为参数。
6、qsort函数使用
qsort函数的定义是:
Sorts the num elements of the array pointed to by base, each element size bytes long, using the compar function to determine the order.
对由指针base指向的array数组的num个元素排序,每个元素的大小是size byte使用compar函数来决定顺序。
也就是说,他是一个万能的排序函数。我们先使用它排序一个整形数组:
cpp
#include<stdio.h>
int compar(const void* x ,const void* y)
{
return *((int*)x) - *((int*)y);//强制转化成整型指针
}
int main()
{
int arr[10] = { 5,6,7,3,1,2,9,8,0,4 };
size_t sz = sizeof(arr) / sizeof(arr[0]);//num
size_t size = sizeof(arr[0]);
qsort(arr, sz, size, compar);
int i = 0;
for (i = 0;i < 10;i++)
{
printf("%d ", arr[i]);
}
return 0;
}
由于他需要传入一个比较函数,所以我们要自己写一个比较函数传进去。
大家可以自己尝试一下让他比较结构体数据。
7、模拟qsort函数
还记得我们之前的练习篇介绍了三种排序算法吗?我们现在就把我们最常用的,最简单的冒泡排序,改造成qsort函数:
cpp
#include<stdio.h>
#include <stdlib.h>
int cmp(const void* p1, const void* p2)
{
return *(int*)p1 - *(int*)p2;
}
void swap(const void*p1, const void* p2,size_t size)
{
char temp;
int i = 0;
for (i = 0;i < size;i++)
{
temp = *((char*)p1+i);
*((char*)p1+i) = *((char*)p2+i);
*((char*)p2+i) = temp;
}
}
void bubble(void *arr,size_t sz,size_t size,int (*cmp)(const void* , const void*))
{
int i = 0;
int j = 0;
for (i = 0;i < sz - 1;i++)
{
for (j = 0;j < sz - 1 - i;j++)
{
if (cmp((char*)arr + j * size, (char*)arr + (j + 1) * size)>0)
{
swap((char*)arr + j * size, (char*)arr + (j + 1) * size,size);
}
}
}
}
int main()
{
int arr[] = { 1, 2, 3, 5, 9, 8, 6, 4, 0, 7 };
int i = 0;
size_t sz = sizeof(arr) / sizeof(arr[0]);
bubble(arr,sz, sizeof(arr[0]),*cmp);
for (i = 0;i < sizeof(arr) / sizeof(arr[0]);i++)
{
printf("%2d", arr[i]);
}
printf("\n");
return 0;
}
天啊这是啥啊,根本就看不懂!!!别急,我们一点一点来分析。
首先,我们把冒泡排序的模子写出来:
cpp
#include<stdio.h>
#include <stdlib.h>
void bubble(int *arr,size_t sz)
{
int i = 0;
int j = 0;
for (i = 0;i < sz - 1;i++)
{
for (j = 0;j < sz - 1 - i;j++)
{
if (*(arr+j) > *(arr + j+1))
{
int temp;
temp = *(arr + j);
*(arr + j) = *(arr + j+1);
*(arr + j+1) = temp;
}
}
}
}
int main()
{
int arr[] = { 1, 2, 3, 5, 9, 8, 6, 4, 0, 7 };
int i = 0;
size_t sz = sizeof(arr) / sizeof(arr[0]);
bubble(arr,sz);
for (i = 0;i < sizeof(arr) / sizeof(arr[0]);i++)
{
printf("%2d", arr[i]);
}
printf("\n");
return 0;
}
我们在这里写了一个用指针来计算的冒泡排序。接着开始改造。
我们上来先想的是,既然我们要模拟,我们的把我们的模拟qsort函数的参数类型和原qsort一样吧?我们第一个先传入数组名作为base,第二个应该传入的是数组元素的个数,由于这里我们要排序的是整形数组,我们用sizeof来计算个数,注意要是size_t(无符号整型)的参数。 然后我们在传入元素的宽度 ,或者叫占用的字节数。 最后传入我们写的比较函数。
接着,我们得写交换函数了。我们先把最基础的交换函数写出来,通过创建临时变量。
然后,输出。
cpp
#include<stdio.h>
#include <stdlib.h>
int cmp(const void* p1, const void* p2)
{
return *(int*)p1 - *(int*)p2;
}
void swap(const void* p1, const void* p2)
{
char temp;
int i = 0;
if (*(int*)p1 > *(int*)p2)//p1,p2是void*型,需要先强制转换
{
temp = *(int*)p1;
*(int*)p1 = *(int*)p2;
*(int*)p2 = temp;
}
}
void bubble(void* arr, size_t sz, size_t size, int (*cmp)(const void*, const void*))
{
int i = 0;
int j = 0;
int* p = arr;
for (i = 0;i < sz - 1;i++)
{
for (j = 0;j < sz - 1 - i;j++)
{
if (cmp(p+ j,p+j + 1) > 0)
{
swap(p+j,p+j+1);
}
}
}
}
int main()
{
int arr[] = { 1, 2, 3, 5, 9, 8, 6, 4, 0, 7 };
int i = 0;
size_t sz = sizeof(arr) / sizeof(arr[0]);
bubble(arr, sz, sizeof(arr[0]), *cmp);
for (i = 0;i < sizeof(arr) / sizeof(arr[0]);i++)
{
printf("%2d", arr[i]);
}
printf("\n");
return 0;
}
注:为了测试方便,我在这里去掉了输入数组的部分,感兴趣的可以自己加上。
到这我们思考一个问题, 为什么我们的qsort函数参数是void* 型呢?还记得上一篇我们说过,空指针最大特点就是来者不拒。在使用qsort时,为了让他能排序各种类型的数据,我们使用void*指针。
那现在我们思考第二个问题, 既然我们不确定传入比较什么数据,我们的交换函数,能是int*的吗?当然不能。那么我们就要使用占用字节最小的char* 对函数进行改造,使他全能一些。我们传入的是int型,那他就占用四个字节,我们现在换成四个char类型了,那是不是交换四次,每次交换完自增一个字节,就相当于交换了一个int型呢?好的,现在我们就完成了这个代码。
8、sizeof和strlen对比
**sizeof是用来计算变量所占内存空间大小的,单位是字节。而strlen是用来计算字符串长度的。**传入字符串的末尾会自带一个\0,strlen会从开始向后寻找,直到找到\0,停止寻找。需要注意的是,strlen需要包含头文件string.h。
下面我们来看这道题:
cpp
#include<stdio.h>
#include<string.h>
int main()
{
char arr[5] = { 'a','b','c','d','e' };
char arr1[] = { "abcde" };
printf("%d ", sizeof(arr));//5
printf("%d ", sizeof(arr1));//6
printf("%d ", strlen(arr));//随机值
printf("%d ", strlen(arr1));//5
}
输出结果如下:
在输出前两个值时,由于arr1数组中是一个字符串,字符串后边有**\0** ,所以多占了一个字节,输出结果为6。那在strlen中同样是因为\0的问题,arr中strlen 没有找到**\0** ,他就会一直往后查找,造成越界,所以输出一个随机值。
9、szieof与数组
下面我们来看这道题:
cpp
#include<stdio.h>
int main()
{
int arr[5] = { 1,2,3,4,5 };
printf("%d ", sizeof(arr));// 20
printf("%d ", sizeof(arr+1));// 4/8
printf("%d ", sizeof(*arr));// 4
printf("%d ", sizeof(arr[1]));// 4
printf("%d ", sizeof(&arr));// 4/8
printf("%d ", sizeof(*&arr));// 20
printf("%d ", sizeof(&arr+1));// 4/8
}
还记得我们之前讲过的两个数组名的特殊情况吗?
(1)当数组名单独放到sizeof中时,表示整个数组
(2)当&数组名时,数组名表示整个数组。
只要是地址,sizeof(地址)就是4/8,X86是4,X64环境是8(上一节讲过)
相信掌握了上的知识点,问题就不难解决了。
我们再来道题:
cpp
#include<stdio.h>
#include<string.h>
int main()
{
char* p = "abcdef";
printf("%d ", strlen(p));// 6
printf("%d ", strlen(p+1));// 5
printf("%d ", strlen(*p));// err
printf("%d ", strlen(p[0]));// err
printf("%d ", strlen(&p));// 6
printf("%d ", strlen(&p+1));// 30
printf("%d ", strlen(&p[0]+1));// 5
}
这时候就有人要问了,你这err是啥意思啊?
err就是错误的意思。 我们可以尝试输出代码,发现他输出到错误代码卡顿一下,就停止输出了。按F11 我们可以一步步调试 一下,发现他会报错。那为啥会报错呢?我们去cplusplus上把函数的原型截过来:
我们捕捉到了一个很关键的信息:**strlen函数传入的参数应该是指针!**那我们往里面传个字符或者数字,怎么可能不报错呢?
我们现在回头看题,第一个是传入指针p,那就正常使用正常查找,输出结果为6。
第二个 传入p+1,p是char*指针,那加一不就是往前跳一个字节吗?所以输出结果为5.
第三第四都是因为传入不是地址,所以报错。
第五个是&p,我们想想,p本身就是个地址,你再取地址,这就相当于一个二级指针:
到这里我们再补充一个小小的点,什么是二级指针?
二级指针
就是指针指向指针,指向指针的指针就叫二级指针。
我们看这个图,p是一个指针,我们&p就相当于p1,p1指向p,p又指向c,那我们&p本质上还是指向c。
另外,我们可以像这样定义二级指针:
cpp
#include<stdio.h>
int main()
{
char* p = "abcdef";
char** p1 = p;
}
好了,我们回到正题:既然&p最终指向a,那还是正常查找呗?结果是6.
第六个,&p+1在这里我们可以这样理解:我们上面二级指针图片那第一个箭头指向的那向下移动一个字符就是&p+1,那你指向的是哪?空的啊!很明显这又越界了。那我们怎么检测到\0呢?检测到的\0也不可能是我们字符串最后的/0。所以结果是随机值。
第七个,我们下标引用操作符[]的实质就是指针的变化,这在前面我们已经讲过了。那p[0]自然就是'a',我们取出他的地址,再加一,就相当于从b的位置开始查找,结果为5。
好了,到这里我们就讲完了指针的内容。但是想要更深入的理解指针,还是得回到题目中去练习,去思考。这一部分比较难,如果看不懂的话建议多看两遍,有问题也可以在交流区提问!感谢大家的支持!期待我们的再见!