进阶指针(2)

简介: 进阶指针(2)

在前面我们已经讲解了进阶指针的一部分,我们回顾一下在进阶指针(1)我们学过的难点知识点:

int my_strlen(const char* str)
{
 return 0;
}
int main()
{
 //指针数组-数组
 //数组的定义:数组元素的类型 数组名 [常量表达式]
 //常量表达式:用来表示数组中元素的个数,即数组的大小(长度)
 char* ch[5];//指针数组,数组5个元素,每个元素的类型是char*
 //数组指针-指针
 //注意:[]的优先级要高于*的,所以必须加上()来保证指针变量先与*结合
 //对数组指针的理解:变量先和*结合,说明变量是一个指针变量,
 //     然后在前面的就是所指向数组的元素类型
 //     后面的[]是所指向的数组的大小
 //指针变量前的第一个*与变量结合表示它是指针,
 //再往前面的所用东西表示这个指针所指向对象的类型
 int arr[10];//整形数组,数组10个元素,每个元素的类型是int
 int(*pa)[10] = &arr;//指针数组,该指针指向一个数组
 //数组10个元素,每个元素是int类型
 //函数指针-指针
 //和数组指针类似,要注意操作符的优先级,所以必须加上()来保证变量先与*结合
 //对函数指针的理解:
 // 变量先与*结合,说明变量是一个函数指针,
 // 然后前面的是所指向函数的返回类型,后面的()是所指向函数的参数列表
 int (*pf)(const char*) = my_strlen;//函数指针,该指针指向一个函数
 //函数的返回类型是int,函数的形参列表为(const char*)
 return 0;
}

没看过的可以点下面链接观看:

指针的进阶——(1)_wangjiushun的博客-CSDN博客

接下来我们继续学习关于进阶指针的知识:

6.  函数指针数组

数组是一个存放相同类型的存储空间,在前面我们学习了指针数组。

比如:存放整形指针的数组

 int* arr[10];//arr先与[]结合,说明arr是数组,数组的元素类型是int*

那要把函数的地址存到一个数组中,那这个数组就叫函数指针数组(即:函数指针数组-存放函数指针(地址)的数组)。

那函数指针数组如何定义呢?

       int (*parr[10])();

       //parr先和[]结合,说明parr是数组;数组的元素类型是int(*)()的函数指针

先回顾:数组的定义:数组的元素类型 数组名 [数组的大小]

函数指针数组的定义:

       ①数组名先和[]结合,说明它是一个数组;

       ②然后数组名和[数组的大小]移到函数指针的*后面

函数指针数组的写法:因为函数指针数组是在函数指针的基础上写出来的,所以我们①先写出函数指针;②再写数组名和数组大小。

例子:写一个计算器整数+ - * /

代码1:一般写法

//头文件
#include<stdio.h>
#include<windows.h>//预处理,对Sleep的声明
#include<stdlib.h>//预处理,对system的声明
//自定义函数-实现打印菜单
void menu()
{
  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选择语句-表达式与case标签后的常量相等,就执行该case后的语句
    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语句
    default:
      printf("选择错误!\n");
      break;//写不写均可
    }
    //每次执行完一次计算器清屏
    Sleep(2000);//Sleep函数是实现睡眠,单位是毫秒
    system("cls");//system是一个库函数,可以执行系统命令,cls是清屏的命令
  } while (input);
  return 0;
}

我们发现:

switch语句,随着功能增加,case增加,并且有部分功能代码重复,所以造成代码冗长。

优化:使用if多分支语句替换。

计算器的功能函数的返回类型和参数一样。

优化:使用函数指针数组——存放函数指针(地址)

代码2:转移表

//头文件
#include<stdio.h>
#include<windows.h>//预处理,对Sleep的声明
#include<stdlib.h>//预处理,对system的声明
//自定义函数-实现打印菜单
void menu()
{
  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);
    //计算器功能函数的返回类型,参数一样——使用函数指针数组
    //数组下标从0开始,由题意0退出计算器,NULL的本质是0
    int (*pf[5])(int, int) = { NULL, Add, Sub, Mul, Div };
    //if多分支选择语句
    if (input == 0)
    {
      printf("退出计算器\n");
    }
    else if(input >= 1 && input <= 4)
    {
      printf("请输入两个操作数:>");
      scanf("%d %d", &x, &y);
      ret = pf[input](x, y);//通过函数指针数组,找到函数地址,然后调用函数
      printf("%d\n", ret);
    }
    else
    {
      printf("选择错误\n");
    }
    //每次执行完一次计算器清屏
    Sleep(2000);//Sleep函数是实现睡眠,单位是毫秒
    system("cls");//system是一个库函数,可以执行系统命令,cls是清屏的命令
  } while (input);
  return 0;
}

函数指针数组——用途:转移表

理解:转移表——数组有跳转的含义:你给我一个下标,我通过下标找到数组里某个函数的地址,然后去调用函数,所以函数指针的用途叫转移表。

7.  指向函数指针数组的指针

指向函数指针数组的指针——指针

指针指向一个数组,数组的元素都是函数指针。

那我们怎么定义呢?

#include<stdio.h>
void test()
{
  printf("hehe\n");
}
int main()
{
  //函数指针
  //①变量先与*结合,说明变量是一个指针;
  //②然后前面的是所指向函数的返回类型,后面的()是所指向函数的参数列表
  void (*pfun)() = test;
  //函数指针数组——数组
  //写法:①先写函数指针;
  //    ②在写数组名和数组大小
  void (*pfunArr[5])() = { test };//pfunArr先与[]结合,说明pfunArr是数组
  //函数指针数组的指针——指针
  //写法:①先写函数指针数组
  //    ②在写指针变量
  void (*(*ppfunArr)[5])() = &pfunArr;//*与ppfunArr先结合,说明ppfunArr是指针
  return 0;
}

总结:函数指针数组是在函数指针的基础上写的,函数指针数组的指针是在函数指针数组的基础上写的。


8.  回调函数

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

简单的说:回调函数就是通过函数指针调用的函数。

例子1:计算器


那我们怎么解决呢?

我们发现只有调用的计算函数函数名不同,函数返回类型和函数参数相同,把计算函数写成函数指针作为calc函数的参数,即是回调函数的应用。如下:

//头文件
#include<stdio.h>
#include<windows.h>//预处理,对Sleep的声明
#include<stdlib.h>//预处理,对system的声明
//自定义函数-实现打印菜单
void menu()
{
  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选择语句-表达式与case标签后的常量相等,就执行该case后的语句
    switch (input)
    {
    case 1:
      calc(Add);//传Add函数的地址
      break;//实现分支
    case 2:
      calc(Sub);
      break;
    case 3:
      cals(Mul);
      break;
    case 4:
      calc(Div);
      break;
    case 0:
      printf("退出计算器\n");
      break;
    //都不符合,执行default语句
    default:
      printf("选择错误!\n");
      break;//写不写均可
    }
    //每次执行完一次计算器清屏
    Sleep(2000);//Sleep函数是实现睡眠,单位是毫秒
    system("cls");//system是一个库函数,可以执行系统命令,cls是清屏的命令
  } while (input);
  return 0;
}

例子2:使用回调函数,模拟实现qsort(采用冒泡的方式)

qsort回调函数经典的例子。

我们先回顾一下冒泡排序:

代码1:对整数数组冒泡排序


总结:冒泡排序

①冒泡排序:我们首先确定趟数,再确定一趟冒泡排序的过程。

②冒泡排序,趟数控制了一趟冒泡排序要进行多少对比较。

③有n个元素,要进行i(i=n-1)趟冒泡,一趟比较i-1对元素。

#include<stdio.h>
//自定义函数——实现对整数数组的冒泡排序
void bubble_sort(int arr[], int sz)
{
  //趟数
  int i = 0;
  for (i = 0; i < sz - 1; i++)
  {
    //一趟冒泡排序的过程
    int j = 0;
    for (j = 0; j < sz - 1 - i; j++)
    {
      //升序
      if (arr[j] > arr[j + 1])
      {
        int tmp = arr[j];
        arr[j] = arr[j + 1];
        arr[j + 1] = tmp;
      }
    }
  }
}
int main()
{
  //对数组进行排序,升序
  int arr[] = { 9, 8, 7, 6, 5, 4, 3, 2, 1, 0 };
  //计算数组的元素个数
  int sz = sizeof(arr) / sizeof(arr[0]);
  //调用冒泡排序函数,实现对数组排序,升序
  bubble_sort(arr, sz);
  //输出
  int i = 0;
  for (i = 0; i < sz; i++)
  {
    printf("%d ", arr[i]);
  }
  printf("\n");
  return 0;
}

因为函数的参数类型已经固定为int了,所以只能排序整数数组。

如果现在我们要比较字符、浮点型、结构体数组,那怎么办呢?


答案是:我们发现冒泡排序进行排序的趟数和每一趟比较的元素对数是不变的,但是不同类型的比较方式不一样,交换的方式不一样;那想比较任意类型的数据,最好是不是把比较两个元素的方法抽离出来成为一个独立的部分(函数)就可以了。


我们模拟在C语言的库函数中有一个排序函数,qsort。qsort是通过快排来实现的,后期会在算法中讲解,今天就先来了解qsort的函数原型和怎么使用即可。

//qsort函数原型:

// void qsort(void* base,

//        size_t num,

//        size_t width,

//        int(__cdecl* compare)(const void*elem1, const void*elem2)

//            );

// 我们简化一下:

//void qsort(void* base,//指向要排序的数组的第一个元素的地址,即待排序数组的起始地址

//        size_t num,//由base指向的数组中元素的个数

//        size_t width,//数组中每个元素的大小(以字节为单位),即一个元素几个字节

//        int(* cmp)(const void*e1, const void*e2)//函数指针,指向两个元素的比较函数

//            );

//对int(* cmp)(const void*e1, const void*e2)函数指针的解读:

//    (1)cmp——指向一个比较函数(自己设计该函数的比较方式),

//            注意该函数的两个形参必须是const void*型

//            同时再调该函数时,传入的实参也必须转换成const void*型。

//            在该函数内部会将const void*型转换成实际类型

// (2)我们发现参数的类型是const void*:

//        ①const——e1不能修改

//        ②void*——设计qsort函数的设计者,不知道你要比较什么类型的元素类型

//                    所以设计成void*,void*是无具体类型指针

//                    void*的好处:可以存储任意类型的地址

//                    void的坏处:不能直接使用,因为无具体类型,自己都不知道是什么类型

//                        那怎么使用void*呢?

//                        答案是:强制类型转换,在设计比较函数时

//                            我们自己知道要进行排序的时候,是什么类型的数据排序,

//    (3)e1——你要比较的两个元素的第1个元素的地址

//    (4)e2——你要比较的两个元素的另一个元素的地址

//    (5)返回类型:

//            ①如果e1>e2,返回>0,那么e1所指向元素会排在e2所指向元素的右面

//            ②如果e1=e2,返回=0,那么e1和e2所指向元素的顺序不确定

//            ③如果e1


首先我们演示几个qsort函数的使用:

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
//我们自己知道是什么类型的数据进行排序,
//自定义函数—— 实现对整数的比较,
int cmp_int(const void* e1, const void* e2)
{
  return *(int*)e1 - *(int*)e2;//升序e1-e2,反之降序e2-e1
}
//①使用qsort对整形数组的排序,升序
void test1()
{
  //定义整形数组,并初始化
  int arr[] = { 9, 8, 7, 6, 5, 4, 3, 2, 1, 0 };
  //计算数组的大小
  int sz = sizeof(arr) / sizeof(arr[0]);
  //调用qsort函数,对数组进行升序
  qsort(arr, sz, sizeof(arr[0]), cmp_int);
  //打印升序后的数组
  int i = 0;
  for (i = 0; i < sz; i++)
  {
    printf("%d ", arr[i]);
  }
  //打印完换行
  printf("\n");
}
//创建简单的学生结构体
struct stu
{
  char name[20];//名字
  int age;//年龄
};
//比较学生,复杂的对象,我们要按照具体的方式进行比较,如该结构体
// ①按照学生的年龄来排序——注意返回类型和参数要与qsort的一致
int cmp_stu_by_age(const void* e1, const void* e2)
{
  return ((struct stu*)e1)->age - ((struct stu*)e2)->age;
}
//②按照学生的名字来排序
int cmp_stu_by_name(const void* e1, const void* e2)
{
  return strcmp(((struct stu*)e1)->name, ((struct stu*)e2)->name);
}
//使用qsort,对结构体数组进行升序
void test2()
{
  //创建结构体数组,并赋初值
  struct stu s[3] = { {"zhangsan",17},{"lisi",18},{"wangwu",19} };
  //调用qsort,
  //①按照学生年龄来升序
  qsort(s, 3, sizeof(s[0]), cmp_stu_by_age);
  //②按照学生名字来升序
  //qsort(s, 3, sizeof(s[0]), cmp_stu_by_name);
  int i = 0;
  for (i = 0; i < 3; i++)
  {
    printf("%s %d\n", s[i].name, s[i].age);
  }
}
int main()
{
  //调用test1测试
  test1();
  test2();
  return 0;
}

有了以上的基础,现在开始用冒泡排序模拟qsort。

1、函数参数的设计:


void bubble_sort(void* base,//为了接收任意类型的地址,所以是void*

   size_t num,//排序数组,要知道元素个数

   size_t width,//第一个参数的类型是void*不知道元素类型,

                //想要知道跳过一个元素几个字节,就需要知道一个元素类型几个字节

   int(*cmp)(const void* e1, const void* e2)//因为不同类型的数据比较方式不同,所以把它

                   //抽离出来,并且参数类型为void*——函数指针指向一个比较函数,

                 //使用函数指针调用该函数

           )

//自定义函数——实现对任意类型数据的交换
void Swap(char*buf1,char*buf2,int width)
{
  int i = 0;
  for (i = 0; i < width; i++)
  {
    char tmp = *buf1;
    *buf1 = *buf2;
    *buf2 = tmp;
    buf1++;
    buf2++;
  }
}
//改造冒泡排序函数,使得这个函数可以排序任意类型的数组
void bubble_sort(void* base, size_t num, size_t width, int(*cmp)(const void* e1, const void* e2))
{
  //趟数
  size_t i = 0;
  for (i = 0; i < num - 1; i++)
  {
    //一趟冒泡排序的过程
    size_t j = 0;
    for (j = 0; j < num - 1 - i; j++)
    {
      //调用cmp所指向的比较函数
      if (cmp((char*)base+j*width,(char*)base+(j+1)*width) > 0)
      {
        //交换
        //元素类型不知道,我们就一个字节一个字节的交换,所以传参时也要传width
        Swap((char*)base + j * width, (char*)base + (j + 1) * width, width);
      }
    }
  }
}

如上就是使用冒泡排序模拟qsort函数的函数,可以实现对任意类型数组的排序。

总结:如果你把函数的指针(地址)作为参数传递给另一个函数,当这个指针被用来调用其所指向的函数时,我们就说这是回调函数。


本次知识点总结:

6.  函数指针数组——存放函数指针(地址)的数组

(1)定义:

               ①数组名先和[]结合,说明它是一个数组;

               ②然后数组名和数组大小移到函数指针的*的后面

(2)用途:转移表——数组有跳转的含义:你给我一个下标,我通过下标找到数组里某个函数的地址,然后去调用函数,所以函数指针的用途叫转移表。

7.指向函数指针数组的指针

       解读:指针,指针指向数组,数组的元素都是函数指针。

小结:函数指针数组是在函数指针的基础上书写的,函数指针数组的指针是在函数指针数组的基础上书写的,只需注意谁先与谁结合,变量先与*结合就是指针,先与[]结合的是数组。

8.回调函数

       ①回调函数就是一个通过函数指针调用的函数。

       ②经典例子:qsort函数

加油站:void*

       ①void*的好处:可以存储任意类型的指针(地址)

       ②void*的坏处:不能直接使用,只用先强制转换为一个明确的类型,才能使用。


指针进阶的主题的知识点就完结了,在下一次文章进行指针进阶的一些笔试题讲解。

有什么不足希望大家指出,我会更加努力写出更好的文章。

相关文章
|
SQL 分布式计算 数据处理
如何充分发挥 SQL 能力?
如何充分发挥 SQL 能力,是本篇文章的主题。本文尝试独辟蹊径,强调通过灵活的、发散性的数据处理思维,就可以用最基础的语法,解决复杂的数据场景。
158256 59
Vue3,setup的使用需要搭配return进行使用,Vue3中带setup的script的标签和不带能不能合并到一起,export default不能放到setup里会报错,script
Vue3,setup的使用需要搭配return进行使用,Vue3中带setup的script的标签和不带能不能合并到一起,export default不能放到setup里会报错,script
|
7月前
|
机器学习/深度学习 算法 Serverless
基于Itô扩散过程的交易策略偏微分方程matlab求解与仿真
本程序基于Itô扩散过程的交易策略偏微分方程,确定了Itô扩散过程,并推导出交易长度的分布和密度函数,计算预期交易频率。核心代码在MATLAB2022A上运行,展示了交易策略的概率分布及卷积结果。算法原理涉及金融衍生品定价与风险管理,利用随机微分方程建模资产价格动态,求解相关偏微分方程以确定最优交易策略。
|
7月前
|
存储 Java 关系型数据库
ssm064农产品仓库管理系统系统(文档+源码)_kaic
农产品仓库管理系统基于现代经济快速发展和信息化技术的升级,采用SSM框架、Java语言及Mysql数据库开发。系统旨在帮助管理者高效处理大量数据信息,提升事务处理效率,实现数据管理的科学化与规范化。该系统涵盖物资基础数据管理、出入库订单管理等功能,界面简洁美观,符合用户操作习惯,并提供数据安全解决方案,确保信息的安全性和可靠性。通过自动化和集中处理,系统显著提高了仓库管理的效率和准确性。
|
10月前
|
前端开发 安全 JavaScript
在阿里云快速启动Appsmith搭建前端页面
本文介绍了Appsmith的基本信息,并通过阿里云计算巢完成了Appsmith的快速部署,使用者不需要自己下载代码,不需要自己安装复杂的依赖,不需要了解底层技术,只需要在控制台图形界面点击几下鼠标就可以快速部署并启动Appsmith,非技术同学也能轻松搞定。
|
机器学习/深度学习 自然语言处理 数据挖掘
机器学习不再是梦!PyTorch助你轻松驾驭复杂数据分析场景
【7月更文挑战第31天】机器学习已深深嵌入日常生活,从智能推荐到自动驾驶皆为其应用。PyTorch作为一个开源库,凭借简洁API、动态计算图及GPU加速能力,降低了学习门槛并提高了开发效率。通过一个使用PyTorch构建简单CNN识别MNIST手写数字的例子,展现了如何快速搭建神经网络。随着技能提升,开发者能运用PyTorch及其丰富的生态系统(如torchvision、torchtext和torchaudio)应对复杂场景,如自然语言处理和强化学习。掌握PyTorch,意味着掌握了数据时代的关键技能。
139 1
|
机器学习/深度学习 存储 算法
卷积神经网络(CNN)的数学原理解析
卷积神经网络(CNN)的数学原理解析
354 2
卷积神经网络(CNN)的数学原理解析
|
关系型数据库 MySQL 数据库
Client does not support authentication protocol requested by server; consider upgrading MySQL client
Client does not support authentication protocol requested by server; consider upgrading MySQL client
245 0
|
存储 前端开发
【大前端】用html和css写一个QQ邮箱登录页面
【大前端】用html和css写一个QQ邮箱登录页面
1102 0
【大前端】用html和css写一个QQ邮箱登录页面
|
数据安全/隐私保护
uview组件中使用MessageInput 验证码输入和Keyboard 键盘制作的简单支付密码输入框
uview组件中使用MessageInput 验证码输入和Keyboard 键盘制作的简单支付密码输入框
286 0