深入C语言指针,使代码更加灵活(三)

简介: 深入C语言指针,使代码更加灵活(三)

一、函数指针

1.1 函数的地址

在讲解函数指针变量之前,我们先思考一下什么是函数指针变量,我们可以同数组指针变量进行类比:

数组指针—是指针—是存放指向数组的指针,是存放数组地址的指针;
函数指针—是指针—是存放指向函数的指针,是存放函数地址的指针;

数组是有地址的,那么函数是否也有地址呢?

我们来做个测试:

#include <stdio.h>
void test()
{
 printf("hehe\n");
}
int main()
{
 printf("test: %p\n", test);
 printf("&test: %p\n", &test);
 return 0;
}


运行结果:

我们发现:确实打印出来了地址,所以函数是有地址的,并且同数组名是数组首元素地址一样,函数名也是函数的地址,我们可以通过 &函数名 的方式来获得函数的地址。


1.2 函数指针变量

如果我们要将函数的地址存放起来,就得创建函数指针变量,而函数指针变量的写法和数组指针也有许多相似之处:

函数的返回值类型(*指针名)(函数的参数类型)

#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
 
int add(int x, int y)
{
  return x + y;
}
 
int main()
{
  int (*pf)(int x, int y) = &add;
  //int——表示pf指向函数的返回类型
  //pf——函数指针变量名
  //int x, int y——pf指向函数的参数类型和个数的交代
 
  int ret = (*pf)(3, 5);
 
  printf("%d\n", ret);
  return 0;
}


运行结果为 8

1.3 函数指针的使用

 参考如下代码:

    int (*pf)(int a, int b) = &Add;
    int ret1 = (*pf)(3, 5);//相当于Add(3,5)
    int ret2 = pf(3, 5);//相当于Add(3,5)
  1. 对pf解引用相当于通过pf找到Add函数名,然后输入参数进行使用。
  2. 而我们知道&Add==Add,所以我们也能通过直接使用函数指针变量来调用函数。
  • 但是函数指针变量不能像其他指针变量进行±运算

1.4 两段有趣的代码

  • 代码1:
(*(void (*)())0)();


首先我们从里往外拆分,在这里,我们把0强制类型转换成函数指针类型,这个函数指针参数是无参,返回值类型是void,然后通过解引用去调用函数,我们可以将其简化为pf。void (*)() — 是函数指针,参数是无参,返回类型是void。

(void (*)()) — 函数指针外面加上括号,表示强制类型转换。

(*(pf)0)();//简化后

这下我们比较容易看出这段代码是先将0强制类型转换为函数指针类型,然后对其解引用。解引用之后相当于调用在0地址的函数,因为其参数为空所以只有一个单独的()。

  • 代码2:
void (*signal(int , void(*)(int)))(int);

首先signal与()结合说明其是一个函数名,它有两个参数,一个整型,另一个是函数指针类型。


我们将signal(int ,void(*)(int))单独拿出来,这段代码只剩void(*)(int),这就说明该函数的返回类型是一个函数指针,指向一个参数为int,返回为void的函数。


可能有小伙伴觉得这种写法太复杂了,想简化成下面这种形式:

void (*)(int) signal(int , void(*)(int))

很遗憾,上面这种写法是错误的

事实上,在C语言中有一个关键字叫typedef,我们可以用它来将复杂的类型简单化。


1.4.1 typedef关键字

我们可以用typedef关键字来简化signal函数:

1.  typedef void(*pfun_t)(int);//将void(*)(int)简化
2.  pfun_t signal(int, pfun_t);//化简之后


二、计算器

2.1 函数指针数组

学习了函数指针数组的创建,可能小伙伴们会想,函数指针数组到底有什么用呢?别着急,函数指针的用途可大了,比如说,我们要写一段代码来实现计算器。

我们可以采用一般写法:

#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
 
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 menu()
{
  printf("**************\n");
  printf("*****1.add****\n");
  printf("*****2.sub****\n");
  printf("*****3.mul****\n");
  printf("*****4.div****\n");
  printf("*****0.exit****\n");
  printf("**************\n");
}
int main()
{
  int x = 0;
  int y = 0;
  int ret = 0;
  int input = 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);
}

我们发现,确实能够实现计算器的加减乘除功能,但是我们也观察到,随着计算器功能增加,代码也会越来越长。显然,这样的代码显得太冗余了,我也需要对其进行改造。而要进行改造,我们就不得不利用函数指针!

改造后:

#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
 
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 menu()
{
  printf("****************\n");
  printf("*****1.add******\n");
  printf("*****2.sub******\n");
  printf("*****3.mul******\n");
  printf("*****4.div******\n");
  printf("*****0.exit*****\n");
  printf("****************\n");
}
int main()
{
  //函数指针的数组
  int(*parr[])(int, int) = { 0, add, sub, mul, div };
 
  int x = 0;
  int y = 0;
  int ret = 0;
  int input = 0;
  do{
    menu();
    printf("请选择:");
    scanf("%d", &input);
    if (input >= 1 && input <= 4)
    {
      printf("请输入两个数:");
      scanf("%d %d", &x, &y);
      ret = parr[input](x, y);
      printf("%d\n", ret);
    }
    else if (input == 0)
    {
      printf("退出计算器\n");
    }
    else
    {
      printf("选择错误,重新选择\n");
    }
  } while (input);
}


我们发现,结果依然是正确的,但是这样的代码就没有了上面那样的冗余,我们通过一个下标,在函数指针数组里面找到了一个函数的地址,然后通过这个地址去调用这个函数,直接传参得出结果,这个效率就快得多。


但这种写法也存在一定的局限性,它里面只能存放相同类型的函数,即只能计算整数,不能计算浮点数!


2.2 回调函数

看到上面我们写的第一种计算器的方式:

#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
 
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 menu()
{
  printf("**************\n");
  printf("*****1.add****\n");
  printf("*****2.sub****\n");
  printf("*****3.mul****\n");
  printf("*****4.div****\n");
  printf("*****0.exit****\n");
  printf("**************\n");
}
int main()
{
  int x = 0;
  int y = 0;
  int ret = 0;
  int input = 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);
}


我们发现代码中存在许多重复的部分那有没有什么方法来简化一下代码呢?

那就是我们今天要介绍的回调函数

那什么是回调函数呢?唉,别着急,我们还是先举一个例子,用calc函数来代替上面计算器代码中冗余的部分:

#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
 
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 menu()
{
  printf("****************\n");
  printf("*****1.add******\n");
  printf("*****2.sub******\n");
  printf("*****3.mul******\n");
  printf("*****4.div******\n");
  printf("*****0.exit*****\n");
  printf("****************\n");
}
 
void calc(int(*p)(int, int))
{
  int x = 0;
  int y = 0;
  int ret = 0;
  printf("请输入两个数:");
  scanf("%d %d", &x, &y);
  ret = p(x, y);
  printf("%d\n", ret);
}
 
int main()
{
  int x = 0;
  int y = 0;
  int ret = 0;
  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);
}

回调函数其实就是通过函数指针调用的函数!

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


简单理解就是,我们通过函数指针来调用其所指向的函数,就被称为回调函数。


三、qsort函数

3.1 qsort函数的使用

在C语言库中,有一个qsort的库函数,它可以用来排序任意类型的数据。它的详细介绍可以参考cplusplus网站:qsort,这里我们只需要掌握它的参数类型、返回值即可。


声明:void qsort(void *base, size_t nitems, size_t size, int (*compar)(const void , const void))


  1. base – 指向要排序的数组的第一个元素的指针。
  2. nitems – 由 base 指向的数组中元素的个数。
  3. size – 数组中每个元素的大小,以字节为单位。
  4. compar – 用来比较两个元素的函数。


作用:对数组元素进行排序(升序)

返回值:void


细心的小伙伴可能会发现,我们这里出现了一个新的指针类型 void*,这是究竟是一种什么类型的指针呢?


void* 也是一种指针类型,这种指针类型我们称之为通用指针类型。void* 类型的指针变量,可以接收任意类型数据的地址。既然void* 可以接收任意类型数据的地址,那么它的“大小”也是未知的,我们无法对p进行加减、解引用等常规操作。


3.1.1 qsort对整型数组的排序

#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>
 
int cmp_int(const void* e1, const void* e2)//这个函数能够比较e1和e2指向的两个元素,并给出返回值(回调函数)
{
  return *(int*)e1 - *(int*)e2;
}
 
void print_arr(int arr[], int sz)
{
  int i = 0;
  for (i = 0; i < sz; i++)
  {
    printf("%d ", arr[i]);
  }
}
 
//test1测试qsort函数排序整型数据
void test1()
{
  int arr[] = { 8, 2, 6, 4, 5, 2, 7, 1, 9 };
  int sz = sizeof(arr) / sizeof(arr[0]);
  qsort(arr, sz, sizeof(arr[0]), cmp_int);
  print_arr(arr, sz);
}
 
int main()
{
  test1();
  return 0;
}

运行结果如下:

3.1.2 qsort对结构体的排序

(一)按年龄来比较

#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>
 
struct stu
{
  char name[20];//名字
  int age;//年龄
};
 
//test2测试qsort函数排序结构体数据
int cmp_stu_by_age(const void* e1, const void* e2)
{
  return ((struct stu *)e1)->age - ((struct stu *)e2)->age;
}
 
void print(struct stu* s, int sz)
{
  int i = 0;
  for (i = 0; i < sz; i++)
  {
    printf("%s %d\n", s[i].name, s[i].age);
  }
}
 
void test2()
{
  struct stu s[] = { { "zhangsan", 20 }, { "lisi", 30 }, { "wangwu", 15 } };
  int sz = sizeof(s) / sizeof(s[0]);
  qsort(s, sz, sizeof(s[0]), cmp_stu_by_age);
  print(s, sz);
}
 
int main()
{
  test2();
  return 0;
}


(二) 按名字来比较(ASCII码)

#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
 
struct stu
{
  char name[20];//名字
  int age;//年龄
};
 
//test2测试qsort函数排序结构体数据
int cmp_stu_by_name(const void* e1, const void* e2)
{
  return strcmp(((struct stu *)e1)->name, ((struct stu *)e2)->name);
}
 
void print(struct stu* s, int sz)
{
  int i = 0;
  for (i = 0; i < sz; i++)
  {
    printf("%s %d\n", s[i].name, s[i].age);
  }
}
 
void test2()
{
  struct stu s[] = { { "zhangsan", 10 }, { "lisi", 30 }, { "wangwu", 15 } };
  int sz = sizeof(s) / sizeof(s[0]);
  qsort(s, sz, sizeof(s[0]), cmp_stu_by_name);
  print(s, sz);
}
 
int main()
{
  test2();
  return 0;
}


3.2 模拟实现qsort函数

讲解完qsort函数的使用,相信小伙伴们对其也有了一定的理解,但仅仅学会用是远远不够的,想要彻底地掌握,我还必须明白它的底层逻辑,这就需要我们去模拟实现qsort函数。


首先我们要理解qsort函数是怎么进行排序的,qsort函数排序和我们之前学过的冒泡排序有类似之处,冒泡排序也是通过比较两个元素大小来确定谁在前、谁在后。但是冒泡排序仅限于比较整型元素,不能对各种类型的变量进行排序,因此,我们可以把qsort函数看作是冒泡排序的一种拓展。


代码示例如下:

#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
 
 
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++;
  }
}
 
 
bublle_arr(void* base, int sz, int width, int(*cmp)(const void* e1, const void* e2))
{
  int i = 0;
  for (i = 0; i < sz - 1; i++)
  {
    int j = 0;
    for (j = 0; j < sz - 1 - i; j++)
    {
      if (cmp((char*)base + j*width, (char*)base + (j + 1)*width) > 0)
      {
        Swap((char*)base + j*width, (char*)base + (j + 1)*width, width);
      }
    }
  }
}
 
 
int cmp_int(const void* e1, const void* e2)
{
  return (*(int*)e1) - (*(int*)e2);
}
 
void test1()//排序整型类型数据
{
  int arr[] = { 7, 5, 3, 6, 9, 8, 1, 2, 0 };
  int sz = sizeof(arr) / sizeof(arr[0]);
  bublle_arr(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;
};
 
int cmp_name(const void* e1, const void* e2)
{
  return strcmp(((struct stu*)e1)->name, ((struct stu*)e2)->name);
}
 
 
void test2()//排序结构体数据
{
  struct stu s[] = { { "zhangsan", 33 }, { "lisi", 45 }, { "wangwu", 25 } };
  int sz = sizeof(s) / sizeof(s[0]);
  bublle_arr(s, sz, sizeof(s[0]), cmp_name);
  int i = 0;
  for (i = 0; i < sz; i++)
  {
    printf("%s %d", s[i].name, s[i].age);
    printf("\n");
  }
}
 
int main()
{
  test1();
  test2();
  return 0;
}


相关文章
|
2天前
|
存储 缓存 关系型数据库
MySQL事务日志-Redo Log工作原理分析
事务的隔离性和原子性分别通过锁和事务日志实现,而持久性则依赖于事务日志中的`Redo Log`。在MySQL中,`Redo Log`确保已提交事务的数据能持久保存,即使系统崩溃也能通过重做日志恢复数据。其工作原理是记录数据在内存中的更改,待事务提交时写入磁盘。此外,`Redo Log`采用简单的物理日志格式和高效的顺序IO,确保快速提交。通过不同的落盘策略,可在性能和安全性之间做出权衡。
1519 4
|
29天前
|
弹性计算 人工智能 架构师
阿里云携手Altair共拓云上工业仿真新机遇
2024年9月12日,「2024 Altair 技术大会杭州站」成功召开,阿里云弹性计算产品运营与生态负责人何川,与Altair中国技术总监赵阳在会上联合发布了最新的“云上CAE一体机”。
阿里云携手Altair共拓云上工业仿真新机遇
|
5天前
|
人工智能 Rust Java
10月更文挑战赛火热启动,坚持热爱坚持创作!
开发者社区10月更文挑战,寻找热爱技术内容创作的你,欢迎来创作!
503 19
|
2天前
|
存储 SQL 关系型数据库
彻底搞懂InnoDB的MVCC多版本并发控制
本文详细介绍了InnoDB存储引擎中的两种并发控制方法:MVCC(多版本并发控制)和LBCC(基于锁的并发控制)。MVCC通过记录版本信息和使用快照读取机制,实现了高并发下的读写操作,而LBCC则通过加锁机制控制并发访问。文章深入探讨了MVCC的工作原理,包括插入、删除、修改流程及查询过程中的快照读取机制。通过多个案例演示了不同隔离级别下MVCC的具体表现,并解释了事务ID的分配和管理方式。最后,对比了四种隔离级别的性能特点,帮助读者理解如何根据具体需求选择合适的隔离级别以优化数据库性能。
179 1
|
8天前
|
JSON 自然语言处理 数据管理
阿里云百炼产品月刊【2024年9月】
阿里云百炼产品月刊【2024年9月】,涵盖本月产品和功能发布、活动,应用实践等内容,帮助您快速了解阿里云百炼产品的最新动态。
阿里云百炼产品月刊【2024年9月】
|
21天前
|
存储 关系型数据库 分布式数据库
GraphRAG:基于PolarDB+通义千问+LangChain的知识图谱+大模型最佳实践
本文介绍了如何使用PolarDB、通义千问和LangChain搭建GraphRAG系统,结合知识图谱和向量检索提升问答质量。通过实例展示了单独使用向量检索和图检索的局限性,并通过图+向量联合搜索增强了问答准确性。PolarDB支持AGE图引擎和pgvector插件,实现图数据和向量数据的统一存储与检索,提升了RAG系统的性能和效果。
|
9天前
|
Linux 虚拟化 开发者
一键将CentOs的yum源更换为国内阿里yum源
一键将CentOs的yum源更换为国内阿里yum源
457 5
|
7天前
|
存储 人工智能 搜索推荐
数据治理,是时候打破刻板印象了
瓴羊智能数据建设与治理产品Datapin全面升级,可演进扩展的数据架构体系为企业数据治理预留发展空间,推出敏捷版用以解决企业数据量不大但需构建数据的场景问题,基于大模型打造的DataAgent更是为企业用好数据资产提供了便利。
314 2
|
23天前
|
人工智能 IDE 程序员
期盼已久!通义灵码 AI 程序员开启邀测,全流程开发仅用几分钟
在云栖大会上,阿里云云原生应用平台负责人丁宇宣布,「通义灵码」完成全面升级,并正式发布 AI 程序员。
|
25天前
|
机器学习/深度学习 算法 大数据
【BetterBench博士】2024 “华为杯”第二十一届中国研究生数学建模竞赛 选题分析
2024“华为杯”数学建模竞赛,对ABCDEF每个题进行详细的分析,涵盖风电场功率优化、WLAN网络吞吐量、磁性元件损耗建模、地理环境问题、高速公路应急车道启用和X射线脉冲星建模等多领域问题,解析了问题类型、专业和技能的需要。
2608 22
【BetterBench博士】2024 “华为杯”第二十一届中国研究生数学建模竞赛 选题分析