深入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;
}


相关文章
|
14天前
|
存储 NoSQL 编译器
【C语言】指针的神秘探险:从入门到精通的奇幻之旅 !
指针是一个变量,它存储另一个变量的内存地址。换句话说,指针“指向”存储在内存中的某个数据。
66 3
【C语言】指针的神秘探险:从入门到精通的奇幻之旅 !
|
14天前
|
存储 编译器 C语言
【C语言】指针大小知多少 ?一场探寻C语言深处的冒险 !
在C语言中,指针的大小(即指针变量占用的内存大小)是由计算机的体系结构(例如32位还是64位)和编译器决定的。
41 9
|
14天前
|
安全 程序员 C语言
【C语言】指针的爱恨纠葛:常量指针vs指向常量的指针
在C语言中,“常量指针”和“指向常量的指针”是两个重要的指针概念。它们在控制指针的行为和数据的可修改性方面发挥着关键作用。理解这两个概念有助于编写更安全、有效的代码。本文将深入探讨这两个概念,包括定义、语法、实际应用、复杂示例、最佳实践以及常见问题。
38 7
|
15天前
|
存储 算法 程序员
C 语言递归算法:以简洁代码驾驭复杂逻辑
C语言递归算法简介:通过简洁的代码实现复杂的逻辑处理,递归函数自我调用解决分层问题,高效而优雅。适用于树形结构遍历、数学计算等领域。
|
17天前
|
存储 程序员 编译器
C 语言数组与指针的深度剖析与应用
在C语言中,数组与指针是核心概念,二者既独立又紧密相连。数组是在连续内存中存储相同类型数据的结构,而指针则存储内存地址,二者结合可在数据处理、函数传参等方面发挥巨大作用。掌握它们的特性和关系,对于优化程序性能、灵活处理数据结构至关重要。
|
17天前
|
算法 C语言
C语言中的文件操作技巧,涵盖文件的打开与关闭、读取与写入、文件指针移动及注意事项
本文深入讲解了C语言中的文件操作技巧,涵盖文件的打开与关闭、读取与写入、文件指针移动及注意事项,通过实例演示了文件操作的基本流程,帮助读者掌握这一重要技能,提升程序开发能力。
51 3
|
16天前
|
并行计算 算法 测试技术
C语言因高效灵活被广泛应用于软件开发。本文探讨了优化C语言程序性能的策略,涵盖算法优化、代码结构优化、内存管理优化、编译器优化、数据结构优化、并行计算优化及性能测试与分析七个方面
C语言因高效灵活被广泛应用于软件开发。本文探讨了优化C语言程序性能的策略,涵盖算法优化、代码结构优化、内存管理优化、编译器优化、数据结构优化、并行计算优化及性能测试与分析七个方面,旨在通过综合策略提升程序性能,满足实际需求。
42 1
|
18天前
|
存储 算法 程序员
C 语言指针详解 —— 内存操控的魔法棒
《C 语言指针详解》深入浅出地讲解了指针的概念、使用方法及其在内存操作中的重要作用,被誉为程序员手中的“内存操控魔法棒”。本书适合C语言初学者及希望深化理解指针机制的开发者阅读。
|
17天前
|
程序员 C语言
C语言中的指针既强大又具挑战性,它像一把钥匙,开启程序世界的隐秘之门
C语言中的指针既强大又具挑战性,它像一把钥匙,开启程序世界的隐秘之门。本文深入探讨了指针的基本概念、声明方式、动态内存分配、函数参数传递、指针运算及与数组和函数的关系,强调了正确使用指针的重要性,并鼓励读者通过实践掌握这一关键技能。
32 1
|
21天前
|
存储 C语言 计算机视觉
在C语言中指针数组和数组指针在动态内存分配中的应用
在C语言中,指针数组和数组指针均可用于动态内存分配。指针数组是数组的每个元素都是指针,可用于指向多个动态分配的内存块;数组指针则指向一个数组,可动态分配和管理大型数据结构。两者结合使用,灵活高效地管理内存。