目录
注:
函数指针
调用函数指针
有趣的代码
函数指针数组
函数指针数组的应用
指向函数指针数组的指针
回调函数
例子:改进计算器
qsort函数(头文件:)
分析
使用例
qsort函数实现降序
模拟实现
小知识(空指针void)
本笔记参考B站up鹏哥C语言的视频
||| 顾名思义,函数指针就是指向函数的指针,即存放函数地址的指针。
首先观察函数的地址
int Add(int x, int y)
{
return x + y;
}
int main()
{
//&函数名 - 取到的就是函数的地址
printf("%p\n", &Add);
printf("%p\n", Add);
return 0;
}
通过%p打印地址,结果发现函数确实存在地址。
注意:此处对Add函数是否取地址(&),并没有影响打印结果。即存在 函数名 == &函数名 。(函数没有首元素的地址这种说法)
想要把函数存入指针内,指针的类型必须和函数匹配。
int(*pf)(int, int) = &Add;
这里的 pf 就是一个函数指针变量。
举一反三
void test(char* str)
{}
int main()
{
void (*pt)(char*) = &test;
return 0;
}
int Add(int x, int y)
{
return x + y;
}
int main()
{
int(*pf)(int, int) = &Add;
int ret = (*pf)(3, 5);
printf("%d\n", ret);
return 0;
}
这就是函数指针调用的一种形式(即对pf进行解引用),输出结果:8。同时,由于取地址(&)可以去掉,所以指针调用也可以写成 int(*pf)(int, int) = Add; 的形式。
从上面可以得出一个结论:Add == pf
这时候再倒回来看最开始调用函数的方式:
int ret = Add(3, 5);
由结论猜想在该函数调用中,是否可以把 Add 换成 pf ?
int ret = pf(3, 5);
结果程序顺利运行。所以由结论引申出:pf == (*pf)
实际上,无论这里的 * 是否存在,乃至数量多少,存在 pf == (**pf)(** = n个*)。即 * 在这里没有实际运算上的意义。
注意:
如*pf(3, 5);这种写法是不行的,因为pf(3, 5)已经完成了对函数的调用,* 在这里是对8进行解引用操作,显然不行。
推荐书籍:《C陷阱和缺陷》 ||| 这本书中提及下面两个代码。
//代码1
(*(void (*) () )0)();
代码意思:调用0地址(首地址是0)处的函数,被调用的函数无参,返回类型是void。
分析:
||| 0是一个数字,本身不能是一种地址。通过把0强制转换成函数指针类型,就可以把0看作一个地址。比如:(char) 0; 把0强制转换成了char类型。
再回来看代码1本身,( (void (*) () )0 - 就是把0强制转换成(被解释成)函数指针类型。此时0这个地址内就 存放着函数 (void (*) () 。
而如果现在要调用放在0这个地址内部的函数,就可以解引用:( * ( (void (*) () )0 ) ( ); (最后面的( )要和函数的参数部分匹配,原函数无参,所以此处不写参数)
ps:由于Windows把0地址都分配给了系统文件,所以平时是不可以调用的。
---
//代码2
void (*signal(int, void(*)(int)))(int);
代码意思:声明了一次signal函数,这个函数返回类型是void (*)(int),有两个参数 - int 和 一个函数指针。
分析:
对signal(int, void(*)(int))
- 首先,signal先和( )结合,说明signal是个函数名。
- 其次,对这个函数而言,第一个参数的类型是int,第二个参数的类型是函数指针。(注意:void(*)(int) 内部的(*)内并没有出现变量名,由此可知这是一个函数指针类型的参数。而这个函数指针,指向的是一个参数是int,返回类型是void的函数。)
现在,函数名和函数的参数已经得到了讨论,还缺少函数的返回类型。
回来看去掉已经讨论过部分的原代码:void (*)(int) 这是一个函数指针类型。结合上文,说明这就是signal函数的返回类型。
综上所述,signal是一次函数的声明。
如果进行简化:void (*)(int) signal(int, void(*)(int)) 这种写法语法是不支持的。函数类型是指针的话,* 必须和函数名和函数参数的部分连在一起。
语法允许的简化:
typedef - 对类型进行重定义/重命名,如:
typedef void (*pfun_t) (int);//对void(*)(int)的函数指针类型重命名为pfun_t
typedef unsigned int uint;//对unsigned int 的整型类型重命名为uint
所以,函数可以简化为
typedef void (*pfun_t) (int);
pfun_t signal(int, pfun_t);
根据学过的知识:
//整型指针 int*
//整型指针数组 int* arr[5]
由以上可知:
函数指针 - 存放函数指针的数组。
int Add(int x, int y)
{
return x + y;
}
int Sub(int x, int y)
{
return x - y;
}
int main()
{
int (*pf_1)(int, int) = Add;
int (*pf_2)(int, int) = Sub;
int (*pf_Arr[2])(int, int) = { Add, Sub };
return 0;
}
上述代码中的 pf_Arr 就是一个函数指针数组。
即:在函数指针数组内,可以存放多个同类型的函数指针。
想看这串代码,写一个计算器,要实现整型的加减乘除:
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 menu()
{
printf("************************\n");
printf("**** 1.add 2.sub ****\n");
printf("**** 3.mul 4.div ****\n");
printf("**** 0.exit ****\n");
printf("************************\n");
}
int main()
{
//计算器 - 计算整型变量的加、减、乘、除
int input = 0;
do
{
menu();
int x = 0;
int y = 0;
int ret = 0;
printf("请选择:");
scanf("%d", &input);
printf("请输入2个操作数:");
scanf("%d %d", &x, &y);
switch (input)
{
case 1:
ret = Add(x, y);
break;
case 2:
ret = Sub(x, y);
break;
case 3:
ret = Mul(x, y);
break;
case 4:
ret = Div(x, y);
break;
case 0:
printf("退出程序\n");
break;
default:
printf("选择错误,请重新选择\n");
break;
}
printf("ret = %d\n", ret);
} while (input);
return 0;
}
这串代码存在问题,会发生:
这就存在问题了:用户明明是输入错误和要求退出,程序却还是要求用户输入数据。
既然如此,要怎么更改代码呢?
比如这样:
case 1:
printf("请输入2个操作数:");
scanf("%d %d", &x, &y);
ret = Add(x, y);
break;
这样写,确实是一种方法,但是这样的代码存在缺陷:
int main()
{
int input = 0;
do
{
menu();
int x = 0;
int y = 0;
int ret = 0;
printf("请选择:");
scanf("%d", &input);
if (input >= 1 && input <= 4)
{
printf("请输入2个操作数:");
scanf("%d %d", &x, &y);
int (*pf_Arr[5])(int, int) = { NULL, Add,Sub,Mul,Div };
//pf_Arr就是函数指针数组
ret = (pf_Arr[input])(x, y);
printf("ret = %d\n", ret);
}
else if (input == 0)
{
printf("退出程序\n");
}
else
{
printf("输入错误,请重新输入\n");
}
} while (input);
return 0;
}
在上述程序中,int (*pf_Arr[5])(int, int) = { NULL, Add,Sub,Mul,Div } 的应用被称为转移表 (来自《C和指针》)
整型数组
int arr_1[5];
int(*p1)[5] = &arr_1;整型指针的数组
int* arr_2[5];
int* (*p2)[5] = &arr_2;
p2是指向【整型指针数组】的指针
函数指针的数组是一个数组,取出函数指针数组的地址:
int(*p)(int, int); 函数指针
int(*p3[4])(int, int); 函数指针的数组
&p3 - 取出的是函数指针数组的地址。
int(* (*p3)[4])(int, int) = &p3;
p3就是一个指向【函数指针数组】的指针
void test(const char* str)
{
printf("%s\n", str);
}
int main()
{
//函数指针 p_fun
void (*p_fun)(const char*) = test;
//函数指针的数组 p_fun_Arr
void (*p_fun_Arr[5])(const char*);
*p_fun_Arr = test;
//指向函数指针数组 p_fun_Arr 的指针 pp_fun_Arr
void (*(*pp_fun_Arr)[10])(const char*) = &p_fun_Arr;
return 0;
}
||| 回调函数就是一个通过函数指针调用的函数。如果你把函数的指针(地址)作为参数传递给另一个函数,当这个函数被用来调用其所指向的函数时,我们就说这是回调函数。回调函数不是由该函数的实现方直接调用,而是在特定的事件或者条件发生时由另外的一方调用的,用于对该事件或条件进行响应。
新建并使用Calc函数
int Calc(int(*pf)(int, int))//参数是一个函数指针
{
int x = 0;
int y = 0;
printf("请输入2个操作数:");
scanf("%d %d", &x, &y);
return pf(x, y);
}
具体使用例:
int Add(int x, int y)
{}
int Sub(int x, int y)
{}
int Mul(int x, int y)
{}
int Div(int x, int y)
{}
void menu()
{}
int Calc(int(*pf)(int, int))
{
int x = 0;
int y = 0;
printf("请输入2个操作数:");
scanf("%d %d", &x, &y);
return pf(x, y);
}
int main()
{
int input = 0;
do
{
menu();
int ret = 0;
printf("请选择:");
scanf("%d", &input);
switch (input)
{
case 1:
ret = Calc(Add);
printf("ret = %d\n", ret);
break;
case 2:
ret = Calc(Sub);
printf("ret = %d\n", ret);
break;
case 3:
ret = Calc(Mul);
printf("ret = %d\n", ret);
break;
case 4:
ret = Calc(Div);
printf("ret = %d\n", ret);
break;
case 0:
printf("退出程序\n");
break;
default:
printf("选择错误,请重新选择\n");
break;
}
printf("ret = %d\n", ret);
} while (input);
return 0;
}
通过回调函数,可以减少冗余。
库函数 - qsort ||| 快速排序
qsort函数的原型(包含了4个参数):
值得注意的是:qsort函数排序时可以接收所有类型的函数
||| 对于 void* base :base中存放的是待排序的数据中的第一个对象的地址。使用 void* 可以承接不同数据类型。
------
||| 对于 size_t num :待排序的数据元素的个数。
------
||| 对于 size_t size :一个元素字节所占空间大小,单位是字节。qsort函数在进行排序时,并没有限制排序元素所占空间的大小,而不同的数据类型之间存在差异。
------
||| 对于 int (*compar)(const void*, const void*) :比较待排序数据的不同元素的函数(返回类型是整型),函数的第一个参数传入要比较的 第一个元素 的地址,第二个函数传入的是要比较的 第二个元素 的地址,同样,函数内的 void 类型是为了接收各种类型的元素。
此处,对于被调用的比较函数要求返回三种情况,分别是:>0 =0 <0
参考冒泡排序
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;
}
}
}
}
比如:如果我们要求是比较字符串类型的数据,可以通过 strcmp函数 比较字符串的大小,这时候最方便的方法就是更改两个不同元素的比较方法:
即原本 if (arr[j] > arr[j + 1]) 内部比较数组的方式,改为使用 strcmp函数 。
排序整型
#include //printf
#include//qsort
void print_arr(int arr[], int sz)
{
int i = 0;
for (i = 0; i < sz; i++)
{
printf("%d ", arr[i]);
}
}
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]);
//排序
qsort(arr, sz, sizeof(arr[0]), cmp_int);
//打印
print_arr(arr, sz);
return 0;
}
排序结构体(按照年龄排序)
#include//qsort
struct Stu
{
char name[20];
int age;
};
int sort_by_age(const void* e1, const void* e2)
{
return ((struct Stu*)e1)->age - ((struct Stu*)e2)->age;
}
int main()
{
//使用qsort函数排序结构体数据
struct Stu s[] = { {"zhangsan", 30}, {"lisi", 34}, {"wangwu", 20}};
int sz = sizeof(s) / sizeof(s[0]);
//按照年龄排序
qsort(s, sz, sizeof(s[0]), sort_by_age);
}
启动调试,完成排序,监视
---
排序结构体(按照名字排序)
#include//qsort函数
#include//strcmp函数
struct Stu
{
char name[20];
int age;
};
int sort_by_name(const void* e1, const void* e2)
{
return strcmp(((struct Stu*)e1)->name, ((struct Stu*)e2)->name);
}
int main()
{
//使用qsort函数排序结构体数据
struct Stu s[] = { {"zhangsan", 30}, {"lisi", 34}, {"wangwu", 20}};
int sz = sizeof(s) / sizeof(s[0]);
//按照名字排序
qsort(s, sz, sizeof(s[0]), sort_by_name);
}
启动调试,完成排序,监视(顺序应该是 l > w > z)
ps:
字符串比较是对于位置的ASCII值进行比较,这和长度无关。
只需要对比较函数的两个参数进行位置交换即可。如:
int cmp_int(const void* e1, const void* e2)
{
return *(int*)e1 - *(int*)e2;
}
这段代码原本是*(int*)e1 - *(int*)e2 ,对应的qsort函数实现的是 升序 。
只要改成 return *(int*)e2 - *(int*)e1 ,即可使qsort函数实现 降序 。(原本应该传回>0的情况传回了<0,<0的情况传回了>0)
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++;
}
}
//模仿qsort实现一个冒泡排序的通用算法
void bubble_sort(
void* base,
int sz,
int width,
int (*cmp)(const void* e1, const void* e2)
)
{
int i = 0;
//趟数
for (i = 0; i < sz - 1; i++)
{
//一趟排序
int j = 0;
for (j = 0; j < sz - i - 1; j++)
{
//两个元素进行比较
if (cmp((char*)base + j * width,
(char*)base + (j + 1) * width) > 0)
{
//交换
Swap((char*)base + j * width,
(char*)base + (j + 1) * width,
width);
}
}
}
}
bubble_sort函数模拟实现。
比较部分(if语句)
对于传来的数据,需要寻找 e1 和 e2 的地址。注意:此时base是无类型指针。
这种方式是对 base 强制类型转换成 char*类型,在加上单个元素所占空间大小 width 得到 (char*)base + width。所以比较部分代码为:
if (cmp((char*)base + j * width, (char*)base + (j + 1) * width) > 0)
通过 j 和 j + 1 寻找任意两个相邻的元素。这就是函数指针实现回调函数。
交换部分(Swap函数)
还是通过寻找函数位置找到任意相邻的两个元素。要注意的是:交换部分是通过交换 每一个字节的内容(char*类型 +1 就是操作1个字节的内容)来实现两个元素的交换。
Swap((char*)base + j * width,
(char*)base + (j + 1) * width,
width);
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,存在:
int main() { int a = 10; char ch = 'w'; void* p = &a; p = &ch; return 0; }
以上操作均可实现。但是,对p进行解引用操作*p,或者p++,这些都是是无法实现的。会出现错误:
*p时报错
p++时报错
毕竟void*类型的指针到底访问几个字节是不知道的。