扫二维码与项目经理沟通
我们在微信上24小时期待你的声音
解答本文疑问/技术咨询/运营咨询/技术建议/互联网交流
指针的进阶⭐博客主页:️CS semi主页
成都创新互联公司主要从事成都网站设计、成都网站建设、网页设计、企业做网站、公司建网站等业务。立足成都服务水磨沟,10余年网站建设经验,价格优惠、服务专业,欢迎来电咨询建站服务:028-86922220
⭐欢迎关注:点赞收藏+留言
⭐系列专栏:C语言进阶
⭐代码仓库:C Advanced
家人们更新不易,你们的点赞和关注对我而言十分重要,友友们麻烦多多点赞+关注,你们的支持是我创作大的动力,欢迎友友们私信提问,家人们不要忘记点赞收藏+关注哦!!!
指针居然有进阶,我一直以为指针是简简单单的找地址,可是整理了一遍发现指针的进阶居然那么细节那么难,最主要的是指针进阶是把前面学习的所有知识,例如数组、函数相结合起来进行操作,是很复杂的,所以大家要多看几遍此篇博客!!!我们今天要讲解的是字符指针、数组指针、指针数组、数组传参和指针传参、函数指针、函数指针数组、指向函数指针数组的指针、回调函数、指针和数组面试题的解析。
指针的基本概念:
#includeint main(){char str1[] = "hello world";
char str2[] = "hello world";
const char* str3 = "hello world";
const char* str4 = "hello world";
//are not same
if (str1 == str2)
printf("str1 and str2 are same\n");
else
printf("str1 and str2 are not same\n");
//are same
if (str3 == str4)
printf("str3 and str4 are same\n");
else
printf("str3 and str4 are not same\n");
return 0;
}
大家可以先看一看之前的指针初阶,可以去看一下关于指针数组的知识。
【C初阶】详解指针
如图,当我们进行打印常量数组的时候,是找到首元素的地址然后用%s打印,如下代码:
//整型数组 - 存放整型的数组
//字符数组 - 存放字符的数组
//指针数组 - 存放指针(地址)的数组
int main() {//存放字符指针的数组
const char* arr[4] = {"abcdef","qwer","yes","no" };
int i = 0;
for (i = 0; i< 4; i++) {printf("%s\n", arr[i]);
}
return 0;
}
(二)整型数组
如图,当我们找到数组的首元素地址的时候,只要根据j作为下标往后找元素。
int main() {int arr1[5] = {1,2,3,4,5 };
int arr2[5] = {2,3,4,5,6 };
int arr3[5] = {3,4,5,6,7 };
int arr4[5] = {4,5,6,7,8 };
//指针数组
int* arr[4] = {arr1,arr2,arr3,arr4 };
int i = 0;
for (i = 0; i< 4; i++) {int j = 0;
for (j = 0; j< 5; j++) { //printf("%d ", arr[i][j]);
printf("%d ", *(arr[i] + j));//找到j下标元素的地址
}
printf("\n");
}
return 0;
}
数组指针是指针?还是数组?
答案是:指针。
我们已经熟悉:
整形指针: int * pint; 能够指向整形数据的指针。
浮点型指针: float * pf; 能够指向浮点型数据的指针。
那数组指针应该是:能够指向数组的指针。
大家可以做一个类比,取出一个字符的地址并存起来,是用字符指针存起来的;整型指针存放的是一个整型数的地址;&数组名是取出的整个数组的地址,再存放到数组指针中,因为[]的优先级大于*,因为pa需要和*先结合,毕竟它是一个指针,所以需要加一个()来保证p和*先结合。
#includeint main() {char ch = 'w';
char* pc = &ch;//字符指针
int num = 10;
int* pi = #//整型指针
int arr[10];
int(*pa)[10] = &arr;//数组指针
//解释:p先和*结合,说明p是一个指针变量,然后指着指向的是一个大小为10个整型的数组。
//所以p是一个指针,指向一个数组,叫数组指针。
//这里要注意:[]的优先级要高于*号的,所以必须加上()来保证p先和*结合。
return 0;
}
(二)&数组名VS数组名我们知道arr是数组名,数组名表示数组首元素的地址。
那&arr数组名到底是什么?
那各位如果想很详细了解可以去看一下下面的链接:
【C初阶】数组详解
#includeint main() {int arr[10] = {1,2,3,4,5,6,7,8,9,10 };
//&arr取出的是数组的地址,只有数组的地址才需要数组来接收
int(*p)[10] = &arr;//数组指针
//数组名 - 数组首元素的地址
//&数组名 - 是整个数组的地址
//数组首元素的地址和数组的地址从值的角度来看是一样的,但是意义不一样
printf("%p\n", arr);//类型:int*
printf("%p\n", arr + 1);//4
printf("%p\n", &arr[0]);//类型:int*
printf("%p\n", &arr[0] + 1);//4
printf("%p\n", &arr);//类型:int(*)[10]
printf("%p\n", &arr + 1);//40
return 0;
}
(三)数组指针的使用既然数组指针指向的是数组,那数组指针中存放的应该是数组的地址。
1.不常用但好理解的代码//不常用但好理解的代码
#includeint main()
{int arr[10] = {1,2,3,4,5,6,7,8,9,10 };
int(*p)[10] = &arr;
int i = 0;
for (i = 0; i< 10; i++) {printf("%d ", (*p)[i]);//p解引用再加下标找到位置
}
return 0;
}
找到整个数组的地址然后解引用以后通过下标去访问数组内的元素。
2.常规代码一般用在二维数组,那我们就看看二维数组是怎么进行传参的吧!
先来一个大家耳熟能详的传参方式:
#includevoid print1(int arr[3][4], int r, int c) {//形参接收的是二维数组
int i = 0;
for (i = 0; i< r; i++) {int j = 0;
for (j = 0; j< c; j++) { printf("%d ", arr[i][j]);
}
printf("\n");
}
}
int main() {int arr[3][4] = {{1,2,3,4},{2,3,4,5},{3,4,5,6} };
print1(arr, 3, 4);//二维数组传参
//print2(arr, 3, 4);
return 0;
}
那可是今天我们要讲的是指针啊,这传参仅仅是二维数组,那指针怎么传参呢?其实还是很简单的,二维数组的首地址就是这个二维数组的第0行,因为二维数组在计算机内部也是连续存放的,那不就是四个元素存放在一个大元素当中,成为一个首地址元素,那再用指针去指向这个数组不就好了吗,那我们试一试吧!
#include//void print1(int arr[3][4], int r, int c) {//形参接收的是二维数组
// int i = 0;
// for (i = 0; i< r; i++) {// int j = 0;
// for (j = 0; j< c; j++) {// printf("%d ", arr[i][j]);
// }
// printf("\n");
// }
//}
void print2(int(*p)[4],int r,int c) {int i = 0;
for (i = 0; i< r; i++) {int j = 0;
for (j = 0; j< c; j++) { printf("%d ", (*(p + i))[j]);//类型本来就是int [j]所以固定格式不变,再p+偏移量是找到后面的地址再解引用访问
//[]的优先级大于*,所以要再加一个()来先解引用操作找到地址
}
printf("\n");
}
}
int main() {int arr[3][4] = {{1,2,3,4},{2,3,4,5},{3,4,5,6} };
//print1(arr, 3, 4);//二维数组传参
print2(arr, 3, 4);
return 0;
}
3.小知识
(1)如何强转int (*p)[10]=arr;如何强转?
形参直接用一级指针接收就好了。
形参直接用二级指针接收传参就好了。
当函数的参数为二级指针的时候,可以接收什么参数?
函数也有地址!?原来函数也是在栈区创建的一片空间,也是有地址的。
int Add(int x, int y) {return x + y;
}
int main() {//int arr[10];
//int(*pa)[10] = &arr;//类比,函数指针与数组指针非常相似
//printf("%p\n", &Add);
int(*pf)(int, int) = &Add;
int ret = (*pf)(2, 3);//调用Add函数,通过pf调用函数
int ret = Add(2, 3);//我们之前写的
//而Add是pf调用的,所以Add和pf是一回事
//所以等价于int ret = pf(2, 3);
//(*pf)中的*是一颗摆设,写几颗都无所谓,但是一旦写上一定要加上括号,因为不加()是解引用pf(2,3)的结果
printf("%d\n", ret);
//pf就是一个存放函数地址的指针变量,被称为函数指针
//&函数名和函数名都是函数的地址
//上式等价于int (*pf)(int, int) = Add;
return 0;
}
关于*的问题,大家想一想,当调用函数的时候,我们用一个pf来接收,那如何接收呢?先要声明,这个pf是个指针,所以星号p,而大家想,这个是不是可以与一维数组指针进行类比,那我们看,这个函数指针的类型是什么,是后面加上(),圆括号里面是int,int两个类型的,而这一整个函数指针的类型又是int,所以就有了int(*pf)(int, int) = &Add;其次大家想,那我想把这个指针存起来该怎么办?那么我们看,先将pf解引用找到这个指针的地址去看里面的内容,那再通过里面的实参去进行进入形参当中,那不就是int ret = (*pf)(2, 3);那其实大家觉得这样很好理解,那就这么用。但是还有更简练的方法是:Add与&Add是一回事,这与数组是不一样的,而我们想之前我们写调用函数是int ret = Add(2, 3);这么方便,现在我哼哧哼哧写了那么多难以理解的东西,怎么经过简化呢?大家看,Add是由pf去进行调用的,那这两个就是等价的,既然等价,那就直接用pf来代替Add好了,那最后的结果就是int ret = pf(2, 3);所以说那颗星星是没用的,加几颗都没事,但是一旦加上了那颗星,就跟我们之前说的一样了,是要先解引用呀,解引用才能找到这个地址,然后再去访问这里面的元素的呀,所以需要加括号。
(二)两个有趣代码何时用的问题:经常用于函数调用函数的情况,调用地址不是更加方便吗?
出自《C的陷阱和缺陷》。
1.代码1
想到的需要是0是一个地址
int main() {//该代码是一次函数调用
//调用0地址处的一个函数
(*(void (*)())0)();
//void (*)()是函数指针类型
//首先是把0(函数的地址)强制类型转换为void (*)()的函数指针
//然后去调用0地址处的函数
return 0;
}
2.代码2
关键需要一步一步剖析,它是大的函数指针包着一个小的函数指针。
int main() {void (*signal(int, void(*)(int)))(int);
//该代码是一次函数的声明
//声明的函数的名字叫signal
//signal函数的第一个参数是一个整型int类型
//signal函数的第二个参数是函数指针类型
//该函数指针指向的那个函数的参数是int,返回类型是void
//signal函数的返回类型是一个函数指针:void (*)(int);
//该函数指针参数是int,返回类型是void
//简化
typedef void(*pf_t)(int);//pf_t就是这个函数指针类型为void(*)(int)
pf_t signal(int pf_t);
return 0;
}
那要把函数的地址存到一个数组中,那这个数组就叫函数指针数组。
#includeint my_strlen(const char* str) {return 0;
}
int main() {//函数指针数组
int(*pf[5])(const char*) = {&my_strlen };//数组内部存放的是函数的地址
return 0;
}
(二)例子(计算器)这是我们能写出来最简单的计算器,但是不是太长了……
//写一个计算器实现加减乘除
#includevoid menu(void) {printf("************************\n");
printf("****1.add 2.sub*****\n");
printf("****3.mul 4.div*****\n");
printf("****** 0.exit ******\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 main() {int input = 0;
int x = 0;
int y = 0;
int ret = 0;
do {menu();
printf("请选择:>");
scanf("%d", &input);
switch (input) {case 1:
printf("请输入两个操作数:>");
scanf("%d %d", &x, &y);
ret = Add(x, y);
printf("%d\n", ret);
break;
case 2:
printf("请输入两个操作数:>");
scanf("%d %d", &x, &y);
ret = Sub(x, y);
printf("%d\n", ret);
break;
case 3:
printf("请输入两个操作数:>");
scanf("%d %d", &x, &y);
ret = Mul(x, y);
printf("%d\n", ret);
break;
case 4:
printf("请输入两个操作数:>");
scanf("%d %d", &x, &y);
ret = Div(x, y);
printf("%d\n", ret);
break;
case 0:
printf("退出计算器\n");
break;
default:
printf("选择错误,请重新选择\n");
break;
}
} while (input);
return 0;
}
我们进行一个优化吧!
我们发现返回类型和参数是一模一样的!
那我们用函数指针数组来简化一下上式的switch吧!
//写一个计算器实现加减乘除
#includevoid menu(void) {printf("************************\n");
printf("****1.add 2.sub*****\n");
printf("****3.mul 4.div*****\n");
printf("****** 0.exit ******\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 (*pf[5])(int, int) = {NULL,&Add,&Sub,&Mul,&Div };//因为我们的1是Add函数,所以第0位空出来
//只能操作整数的双目操作
int main() {int input = 0;
int x = 0;
int y = 0;
int ret = 0;
do {menu();
printf("请选择:>");
scanf("%d", &input);
if (input == 0) { printf("退出计算器\n");
break;
}
else if (input >= 1 && input<= 4) { printf("请输入两个操作数:>");
scanf("%d %d", &x, &y);
int ret = pf[input](x, y);
printf("%d\n", ret);
}
else { printf("选择错误\n");
}
} while (input);
return 0;
}
#includeint main() {//函数指针数组
int (*pf[5])(int, int);
//ppf是指向函数指针数组的指针
int (*(*ppf)[5])(int, int) = &pf;//&pf就是对数组名取地址,取的是整个数组的地址,存放到数组指针中去
//但pf为一个函数指针数组,所以需要放到函数指针当中,所以加了函数指针的框架
return 0;
}
(二)解释回调函数是本课最难的也是最复杂的,要理解起来非常麻烦,所以需要大家细细去品味,或许会很枯燥,但也需要好好品味去学习。
(一)概念回调函数就是一个通过函数指针调用的函数。如果你把函数的指针(地址)作为参数传递给另一个函数,当这个指针被用来调用其所指向的函数时,我们就说这是回调函数。回调函数不是由该函数的实现方直接调用,而是在特定的事件或条件发生时由另外的一方调用的,用于对该事件或条件进行响应。
即:用函数指针调用函数。
case里面都有相同的参数,我们之前已经使用了将Add,Sub,Mul和Div这四个函数进行统一的分装到一个函数指针数组当中,这样只要用到哪个函数就直接利用函数指针去进行访问函数指针数组即可,但是,我们现在想用case语句,那可是发现四个case内部除了调用函数不同,其他全部相同,那我们就想了,能不能找一个共同的函数,直接通过这个函数去进行访问其他函数的地址进而进入函数内部进行计算呢?那我们就来一个比较简单的函数就是calc();如下代码:
//写一个计算器实现加减乘除
#includevoid menu(void) {printf("************************\n");
printf("****1.add 2.sub*****\n");
printf("****3.mul 4.div*****\n");
printf("****** 0.exit ******\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;
}
void calc(int(*pf)(int, int)) {int x = 0;
int y = 0;
int ret = 0;
printf("请输入两个操作数:>");
scanf("%d %d", &x, &y);
ret = pf(x, y);
printf("%d\n", ret);
}
int main() {int input = 0;
do {menu();
printf("请选择:>");
scanf("%d", &input);
switch (input) {case 1:
calc(&Add);
break;
case 2:
calc(&Sub);
break;
case 3:
calc(&Mul);
break;
case 4:
calc(&Div);
break;
case 0:
printf("退出计算器\n");
break;
default:
printf("选择错误,请重新选择\n");
break;
}
} while (input);
return 0;
}
所以这就是很好的解释了回调函数是什么,当我们发现有四个case操作是一样的,但是就是所需要调用的函数不一样,那就很好办了,找一个函数中介去进行调用,就相当于大家都在一个小姜中介所看中了这一套房子,但出于价格稍微有点贵都相等房价下跌,所以你们告诉了我电话和家庭住址,我有了你们的地址了,存起来但还没用,突然有一天,小徐打电话给我说:“小姜中介,我想直接全款拿下这套房子!”,那此时小徐是已经告诉了我他的地址和电话,此时我是知道他的地址的,所以我的指针是指向他的,但此时还没用到他的地址,在后续的操作中,当我需要去找他并带他一起去看房的时候,那我就需要他的地址并带他去看房了,也就是说,calc函数是一个中介函数,而真正的回调函数是Sub函数。
九、指针综合练习笔试题来个传送门,传送大家到qsort函数那里去,模拟实现qsort函数就是一个非常经典的回调函数的例子,那可是干货满满的!!
传送门【<<点此>>进行传送】
这里需要给两个传送门,直接跳转到我们的笔试题解析当中,将这几年所有的笔试题精妙绝伦地演示出来,供大家学习一下:
传送门<<点此>>【C进阶】指针笔试题汇总
传送门<<点此>>【C进阶】指针和数组综合题
此篇博客干货满满,先是以很长的篇幅讲解了关于各类复合指针,后续主要介绍了回调函数中的qsort函数,可谓是十分巧妙,以及讲解了关于指针和各类数组的配合使用,是非常详细的,干货满满,读完这里大概已经读了20分钟,但仔细学这篇博客需要一天的时间,所以大家需要耐心阅读与学习总结。
客官,码字不易,来个三连支持一下吧!!!
你是否还在寻找稳定的海外服务器提供商?创新互联www.cdcxhl.cn海外机房具备T级流量清洗系统配攻击溯源,准确流量调度确保服务器高可用性,企业级服务器适合批量采购,新人活动首月15元起,快前往官网查看详情吧
我们在微信上24小时期待你的声音
解答本文疑问/技术咨询/运营咨询/技术建议/互联网交流