本篇是继续上篇内容进行学习【C语言进阶学习】二、指针的进阶(1)
函数指针
我们在前篇讲数组指针的时候,是这样引出数组指针的概念的:
整型指针: 存放整型的地址 - - - 是指向整型的指针
字符指针: 存放字符的地址 - - - 是指向字符的指针
所以 - - >
数组指针:存放数组的地址 - - - 是指向数组的指针
那么,我们就此可以引出函数指针的概念了:
函数指针:存放函数的地址 - - - 是指向函数的指针
我们可能有些疑问,函数也有地址的吗?是的,函数是有地址的
我们创建函数之后,调用它的时候,就会在内存中开辟一块空间,既然开辟了内存空间,那就有对应的内存空间地址。
接下来我们来了解一下以下内容:
&数组名 - - - 取出的是数组的地址
数组名 - - - 数组首元素地址
这我们在前篇已经讲过,那么 &函数名 与 函数名 所代表的意义是否也不一样呢?
请看下面代码:
int Add(int x, int y)
{
return x + y;
}
int main()
{
printf("%p\n", &Add);
printf("%p\n", Add);
return 0;
}
总结:&函数名与函数名都是指函数的地址
数组名 != &数组名
函数名 == &函数名
既然函数有地址,那函数的地址该如何存放呢?
如图:
了解函数指针是如何定义之后,那函数指针可以如何运用呢?
假如我们要实现一个加法运算,在我们没学过函数指针之前是这样调用函数的,如图:
当我们使用函数指针,我们就可以调用函数了:
.
而我们一开始就讲了,函数名与&函数名都是表示函数的地址,所以我们不可并不用&,而当我们将数组名传给pf后,pf就是存放Add函数的地址,所以也可以不用 * 操作符, 其实 * 号在这里就是一个摆设,无论放几个 * 都无所谓,是不会报错的,这个地方放 * 是为了方便理解,如图:
了解了这些后,我们来看下面代码:
//代码1
(*(void (*) ())0)();
//代码2
void(*signal(int, void(*)(int)))(int);
这两个代码分别表示什么意思呢?
首先我们看代码1:
( * ( void(*)() )0 )();
代码分析:
这个代码是一次函数调用
1.首先代码中把0强制类型转换为类型为void(*)()的一个函数的地址。
2.然后解引用0的地址,就是取0地址处的这个函数,被调用的函数是无参的,返回类型是void
代码2:
void(* signal(int, void(*)(int) ) )(int);
代码分析:
这个代码是一次函数声明
声明的函数名是signal
signal函数有2个参数,第一个参数是int类型,第二个是void(*)(int)的函数指针类型
signal函数的返回类型依然是:void(*)(int)的函数指针类型
或许代码2我们看起来有点复杂,那我们是否可以将其简化一下呢?如下:
typedef void(*pfun_t)(int);
//typedef类型重定义关键字,将void(*)(int)类型的函数指针,重新命名为pfun_t
//所以代码2可以写为:
pfun_t signal(int,pfun_t);
注:void ( * )(int) signal(int, void(*)(int));
这种写法虽然很容易理解,但语法是错误的,所以一定不能这样写!!
函数指针数组
什么叫函数指针数组呢?
我们来类比一下:
整型指针数组:存放整型指针的数组
int* arr[10];
//整型指针数组---存放整型指针的数组
//该数组有10个元素,每个元素都是int*类型
那我们是不是可以将函数指针数组这样定义,如下:
函数指针数组:存放函数指针的数组
那函数指针数组该如何定义呢?如下
#include <stdio.h>
//加法
int Add(int x, int y)//函数指针类型:int (*)(int, int)
{
return x + y;
}
//减法
int Sub(int x, int y)//函数指针类型:int (*)(int, int)
{
return x - y;
}
//乘法
int Mul(int x, int y)//函数指针类型:int (*)(int, int)
{
return x * y;
}
//除法
int Div(int x, int y)//函数指针类型:int (*)(int, int)
{
return x / y;
}
int main()
{
//pfArr就是一个函数指针的数组
int (* pfArr[4])(int, int) = {Add, Sub, Mul, Div};
//去掉pfArr[4]剩下的int(*)(int,int)就是函数指针数组的元素的类型
return 0;
}
注:数组是一个存放相同类型数据的存储空间,所以我们要注意函数指针数组里的元素类型是否一致(返回类型,参数)
了解到这,那我们该如何去使用函数指针数组呢?请看下面例子:
假如我要实现一个计算器:
最简单的写法:
#include <stdio.h>
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)//函数指针类型:int (*)(int, int)
{
return x + y;
}
//减法
int Sub(int x, int y)//函数指针类型:int (*)(int, int)
{
return x - y;
}
//乘法
int Mul(int x, int y)//函数指针类型:int (*)(int, int)
{
return x * y;
}
//除法
int Div(int x, int y)//函数指针类型:int (*)(int, int)
{
return x / y;
}
int main()
{
int input = 0;
int x = 0;
int y = 0;
int ret = 0;//接收结果
do
{
menu();
printf("请选择:>");
scanf("%d", &input);
switch (input)
{
case 1:
printf("请输入两个操作数:");
scanf("%d %d", &x, &y);
ret = Add(x, y);
printf("ret = %d\n", ret);
break;
case 2:
printf("请输入两个操作数:");
scanf("%d %d", &x, &y);
ret = Sub(x, y);
printf("ret = %d\n", ret);
break;
case 3:
printf("请输入两个操作数:");
scanf("%d %d", &x, &y);
ret = Mul(x, y);
printf("ret = %d\n", ret);
break;
case 4:
printf("请输入两个操作数:");
scanf("%d %d", &x, &y);
ret = Div(x, y);
printf("ret = %d\n", ret);
break;
case 0:
printf("退出游戏\n");
break;
default:
printf("输入错误,请重新输入\n");
}
} while (input);
return 0;
}
运行结果:
我们可以看到这种写法,switch语句里的代码有点冗余,有很多重复的代码,所以有没有更好的写法呢?如下:
使用函数指针简化:
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)//int (*)(int, int)
{
return x + y;
}
int Sub(int x, int y)//int (*)(int, int)
{
return x - y;
}
int Mul(int x, int y)//int (*)(int, int)
{
return x * y;
}
int Div(int x, int y)//int (*)(int, int)
{
return x / y;
}
void Calc(int(*pf)(int, int))
{
int x = 0;
int y = 0;
int ret = 0;
printf("请输入2个操作数:>");
scanf("%d %d", &x, &y);
ret = pf(x, y);
printf("ret = %d\n", ret);
}
int main()
{
int input = 0;
do
{
menu();
printf("请选择:>");
scanf("%d", &input);
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;
}
其实这里我们涉及到一个概念,叫做回调函数,这里我们通过代码可以知道,我们没有主动去调用那些算法的函数,而是将这些函数的地址,传给Calc函数,在Calc函数里再通过函数指针去调用这些函数,而这种通过函数指针去调用的函数,就叫回调函数,在本片最后一个内容我们会详细讲解。
注:回调函数的这种机制,必须用函数指针来实现,否则做不到
使用函数指针数组的实现:
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)//函数指针类型:int (*)(int, int)
{
return x + y;
}
//减法
int Sub(int x, int y)//函数指针类型:int (*)(int, int)
{
return x - y;
}
//乘法
int Mul(int x, int y)//函数指针类型:int (*)(int, int)
{
return x * y;
}
//除法
int Div(int x, int y)//函数指针类型:int (*)(int, int)
{
return x / y;
}
int main()
{
int input = 0;
int x = 0;
int y = 0;
int ret = 0;//接收结果
do
{
menu();
printf("请选择:>");
scanf("%d", &input);
//pfArr就是函数指针数组
int(*pfArr[5])(int, int) = { 0, Add, Sub, Mul, Div };
//下标 0 1 2 3 4
if (input == 0)
{
printf("退出计算器\n");
}
else if (input >= 1 && input <= 4)
{
printf("请输入两个操作数:");
scanf("%d %d", &x, &y);
ret = pfArr[input](x, y);
printf("ret = %d\n", ret);
}
else
{
printf("选择错误\n");
}
} while (input);
return 0;
}
运行结果:
这种写法就可以防止主函数中的代码,会随着其他算法的增加,而导致代码增多的现象,它增加的只有函数指针数组的元素个数,以及else if里的判断条件的范围。
指向函数指针数组的指针
指向函数指针数组的指针,意思是一个指针,指针指向一个数组,数组的元素是函数指针类型。
那函数指针数组的指针怎么定义呢?
#include <stdio.h>
int Add(int x, int y)
{
return x + y;
}
int main()
{
int arr1[10];//整型数组
int(*p1)[10] = &arr1;
//p1是指向一个整型数组的指针
int* arr2[10];//整型指针数组
int*(*p2)[10] = &arr2;
//p2是一个指向整型指针数组的指针
int(* pf)(int, int) = Add;
//pf是一个函数指针
int(* pfArr[5])(int, int);
//pfArr是一个函数指针数组,该数组有五个元素,每个元素都是int(*)(int,int)类型的函数指针
int(* (*ppfArr)[5] )(int,int) = &pfArr;
//ppfArr就是一个函数指针数组的指针
//它指向一个函数指针数组,该数组有五个元素,每个元素都是函数指针,类型为int(*)(int,int)
return 0;
}
回调函数
概念:
回调函数就是一个通过函数指针调用的函数。如果你把函数的指针(地址)作为参数传递给另一个函数,当这个指针被用来调用其所指向的函数时,我们就说这是回调函数。回调函数不是由该函数的实现方直接调用,而是在特定的事件或条件发生时由另外的一方调用的,用于对该事件或条件进行响应
总结:回调函数就是一个通过函数指针来调用的函数。
举例:比如上面计算器的实现,使用函数指针简化的方法,其中就是运用回调函数的方法
了解回调函数的概念之后,我们来进行对其熟悉练习。
我们在前面的学习中,已经学习过冒泡法排序一个整型数组,这里我们进行一下复习。
我们可以发现:
每一趟只解决一个数字的排序问题,所以需将10个数字全部排序完成,需要进行9躺冒泡排序,
因为第9趟排序完成后,最后剩下的一个数字,已经会在它应该在的位置上。
所以n个数字,只需n-1趟。
而每一趟排序的内部:
第一趟:10个数字待排序,9对比较
第二趟:9个数字待排序,8对比较
第三趟:8个数字待排序,7对比较
……
第九趟:2个数字待排序,1对比较
代码实现:
#include <stdio.h>
void print(int* arr, int sz)
{
int i = 0;
for (i = 0; i < sz; i++)
{
printf("%d ", *(arr + i));
}
printf("\n");
}
void bubble_sort(int* arr, int sz)
{
int i = 0;
for (i = 0; i < sz - 1; i++)//确定冒泡排序的趟数
{
int j = 0;
int flag = 1;//假设本趟已经有序了
for (j = 0; j < sz - 1 - i; j++)//确定每一趟两两比较的次数
{
if (*(arr + j) > *(arr + j + 1))
{
int tmp = *(arr + j);
*(arr + j) = *(arr + j + 1);
*(arr + j + 1) = tmp;
flag = 0;//本趟无序
}
}
if (flag == 1)
{
break;
}
}
}
int main()
{
int arr[10] = { 9, 8, 7, 6, 5, 4, 3, 2, 1, 0 };
int sz = sizeof(arr) / sizeof(arr[0]);
printf("排序前:\n");
print(arr, sz);
//通过冒泡法,进行升序排列
bubble_sort(arr, sz);
printf("排序后:\n");
print(arr, sz);
return 0;
}
运行结果:
这里我们只学了使用冒泡法排序一个整型的数组内容,但它不能实现浮点型、字符型、结构体型等等除整型之外的类型的排序。
那如果出现整型数据之外的数据类型需要进行排列,那我们需要如何做呢?请看下面解答:
在C语言的库函数中有一个用于任意类型排序的函数 qsort
qsort - - - 使用快速排序的方法
qsort库函数的相关信息:
下面对qsort函数详细解读:
void qsort(
void* base,//待排序目标的起始位置
size_t num,//待排序的元素个数
size_t width,//一个元素的大小,单位是字节
int(* cmp)(const void* e1, const void* e2)
//函数指针,排序所使用的比较函数
//不同类型的数据比较的方法是不一样的,所以需要将比较方法写成函数传递给qsort
//函数指针,指针指向的参数有两个,参数类型都是const void*,函数返回类型是int
)
那这里的函数指针所指向的函数返回的值是什么呢?如图:
当参数e1小于e2时,返回一个小于0的数字
当参数e1等于e2时,返回0
当参数e1大于e2时,返回一个大于0的数字
在这个函数中,我们还发现了一个我们不常见的类型:void*
注释: void是无,空的含义,void * 表示的是无类型的指针。 void* 类型的指针可以接收任意类型的地址(void* 也可以被称为万能指针)。
void*类型指针使用注意类项:
- void* 类型指针不能进行解引用操作的。
(原因:指针的类型决定了指针解引用操作时访问字节的个数,如果指针类型是void *,无类型指针,那么就无法得知该指针解引用访问多少个字节数。)- void* 类型指针不能进行++ / ±整数运算的操作。
(原因:指针类型的意义决定了指针 + -整数运算的步长,如果指针类型是void *无类型指针,那么就无法得知该指针 + -的步长到底是多少个字节。)
(1)下面我们来试着用qsort排序一下整型数组,熟悉qsort的使用:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
//比较2个整型的函数
//为了调用qsort,需要将比较函数传给qsort
//按照qsort函数参数的类型,写出需要使用的比较函数
int cmp_int(const void* e1, const void*e2)
{
return *(int*)e1 - *(int*)e2;//升序
//return *(int*)e2 - *(int*)e1;//降序
}
void print_arr(int arr[], int sz)
{
int i = 0;
for (i = 0; i < sz; i++)
{
printf("%d ", arr[i]);
}
}
void test1()
{
int arr[] = { 10, 8, 9, 7, 5, 3, 4, 2, 1, 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;
}
(2)利用qsort排序结构体数据
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
struct stu
{
char name[20];
int age;
};
int cmp_by_name(const void*e1,const void*e2)
{
//字符串比较需要用到字符串比较库函数strcmp
return strcmp(((struct stu*)e1)->name, ((struct stu*)e2)->name);
}
int cmp_by_age(const void*e1, const void*e2)
{
return ((struct stu*)e1)->age - ((struct stu*)e2)->age;
}
void print_s(struct stu s[], int sz)
{
int i = 0;
for (i = 0; i < sz; i++)
{
printf("%s,%d\n", s[i].name, s[i].age);
}
printf("\n");
}
void test2()
{
struct stu s[3] = { { "zhangsan", 15 }, { "lisi", 10 }, { "wangwu", 30 } };
int sz = sizeof(s) / sizeof(s[0]);
int i = 0;
qsort(s, sz, sizeof(s[0]), cmp_by_name);//按名字来排
//打印
printf("按名字排序:\n");
print_s(s, sz);
qsort(s, sz, sizeof(s[0]), cmp_by_age);//按名字来排
//打印
printf("按年龄排序:\n");
print_s(s, sz);
}
int main()
{
test2();//排序结构体
return 0;
}
(3)利用qsort排序浮点型数据:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int cmp_float(const void* e1, const void* e2)
{
//强制类型转换成int类型---float类型转换为int类型会造成精确度缺失
return (int)(*(float*)e1 - *(float*)e2);
}
void print_float(float arr2[], int sz)
{
int i = 0;
for (i = 0; i < sz; i++)
{
printf("%.1f ",arr2[i]);
}
printf("\n");
}
void test3()
{
float arr2[] = { 4.0, 5.0, 1.0, 2.0, 3.0, 9.0 };
int sz = sizeof(arr2) / sizeof(arr2[0]);
int i = 0;
qsort(arr2, sz, sizeof(arr2[0]), cmp_float);
//打印
print_float(arr2, sz);
}
int main()
{
test3();//排序浮点型数组
return 0;
}
经过这三种示例,我们也熟悉了qsort函数的使用了。
那这个库函数究竟是如何实现的呢?接下来我们就模拟实现qsort排序任意类型的排序函数- - - – - BubbleSort
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
struct stu
{
char name[20];
int age;
};
int cmp_int(const void*e1, const void*e2)
{
return (*(int*)e1) - (*(int*)e2);
}
int cmp_by_name(const void*e1,const void*e2)
{
return strcmp(((struct stu*)e1)->name, ((struct stu*)e2)->name);
}
int cmp_by_age(const void*e1, const void*e2)
{
return ((struct stu*)e1)->age - ((struct stu*)e2)->age;
}
//交换函数
void Swap(char* buf1, char* buf2, int width)
{
int i = 0;
for (i = 0; i < width; i++)//一个一个字节进行交换
{
char tmp = *buf1;
*buf1 = *buf2;
*buf2 = tmp;
buf1++;
buf2++;
}
}
//使用回调函数实现一个通用的冒泡排序函数
void BubbleSort(void* base, size_t num, size_t width, int (*cmp)(const void*e1, const void*e2))
{
size_t i = 0;
//趟数
for (i = 0; i < num - 1; i++)
{
//比较的对数
size_t 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);//只有一个一个字节才可以完成交换
}
}
}
}
//测试自定义的BubbleSort()排序整型数组
void test3()
{
int arr[] = { 9,8,7,6,5,4,3,2,1,0 };
int sz = sizeof(arr) / sizeof(arr[0]);
BubbleSort(arr, sz, sizeof(arr[0]), cmp_int);
//打印
int i = 0;
for (i = 0; i < sz; i++)
{
printf("%d ", arr[i]);
}
printf("\n");
}
//测试自定义的BubbleSort() 排序结构体
void test4()
{
struct stu s[3] = { {"zhangsan", 15}, {"lisi", 30},{"wangwu", 10} };
int sz = sizeof(s) / sizeof(s[0]);
//按照名字排序
BubbleSort(s, sz, sizeof(s[0]), cmp_by_name);
//打印
int i = 0;
printf("\n按名字排序:\n");
for (i = 0; i < sz; i++)
{
printf("%s,%d\n", s[i].name, s[i].age);
}
printf("\n");
//按照年龄来排序
BubbleSort(s, sz, sizeof(s[0]), cmp_by_age);
//打印
printf("按年龄排序:\n");
for (i = 0; i < sz; i++)
{
printf("%s,%d\n", s[i].name, s[i].age);
}
printf("\n");
}
int main()
{
//测试自定义的BubbleSort();
test3();//测试排序整型数组
test4();//排序结构体数据
return 0;
}
从两个测例中我们可以发现,我们是将一个函数名作为参数传给一个函数指针,在由函数指针去调用