C语言指针进阶(中)

简介: C语言指针进阶(中)

前言

七、指向函数指针数组的指针

含义:🐇

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

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

区分函数指针,函数指针数组,指向函数指针数组的指针

int Add(int x, int y) {
  return x + y;
}
int Sub(int x, int y) {
  return x - y;
}
int main() {
  int (*pf)(int, int) = Add;  //函数指针变量
  int (*pfArr[4])(int, int) = { Add ,Sub};  //函数指针数组
  //pfArr是数组名,&pfArr是取出数组的地址,数组的地址应该放到数组指针里
  int (*(*ppfArr)[4])(int,int)= &pfArr; //ppfArr是一个指向函数指针数组的指针变量,把指针和指向的四个元素(*ppfArr[4])去掉,剩下的是它的类型
  return 0;
}

图解:

八、回调函数

含义:🐇

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

通俗来讲:🐻‍❄️把一个函数指针(用于接收A函数的地址值)作为形式参数传递给B函数,在B函数内部通过函数指针来调用A函数.A就被称为回调函数.

图解

1.举例说明:

在上篇博客中最后的计算机题目中,我们发现有很多代码其实是冗余的,如何简化呢?

(1)简化思路图解:

(2)代码:

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);
    //只有输入1234的时候才需要打印结果
    //为了避免无关紧要的一些结果的输出
    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;
}

(3)代码逻辑图:

在这道题目中,没有直接调用加减乘除的函数,而是在特定的事件下去调用函数,用来响应这一事件.

(4)对比上篇博客中主函数的简化代码与本篇中的主函数简化代码有什么区别?

解决的问题不一样,没有哪个比哪个好的说法

上篇:当在计算机中不断增加运算的功能的时候,觉得swith…case语句中的case太多,用函数指针数组更好一些

本篇:解决的问题是如果使用原来的代码有些代码是重复冗余的,原来的代码如下图所见:

本篇是为了解决代码的冗余,使用回调函数,当特定事件结果为不同值时调用函数,形参传入不同的函数地址.

2.演示qsort函数

qsort是一个库函数,用来排序的库函数

底层用的是快速排序的方法,不是冒泡排序

q:quick quicksort

为什么是快速排序的方法不是冒泡排序的方法呢?

接下来我们来体验一下冒泡排序有哪里不好的地方🥱🥱

(1)复习冒泡排序

冒泡排序的思想分析:🐇🐇

两两相邻的元素比较.

void bubble_sort(int arr[], int sz) {
  int i = 0;
  for (i = 0; i < sz-1; i++) {  //循环sz-1趟冒泡排序
    //一趟冒泡排序的过程
    int j = 0;
    for (j = 0; j < sz-1-i; j++) {   //每趟冒泡排序:第一趟九对进行比较,第二趟八对进行比较,以此类推,这里的j应该是<sz-1-i
      if (arr[j] > arr[j + 1]) {
        int temp = 0;
        temp = arr[j];
        arr[j] = arr[j + 1];
        arr[j + 1] = temp;
      }
    }
  }
}
void print_arr(int arr[],int sz) {
  int i = 0;
  for (i = 0; i < sz; i++) {
    printf("%d ", arr[i]);
  }
}
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);
  //打印
  print_arr(arr,sz);
  return 0;
}

冒泡排序有哪里不好呢?🐇

:只能排固定类型的数据,不是想排什么类型的数据就排序什么类型的数据

因为函数void bubble_sort(int arr[], int sz)的形式参数固定写死的,未来想要排序一些浮点型数据,结构体类型的数据等等是不行的

(2)qsort函数的好处

qsort函数的好处:🤞

1.现成的

2.可以排序任意类型的数据

I.认识一下qsort函数的形式参数

void qsort(void* base,  //指向了待排序数组的第一个元素
  size_t num,         //待排序的元素个数
  size_t size,    //每个元素的大小,单位是字节
  int (*compar)(const void*, const void*)   //指向一个函数,这个函数可以比较两个元素的大小.(这个形式参数是函数指针)
  //const void*, const void*是待比较的两个元素的地址
);

II.qsort可以排序任意类型的数据

1.比较2个整数的大小 > < ==

2.比较2个字符串,strcmp

3.比较2个结构体数据(学生,张三,李四) 指定比较的标准,拿什么比较

假如用冒泡排序,来排序整型数据,字符型数据,结构体数据

需要变的是比较方法和交换方式

III.注意qsort参数中void*的用法

(1)void* 就是无具体类型的指针,可以接收任意类型的指针.

(2)有时候不知道别人给传一个什么类型的地址的时候,我又要把它存起来,这时要创建一个void指针接收地址
(3)void
的指针不能解引用操作

(4)void的指针不能进行++或者–,因为他不知道跳过几个字节
(5)可以将void
类型的指针进行强制转换再解引用

IV.qsort函数的头文件

#include<stdlib.h>

V.测试qsort排序整型数据(qsort默认是升序排序)
#include<stdlib.h>  //qsort函数的头文件
#include<stdio.h>
//cmp_int是qsort函数的使用者提供这个函数
int cmp_int(const void* p1, const void* p2) {
  return *(int*)p1 - *(int*)p2;
}
//打印
void print_arr(int arr[],int sz) {
  int i = 0;
  for (i = 0; i < sz; i++) {
    printf("%d ", arr[i]);
  }
  printf("\n");
}
void test1() {
  int arr[] = { 9,8,7,6,5,4,3,2,1,0 };
  int sz = sizeof(arr) / sizeof(arr[0]);
  //使用qsort来排序整型数组,这里要提供一个比较函数,这个比较函数能够比较两个整数的大小
  //qsort是默认排序成升序的,如果想要排成降序,将cmp_int函数里面的return *(int*)p1 - *(int*)p2;改为return *(int*)p2 - *(int*)p1;
  qsort(arr, sz,sizeof(arr[0]),cmp_int);//数组名表示首元素第一个地址
  print_arr(arr, sz);
}
int main() {
  test1();
  return 0;
}

代码详解图:

VI.测试qsort排序结构型数据
(1)按照年龄来排序
//打印
void print_arr(int arr[], int sz) {
  int i = 0;
  for (i = 0; i < sz; i++) {
    printf("%d ", arr[i]);
  }
  printf("\n");
}
struct Stu {
  char name[20];
  int age;
};
//按照年龄来比较
int cmp_stu_by_age(const void* p1, const void* p2) {  //p1,p2分别指向结构体数据
  return ((struct Stu*)p1)->age - ((struct Stu*)p2)->age;//将void*类型的指针转换成结构体指针,但是是临时的,所以要加上()再->访问结构体成员
                              //p1指向的元素等于p2指向的元素,返回0;p1指向的元素<p2指向的元素,这里返回<0的数字;p1指向的元素>p2指向的元素,这里返回>0的数字
}
test2() {
  struct Stu s[] = { {"zhangsan",30},{"lisi",25},{"wangwu",50} };
  int sz = sizeof(s) / sizeof(s[0]);
  qsort(s, sz, sizeof(s[0]), cmp_stu_by_age);
  print_arr(s,sz);
}
int main() {
  test2();
  return 0;
}

执行qsort函数前,结构体数组元素分别为:

执行qsort函数后,结构体数组元素分别为:

(2)按照名字来排序

strcmp的用法:👻👻

阅读可以发现:

当str1等于str2,返回0;当str1大于st2,返回大于0的数字;当str1小于str2,返回小于0的数字.

需要头文件#include<string.h>

在比较的时候,先比较首字母,一样的话比较下一个字母,直到比到不相等或者全部比完

代码展示:

#include<string.h>  //strcmp头文件
#include<stdlib.h>  //qsort函数的头文件
#include<stdio.h>
//打印
void print_arr(int arr[], int sz) {
  int i = 0;
  for (i = 0; i < sz; i++) {
    printf("%d ", arr[i]);
  }
  printf("\n");
}
//测试qsort排序结构体数据
struct Stu {
  char name[20];
  int age;
};
//按照名字来排序(注意两个名字不能相减,要用strcmp比较)
int cmp_stu_by_name(const void* p1, const void* p2) {  //p1,p2分别指向结构体数据
  return strcmp(((struct Stu*)p1)->name ,((struct Stu*)p2)->name);//将void*类型的指针转换成结构体指针,但是是临时的,所以要加上()再->访问结构体成员
  test2() {
  struct Stu s[] = { {"zhangsan",30},{"lisi",25},{"wangwu",50} };
  int sz = sizeof(s) / sizeof(s[0]);
  //测试按照名字来排序
  qsort(s, sz, sizeof(s[0]), cmp_stu_by_name);
  print_arr(s,sz);
}
int main() {
  test2();
  return 0;
}
}

排序前:

排序后:

3.模拟实现qsort功能

用冒泡排序模拟一下`qsort怎么用

但是我们没有学快速排序的思想,所以我们使用冒泡排序的思想来实现一个类似于qsort这个功能的冒泡排序bubble_sort()

🐇🐇🐇注意qsort底层是快速排序,这里只是模拟一下这个函数的功能

(1)排序整型数据
拆分各部分讲解

test3:

I.首先准备一个数组

II.求元素个数

III.调用:用冒泡排序模拟qsort函数的bubble_sort(待排序数组的第一个元素,待排序数组的元素个数,每个元素的大小,使用者提供的比较大小的函数的地址)

IV.打印

bubble_sort:

I.形式参数(voidbase指向起始位置,size_t num元素的个数,size_t width 宽度这里是4个字节,int (cmp)(const void p1, const void p2)用函数指针来接收使用者创建的比较函数)

用函数指针调用cmp,这里的cmp实质上是一个回调函数

II.内容:

✨确定冒泡排序的趟数

✨一趟内部进行多少对比较

✨比较的时候用使用者创建的比较函数比,将要比较的两个元素的地址传进来

元素的地址怎么表示?

(char*)base + j * width

什么样的元素+1跳过一个字节? char*

所以把base强制转换成char类型的
起始位置base转换为char
指针

跳过(j+1)个元素,每个元素width这么宽,所以跳过的总字节是(j+1)* width个字节,在起始地址的基础上,所以是(char*)base + j * width

✨交换:创建一个交换函数(将这两个元素的起始地址传进来)

参数:创建一个字符指针接收第一个元素的第一个字节char* buf1,创建一个字符指针接收第二个元素的第一个字节char* buf2,创建一个宽度变量来接收一个元素总共有多少个字节int width

交换的时候一个字节一个字节的交换

用width控制交换多少遍,在循环内部创建临时变量tmp作为中间变量与buf1,buf2进行交换

比较函数:(下图所示默认为升序)

如果是降序排列,将p1,p2互换

交换函数:

主函数:

完整代码展示
//打印
void print_arr(int arr[], int sz) {
  int i = 0;
  for (i = 0; i < sz; i++) {
    printf("%d ", arr[i]);
  }
}
//比较函数
int cmp_int(const void* p1, const void* p2)
  {
    return *(int*)p1 - *(int*)p2;
  }
//交换
void Swap(char* buf1, char* buf2, int width) {
  int i = 0;
  for (i = 0; i < width; i++) {
    char tmp = *buf1;//先把buf1内指向的第一个字节放入到tmp里面,
    *buf1 = *buf2;
    *buf2 = tmp;
    buf1++;  //找buf1的下一个字节
    buf2++;  //找buf2的下一个字节
  }
}
//模拟qsort函数的bubble_sort
void bubble_sort(void* base, size_t num, size_t width, int (*cmp)(const void* p1, const void* p2)){
  size_t i = 0;   //将int类型的i改为无符号类型,不然在下面i < num - 1处会有小问题,因为num是size_t类型,i是整型类型
  for (i = 0; i < num - 1; i++) {
    //一趟冒泡排序的过程
    size_t j = 0;
    for (j = 0; j < num - 1 - i; j++) {  //这个for循环确定一趟比多少对
    if (cmp(((char*)base + j * width), (char*)base + (j + 1) * width) > 0)    //跳过(j+1)个元素,每个元素width这么宽,所以跳过的总字节是(j+1)*width个字节,在起始地址的基础上,所以是(char*)base + j * width
      {   //前面的元素大于后面的元素
         //交换
        Swap((char*)base + j * width, (char*)base + (j + 1) * width, width);
      }
    }
  }
}
//排序一个整形数组
void test3() {
  int arr[] = { 3,4,5,6,1,2,8,7,9 };
  int sz = sizeof(arr) / sizeof(arr[0]);
  bubble_sort(arr, sz, sizeof(arr[0]), cmp_int);
  print_arr(arr, sz);
}
int main() {
  test3();
  return 0;
}
运行结果

注意点:

注意各个函数在定义时候的先后顺序,只有在前面定义了,后面才可以使用

(2)排序结构体类型数据

创建结构体:

测试按照年龄排序代码展示
//测试qsort排序结构体数据
struct stu {
  char name[20];
  int age;
};
//按照年龄来比较
int cmp_stu_by_age(const void* p1, const void* p2) {  //p1,p2分别指向结构体数据
  return ((struct stu*)p1)->age - ((struct stu*)p2)->age;//将void*类型的指针转换成结构体指针,但是是临时的,所以要加上()再->访问结构体成员
                              //p1指向的元素等于p2指向的元素,返回0;p1指向的元素<p2指向的元素,这里返回<0的数字;p1指向的元素>p2指向的元素,这里返回>0的数字
}
//交换
void Swap(char* buf1, char* buf2, int width) {
  int i = 0;
  for (i = 0; i < width; i++) {
    char tmp = *buf1;//先把buf1内指向的第一个字节放入到tmp里面,
    *buf1 = *buf2;
    *buf2 = tmp;
    buf1++;  //找buf1的下一个字节
    buf2++;  //找buf2的下一个字节
  }
}
//模拟qsort函数的bubble_sort
void bubble_sort(void* base, size_t num, size_t width, int (*cmp)(const void* p1, const void* p2)){
  size_t i = 0;   //将int类型的i改为无符号类型,不然在下面i < num - 1处会有小问题,因为num是size_t类型,i是整型类型
  for (i = 0; i < num - 1; i++) {
    //一趟冒泡排序的过程
    size_t j = 0;
    for (j = 0; j < num - 1 - i; j++) {  //这个for循环确定一趟比多少对
    if (cmp(((char*)base + j * width), (char*)base + (j + 1) * width) > 0)    //跳过(j+1)个元素,每个元素width这么宽,所以跳过的总字节是(j+1)*width个字节,在起始地址的基础上,所以是(char*)base + j * width
      {   //前面的元素大于后面的元素
         //交换
        Swap((char*)base + j * width, (char*)base + (j + 1) * width, width);
      }
    }
  }
}
//打印
void print_arr(int arr[], int sz) {
  int i = 0;
  for (i = 0; i < sz; i++) {
    printf("%d ", arr[i]);
  }
}
  int cmp_int(const void* p1, const void* p2)
  {
    return *(int*)p1 - *(int*)p2;
  }
void test4() {
  struct stu s[] = { {"zhangsan",30},{"lisi",25},{"wangwu",50} };
  int sz = sizeof(s) / sizeof(s[0]);
  //测试按照年龄来排序
  bubble_sort(s, sz, sizeof(s[0]), cmp_stu_by_age);
  print_arr(s, sz);
}
int main() {
  test4();
  return 0;
}
按年龄排序前:

按年龄排序后:

测试按照名字排序代码展示
struct stu {
  char name[20];
  int age;
};
//按照名字来排序(注意两个名字不能相减,要用strcmp比较)
int cmp_stu_by_name(const void* p1, const void* p2) {  //p1,p2分别指向结构体数据
  return strcmp(((struct stu*)p1)->name, ((struct stu*)p2)->name);//将void*类型的指针转换成结构体指针,但是是临时的,所以要加上()再->访问结构体成员
                              //p1指向的元素等于p2指向的元素,返回0;p1指向的元素<p2指向的元素,这里返回<0的数字;p1指向的元素>p2指向的元素,这里返回>0的数字
}
void Swap(char* buf1, char* buf2, int width) {
  int i = 0;
  for (i = 0; i < width; i++) {
    char tmp = *buf1;//先把buf1内指向的第一个字节放入到tmp里面,
    *buf1 = *buf2;
    *buf2 = tmp;
    buf1++;  //找buf1的下一个字节
    buf2++;  //找buf2的下一个字节
  }
}
//模拟qsort函数的bubble_sort
void bubble_sort(void* base, size_t num, size_t width, int (*cmp)(const void* p1, const void* p2)){
  size_t i = 0;   //将int类型的i改为无符号类型,不然在下面i < num - 1处会有小问题,因为num是size_t类型,i是整型类型
  for (i = 0; i < num - 1; i++) {
    //一趟冒泡排序的过程
    size_t j = 0;
    for (j = 0; j < num - 1 - i; j++) {  //这个for循环确定一趟比多少对
    if (cmp(((char*)base + j * width), (char*)base + (j + 1) * width) > 0)    //跳过(j+1)个元素,每个元素width这么宽,所以跳过的总字节是(j+1)*width个字节,在起始地址的基础上,所以是(char*)base + j * width
      {   //前面的元素大于后面的元素
         //交换
        Swap((char*)base + j * width, (char*)base + (j + 1) * width, width);
      }
    }
  }
}
//打印
void print_arr(int arr[], int sz) {
  int i = 0;
  for (i = 0; i < sz; i++) {
    printf("%d ", arr[i]);
  }
}
  int cmp_int(const void* p1, const void* p2)
  {
    return *(int*)p1 - *(int*)p2;
  }
void test4() {
  struct stu s[] = { {"zhangsan",30},{"lisi",25},{"wangwu",50} };
  int sz = sizeof(s) / sizeof(s[0]);
  //测试按照年龄来排序
  //bubble_sort(s, sz, sizeof(s[0]), cmp_stu_by_age);
  //测试按照名字来排序
  bubble_sort(s, sz, sizeof(s[0]), cmp_stu_by_name);
  print_arr(s, sz);
}
int main() {
  test4();
  return 0;
}
按名字排序前:

按名字排序后:

(3)用flag优化

✨比如说在中间某个过程冒泡排序已经得到最终排序结果,剩下的排序过程就没有必要再进行

✨这时使用flag来优化

✨假设每一趟排序已经是有序的,flag置为1,要写在循环里面,每一趟都认为是有序的

✨只要进入交换函数进行交换,flag就会被更改为0,如果这一趟里面没有进行交换,flag不会被修改,说明已经是有序的了,不需要再排序

✨判断flag的值是多少,如果是1的话,跳出循环,结束排序

注意👻👻👻

(1)以上是用冒泡排序实现了一个通用的冒泡排序,只不过是这个通用的冒泡排序与qsort的使用是一模一样的

不要认为上面的代码是用冒泡排序模拟qsort,qsort底层用的是快速排序的思想

(2)正是由于回调函数的使用,才可以用函数指针去调用函数去完成不同的功能,使得冒泡排序能够适配更多的类型,这就是回调函数的好处


总结

指针进阶(中)的内容就到这里啦,创作不易如果对友友们有帮助的话,记得点赞收藏博客,关注后续的指针进阶下集内容哦~👻👻👻

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