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


相关文章
|
18天前
|
C语言
【c语言】指针就该这么学(1)
本文详细介绍了C语言中的指针概念及其基本操作。首先通过生活中的例子解释了指针的概念,即内存地址。接着,文章逐步讲解了指针变量的定义、取地址操作符`&`、解引用操作符`*`、指针变量的大小以及不同类型的指针变量的意义。此外,还介绍了`const`修饰符在指针中的应用,指针的运算(包括指针加减整数、指针相减和指针的大小比较),以及野指针的概念和如何规避野指针。最后,通过具体的代码示例帮助读者更好地理解和掌握指针的使用方法。
44 0
|
17天前
|
C语言
【c语言】指针就该这么学(3)
本文介绍了C语言中的函数指针、typedef关键字及函数指针数组的概念与应用。首先讲解了函数指针的创建与使用,接着通过typedef简化复杂类型定义,最后探讨了函数指针数组及其在转移表中的应用,通过实例展示了如何利用这些特性实现更简洁高效的代码。
13 2
|
18天前
|
C语言
如何避免 C 语言中的野指针问题?
在C语言中,野指针是指向未知内存地址的指针,可能引发程序崩溃或数据损坏。避免野指针的方法包括:初始化指针为NULL、使用完毕后将指针置为NULL、检查指针是否为空以及合理管理动态分配的内存。
|
18天前
|
C语言
C语言:哪些情况下会出现野指针
C语言中,野指针是指指向未知地址的指针,通常由以下情况产生:1) 指针被声明但未初始化;2) 指针指向的内存已被释放或重新分配;3) 指针指向局部变量,而该变量已超出作用域。使用野指针可能导致程序崩溃或不可预测的行为。
|
24天前
|
存储 C语言
C语言32位或64位平台下指针的大小
在32位平台上,C语言中指针的大小通常为4字节;而在64位平台上,指针的大小通常为8字节。这反映了不同平台对内存地址空间的不同处理方式。
|
24天前
|
存储 算法 C语言
C语言:什么是指针数组,它有什么用
指针数组是C语言中一种特殊的数据结构,每个元素都是一个指针。它用于存储多个内存地址,方便对多个变量或数组进行操作,常用于字符串处理、动态内存分配等场景。
|
24天前
|
存储 C语言
C语言指针与指针变量的区别指针
指针是C语言中的重要概念,用于存储内存地址。指针变量是一种特殊的变量,用于存放其他变量的内存地址,通过指针可以间接访问和修改该变量的值。指针与指针变量的主要区别在于:指针是一个泛指的概念,而指针变量是具体的实现形式。
|
25天前
|
C语言
C语言指针(3)
C语言指针(3)
11 1
|
25天前
|
C语言
C语言指针(2)
C语言指针(2)
13 1
|
17天前
|
编译器 C语言
【c语言】指针就该这么学(2)
本文详细介绍了指针与数组的关系,包括指针访问数组、一维数组传参、二级指针、指针数组和数组指针等内容。通过具体代码示例,解释了数组名作为首元素地址的用法,以及如何使用指针数组模拟二维数组和传递二维数组。文章还强调了数组指针与指针数组的区别,并通过调试窗口展示了不同类型指针的差异。最后,总结了指针在数组操作中的重要性和应用场景。
15 0