【C语言进阶技巧】指针掌握之道:深入挖掘指针的无尽潜力(第二部)
【C语言进阶技巧】指针掌握之道:深入挖掘指针的无尽潜力(第二部))
❤️博客主页: 小镇敲码人
🍏 欢迎关注:👍点赞 👂🏽留言 😍收藏
🌞回来5天了,加油!!!🍎🍎🍎
💗疲倦的生活里,总要有些温柔的梦想,愿一切真心不被辜负,愿一切努力终有收获,愿一切如你所愿。✡️✡️✡️
1. 函数指针数组
函数指针数组,顾名思义,即存放函数的地址的数组,这个数组的类型是函数指针,在指针的进阶(一)中我们学习了函数指针,它就是让*号和变量名先去结合,使变量变为指针类型,而如果[]和变量名先结合,从而这就是一个数组了。
1.1 函数指针数组的创建和初始化
下面是一个函数
int Add(int x, int y)
{
return x + y;
}
这是函数指针:
int (*pf)(int,int) = Add;
这是函数指针数组:
int (*ppf[])(int,int) = {Add};
我们可以通过类比其他类型的数组来理解,比如int
型指针数组int* arr[] = {NULL};
我们可以知道指针数组去掉[]
和数组名就是就是数组每个元素的类型,去掉之后就是int*
,所以这是一个int*
类型的数组。
上述函数指针数组,去掉[]
和数组名为,int (*)(int,int)
所以上述数组每个元素的类型是一个函数指针,它有两个int
型的参数,返回值也是int
。
阅读如下代码,帮助你更好的理解函数指针数组:
#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 (*pf1)(int, int) = Add; // 定义函数指针并初始化为 Add 函数的地址
int (*pf2)(int, int) = Sub; // 定义函数指针并初始化为 Sub 函数的地址
int (*pf3)(int, int) = Mul; // 定义函数指针并初始化为 Mul 函数的地址
int (*pf4)(int, int) = Div; // 定义函数指针并初始化为 Div 函数的地址
// 定义函数指针数组,存储四个函数的地址
int (*pfArr[4])(int, int) = { pf1, pf2, pf3, pf4 };
// 或者 int (*pfArr[4])(int, int) = { Add, Sub, Mul, Div };
return 0;
}
- 注意:通过指针的进阶(一)我们已经知道&函数名和函数名,
%p
打印出来的地址是一样的,所以函数指针数组进行初始化这两种编译器都是允许的,但是数组指针不行,会发生类型不匹配的问题,像vs2019的编译器下会报出警告,如图:
而函数指针数组的初始化,不管是否在函数名前加&
,vs2019编译器下都是不报警告的。
- 不加
&
符号:
- 加
&
符号:
总结:1. 函数指针数组是数组,数组每个元素类型是函数指针类型。
2.*
号的优先级没有[]
高,所以没有()
时[]
先和变量名结合,该变量就成了数组类型。
1.2 函数指针数组的应用
如果我们想用C语言实现一个简单的整数计算器(加减乘除)在不了解函数指针数组前,可能我们会这样去实现:
#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("******* 0.exit ******\n");
printf("**********************************\n");
}
int main() {
int x = 0;
int y = 0;
int ret = 0;
int input = 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");
break;
}
} while (input); // 当 input 不为 0 时继续循环
return 0;
}
通过测试这段代码实现加减乘除功能是可行的,但是存在一定的问题:
- 代码冗余
- 后续想增加功能,会出现很多的
case
语句。
考虑使用函数指针数组实现,请看如下代码:
#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("******* 0.exit ******\n");
printf("**********************************\n");
}
int main() {
int x = 0;
int y = 0;
int ret = 0;
int input = 0;
// 函数指针数组的使用 -> 转移表
int (*pfArr[5])(int, int) = {NULL, Add, Sub, Mul, Div};
// 0 1 2 3 4
do {
menu();
printf("请选择:>");
scanf("%d", &input);
if (input >= 1 && input <= 4) {
printf("请输入两个操作数:");
scanf("%d %d", &x, &y);
ret = pfArr[input](x, y); // 使用函数指针调用对应的函数
printf("ret = %d\n", ret);
}
else if (input == 0) {
printf("退出计算器\n");
}
else {
printf("选择错误,重新选择\n");
}
} while (input);
return 0;
}
- 使用函数指针数组就省了很多不必要的冗余代码,后续想要添加其它的计算功能,直接添加函数定义,修改函数指针数组的初始化部分和
input
的范围就行。
2. 指向函数指针数组的指针
有没有感觉像是套娃呢?没错,你感觉对了🤪回到正题,指向函数指针数组的指针,它是一个指针,指向的对象的类型函数指针数组,可类比
int []
型的数组指针理解。
这是一个int
型的数组:
int a[10] = {0};
a
的类型为:int [10]
这是指向这个数组的指针:
int (*p) [10] = &a;
同理,这是一个函数指针类型的数组:
int (* pf[4])(int,int);
pf
的类型为int (* [4])(int,int)
这是指向这个函数指针数组的指针:
int(*(*ppf)[4])(int,int) = &pf;
- 细心的小伙伴已经发现,不管是定义指向整数数组的指针还是定义指向函数指针数组的指针,都只需要在原先数组类型的基础上加上一个
(*指针的变量名)
就行了。
3. 回调函数
当我们在定义函数时,如果其中一个参数是函数指针的类型时,实参传的是函数的地址,当通过函数指针调用其指向的函数,那个被调用的函数,我们就称之为回调函数。回调函数不是该函数的实现方直接调用,而是在特定的事件或条件发生时由另外一方调用,用于对该事件的或条件进行响应。
下面通过qsort
函数的例子,来帮助我们更好理解回调函数
3.1 qsort函数的定义及参数说明
3.1.1 qsort函数定义
qsort
函数是C语言里面的一种排序函数,它是标准库函数'stdlib.h'
的一部分,提供了一种通用的排序机制,可以用于各种类型的排序。
3.1.2 参数说明
这里引用cplusplus.com网站上对qsort
函数的声明,来让读者了解我们在使用qsort
函数时应该传什么参数。
- 参数一:
base
代表要排序的那个数组。 - 参数二 :
num
代表要排序的元素的个数。 - 参数三:
size
代表排序的每个元素的内存大小(以字节为单位)。 - 参数四:
compar
是一个指向函数的指针。由于类型不同,比较方式也不同(例如,整数的大小比较与字符串的大小比较),所以比较函数需要程序员自己实现,标准函数不提供比较函数,我们把这种专门用于比较的函数又称为比较器,这里的compar
指向的函数就是我们所说的回调函数。
3.1.3 利用标准库函数qsort排序函数
这里先对cmpar
所指向的函数做一下阐述,因为这是需要我们自己设计的比较器,下面是cplusplus文档的一些阐述:
下面我们将使用qsort标准函数对指针和结构体进行排序,我们对一些地方做一下阐释:
3.1.3.1 比较器的实现
void*
指针强制转换为用户需要比较的元素指针类型后,要先强制转换为相应元素的类型才可以使用,我们这里是升序所以是前者减后者(qsort
函数内部所决定),如果大于0就交换。- 如果想要设计降序的比较器将两个变量的顺序交换就可以了,同样是返回大于0就交换。
3.1.3.1.1 整数比较器
int cmp_int(const void* a,const void* b)
{
return *(int*)a - *(int*)b;
}
3.1.3.1.2 结构体字符串比较器
- 字符串比较在我们的标准库
'string.h'
中有专门的比较函数strcmp
。
strcmp
字符串比较函数有两个参数,类型都是常量字符串,返回值是int
类型。
简单点来说就是:
- 当字符串1和字符串2相等时,返回值为0。
- 当字符串1与字符串2开始出现不相等字符时,将字符转换为ASCII码值,如果字符串1更大返回大于0的数,反之返回小于0的数。
- 细心的佬可能会发现这和我们整数的比较规则是相似的,即a如果大于b,返回一个大于0的数,当得到的数大于0时,
qsort
函数内部就会交换,下面代码采用升序的方式:
int cmp_stu_by_name(const void* a, const void* b)
{
return strcmp(((struct Stu*)a)->name, ((struct Stu*)b)->name);
}
完整代码:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
// 定义结构体 Stu
struct Stu {
char name[20];
int score;
};
// 按照 score 升序排序的比较函数
int cmp_stu_by_score(const void* a, const void* b) {
return ((struct Stu*)a)->score - ((struct Stu*)b)->score;
}
// 打印按照 score 升序排序的结构体数组
void print1(struct Stu arr1[], int sz) {
printf("按照成绩升序排序为:\n");
for (int i = 0; i < sz; i++) {
printf("%s %d\n", arr1[i].name, arr1[i].score);
}
}
// 测试排序结构体按照 score 升序排序
void test1() {
struct Stu arr1[] = { {"zhangsan", 86}, {"lisi", 90}, {"wangwu", 66} };
int sz = sizeof(arr1) / sizeof(arr1[0]);
qsort(arr1, sz, sizeof(arr1[0]), cmp_stu_by_score);
print1(arr1, sz);
}
// 按照 name 升序排序的比较函数
int cmp_stu_by_name(const void* a, const void* b) {
return strcmp(((struct Stu*)a)->name, ((struct Stu*)b)->name);
}
// 打印按照 name 升序排序的结构体数组
void print2(struct Stu arr2[], int sz) {
printf("按照名称升序排序为:\n");
for (int i = 0; i < sz; i++) {
printf("%s %d\n", arr2[i].name, arr2[i].score);
}
}
// 测试排序结构体按照 name 升序排序
void test2() {
struct Stu arr2[] = { {"zhangsan", 86}, {"lisi", 90}, {"wangwu", 66} };
int sz = sizeof(arr2) / sizeof(arr2[0]);
qsort(arr2, sz, sizeof(arr2[0]), cmp_stu_by_name);
print2(arr2, sz);
}
// 比较函数,用于整型数组的升序排序
int cmp_int(const void* a, const void* b) {
return *(int*)a - *(int*)b;
}
// 打印升序排序的整型数组
void print3(int* arr3, int sz) {
printf("数组升序排序:\n");
for (int i = 0; i < sz; i++) {
printf("%d ", *(arr3 + i));
}
}
// 测试整型数组升序排序
void test3() {
int arr3[] = { 10, 9, 8, 3, 6, 5, 4, 7, 1, 2 };
int sz = sizeof(arr3) / sizeof(arr3[0]);
qsort(arr3, sz, sizeof(arr3[0]), cmp_int);
print3(arr3, sz);
}
int main() {
test1();
test2();
test3();
return 0;
}
运行的结果:
3.1.4 使用回调函数模拟实现qsort(采用冒泡的方式)
对核心代码做一下阐述:
my_qsort
函数的实现:
- 首先要明白为什么
base
是void*
类型的指针,因为qsort
函数可以实现各种排序,而我不知道用户究竟要传一个什么类型的指针,所以只能以用万能指针void*
来接受,后续利用强制转换和一个元素的大小(跨度)等信息找到元素的地址来进行比较和交换。 -
(void*)
指针不能直接进行指针的加减运算,进行加减运算的目的是得到不同元素的地址,如果使用(void*)
指针进行加减运算编译器就不知道要跳几个字节,所以使用前要强制类型转换为元素相应的类型,而且(void*)
类型的指针在使用前也需要进行强制类型转换,因为不管是整数的加减法和还是结构体的->
操作符的使用前提是给出这个变量的类型,才能实现相应变量的功能。
- 我们按照cplusplus文档对qsort函数的声明,设计函数的参数。
- 冒泡排序的思路很简单,下次再一起总结,现在假设读者都已经会了冒泡排序,关键是回调函数
cmpar
的实参如何传?通过上述对比较器的介绍,我们可以知道这个函数的两个参数都是void*
类型,我们只需要传不同元素的地址过去就行了,想得到不同元素的地址,我们就要对指针进行加法(减法)运算,但是我们知道void *
类型的指针不能直接进行加减运算,需要强制转换base
为一个具体的指针类型,那么问题来了,应该强制转换为哪一种类型呢?char*
类型的指针加减1跳的字节数最少只有1个字节,如果我们强制转换的类型比这个大,那么在进行指针的加减时就无法准确得到每个字符元素的地址了(这里涉及指针初阶的内容后续会写,所以我们应该将base
强制转换为char*
类型,然后通过宽度size
和j
的值确定每个元素的地址,比如((char*)base+1*size)
就是第二个元素的地址,通过它可以找到第二个元素。在弄明白这个之后我们swap
函数的实现也应该用char*
的指针来传参,以满足各种类型的需求,然后再传一个元素类型的大小就行了。
代码如下:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
void swap(char* a, char* b, int size) {
for (int i = 0; i < size; i++) {
char temp = *a;
*a = *b;
*b = temp;
a++;
b++;
}
}
void my_qsort(void* base, size_t num, size_t size, int (*cmpar)(void*, void*)) {
for (int i = 0; i < num - 1; i++) {
for (int j = 0; j < num - i - 1; j++) {
if (cmpar((char*)base + j * size, (char*)base + (j + 1) * size) > 0) {
swap((char*)base + j * size, (char*)base + (j + 1) * size, size);
}
}
}
}
// 测试 qsort 排序的结构体数据
struct Stu {
char name[20];
int score;
};
// 按照 score 升序排序的比较函数
int cmp_stu_by_score(const void* a, const void* b) {
return ((struct Stu*)a)->score - ((struct Stu*)b)->score;
}
// 打印按照 score 升序排序的结构体数组
void print1(struct Stu* arr1, int sz) {
printf("结构体按照分数升序排序为:\n");
for (int i = 0; i < sz; i++) {
printf("%s %d\n", (arr1 + i)->name, (arr1 + i)->score);
}
}
// 测试排序结构体数据按 score 升序排序
void test1() {
struct Stu arr1[] = { {"zhangsan", 86}, {"lisi", 90}, {"wangwu", 66} };
int sz = sizeof(arr1) / sizeof(arr1[0]);
my_qsort(arr1, sz, sizeof(arr1[0]), cmp_stu_by_score);
print1(arr1, sz);
}
// 按照 name 升序排序的比较函数
int cmp_stu_by_name(void* a, void* b) {
return strcmp(((struct Stu*)a)->name, ((struct Stu*)b)->name);
}
// 打印按照 name 升序排序的结构体数组
void print2(struct Stu arr2[], int sz) {
printf("结构体按照名称升序排序为:\n");
for (int i = 0; i < sz; i++) {
printf("%s %d\n", arr2[i].name, arr2[i].score);
}
}
// 测试排序结构体按 name 升序排序
void test2() {
struct Stu arr2[] = { {"zhangsan", 86}, {"lisi", 90}, {"wangwu", 66} };
int sz = sizeof(arr2) / sizeof(arr2[0]);
my_qsort(arr2, sz, sizeof(arr2[0]), cmp_stu_by_name);
print2(arr2, sz);
}
// 比较函数,用于整型数组的升序排序
int cmp_int(const void* a, const void* b) {
return *(int*)a - *(int*)b;
}
// 打印升序排序的整型数组
void print3(int* arr3, int sz) {
printf("数组升序排序:\n");
for (int i = 0; i < sz; i++) {
printf("%d ", *(arr3 + i));
}
}
// 测试整型数组升序排序
void test3() {
int arr3[] = { 10, 9, 8, 3, 6, 5, 4, 7, 1, 2 };
int sz = sizeof(arr3) / sizeof(arr3[0]);
my_qsort(arr3, sz, sizeof(arr3[0]), cmp_int);
print3(arr3, sz);
}
int main() {
test1();
test2();
test3();
return 0;
}
运行结果如下: