C语言——指针进阶终章

简介: 战至终章哈哈,加油


🚀前言

回顾我们前面学习了指针数组、数组指针以及简单介绍了函数指针,传参问题等。下面我们将在这些学习过的内容之上继续延展下去,通过这一篇博客,你可以收获更多的知识与内容,同时夯实自己的基础。本篇内容可能比较多,请耐心仔细阅读!💖


🚀有趣的代码

开始之前,基于前面的基础,我们先来看看两个有趣的代码

//代码1 
(*(void (*)())0)();
//代码2
void (*signal(int , void(*)(int)))(int);

这两个代码是什么意思呢?先想一想

代码1:

想看里面的部分void(*p)();p是函数指针,所以对于void(*)()是函数指针类型,0本身是个值,0之前放了个类型,强制类型转换,然后进行解引用。所以说代码1是一次函数调用,调用的0作为地址处的函数.

1.把0强制类型转换为:无参,返回类型是void的函数的地址

2.调用0地址处的这个函数

代码2:

signal是函数名,有两个参数,一个是整型,一个是函数指针类型,此时简单来说只剩下void(*)(int),这又是一个函数指针类型。所以说,

代码2是一次函数声明,声明的signal函数的第一个参数的类型是int,第二参数的类型是函数指针,该函数指针指向的函数参数是int,返回类型是void,signal函数的返回类型也是一个函数指针

其实这看起来是难以理解的,所以我们可以用typedf(起别名)来简化一下,更加容易认识代码:

所以,我们要学会去拆分一下代码,不会导致看不懂别人写的代码是什么意思。

我们前面学到了函数指针,但是却没有举例说到函数指针的用途,函数指针究竟能够去做些什么呢?下面,我们一起来看一看。👇


🚀简单计算器

我们将基于简单计算器这个例子来阐述函数指针的用处在于哪,或者说怎么去用上函数指针呢?我们先来简单模拟实现一下简单计算器(基于整型类型)

实现整型加减乘除的功能

#include <stdio.h>
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);
    printf("请选择2个操作数:>");
    scanf("%d %d", &x, &y);
    switch (input)
    {
    case 1:
      ret = Add(x, y);
      printf("%d\n", ret);
      break;
    case 2:
      ret = Sub(x, y);
      printf("%d\n", ret);
      break;
    case 3:
      ret = Mul(x, y);
      printf("%d\n", ret);
      break;
    case 4:
      ret = Div(x, y);
      printf("%d\n", ret);
      break;
    case 0:
      printf("退出计算器\n");
      break;
    default:
      printf("选择错误\n");
      break;
    }
  } while (input);
  return 0;
}

简单测试运行一下:

你会发现,输入0退出居然要输入两个操作数,这是为什么呢?因为在操作之前我们把输入的数放在前面了,非常的奇怪,退出前居然还要输入两个数,这时候我们稍微改进一下:

#include <stdio.h>
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 (input)
    {
    case 1:
      printf("请选择2个操作数:>");
      scanf("%d %d", &x, &y);
      ret = Add(x, y);
      printf("%d\n", ret);
      break;
    case 2:
      printf("请选择2个操作数:>");
      scanf("%d %d", &x, &y);
      ret = Sub(x, y);
      printf("%d\n", ret);
      break;
    case 3:
      printf("请选择2个操作数:>");
      scanf("%d %d", &x, &y);
      ret = Mul(x, y);
      printf("%d\n", ret);
      break;
    case 4:
      printf("请选择2个操作数:>");
      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);
  return 0;
}

稍微测试运行一下:

总算解决刚开始出现的问题了,但是这时候又有一个问题:

这一段代码太过冗余了,有点重复性,重复度太高了,我们想想办法解决?

能不能把相同的代码抽离出来,把相同的代码封装成一个函数,下面我们利用函数指针进行改进一下,避免代码的冗余

#include <stdio.h>
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("请选择2个操作数:>");
  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 (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);
  return 0;
}

简单测试运行一下:

没有任何问题,解决了代码的冗余问题。这就是函数指针的作用。

通过函数地址传递给函数参数,进入函数内部,去调用函数,这就是回调函数。后面会讲到。


🚀函数指针数组

指向函数指针数组的指针是一个 指针

开始之前,我们先来理解函数指针数组:把函数和指针放在数组中,其实就是函数指针数组,怎么理解呢?看一段代码:

怎么去调用里面的函数呢?

所以函数指针数组有什么用?怎么去用?下面来进行演示

还是刚开始冗余代码版本的计算器:

#include <stdio.h>
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 (input)
    {
    case 1:
      printf("请选择2个操作数:>");
      scanf("%d %d", &x, &y);
      ret = Add(x, y);
      printf("%d\n", ret);
      break;
    case 2:
      printf("请选择2个操作数:>");
      scanf("%d %d", &x, &y);
      ret = Sub(x, y);
      printf("%d\n", ret);
      break;
    case 3:
      printf("请选择2个操作数:>");
      scanf("%d %d", &x, &y);
      ret = Mul(x, y);
      printf("%d\n", ret);
      break;
    case 4:
      printf("请选择2个操作数:>");
      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);
  return 0;
}

我们想一下,如果要添加实现x&y,x^y,x>>y,x<<y的功能,此时就是添加4,5,6,7,8之类的选项,case的选项越来越多以此类推,代码会变得越来越长,这时候,把代码写得整洁一些:把switch语句去掉,创建一个函数指针数组存放函数,通过输入的选择作为下标去调用即可,下面我们来看看代码的修改:

#include <stdio.h>
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;
  int (*pfArr[5])(int, int) = { 0,Add, Sub,Mul,Div};
  //添加0进去,使之下标对应起来
  do
  {
    menu();
    printf("请选择:>");
    scanf("%d", &input);
    //直接让input作为下标
    if (input == 0)
    {
      printf("退出计算器");
    }
    else if (input >= 1 && input <= 4)
    {
      printf("请选择2个操作数:>");
      scanf("%d %d", &x, &y);
      ret = pfArr[input](x, y);
      printf("%d\n", ret);
    }
    else
    {
      printf("选择错误\n");
    }
  } while (input);
  return 0;
}

这样修改的好处在于,以后想添加新的功能,只需要把函数的地址放在数组里面即可,改变范围即可,稍微调整一下代码即可。通过函数指针数组便于以后修改代码。通过这个简单的例子,演示了函数指针数组的作用。下面我们来简单测试一下:

🚩指向函数指针数组的指针

在这里,顺便提一提指向函数指针数组的指针。前面我们写了函数指针数组,是数组,我们对它&,放到一个指针里面即可。

这里基于上述的函数指针数组来用代码简单表示一下:

当然,你会发现,可以套娃套下去…这里就不展开说明了


🚀回调函数

刚开始,实现计算机的时候有说到:

这就是用了回调函数的机制,什么是回调函数?

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

说到这里,太抽象了,难以理解,这时候,我们请出一个例子qsort函数的使用。

说到qsort函数,我们先来说一说冒泡排序

🚩冒泡排序优化版

void bubble_sort(int arr[], int sz)
{
  int i = 0;
  for (i = 0; i < sz - 1; i++)
  {
    int flag = 1;
    //用来判断数组是否有序,提高效率
    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;
        flag = 0;
      }
    }
    if (flag == 1)
    {
      break;
    }
  }
}
#include <stdio.h>
int main()
{
  int arr[] = { 10,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]);
  }
  return 0;
}

虽然优化了一下下,提高了效率,但是不管怎么优化,都只能排序整型数组。那怎么办呢?qsort

🚩qsort函数的使用

使用快速排序的思想实现的一个排序函数

下面,我们来简单理解一下qsort函数的参数的意思:

可以看到,比较函数有void*,所以我们很有必要来理解一下void*指针:

这时候,如果用void*来接收,就不会报警告了:

void*是无具体类型的指针,这种指针可以接收任意类型的地址,void*是无具体类型的指针,所以不能解引用操作,也不能±整数操作。这里的参数是void*的原因是因为不知道传过来的类型的指针是什么,所以定义为void*

下面我们还是通过上面的例子来对qsort函数进行简单应用:(记得引用头文件 #include <stdlib.h>)

通过qsort成功实现排序,那能不能实现降序?

我们只要通过改变e1和e2相减的位置即可实现。使之逻辑相反。这个就是回调函数来实现qsort的功能!

这里只是qsort的基本使用。

上面是利用qsort函数来排序整型的,下面我们利用qsort函数来排序结构体

通过结构体的名字进行排序:

通过结构体的年龄来进行排序:

好了,通过上面,已经对qsort有了一定的认识,并且会逐渐的运用,这时候,想想:怎么把冒泡排序改造一下

🚩冒泡排序通用版

把冒泡排序改造成类似qsort函数的实现

void Swap(char* a, char* b,int width)
{
  int i = 0;
  for (i = 0; i < width; i++)
  {
    char tmp = *a;
    *a = *b;
    *b = tmp;
    a++;
    b++;
  }
}
int cmp_int(const void* e1, const void* e2)
{
  return *(int*)e2 - *(int*)e1;
}
void bubble_sort(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 flag = 1;
    //用来判断数组是否有序,提高效率
    int j = 0;
    for (j = 0; j < sz - 1 - i; j++)
    {
      //通过利用强转base为(char*)乘以宽度的多少来进行比较
      if (cmp((char*)base+j*width,(char*)base+(j+1)*width)>0)
      {
        Swap((char*)base + j * width, ((char*)base + (j + 1) * width),width);
        flag = 0;
      }
    }
    if (flag == 1)
    {
      break;
    }
  }
}
void test()
{
  int arr[] = { 1,2,3,4,5,6,7,8,9,10 };
  int sz = sizeof(arr) / sizeof(arr[0]);
  bubble_sort(arr, sz, sizeof(arr[0]), cmp_int);
  int i = 0;
  for (i = 0; i < sz; i++)
  {
    printf("%d ", arr[i]);
  }
}
int main()
{
  test();
}

运行成功!在这里,我们稍微改造了冒泡排序,现在,来通过冒泡排序,排序结构体:

struct Stu
{
  char name[20];
  int age;
};
int cmp_stu_by_age(const void* e1, const void* e2)
{
  return ((struct Stu*)e1) ->age - ((struct Stu*)e2)->age;
}
void Swap(char* a, char* b,int width)
{
  int i = 0;
  for (i = 0; i < width; i++)
  {
    char tmp = *a;
    *a = *b;
    *b = tmp;
    a++;
    b++;
  }
}
int cmp_int(const void* e1, const void* e2)
{
  return *(int*)e2 - *(int*)e1;
}
void bubble_sort(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 flag = 1;
    //用来判断数组是否有序,提高效率
    int j = 0;
    for (j = 0; j < sz - 1 - i; j++)
    {
      //通过利用强转base为(char*)乘以宽度的多少来进行比较
      if (cmp((char*)base+j*width,(char*)base+(j+1)*width)>0)
      {
        Swap((char*)base + j * width, ((char*)base + (j + 1) * width),width);
        flag = 0;
      }
    }
    if (flag == 1)
    {
      break;
    }
  }
}
void test()
{
  int arr[] = { 1,2,3,4,5,6,7,8,9,10 };
  int sz = sizeof(arr) / sizeof(arr[0]);
  bubble_sort(arr, sz, sizeof(arr[0]), cmp_int);
  int i = 0;
  for (i = 0; i < sz; i++)
  {
    printf("%d ", arr[i]);
  }
}
void test1()
{
  struct Stu s[] = { {"zhangsan",1} ,{"lisi",2},{"wangwu",3} };
  int sz = sizeof(s) / sizeof(s[0]);
  //qsort(s, sz, sizeof(s[0]), cmp_stu_by_name);
  bubble_sort(s, sz, sizeof(s[0]), cmp_stu_by_age);
  for (int i = 0; i < sz; i++)
  {
    printf("%d\n", s[i].age);
  }
}
int main()
{
  //test();
  test1();
}

简单测试运行:

好了,关于其他排序的话这里就先不展开了,就先到这里结束了。



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