有希带你深入理解指针(4)

2024-09-08 02:52
文章标签 指针 深入 理解 有希带

本文主要是介绍有希带你深入理解指针(4),希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

目录

  • 前言🥰
  • 1.回调函数😺
    • 1.1回调函数的概念😋
  • 2.qsort使用🤯
    • 2.1什么是qsort👻
    • 2.2 qsort函数的使用🧐
  • 3.模拟实现qsort😎

前言🥰

本篇文章是对指针知识的进一步讲解,如果对部分知识有不了解的地方可以移步前文进行学习!😶‍🌫️
在这里插入图片描述

1.回调函数😺

1.1回调函数的概念😋

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

现在我们用一个简单的例子来理解:

#include<stdio.h>
int Add(int x, int y)
{return x + y;
}void test(int (*pf)(int, int))
{int ret = pf(4, 5);printf("%d\n", ret);
}int main()
{test(Add);return 0;
}

这里我们有一个Add函数来实现加法,还有有一个test函数,它的参数部分我设置为pf(函数指针),里面的参数我设置为int,并且返回类型设置为int,在test函数内部我利用pf去调用一个函数(Add函数),并把两个值传过去,之后能计算出一个结果(存到ret中)。
在主函数部分我们调用了test函数,并把Add函数的地址传了过去,此时pf就指向了Add函数,相当于调用了Add函数完成4和5的相加。

注意:
在主函数部分我并没有直接调用Add函数,我把Add函数的地址传递给了pf,此时pf指向的就是Add函数。当我们用pf去调用函数时,调用的就是Add函数并且可以完成相应的计算。这里的Add就可以称为回调函数。这和函数的嵌套是不同的,函数的嵌套是直接通过函数名去调用。而今天我们所讲的是通过函数指针去调用的。

2.qsort使用🤯

2.1什么是qsort👻

qsort是一个库函数,可以直接使用。它可以实现任意类型数据的排序。它的底层是快速排序的方式。它通过回调函数的方式实现对各种类型的函数的排序。

这里我们先引入冒泡排序的写法,重点是两两相邻元素进行比较。
代码演示:

#include<stdio.h>
void bubble_sort(int arr[],int sz)
{int i = 0;//趟数for (i = 0; i < sz - 1; i++){int j = 0;for (j = 0; j < sz - i - 1; j++){if (arr[j] > arr[j + 1]){int tmp = arr[j];arr[j] = arr[j + 1];arr[j + 1] = tmp;}}}
}void print_arr(int arr[], int sz)
{int i = 0;for (i = 0; i < sz; i++){printf("%d ", arr[i]);}printf("\n");
}int main()
{int arr[10] = { 3,1,9,8,5,4,0,2,7,6 };//升序int sz = sizeof(arr) / sizeof(arr[0]);print_arr(arr, sz);bubble_sort(arr, sz);print_arr(arr, sz);return 0;
}

这里我对进行冒泡排序前后的数组进行了打印比较。我们这里的bubble_sort函数只能排序整型数组,不能对其他类型的数组进行处理,具有很大的局限性。现在我们想要对函数进行改造,使它可以对其他类型的数据进行排序。
首先对于趟数,我们并不需要进行改变,我们进行改变的部分如下:
在这里插入图片描述
我们有两个需要思考的地方:

  1. 两个变量怎么比较
  2. 两个变量怎么交换

注意:

  1. 不是所有的数据都来可以用>进行比较,例如:结构体变量、字符串。
  2. 这里的临时变量也不能直接定为int
  3. 参数的类型也是有问题的,如图:
    在这里插入图片描述

此时,我们先不着急改我们的代码,我们可以先学习qsort函数的使用。

2.2 qsort函数的使用🧐

函数的原型(函数名,参数,返回类型):

void qsort (void* base, size_t num, size_t size,int (*compar)(const void*,const void*));

其中第四个参数最为复杂,是一个函数指针。
这么多的参数我们可以对比bubble_sort函数进行理解,我们肯定需要知道所要排序数据的类型等等,我们通过下图进行理解:
在这里插入图片描述

在这里插入图片描述
因为我们要对不同类型的数据进行比较,不同类型的数据的比较类型是不一样的。根据不同的数据,我们通过传不同的比较方法(自己写一个比较函数),把该比较函数的地址传进去,来实现比较。我们可以通过第四个参数传递该比较函数的地址。

现在我们要对一个整型数组arr使用qsort排序,这里我会对函数进行分装。我们需要根据qsort的参数,填写对应的内容。
对于qsort的实现与两个人有关,如图:
在这里插入图片描述
这里我们的任务是:提供一个函数,能实现两个整型元素的比较。我们需要把函数名传过去,为了实现这一点,我们写出的参数和返回类型一个和函数指针的参数和返回类型一致,才能将函数名传过去。

这里的e1和e2分别指向一个元素。void * 是无具体类型的指针,它可以存放地址,也可以指向对象。我们先进行两个整型元素的比较。我们需要进行解引用再进行整型元素的比较,如果我们采用下图,会报错。因为void * 是无具体类型指针,无法进行解引用操作。
在这里插入图片描述
注意
void* 类型的指针不能解引用操作符,也不能+/-整数的操作。这种指针变量一般是用来存放地址的。使用之前要强制类型转换成想要的类型。
现在我们进行修改:

int cmp_int(const void* e1, const void* e2) 
{if (*(int*)e1 > *(int*)e2)return 1;else if (*(int*)e1 < *(int*)e2)return -1;elsereturn 0;
}

现在,我们对该代码进行简化:

int cmp_int(const void* e1, const void* e2)
{return *(int*)e1 - *(int*)e2;
}

现在我们可以打印看看我们写的代码的结果(不要忘记头文件):

#include<stdio.h>
#include<stdlib.h>
void print_arr(int arr[], int sz)
{int i = 0;for (i = 0; i < sz; i++){printf("%d ", arr[i]);}printf("\n");
}int cmp_int(const void* e1, const void* e2)
{return *(int*)e1 - *(int*)e2;
}void test1()
{int arr[10] = { 3,1,9,8,5,4,0,2,7,6 };int sz = sizeof(arr) / sizeof(arr[0]);qsort(arr, sz, sizeof(arr[0]), cmp_int);print_arr(arr, sz);
}int main()
{test1();return 0;
}

在这里插入图片描述
此时我们可以看到是默认升序的,我们可以改为降序,改以下部分即可:

int cmp_int(const void* e1, const void* e2)
{return *(int*)e2 - *(int*)e1;
}

我们现在也可以举一些其他例子,我们现在排列一下结构体数据。我们现在假设学生有名字和年龄,如下:

struct Stu
{char name[20];//名字int age;//年龄
};

我们需要确定比较的方式,通过年龄还是名字。不能盲目的进行比较。先进行年龄的比较。此时的e1,e2分别指向结构体对象。根据上述知识,我们可以实现以下代码:

#include<stdio.h>
#include<stdlib.h>
struct Stu
{char name[20];//名字int age;//年龄
};
//cmp_stu_by_age用来比较结构体对象int cmp_stu_by_age(const void* e1, const void* e2)
{return (*(struct Stu*)e1).age - (*(struct Stu*)e2).age;
}void test2()
{struct Stu s[3] = { {"zhangsan",18},{"lisi",25},{"wangwu",28} };int sz = sizeof(s) / sizeof(s[0]);qsort(s, sz, sizeof(s[0]), cmp_stu_by_age);
}
int main()
{test2();return 0;
}

但是用名字来比较大小的时候,就不能直接减。字符串的比较要利用strcmp函数。在这里插入图片描述
代码:

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
struct Stu
{char name[20];//名字int age;//年龄
};int cmp_stu_by_name(const void* e1, const void* e2)
{return strcmp((*(struct Stu*)e1).name ,(*(struct Stu*)e2).name);
}void test2()
{struct Stu s[3] = { {"zhangsan",18},{"lisi",25},{"wangwu",28} };int sz = sizeof(s) / sizeof(s[0]);qsort(s, sz, sizeof(s[0]), cmp_stu_by_name);
}
int main()
{test2();return 0;
}

现在,我们已经会使用qsort函数了。

3.模拟实现qsort😎

现在我们接着对冒泡排序的函数进行改造。前面我说过趟数不用改,需要改的部分是比较的方法、怎么交换和参数。
我们可以先想想qsort函数为什么设计成那样?
void * base
设计为void * ,是因为我们可能排序不同类型的数据,不能写死。void * 类型的指针可以接收任意类型变量的地址。
size_t num
是base指向的数组中元素的个数。
size_t szie:
是base指向数组的一个元素的长度,单位是字节。大家可能认为这个参数是不必要的,但是它决定了在访问时,一步走多远。
int ( * compar) (const void * ,const void * )
第四个参数,是因为对于不同类型的数据比较方法不同,我们需要自己造一个函数,通过函数指针传地址过去,进行比较。

代码:

void bubble_sort(void*base, int num,int width,int(*cmp)(const void*e1, const void* e2))

前面我们在参数部分设置为size_t,这里我写了int,但是size_t的写法更好,int类型可正可负,这里的num和size并不会出现该情况,所以接下来我改为size_t。

void bubble_sort(void* base, size_t  num, size_t  width, int(*cmp)(const void* e1, const void* e2))

在比较时,我们需要借助cmp这个函数进行比较(此时用升序),cmp这里的返回值如果大于0,就交换。现在我们需要将arr[j]和arr[j+1]的地址传过去。
这里的关键是用base计算出arr[j]和arr[j+1]的地址 。现在我们通过width知道了一个元素的宽度,但是我们不知道base里面存了什么类型的数据。这里的base不能直接+j或+j+1。base是void * 类型。我们还需要将base进行强制类型转换,转换为char *,此时+1跳过一个字节。
在比较之后怎么进行交换呢?
这里我们分装一个Swap函数来完成。之前我们创建了一个临时变量tmp,这个临时变量的类型比较棘手,不能定死。
在满足交换的条件时,我们把两个指针指向的元素进行交换。不要忘记把width(类型设置为size_t)传给Swap函数。Swap函数的参数部分我们写为char * 就行。
在写代码之前我们需要知道当我们需要交换40个字节的数据时,我们可以把空间切为40份,一对字节一对字节的进行交换。所以在Swap函数内部我们写一个循环就行。因为交换的是字节,我们创建一个一个字节大小的临时变量就行交换,并通过++不断交换。现在我们的理论知识已经充分了,现在完成代码。


#include<stdio.h>
#include<string.h>
int cmp_int(const void* e1, const void* e2)
{return *(int*)e1 - *(int*)e2;
}void Swap(char* buf1, char* buf2, size_t width)
{int i = 0;for (i = 0; i < width; i++){char tmp = *buf1;*buf1 = *buf2;*buf2 = tmp;buf1++;buf2++;}
}void print_arr(int arr[], int sz)
{int i = 0;for (i = 0; i < sz; i++){printf("%d ", arr[i]);}printf("\n");
}void bubble_sort(void* base, size_t  num, size_t  width, int(*cmp)(const void* e1, const void* e2))
{int i = 0;//趟数for (i = 0; i < num -1;i++){int j = 0;for (j = 0; j < num -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);}}}
}void test3()
{int arr[10] = { 3,1,9,8,5,4,0,2,7,6 };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;
}

在这里插入图片描述
现在我们已经完成qsort的模拟了,这就是泛型编程,适配各种类型的数据。

好了,今天我们的指针知识就讲到这里。如果文章内容有误,请大佬在评论区斧正!谢谢大家!
在这里插入图片描述

这篇关于有希带你深入理解指针(4)的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1146911

相关文章

一文带你理解Python中import机制与importlib的妙用

《一文带你理解Python中import机制与importlib的妙用》在Python编程的世界里,import语句是开发者最常用的工具之一,它就像一把钥匙,打开了通往各种功能和库的大门,下面就跟随小... 目录一、python import机制概述1.1 import语句的基本用法1.2 模块缓存机制1.

深入理解C语言的void*

《深入理解C语言的void*》本文主要介绍了C语言的void*,包括它的任意性、编译器对void*的类型检查以及需要显式类型转换的规则,具有一定的参考价值,感兴趣的可以了解一下... 目录一、void* 的类型任意性二、编译器对 void* 的类型检查三、需要显式类型转换占用的字节四、总结一、void* 的

深入理解Redis大key的危害及解决方案

《深入理解Redis大key的危害及解决方案》本文主要介绍了深入理解Redis大key的危害及解决方案,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着... 目录一、背景二、什么是大key三、大key评价标准四、大key 产生的原因与场景五、大key影响与危

深入理解C++ 空类大小

《深入理解C++空类大小》本文主要介绍了C++空类大小,规定空类大小为1字节,主要是为了保证对象的唯一性和可区分性,满足数组元素地址连续的要求,下面就来了解一下... 目录1. 保证对象的唯一性和可区分性2. 满足数组元素地址连续的要求3. 与C++的对象模型和内存管理机制相适配查看类对象内存在C++中,规

【前端学习】AntV G6-08 深入图形与图形分组、自定义节点、节点动画(下)

【课程链接】 AntV G6:深入图形与图形分组、自定义节点、节点动画(下)_哔哩哔哩_bilibili 本章十吾老师讲解了一个复杂的自定义节点中,应该怎样去计算和绘制图形,如何给一个图形制作不间断的动画,以及在鼠标事件之后产生动画。(有点难,需要好好理解) <!DOCTYPE html><html><head><meta charset="UTF-8"><title>06

认识、理解、分类——acm之搜索

普通搜索方法有两种:1、广度优先搜索;2、深度优先搜索; 更多搜索方法: 3、双向广度优先搜索; 4、启发式搜索(包括A*算法等); 搜索通常会用到的知识点:状态压缩(位压缩,利用hash思想压缩)。

深入探索协同过滤:从原理到推荐模块案例

文章目录 前言一、协同过滤1. 基于用户的协同过滤(UserCF)2. 基于物品的协同过滤(ItemCF)3. 相似度计算方法 二、相似度计算方法1. 欧氏距离2. 皮尔逊相关系数3. 杰卡德相似系数4. 余弦相似度 三、推荐模块案例1.基于文章的协同过滤推荐功能2.基于用户的协同过滤推荐功能 前言     在信息过载的时代,推荐系统成为连接用户与内容的桥梁。本文聚焦于

【生成模型系列(初级)】嵌入(Embedding)方程——自然语言处理的数学灵魂【通俗理解】

【通俗理解】嵌入(Embedding)方程——自然语言处理的数学灵魂 关键词提炼 #嵌入方程 #自然语言处理 #词向量 #机器学习 #神经网络 #向量空间模型 #Siri #Google翻译 #AlexNet 第一节:嵌入方程的类比与核心概念【尽可能通俗】 嵌入方程可以被看作是自然语言处理中的“翻译机”,它将文本中的单词或短语转换成计算机能够理解的数学形式,即向量。 正如翻译机将一种语言

【C++高阶】C++类型转换全攻略:深入理解并高效应用

📝个人主页🌹:Eternity._ ⏩收录专栏⏪:C++ “ 登神长阶 ” 🤡往期回顾🤡:C++ 智能指针 🌹🌹期待您的关注 🌹🌹 ❀C++的类型转换 📒1. C语言中的类型转换📚2. C++强制类型转换⛰️static_cast🌞reinterpret_cast⭐const_cast🍁dynamic_cast 📜3. C++强制类型转换的原因📝

深入手撕链表

链表 分类概念单链表增尾插头插插入 删尾删头删删除 查完整实现带头不带头 双向链表初始化增尾插头插插入 删查完整代码 数组 分类 #mermaid-svg-qKD178fTiiaYeKjl {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg-