[深入浅出C语言]深析指针(篇四)

90 阅读5分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第12天,点击查看活动详情

前言

        学习C语言,不得不学到的就是指针,甚至可以这么说:指针是C语言的精髓之所在。

        本文就来分享一波作者的C指针学习见解与心得。本篇属于进阶第四篇,主要讲解了回调函数的一些内容。后续还有笔试面试真题讲解,可以期待一下。

        笔者水平有限,难免存在纰漏,欢迎指正交流。


回调函数

概念讲解

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

解说一波:

        光看定义可能会不知所云,还记得上面讲函数指针时写的计算器小程序吗?我们觉得原代码冗余,于是用函数指针传参来实现将运算功能统一起来放在calc函数里,我们是在main函数里实现的,调用calc函数,假设程序需要进行加法运算,只需要把add函数地址传给calc函数,然后在calc函数内通过所给的函数指针间接地调用add函数完成加法,若要进行其他运算也类同,只需要传入对应的函数地址即可。

        并且传入的地址是从主函数传进去的,是根据主函数中满足某种条件来确定传入哪一种函数的地址的,但不是主函数直接调用add等函数,而是主函数调用calc函数,再经由calc函数来调用的。

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);
}

        说了那么一大堆,我们还是来看看实例吧。

演示qsort函数的使用

函数说明

所在头文件:<stdlib.h>

函数原型:

void qsort( void *base, size_t num, size_t width, int (__cdecl *compare )(const void *elem1, const void *elem2 ) );

我们这里简化一下:

void qsort(
void *base, //你要排序的数据的起始位置
size_t num, //待排序的数据元素的个数
size_t width, //待排序的数据元素的大小(单位:字节)
int(*cmp )(const void *e1, const void *e2 )//一个比较函数的函数指针,e1和e2接收的是要比较的两个元素的地址
 );

        最后一个参数用来干什么的呢?实际上qsort函数可以排序任意类型的数据,这也归功于这个比较函数的实现逻辑。有些数据类型不能直接用关系运算符>或<来比较,需要在比较函数里面做出特定的比较,然后再用函数指针来调用。

        这样的话,要是想排序数组的话,比较函数传入的就是数组元素,然后实现对应的比较逻辑,再通过函数指针调用比较函数;而要是想排序结构体成员,比较函数传入的就是结构体成员,然后实现对应的比较逻辑,再通过函数指针调用比较函数,诸如此类。

        void*是没有具体类型的指针,可以用来接收任意类型的地址,但是不能解引用,也不能进行指针运算,要使用根据需要强制类型转换一下。

        下表是比较函数返回值对应的逻辑,据此可以return (*( int *)e1 - *(int *) e2);,只要解引用e1比解引用e2大就返回正数,相等返回0,小于返回负数。

image.png

排序一下数组

#include <stdio.h>
#include<stdlib.h>
//要使用qosrt函数得先实现一个比较函数
int cmp_int(const void * e1, const void * e2)
{
	//注意强制类型转换
  return (*( int *)e1 - *(int *) e2);
}
int main()
{
    int arr[] = { 1, 3, 5, 7, 9, 2, 4, 6, 8, 0 };
    int i = 0;
    
    qsort(arr, sizeof(arr) / sizeof(arr[0]), sizeof (int), cmp_int);
    for (i = 0; i< sizeof(arr) / sizeof(arr[0]); i++)
   {
       printf( "%d ", arr[i]);
   }
    printf("\n");
    return 0;
}

image.png

其实qsort()默认排升序,要排降序的话把指针解引用相减对象交换一下。

return (*( int *)e2 - *(int *) e1);

排序一下结构体数组

#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)
{
	//比较字符串长度要用strcmp(),正好其返回值也满足要求
	return strcmp(((struct Stu*)e1) -> name, ((struct Stu*)e2) ->name);
}
void test1()
{
	//根据结构的字符数组长度来排序
	struct Stu s[] = {{"zhangsan", 15}, {"lisi", 25}, {"wangwu", 30}};
	int sz = sizeof(s) / sizeof(s[0]);
	qsort(s, sz, sizeof(s[0], cmp_stu_by_name));
}
int cmp_stu_by_age(const void* e1, const void* e2)
{
	//直接相减即可
	return ((struct Stu*)e1) -> age - ((struct Stu*)e2) ->age;
}
void test2()
{
	//根据结构的age变量大小来排序
	struct Stu s[] = {{"zhangsan", 15}, {"lisi", 25}, {"wangwu", 30}};
	int sz = sizeof(s) / sizeof(s[0]);
	qsort(s, sz, sizeof(s[0], cmp_stu_by_age));
}

int main()
{
	test1();
	test2();
	return 0;
}

模拟实现qsort(采用冒泡的方式)

先看看冒泡排序

void BubbleSort(int *arr, int sz)
{
	int i = 0;
	int j = 0;

	for (i = 0; i < sz - 1; i++)
	{
		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] = arr[j];
			}
		}
	}
}

        这是对应的升序排列的代码,要降序的话修改一下if()的判断逻辑即可:arr[j] < arr[j + 1]

        感觉好像基本实现了功能,但是还有点"蠢",何出此言?要是数组原来就已经是排好序的还需要再继续执行遍历吗?这段代码运行起来无法分辨数组是否已经排好序,不管三七二十一直接遍历,虽然结果也不会有问题,但是明显可以修改。

        定义一个flag变量来确定数组状态(是否排好序),初始时默认已排序flag=1,一旦发现数组不是完全排好序的话就把flag置零,表示未排好序。每轮结束(内层循环结束)后判断一下flag,若为1说明已排好序,直接跳出循环,不然继续排序。

代码如下:

void BubbleSort(int *arr, int sz)
{
	int i = 0;
	int j = 0;
	
	for (i = 0; i < sz - 1; i++)
	{
		int flag = 1;
		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] = arr[j];
				flag = 0;
			}
		}
		if(flag)
		      break;
	}
}

        然而这段排序代码只能排序整型数组,因为函数的参数给的就是整型指针。

分析回调函数的构成

参数列表

void qsort(

void *base, //你要排序的数据的起始位置

size_t num, //待排序的数据元素的个数

size_t width, //待排序的数据元素的大小(单位:字节)

int(*cmp )(const void *e1, const void *e2 )//一个比较函数的函数指针,e1和e2接收的是要比较的两个元素的地址

 );

进一步说明:

void*base,为什么是void*类型?因为要使用qsort()排序的数据类型不确定,这里设计成一种泛型类型,使用时强制类型转换即可。

size_t numsize_t width (size_t是sizeof()特定的无符号整数类型)可以给出元素数量和单个元素内存大小,这样就很容易用指针偏移找个各个元素,方便排序。

int(*cmp )(const void *e1, const void *e2 )用函数指针指向一个比较函数,用来确定升序或降序逻辑。

如何改动

        一个方面是把参数列表改成qsort的,然后就是看看在哪插入比较函数的调用了。

        原代码中有比较的地方在哪?

        就是if(arr[j] > arr[j + 1])处,改成

if(cmp((char*)base + j*width, (char*)base + (j + 1)*width))

image.png

   再有就是需要一个交换函数,因为待交换的数据类型不确定,所以不止要传递两元素的地址,还要传递元素大小,再一个字节一个字节交换。

代码如下:

int cmp_int(const void * e1, const void * e2)
{
	//注意强制类型转换
  return (*( int *)e1 - *(int *) e2);
}

void swap(char* buf1, char* buf2, width)
{
	int i = 0;
	for(i = 0; i < width; i++)
	{
		char tmp = *buf1;
		*buf1 = * buf2;
		*buf2 = *buf1;
		buf1++;
		buf2++;
	}
}

void BubbleSort(void *base, int size, int width, int(*cmp )(const void *e1, const void *e2 )
{
	int i = 0;
	int j = 0;
	
	for (i = 0; i < sz - 1; i++)
	{
		int flag = 1;
		for (j = 0; j < sz - i - 1; j++)
		{
			if (cmp((char*)base + j*width, (char*)base + (j + 1)*width))
			{
				swap((char*)base + j*width, (char*)base + (j + 1)*width, width
				);
				flag = 0;
			}
		}
		if(flag)
		      break;
	}
}
int main()
{
    int arr[] = { 1, 3, 5, 7, 9, 2, 4, 6, 8, 0 };
    int i = 0;
    
    BubbleSort(arr, sizeof(arr) / sizeof(arr[0]), sizeof (int), cmp_int);
    for (i = 0; i< sizeof(arr) / sizeof(arr[0]); i++)
   {
       printf( "%d ", arr[i]);
   }
    printf("\n");
    return 0;
}

image.png

        这一种版本适合任何类型数据排序,原来的冒泡排序只能排整型。

        但库函数里实现qsort的内部逻辑框架用的不是冒泡而是快速排序。


以上就是本文全部内容了,感谢观看,你的支持就是对我最大的鼓励~

v2-9f0f5f1d3d0cb28ab9c44f5ba938146f_720w.jpg