【C语言指针进阶(2)】函数指针+回调函数+qsort()库函数

提示:本文主要讲解函数指针,函数指针数组的使用,qsort库函数的使用,及模拟实现。但qsort中的第四个参数是一个函数指针,这就离不开函数指针的讲解,函数指针最典型的例子就是回调函数的使用。本文将依此展开讨论,学习。


前言

记录学习日常,如有错误,请大家批评指出,谢谢大家。


一、函数指针

首先指针我们了解,但什么是函数指针呢?

  • 我们知道,指向字符的指针叫字符指针,指向整型的指针叫整型指针, 指向数组的指针叫数组指针,那么,顾名思义,函数指针就是指向函数的指针。
  • 他和普通指针一样,都指向一个地址,这个地址是一个函数的地址。

1.1 函数指针

形如:int (*pf)(int, int) = &Add;
pf就是函数指针,指向函数的指针。

函数的地址通过取地址操作符“&”,来取出函数地址。例如:我们有一个加法函数Add, 有两个整型参数,返回值是int。我们用 &Add 把函数Add的地址取出,存放在一个指针变量pf中,那么这个pf就是一个函数指针,pf指向了Add这个函数。

那么函数指针pf的类型是什么?
函数指针类型是:int ( * )(int, int)
我们先来看这句代码: int ( * pf)(int, int)=&Add; 首先,&Add是一个地址,要放在一个指针变量里,那么指针变量pf就要先与*结合(*pf), 指针指向的是一个函数,函数有两个参数,所以是(int, int)——(函数再传递参数时,形参并不会真正的创建,所以形参名可以省去),函数的返回值是int类型,所以结合起来就是:int ( * pf)(int, int)。这只是创建了一个函数指针变量,去除变量名剩下的就是变量的类型,所以函数指针变量的类型是:int ( * )(int, int)。

函数指针的创建:代码展示

#include <stdio.h>

//加法函数
int Add(int x, int y)
{
	return x+y;
}

int main()
{
	int (*pf)(int, int) = &Add;//创建函数指针pf, 并赋值,使pf指向函数Add
	//对指针变量解引用得到的使指针指向的内容,*pf得到的是函数Add
	int ret = (*pf)(3, 5); //解引用得到函数Add,并调用函数
	printf("ret = %d\n", ret);
	//输出结果:ret = 8
	return 0;
}

1.2 函数的地址

接下来我们理解:关于函数地址的三个小点

  • 函数名和&函数名的类型相同,即函数名就是函数的地址。
  • 直接使用函数指针调用函数,而不解引用。
  • 可以多次解引用, 解引用操作符只是摆设,无实际意义。

我们先来看函数的地址,发现&Add 和 Add 相等,即地址相等。所以&Add 和Add等价。
在这里插入图片描述

代码解释:

#include <stdio.h>

//加法函数
int Add(int x, int y)
{
	return x + y;
}

int main()
{
	//验证1:函数名和&函数名相同
	int (*pf1)(int, int) = Add;
	int (*pf2)(int, int) = &Add;//创建函数指针pf, pf指向Add

	int (*pfArr[])(int, int) = { pf1, pf2 };//创建函数指针数组,本质是一个数组,存放的是指针变量。——后面也会细说,了解一下拓宽视野
	//我们知道,数组时存放一组相同类型数据的集合,如果pf1, 和pf2的类型不相同,则编译会报错
	//所以Add和&Add类型相同,即Add和&Add等价,他们都是函数Add的地址,



	//验证2:直接使用函数指针调用函数,而补解引用
	//解引用
	int ret1 = (*pf1)(3, 5);
	printf("ret1 = %d\n", ret1);
	//直接使用函数指针调用
	int ret2 = pf1(3, 5);
	//因为函数名就是函数地址,函数指针变量等价于函数名,pf===Add,所以可以直接使用函数指针变量调用函数,而不解引用
	printf("ret2 = %d\n", ret2);



	//验证3:可以多次解引用, 解引用操作符只是摆设,无实际意义。
	int ret3 = (*pf1)(3, 5);
	int ret4 = (*****pf1)(3, 5);
	int ret5 = pf1(3, 5);//也可以不解引用,直接调用
	printf("ret3 = %d\n", ret3);
	printf("ret4 = %d\n", ret4);
	printf("ret5 = %d\n", ret5);


	return 0;
}

验证(1):
在这里插入图片描述
验证(2)
在这里插入图片描述

验证(3):
在这里插入图片描述

由于函数名和&函数名相同,所以在取函数地址时,可以直接使用函数名。在函数指针调用其所指向的函数时,可以直接使用函数指针+(参数1, 参数2…),即不需要解引用,也可以多次解引用(解引用操作符在这里就是摆设,无实际意义)。
总结: 创建函数指针变量时,数组名和&数组名相同,可以直接将函数名赋值给函数指针变量。这时,指针变量和指针变量所指向的函数等价,即指针变量就是这个函数的函数名。在使用指针变量时, 可以直接把指针当成函数名使用。

1.3 函数指针数组

与数组指针和指针数组类似。
小扩展:
目录:二、数组指针和指针数组的区别

形如:int (*pf[2])(int, int) ={ Add, Sub};
pfArr就是函数指针数组,本质是数组,数组里面的元素是函数指针。

函数指针数组——用来存放函数指针的数组,本质是数组。数组里存放的每个元素都是函数指针。这个指针指向一个函数,指针叫做函数指针,这个数组叫做函数指针数组。

#include <stdio.h>

int Add(int x, int y)
{
	return x + y;
}


int Sub(int x, int y)
{
	return x - y;
}

int main()
{
	int (*pfArr[2])(int, int) = { Add, Sub };
	//pfArr就是一个函数指针数组,本质是数组,用来存放函数指针的数组。
	//[]优先级比* 高,所以pfArr先和[]结合,所以pfArr是数组。数组名:pfArr, 数组元素个数2, 数组元素的类型:int (*)(int, int)
	//数组里面有两个元素,每个元素都是int (*)(int, int)类型的函数指针

	//访问
	//pfArr[0] --- 数组pfArr的第一个元素--Add
	int ret = pfArr[0](3, 5);//直接调用
	printf("ret = %d\n", ret);

	//pfArr[1] --- 数组pfArr的第二个元素--Sbu
	int ret2 = pfArr[1](3, 5);//直接调用
	printf("ret2 = %d\n", ret2);

	return 0;
}

结果:
在这里插入图片描述

1.4 指向函数指针数组的指针(拓展)

指向函数指针数组的指针,本质是指针,这个指针指向一个函数指针数组。

形如:int (*(*pf)[2])(int, int) = &Arr;
pf就是指向函数指针数组的指针,本质是指针,指针指向了一个函数指针数组。

1.5 辨别不同

函数指针,函数指针数组,指向函数指针数组的指针,三者的区分:

#include <stdio.h>

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; 
	//pf是函数指针,指向了Add函数
	//函数有两个int类型的参数,函数返回值是int类型

	int (*pfArr[2])(int, int) = { Add, Sub };
	//pfArr是函数指针数组,存放函数指针的数组,本质是数组
	//数组中有两个元素,每个元素都是int (*) (int, int)类型的函数指针。

	int (*(*pf2)[2])(int, int) = &pfArr;
	//pf2是指向函数指针数组的指针,本质是指针,指向了一个函数指针数组
	//pf2先于*结合说明pf2是一个指针,在与[]结合说明pf2指向的是一个数组
	//去除变量名和数组名剩下的就是数组元素的类型。
	//所以pf2指向的数组元素的类型是:int (*)(int, int)函数指针类型。


	return 0;
}

如果理解上述这三者的区别,那么请看下面两句代码:
代码出自《C陷阱和缺陷》

#include <stdio.h>

int main()
{
	//代码1
	(*(void (*)())0)();
	//这行代码我们所认识的只有一个0,那我们就从此入手
	//看0前面的一个小括号,(void (*)()),有没有熟悉的感觉?
	//void (*)() ---- int (*)(int, int) -- 函数指针的类型
	//这个函数指针指向的函数是无参的,函数的返回值是void
	//这句话的意思就是,将0这个地址强制类型转换为函数指针类型
	//最前面有一颗星,这颗星是解引用,
	//(*(void (*)())0),这句话的意思是,解引用找到这个指针(0地址)所指向的函数
	//最右边的括号就是调用这个函数,函数是无参的。




	//代码2
	void (*signal(int, void(*)(int)))(int);
	//我们先来划分一下层次关系
	//void      (*   signal(int,void(*)(int))   )     (int);
	//signal是一个变量,我们目前还不知道他是什么变量
	//我们发现,signal右边有一个小括号, 括号里面有两个参数,一个是整型类型,一个是void*(int) 类型的函数指针类型
	//我们把 signal(int,void(*)(int)) 去掉,剩余 void (*) (int),这是一个函数指针类型。
	//说明signal是一个函数,这个函数接收两个参数,一个整型int,一个指向带有一个整型参数并返回void的函数指针void(*)(int)。
	//他的返回类型是一个指向带有一个整型参数并且返回void的函数指针void(*)(int)。

	//我们简化一下
	typedef void(*pfun_t)(int); //将void (*)(int) 类型重命名为 pfun_t类型
	pfun_t signal(int, pfun_t);	//signal是函数名,返回类型是void(*)(int)的函数指针。参数是:int类型, 和 void(*)(int)类型。


	return 0;
}

二、回调函数

2.1 定义

理解了函数指针,那么我们来看看回调函数。

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

回调函数实现简易计算机:

#include <stdio.h>

void menu()
{
	printf("===========================\n");
	printf("===1. 加法    2. 减法======\n");
	printf("===3. 乘法    2. 除法======\n");
	printf("===0. 退出           ======\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 clc(int (*pf)(int, int))
{
	int x = 0;
	int y = 0;
	printf("请输入两个操作数:");
	scanf("%d %d", &x, &y);
	int ret = (*pf)(x, y);//使用函数指针pf 调用指针指向的函数,被调用的函数就称为回调函数
	printf("ret = %d\n", ret);
}

int main()
{

	int input = 0;
	do
	{
		menu();
		printf("请选择:>");
		scanf("%d", &input);

		switch (input)
		{
		case 1:
			clc(Add);//加法
			break;
		case 2:
			clc(Sub);//减法
			break;
		case 3:
			clc(Mul);//乘法
			break;
		case 4:
			clc(Div);//除法
			break;
		case 0:
			printf("退出计算器!\n");
			break;
		default:
			printf("选择错误,请重新选择!\n");
			break;
		}
	} while (input);

	return 0;
}

【代码优化】

#include <stdio.h>

void menu()
{
	printf("===========================\n");
	printf("===1. 加法    2. 减法======\n");
	printf("===3. 乘法    2. 除法======\n");
	printf("===0. 退出           ======\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;
	do
	{
		menu();
		printf("请选择:>");
		scanf("%d", &input);
		int (*pf[])(int, int) = { NULL, Add, Sub, Mul, Div };//使用函数指针数组,存放函数指针,使用时在用下标索引找到对应的函数指针
		
		if (input >= 1 && input <= 4)
		{
			int x = 0;
			int y = 0;
			printf("请输入两个操作数:");
			scanf("%d %d", &x, &y);
			int ret = pf[input](x, y);
			printf("ret = %d\n", ret);
		}
		else if (0 ==input)
		{
			printf("退出计算器!\n");
		}
		else
		{
			printf("选择错误,请重新选择!\n");
		}
	} while (input);

	return 0;
}

回调函数最常见的就是库函数qsort的使用,qsort的有4个参数,其中第四个参数就是一个函数指针,实参需要传递一个函数的地址,形参用一个函数指针接收。
接下来我们来看回调函数在库函数qsort()中的使用。

三、库函数qsort()

3.1 qsort()库函数介绍

qsort()是C语言中用来排序的库函数,底层是使用快速排序的思想。包含在头文件 #include <stdlib.h>中。作用是:可以对任意类型的数据进行排序。

  • 函数原型:
    void qsort (void* base, size_t num, size_t size,
    int (compar)(const void,const void*));
  • 参数:
    参数1:void* base, 待排序的起始地址。由于不知道要排序的一组数据的类型是什么,所以使用void* 来表示。
    void* 类型的指针,用来存放任意类型的地址
    参数2:size_t num, 待排数据的元素个数。
    参数3:size_t size, 一个待排数据的大小,单位是字节。
    参数4:int(*compar)(const void * , const void * ), 函数指针,实参需要传递一个函数的地址。
  • 参数4详解:
    返回值有3中情况,大于0升序,等于0不变和小于0降序。两个参数,参数的类型是void * , 用来接收待排元素的地址。const在 * 左边,用来限制指针指向的内容,即*ptr不能改变。

小扩展:
const修饰指针

3.2 qsort排序整型数据

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


int main()
{
	int arr[] = { 9,8,7,6,5,4,3,2,1,0 };
	int sz = sizeof(arr) / sizeof(arr[0]);
	Print_Arr(arr, sz);//排序前
	qsort(arr, sz, sizeof(arr[0]), cmp_int);//调用库函数
	Print_Arr(arr, sz);//排序后

	return 0;
}

在这里插入图片描述

3.3 qsort排序结构体数据

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

struct Student
{
	char name[20];
	int age;
};

int cmp_by_age(const void* e1, const void* e2)
{
	return ((struct Student*)e1)->age - ((struct Student*)e2)->age;
}

int cmp_by_name(const void* e1, const void* e2)
{
	return strcmp(((struct Student*)e1)->name, ((struct Student*)e2)->name);
}

int main()
{
	struct Student stu[] = { {"zhangsan", 18}, {"lisi", 27}, {"wangwu", 20}};
	int sz = sizeof(stu) / sizeof(stu[0]);

	//qsort(stu, sz, sizeof(stu[0]), cmp_by_age);
	qsort(stu, sz, sizeof(stu[0]), cmp_by_name);

	return 0;
}

cmp_by_age;在这里插入图片描述cmp_by_name;
在这里插入图片描述

3.4 qsort的模拟实现

下面代码,以冒泡排序为例,模拟实现库函数qsort的底层原理。
qsort底层实现是使用快速排序的方法,后续我们会展开排序系列讨论。

//使用冒泡排序,模拟实现一个qsort
#include <stdio.h>

//交换两个地址中的数据--交换两个元素
void swap(char* buf1, char* buf2, size_t sz)
{
	int i = 0;
	for (i = 0; i < sz; i++)
	{
		int* tmp = *buf1;
		*buf1++ = *buf2;
		*buf2++ = tmp;
	}
}

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

//以冒泡排序模拟实现qsort
void bubble_sort(void* base, size_t num, size_t size, int (*cmp)(const void*, const void*))
{
	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*size, (char*)base+(j+1)*size) > 0)
			{
				swap((char*)base + j * size, (char*)base + (j + 1) * size, size);
			}
		}
	}
}

void print(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]);
	print(arr, sz);
	bubble_sort(arr, sz, sizeof(arr[0]), cmp_int);
	print(arr, sz);
}

struct Student
{
	char name[20];
	int age;
};

int cmp_by_name(const void* e1, const void* e2)
{
	return strcmp(((struct Student*)e1)->name, ((struct Student*)e2)->name);
}

int cmp_by_age(const void* e1, const void* e2)
{
	return ((struct Student*)e1)->age - ((struct Student*)e2)->age;
}


void test2()
{
	struct Student arr[] = { {"zhangsan", 21}, {"lisi", 18}, {"wangwu", 30}};
	int sz = sizeof(arr) / sizeof(arr[0]);
	//bubble_sort(arr, sz, sizeof(arr[0]), cmp_by_name);
	bubble_sort(arr, sz, sizeof(arr[0]), cmp_by_age);

}

int main()
{	
	//测试整型数据
	//test1();
	//测试结构体数据
	test2();

	return 0;
}

总结

重点理解,函数指针,函数指针数组,回调函数,库函数qsort中回调函数的应用。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值