提示:本文主要讲解函数指针,函数指针数组的使用,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中回调函数的应用。