1.函数指针
上一篇的指针博客中,我们讲了数组指针——存放数组地址的指针。
那函数在内存中是否有地址呢?
我们来看一组代码:
#include <stdio.h>
void test()
{
printf("函数指针\n");
}
int main()
{
test();
printf("%p\n", test);
printf("%p\n", &test);
return 0;
}
我们可以直观的看到,函数是有地址的。并且,无论是否有&符号,地址总是一样的。
按照惯例,我们现在要将地址存储在指针变量当中。
#include <stdio.h>
void test()
{
printf("函数指针\n");
}
int main()
{
test();
void (*p1)() = test;
void (*p2)() = &test;
if (p1 == p2)
printf("相同\n");
return 0;
}
可以看到,我们将单独的 test 的地址存储在 p1 指针变量中;将单独的 &test 的地址存储在 p2 指针变量中。为了验证 p1 与 p2 内存储的地址相同,我们添加了一条 if 语句。
由此进一步证明 test 与 &test 是相同的,无差别的。
现在需要着重分析:为什么函数指针要这样写?
我们可以发现,函数指针与数组指针非常类似,这里就不再赘述。
我们需要注意的是,在 main 函数里面的定义的函数指针,是一个指针,它是指向具有 void 类型的返回值(无返回值),并且无参数的 test 函数。
一个问题: void * p () 正确吗?
了解了函数指针的创建,接下来我们聊聊如何使用。
阅读这组代码:
#include <stdio.h>
//定义test函数
void test()
{
printf("函数指针\n");
}
int main()
{
//将test函数的地址存放在p指针中
void (*p) () = &test;
//???
(*p) ();
return 0;
}
其实不难理解,注释 “???” 部分的那条语句正是函数指针的使用。
我们分析一下:我们将 test 函数的地址存放在函数指针p中,p解引用 (*p)得到函数,函数需要参数,所以就写成了 (*p) () ;
我们将 (*p) 的括号去掉行吗?不行,还是结合性问题,去掉了括号,意义就变了。
1.1函数指针拓展
1)阅读这串代码:(* (void (*) () ) 0 ) () ;
2) 阅读这串代码:void (* signal(int ,void (*) (int) ) ) (int);
这样实际上我们还不是很好理解,我们运用一个关键字——typedef。
#include <stdio.h>
typedef void(*pf)(int);//将 void(*)(int) 这个函数指针类型重命名为 pf
int main()
{
void(*signal(int, void(*)(int)))(int);
//重命名之后可以写成:
pf signal(int, pf);
//可以看到这是一次函数声明,void(*)(int) 作为signal函数的返回值
return 0;
}
事实上我们重命名之后我们会发现一个矛盾:在重命名之前我们为什么要把 signal函数写在里面?直接写在外面不就好了吗?
#include <stdio.h>
//typedef void(*pf)(int);//将 void(*)(int) 这个函数指针类型重命名为 pf
int main()
{
void(*)(int) signal(int, void(*)(int));
//void(*signal(int, void(*)(int)))(int);
//重命名之后可以写成:
//pf signal(int, pf);
//可以看到这是一次函数声明,void(*)(int) 作为signal函数的返回值
return 0;
}
这不是更好理解吗?事实上编译器会报错。
所以说,我们可以那么理解,但是因为语法不支持,要用一种“不科学”的表示方法描述。
以上两个例子均出自——《C陷阱和缺陷》。
2.函数指针数组
这个就与上一篇的博客十分类似啦,数组的定义我也不赘述了,指针数组我也不赘述了。
我们先来做一个判断题:
#include <stdio.h>
//加
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("************************\n");
}
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("%d+%d=%d\n", x, y, ret);
break;
case 2:
printf("请输入两个操作数:>");
scanf("%d%d", &x, &y);
ret = Sub(x, y);
printf("%d-%d=%d\n", x, y, ret);
break;
case 3:
printf("请输入两个操作数:>");
scanf("%d%d", &x, &y);
ret = Mul(x, y);
printf("%d*%d=%d\n", x, y, ret);
break;
case 4:
printf("请输入两个操作数:>");
scanf("%d%d", &x, &y);
ret = Div(x, y);
printf("%d/%d=%d\n", x, y, ret);
break;
default:
printf("输入有误\n");
break;
}
} while (input);
return 0;
}
我们模拟实现一个简单的计算器。
但是大家可以看到,这组代码非常多,虽然逻辑简单,但是看起来就是很low,不能拿出去装逼。
我们就要在这个基础上,通过函数指针数组实现。
原理是什么?我们可以看到独立的加、减、乘、除这四个函数的返回值,参数都一样,我们可以把这些函数存放在数组中,通过数组下标找到对应的函数。
我们改进的代码:
#include <stdio.h>
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;
int x = 0;
int y = 0;
int ret = 0;
int (*parr[5])(int , int ) = { 0,Add,Sub,Mul,Div };//转移表
do
{
printf("************************\n");
printf("***1.Add 2.Sub*******\n");
printf("***3.Mul 4.Div*******\n");
printf("************************\n");
printf("请选择:>");
scanf("%d", &input);
if (input >= 1 && input <= 4)
{
printf("请输入两个操作数:>");
scanf("%d%d", &x, &y);
ret = (*parr[input])(x, y);
}
else
printf("输入有误\n");
printf("ret=%d\n", ret);
} while (input);
return 0;
}
大家可以对比一下前后的代码。第二组代码明显更加简洁,并且阅读起来非常也并不困难。实质操作只是把加、减、乘、除这几个独立的函数都存入函数指针数组中 parr[5] ,除了0以外(我们规定0是退出程序),剩余四个函数我们都可以通过数组下标找到, 这也是函数指针数组的高端操作——转移表。
运行起来的效果除了输出没有运算符号了,其他都一摸一样。
2.2指向函数指针数组的指针
这个概念非常绕口, 但事实上并不复杂。我们看一组代码。
#include <stdio.h>
void test()
{
printf("指向函数指针数组的指针\n");
}
int main()
{
//把test函数地址存放到函数指针变量当中
void (*pf)() = &test;
//创建一个大小为1的函数指针数组,并把test函数放进去
void (*pfarr[1])() = { test };
//把函数指针数组的地址存放到指向函数指针数组的指针变量当中
void (*(*ppfarr)[1])() = &pfarr;
return 0;
}
实际上,这种方式是很少使用的,我们只需要知道有这个东西就好了。
3.回调函数
我们解释一下什么是回调函数:
qsort 函数的作用是对数组元素进行排序,不管是什么类型的数据。可以用通过 qsort 函数的参数使用了多个 void 确定。
我们来分析一下各个参数的意义。
那么参数里面的函数指针指向谁呢?
cplusplus给出解释:
我们进一步补充:
当 p1 p2 两个指针指向的元素相减,大于0返回1;等于0返回0;小于0返回-1;
p1 p2 的类型都是 void* 类型的,在比较前需要强制类型转换。
我们举几个 qsort 函数对不同数据进行排序的例子:
1)对整型数据排序
#include <stdio.h>
int cmp(const void* p1, const void* p2)
{
return *(int*)p2 - *(int*)p1;//后一个元素大于第一个元素,返回1,qsort内部进行交换
}
int main()
{
int arr[10] = { 1,2,6,5,4,7,3,8,9,0 };
qsort(arr, 10, sizeof(int), cmp);
int i = 0;
for (i = 0; i < 10; i++)
{
printf("%d ", arr[i]);
}
return 0;
}
2)对字符型数据排序
#include <stdio.h>
int cmp(const void* p1, const void* p2)
{
return *(char*)p2 - *(char*)p1;//后一个元素大于第一个元素,返回1,qsort内部进行交换
}
int main()
{
char arr[10] = "abcdefghij";
qsort(arr, 10, sizeof(char), cmp);
int i = 0;
for (i = 0; i < 10; i++)
{
printf("%c ", arr[i]);
}
return 0;
}
3)对结构体数据排序
#include <stdio.h>
struct Stu
{
char name[20];
int age;
};
int cmp(const void* p1, const void* p2)
{
return ((struct Stu*)p2)->age - ((struct Stu*)p1)->age;
}
int main()
{
struct Stu arr[3] = { {"zhangsan",20},{"lisi",17},{"wangwu",33} };
qsort(arr, 3, sizeof(struct Stu), cmp);//按年龄排序
int i = 0;
for (i = 0; i < 3; i++)
{
printf("%d ", arr[i].age);
}
return 0;
}
#include <stdio.h>
#include <stdlib.h>
struct Stu
{
char name[20];
int age;
};
int cmp(const void* p1, const void* p2)
{
return strcmp(((struct Stu*)p2)->name , ((struct Stu*)p1)->name);
}
int main()
{
struct Stu arr[3] = { {"zhangsan",20},{"lisi",17},{"wangwu",33} };
qsort(arr, 3, sizeof(struct Stu), cmp);//按名字比较
int i = 0;
for (i = 0; i < 3; i++)
{
printf("%s,%d ", arr[i].name, arr[i].age);
}
return 0;
}
那我们为了进一步理解回调函数,我们自己来模拟一个qosrt对整型排序的例子。用冒泡排序的思维。
#include <stdio.h>
int cmp(const void* p1, const void* p2)
{
return *(int*)p2 - *(int*)p1;
}
void Swap(char* p1, char* p2,int width)
{
//因为传进来的char*类型,对应的元素只有一个字节
//我们需要知道外部元素的真正大小,所以需要把width也传进来
//实现元素之间一个字节一个字节的交换
int i = 0;
for (i = 0; i < width; i++)
{
char tmp = *p1;
*p1 = *p2;
*p2 = tmp;
p1++;
p2++;
}
}
void my_qsort(void* base, int num, int width, int (*cmp)(const void*, const void*))
{
int i = 0;
int j = 0;
for (i = 0; i < num - 1; i++)
{
for (j = 0; j < num - 1 - i; j++)
{
//先将元素强转char*类型(指向的对象只有1字节)再乘以元素大小
//当main函数的数组不是int类型的时候,我们不需要进到qsort来进行修改
//增强了函数的可移植性
if ( (cmp ( (char*)base + j * width , (char*)base + (j + 1) * width ) ) >0)
{
//定义一个交换函数实现元素的交换
Swap((char*)base + j * width, (char*)base + (j + 1) * width,width);
}
}
}
}
int main()
{
//创建需要操作的数组
int arr[10] = { 0,9,8,6,5,4,2,1,3,7 };
//模拟实现qsort,用冒泡排序的思维
my_qsort(arr, 10, sizeof(int), cmp);
//打印
int i = 0;
for (i = 0; i < 10; i++)
{
printf("%d ", arr[i]);
}
return 0;
}
4.结束语
以上就是对函数指针及其进阶的一小部分的使用与分析。
个人水平有限可能会解释不清、定义模糊、语言逻辑牛头不对马嘴的现象。
以上代码的思想不是很深,我本人对此指针的理解也不是很深,基本上是面向初学者的。
希望进阶学者不要贬低。
有问题欢迎大家积极评论区留言!