• 【再识C进阶2(中)】详细介绍指针的进阶——函数指针数组、回调函数、qsort函数


    前言

    💓作者简介: 加油,旭杏,目前大二,正在学习C++数据结构等👀
    💓作者主页:加油,旭杏的主页👀

    ⏩本文收录在:再识C进阶的专栏👀

    🚚代码仓库:旭日东升 1👀

    🌹欢迎大家点赞 👍 收藏 ⭐ 加关注哦!💖💖

    学习目标:

           在这一篇博客中,我们要认识并理解函数指针数组的概念,再学会在特定情境下使用函数指针数组;简单认识一下指向函数指针数组的指针;认识一下回调函数,并通过qsort函数来认识一下回调函数。这就是本博客的学习目标。


    学习内容:

    通过上面的学习目标,我们可以列出要学习的内容:

    1. 认识并理解函数指针数组的概念
    2. 学会在特定情境下使用函数指针数组
    3. 简单认识一下指向函数指针数组的指针
    4. 认识一下回调函数
    5. 通过qsort函数来认识一下回调函数

    一、函数指针数组

    1.1 函数指针

           我们先来简单回顾一下函数指针,我们在初始C语言中学过指针的初阶,我们认识了整形指针字符指针基本数据类型的指针整形指针是存放整形类型变量的地址字符指针是存放字符类型变量的地址……那么我们来看这个函数指针,明显是一个指针。通过小学学习的找规律进行编写句子,可以轻松地得到:函数指针是存放函数类型变量的地址(可能描述有些不正确)。看下图方便理解:

    整形指针:存放整形类型变量的地址,32位平台下是4个字节,在64位平台下是8个字节

    字符指针:存放字符类型变量的地址,32位平台下是4个字节,在64位平台下是8个字节

    函数指针:存放函数类型变量的地址,32位平台下是4个字节,在64位平台下是8个字节

    1.1.1 了解什么是函数的地址? 

    啊,读者可能会感觉到有点奇怪!为什么函数也有地址呢?

           因为函数是由一些运行的语句组成的,程序运行的时候就会把函数中的语句调用到内存中去,那么函数代码在内存中开始的那个内存空间的地址就是函数的地址

    接下来,让我们用代码来认识一下函数的地址:

    1. void test()
    2. {
    3. printf("Hellod,worid!");
    4. }
    5. int main()
    6. {
    7. printf("%p\n", test); //函数与数组类似,数组名表示数组首元素的地址,函数名表示函数的地址
    8. printf("%p\n", &test); //&函数名拿到的是函数的地址
    9. }

    1.1.2 学习如何使用函数指针? 

           在了解完函数指针是什么,可能大家还不知道什么是函数指针?书接上文,函数的地址要想保存下来,需要怎么保存呢?下面,我们来看代码:

    1. void test()
    2. {
    3. printf("Hellod,worid!");
    4. }
    5. //下面pfun1和pfun2哪个有能力存放test函数的地址?
    6. void (*pfun1)();
    7. void *pfun2();

           首先,能够用于存储地址,就要求pfun1或者pfun2是指针,看上面,pfun1先与 * 结合,说明pfun1是指针,去掉指针就是指针所指向的类型是:void (),返回值类型为void。

    如何使用函数指针呢?下面来看代码:

    1. void test()
    2. {
    3. printf("Hellod,worid!\n");
    4. }
    5. int main()
    6. {
    7. void (*pf)() = &test; //用pf指针存储函数的地址
    8. (*pf)();
    9. pf();
    10. test();
    11. }

    1.2 利用计算器代码来具体介绍函数指针数组

    1.2.1 函数指针数组是什么?

           在学习完函数指针后,我们来认识一下函数指针数组是什么?和介绍函数指针一样,函数指针数组的主语是数组,在初始C语言中,我们学过数组的内容中介绍了一些常见的数组类型:整形数组字符数组基本数据类型的数组整形数组是存放一些整形类型的变量字符数组是存放一些字符类型的变量……那么函数指针数组存放的值一些函数指针类型的变量。看下图,方便理解:

    整形数组是存放一些整形类型的变量,数组存放的是想同类型的变量;

    字符数组是存放一些字符类型的变量;

    函数指针数组是存放一些函数指针类型的变量。

     下面用代码来认识一下函数指针数组:

    1. int Add(int x, int y)
    2. {
    3. return x + y;
    4. }
    5. int Sub(int x, int y)
    6. {
    7. return x - y;
    8. }
    9. int main()
    10. {
    11. int (*pf1)(int, int) = &Add;
    12. int (*pf2)(int, int) = ⋐
    13. int (*pfarr[2])(int, int) = { pf1, pf2 };
    14. }

    1.2.2 利用画图对函数指针数组的写法详解:

    1.2.3 计算器代码的实现

           在详细介绍分支与循环语句中,我们讲过像这种代码的设计结构,先有一个函数的主题、再有一个菜单、之后根据菜单的内容进行功能的设计,最后进行结束游戏。大致思路就是这,请读者继续跟着我的思路进行设计计算器:

    1.2.3.1 计算器代码的主体
    1. int main()
    2. {
    3. int input = 0;
    4. do {
    5. menu();
    6. printf("请选择:>");
    7. scanf("%d", &input);
    8. switch (input)
    9. {
    10. case 1:
    11. break;
    12. //……
    13. default:
    14. break;
    15. }
    16. } while (input);
    17. return 0;
    18. }
     1.2.3.2 计算器的菜单
    1. void menu()
    2. {
    3. printf("***************************\n");
    4. printf("***** 1. Add 2. Sub *****\n");
    5. printf("***** 3. Mul 4. Div *****\n");
    6. printf("***** 0. exit *****\n");
    7. printf("***************************\n");
    8. }
    1.2.3.3 计算器的自定义函数部分 

    这个简单的计算器将实现四种计算的功能,分别是:加、减、乘、除

    1. int Add(int x, int y)
    2. {
    3. return x + y;
    4. }
    5. int Sub(int x, int y)
    6. {
    7. return x - y;
    8. }
    9. int Mul(int x, int y)
    10. {
    11. return x * y;
    12. }
    13. int Div(int x, int y)
    14. {
    15. return x / y;
    16. }
    1.2.3.4 计算器函数主体的选择部分
    1. switch (input)
    2. {
    3. case 1:
    4. printf("请输入两个数:>");
    5. scanf("%d %d", &x, &y);
    6. ret = Add(x, y);
    7. printf("结果是:> %d\n", ret);
    8. break;
    9. case 2:
    10. printf("请输入两个数:>");
    11. scanf("%d %d", &x, &y);
    12. ret = Sub(x, y);
    13. printf("结果是:> %d\n", ret);
    14. break;
    15. case 3:
    16. printf("请输入两个数:>");
    17. scanf("%d %d", &x, &y);
    18. ret = Mul(x, y);
    19. printf("结果是:> %d\n", ret);
    20. break;
    21. case 4:
    22. printf("请输入两个数:>");
    23. scanf("%d %d", &x, &y);
    24. ret = Div(x, y);
    25. printf("结果是:> %d\n", ret);
    26. break;
    27. case 0:
    28. printf("退出计算器!\n");
    29. break;
    30. default:
    31. printf("选择错误,请重新选择!\n");
    32. break;
    33. }
    1.2.3.5 这种计算器代码的不足之处
    1. 当计算器的功能逐渐增加的时候,菜单会越来越长,所需要写出的函数也会越来越多,最重要的是switch()语句会越来越长
    2. case语句中的代码存在冗余

    1.2.4 对计算器代码进行改进

           这种改进使得在以后对计算器的功能进行升级的时候,会非常方便!因为你只需要将函数指针数组进行修改,以及input的范围进行修改即可。

    1. do {
    2. menu();
    3. printf("请选择:>");
    4. scanf("%d", &input);
    5. int (*pfarr[])(int, int) = { NULL, &Add, &Sub, &Mul, &Div };
    6. if (0 == input)
    7. {
    8. printf("退出计算器!\n");
    9. }
    10. else if (input >= 1 && input <= 4)
    11. {
    12. printf("请输入两个数:>");
    13. scanf("%d %d", &x, &y);
    14. ret = (*pfarr[input])(x, y);
    15. printf("结果是:> %d\n", ret);
    16. }
    17. else
    18. {
    19. printf("选择错误,请重新选择!\n");
    20. }
    21. } while (input);

    1.3 函数指针数组的用途 

    函数指针数组的用途是:转义表

           正确使用函数指针数组的前提条件是,这若干个需要通过函数指针数组保存的函数必须有相同的输入、输出值

           函数指针数组的好处:只要少许行代码,就完成了许多条case语句要做的事,减少了编写代码时工作量,将nStreamType作为数组下标,直接调用函数指针,从代码执行效率上来说,也比case语句高。假如多个函数中均要作如此处理,函数指针数组更能体现出它的优势。

     二、指向函数指针数组的指针(了解)

           我们可以先从简单的入手,我现在有一堆整形变量,现在需要将这些整形变量的地址存储起来,那需要怎么进行存储呢?答用整形指针数组进行存储。如果我现在想拿到这个整形指针数组的地址,需要用什么来接收呢?答用指向整形指针数组的指针来接收

           同理,指向函数指针数组的指针是一个指针指针指向一个数组数组的元素都是函数指针。这就跟俄罗斯套娃一样,一层一层的,我们需要先找到主语,得知类型;继续找主语,得知类型……有点耐心,像剥洋葱一样,慢慢拨开!

    下面,我们用代码来认识一下:

    1. void test(const char* str)
    2. {
    3. printf("%s\n", str);
    4. }
    5. int main()
    6. {
    7. //函数指针pf
    8. void (*pf)(const char*) = test;
    9. //函数指针数组pfarr
    10. void (*pfarr[10])(const char*);
    11. pfarr[0] = test;
    12. //指向函数指针数组pfarr的指针ppfarr
    13. void (*(*ppfarr)[10])(const char*) = &pfarr;
    14. return 0;
    15. }

    扩展视野(没必要): 函数指针数组指针数组,函数指针数组指针数组指针

    三、回调函数(重要)

           回调函数是非常重要的知识点,可以玩出许多高端操作,其依赖于函数指针,有了函数指针我们才能实现回调函数。下面来看一下回调函数的概念:

    3.1 回调函数的概念

           回调函数就是一个通过函数指针调用的函数。如果你把函数的指针(地址)作为参数传递给另一个函数,当这个指针被用于调用其所指向的函数时,我们就说这是回调函数。回调函数不是由该函数的实现方直接调用而是在特定的事件或条件发生时由另外一方调用的,用于对该事件或条件进行响应。

           简单来说,就是函数作为参数放在另一个函数的形参中,当另一函数在使用时,会调用作为参数的函数,那么这个作为参数的函数就是回调函数

    接下来,我们用计算器来举个例子:

           在1.2.3中实现的计算器代码中,switch()语句中的代码有点冗余,如果我们将这些冗余的部分封装成一个函数,那么这个代码看上去就会好很多。但是冗余的这一部分有一点是不同的,就是所使用的函数,所以我们需要在封装函数的形参中传递功能函数的地址,使封装函数进行调用,这就是回调函数。下面看代码:

    1. void cal(int (*pf)(int, int))
    2. {
    3. printf("请输入两个数:>");
    4. scanf("%d %d", &x, &y);
    5. ret = (*pf)(x, y);
    6. printf("结果是:> %d\n", ret);
    7. }

    3.2 回调函数的案例:qsort函数 

           先来介绍一下qsort函数qsort函数:其是一个库函数,底层使用的快速排序的方式,对数据进行排序的;这个函数可以直接使用,这个函数可以用来排序任意类型的数据。 

    在qsort函数中,有四个参数,我们来认识一下每一个参数所表示的意思:

    • void* base :待排序数组的第一个元素的地址;
    • size_t num:待排序数组的元素个数;
    • size_t size:待排序数组中一个元素的大小;
    • int (*compar) (const void*, const void*) :函数指针——cmp指向了一个函数,这个函数是用来比较两个元素的。

           排序就是有比较组成的,qsort函数可以排序不同类型的数据,但不是所有类型都可以用不等号比较出来,方法是有差异的。比如说,整形可以直接用>比较,而两个结构体的数据可能不能直接用>比较。

           在这个qsort函数中,最难的是第四个形参,这个形参所指向的函数的作用如下图:让两个数进行相减,根据结果与0进行比较判断谁在前,谁在后。

    介绍一下void* 的指针

    1. void* 是无类型指针,可以接收任意类型的地址;
    2. 不能进行解引用操作;
    3. 不能进行加、减整数的操作。

    3.3 qsort函数的使用

    1. //排序整形数组
    2. int int_cmp(const void* e1, const void* e2)
    3. {
    4. return (*(int*)e1 - *(int*)e2);
    5. }
    6. int main()
    7. {
    8. int arr[10] = { 3,4,5,6,7,8,9,2,1,0 };
    9. int sz = sizeof(arr) / sizeof(arr[0]);
    10. qsort(arr, sz, sizeof(arr[0]), int_cmp);
    11. for (int i = 0; i < 10; i++)
    12. {
    13. printf("%d ", arr[i]);
    14. }
    15. return0;
    16. }
    1. //排序结构体中名字的顺序
    2. struct student {
    3. char name[40];
    4. int age;
    5. };
    6. int name_cmp(const void* p5, const void* p6)
    7. {
    8. return strcmp(((struct student*)p5)->name, ((struct student*)p6)->name);
    9. }
    10. int main()
    11. {
    12. struct student s[3] = { {"zhangsan", 23}, {"lisi", 45}, {"wangwu", 78} };
    13. int sz2 = 3;
    14. qsort(s, sz2, sizeof(s[0]), name_cmp);
    15. for (int i = 0; i < 3; i++)
    16. {
    17. printf("%s\n", s[i].name);
    18. }
    19. return 0;
    20. }
    1. //排序结构体中年龄的大小
    2. struct student {
    3. char name[40];
    4. int age;
    5. };
    6. int age_cmp(const void* p3, const void* p4)
    7. {
    8. return ((struct student*)p3)->age - ((struct student*)p4)->age;
    9. }
    10. int main()
    11. {
    12. qsort(s, sz2, sizeof(s[0]), age_cmp);
    13. for (int i = 0; i < 3; i++)
    14. {
    15. printf("%d\n", s[i].age);
    16. }
    17. return 0;
    18. }

    3.4 回调函数的作用

    1. 恰当时间发送通知;
    2. 让代码更加灵活;
    3. 提高运行效率。

    学习产出:

    1. 认识并理解函数指针数组的概念
    2. 学会在特定情境下使用函数指针数组
    3. 简单认识一下指向函数指针数组的指针
    4. 认识一下回调函数
    5. 通过qsort函数来认识一下回调函数
  • 相关阅读:
    C#进阶08——迭代器、特殊语法
    3D开发工具HOOPS Publish如何快速创建交互式3D PDF文档?
    【标准化封装 SOT系列 】 D SOT-323 SOT-363
    如何注册微信小程序
    EMR 集群时钟同步问题及解决方案An error occurred (InvalidSignatureException)
    【技术实战】Java开发岗位的八股文积累【一】
    木棒组合问题
    .net VB中字符串按照换行符号俩来进行分割
    牛客网专项练习30天Pytnon篇第16天
    JavaScript排他思想小例子之按钮的点击效果
  • 原文地址:https://blog.csdn.net/2301_77868664/article/details/132774518