初学者小白复盘15之指针(4)
1.计算器的一般实现
1.1switch形式
#define _CRT_SECURE_NO_WARNINGS 1
#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");
}
int main()
{int input = 0;do{int x = 0;int ret = 0;int y = 0;menu();printf("请选择计算方式\n");scanf("%d", &input);switch (input){case 0:printf("程序结束\n");break;case 1:printf("请输入两个数字\n");scanf("%d %d", &x, &y);ret = Add(x, y);printf("%d\n", ret);break;case 2:printf("请输入两个数字\n");scanf("%d %d", &x, &y);ret = Sub(x, y);printf("%d\n", ret);break;case 3:printf("请输入两个数字\n");scanf("%d %d", &x, &y);ret = Mul(x, y);printf("%d\n", ret);break;case 4:printf("请输入两个数字\n");scanf("%d %d", &x, &y);ret = Div(x, y);printf("%d\n", ret);break;default :printf("选择错误,请重新选择\n");break;}} while (input);return 0;
}
我们通过switch可以设计一个菜单,每个case对应计算器加减乘除的功能,不过我们看,switch这种实现计算器功能少还好,功能要是多的话是不是代码看着就比较长比较乱呀,而且我么可以发现,每个case语句的内容都是高度重合的,所以我们有没有什么办法能让代码变得更简洁,更美观点呢?
答案是有的兄弟有的,接下来我们通过函数指针数组,也就是转移表来实现我们的想法。
1.2转移表
转移表(Jump Table/ Dispatch Table) 是一个函数指针数组,通过数组索引来选择和调用相应的函数。它提供了一种高效、清晰的方式来替代复杂的条件判断语句。
下面我们通过转移表实现上述功能:
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");
}
int main()
{int input = 0;do{int x = 0;int ret = 0;int y = 0;menu();printf("请选择计算方式\n");scanf("%d", &input);int (*p[5])(int, int) = { 0,Add,Sub,Mul,Div };if (input >= 1 && input <= 4){printf("请输入两个数字\n");scanf("%d %d", &x, &y);ret = p[input](x, y);printf("%d\n", ret);}else if (input == 0){printf("程序结束\n");break;}else{printf("输入错误,请重新输入\n");}} while (input);return 0;
}
我们在这里创建了一个函数指针数组,数组名为p,包含5个元素,每个元素是一个指向函数的指针,这个函数接受两个int类型的变量,并且返回类型也是int。
这里为什么第一个位置放0呢?这样设计是为了让用户输入的数字直接对应数组索引,而下面就是通过函数指针数组调用对应的运算函数,比如input等于1,那么就是指向数组中下标为1的函数指针,也就是加法函数,这就是转移表。
这样的代码是不是在不使用switch之后看着简洁明了了呀!只需要使用if,else循环判断下input的值,并且也不用多次使用重复的代码了,不过下面我们还有一种方法实现计算器哦,那就是使用回调函数。
1.3回调函数
回调函数是通过函数指针调用的函数。你把一个函数的指针作为参数传递给另一个函数,当这个指针被用来调用其所指向的函数时,我们就说这是回调函数。
回调函数就是一个通过函数指针调用的函数。
我们接下来在switch中优化下代码,使得代码更简洁:
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");
}
int calc(int(*p)(int, int))
{int ret = 0;int x = 0;int y = 0;printf("请输入两个数字\n");scanf("%d %d", &x, &y);ret = p(x, y);printf("%d\n", ret);
}
int main()
{int input = 0;do{menu();printf("请选择计算方式\n");scanf("%d", &input);switch (input){case 0:printf("程序结束\n");break;case 1:calc(Add);break;case 2:calc(Sub);break;case 3:calc(Mul);break;case 4:calc(Div);break;default :printf("选择错误,请重新选择\n");break;}} while (input);return 0;
}
我们在这里又写了一个函数名为calc,作用就是接受传过来的函数指针,然后使用这个函数指针来调用具体的函数。
实现回调机制:
当调用 calc(Add) 时:
Add 函数的地址被传递给 calc,在 calc 内部,通过 p(x, y) 实际上调用了 Add(x, y),calc 函数不知道也不关心 p 具体指向哪个函数,它只是调用传入的函数。这就是回调函数。
2.冒泡排序与qsort函数
2.1冒泡排序
关于冒泡排序呢我们之前已经说过了,这里主要说明qsort函数,所以这里稍微带一下冒泡排序:
int main()
{int arr[10] = { 9,8,7,6,5,4,3,2,1,0 };int sz = sizeof(arr) / sizeof(arr[0]);for (int i = 0; i < sz - 1; i++){for (int 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;}}}for (int i = 0; i < sz; i++){printf("%d " ,arr[i]);}return 0;
}
这是一个典型的冒泡排序对吧,不过我们还可以进一步改进一下:
int main()
{int flag = 1;int total_rounds = 0;int arr[10] = { 9,8,7,6,5,4,3,2,1,0 };int sz = sizeof(arr) / sizeof(arr[0]);for (int i = 0; i < sz - 1; i++){total_rounds++;for (int 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;}}printf("共交换了%d行\n", total_rounds);for (int i = 0; i < sz; i++){printf("%d " ,arr[i]);}return 0;
}
下面我们来进入正题,有关qsort函数的使用:
void qsort(void* base, //指针,指向的是待排序的数组的第一个元素size_t num, //是base指向的待排序数组的元素个数size_t size, //base指向的待排序数组的元素的大小int (*compar)(const void*, const void*)//函数指针 I 指向的就是两个元素的比较函数
接下来我们使用qsort函数来排序:
void Print(int arr[], int sz)
{for (int i = 0; i < sz; i++){printf("%d ", arr[i]);}
}
int cmp(const void* p1, const void* p2)
{return (*(int*)p1 - *(int*)p2);
}
int main()
{int arr[10] = { 9,8,7,6,5,4,3,2,1,0 };int sz = sizeof(arr) / sizeof(arr[0]);qsort(arr, sz, sizeof(arr[0]), cmp);Print(arr, sz);return 0;
}
记得使用qsort函数时要包含头文件stdlib.h。
那么qsort是否只能用来排序整型类型的呢?结构体能不能进行排序呢?
2.2使用qsort排序结构数据
struct Student
{char name[20];int age;
};
int cmp_stu_by_name(const void* p1, const void* p2)
{return strcmp(((struct Student*)p1)->name,((struct Student*)p2)->name);
}
int cmp_stu_by_age(const void* p1, const void* p2)
{return (((struct Student*)p1)->age - ((struct Student*)p2)->age);
}
void Print(struct Student s[],int sz)
{for (int i = 0; i < sz; i++){printf("姓名 :%s 年龄:%d\n", s[i].name, s[i].age);}
}
void test2()
{struct Student s[] = { {"zhangsan",20},{"lisi",30},{"wangwu",15}};int sz = sizeof(s) / sizeof(s[0]);printf("按名字排序前\n");Print(s, sz);qsort(s, sz, sizeof(s[0]), cmp_stu_by_name);printf("按名字排序后\n");Print(s, sz);
}
void test3()
{struct Student s[] = { {"zhangsan",20},{"lisi",30},{"wangwu",15} };int sz = sizeof(s) / sizeof(s[0]);printf("按年龄排序前\n");Print(s, sz);qsort(s, sz, sizeof(s[0]), cmp_stu_by_age);printf("按年龄排序后\n");Print(s, sz);
}
int main()
{test2();test3();return 0;
}

可以发现,年龄和名字都已经排好序,说明结构体也是可以排序的。
2.3qsort的模拟实现
我们既然已经会用qsort函数了,我们不妨来研究一下qsort函数究竟是如何实现的?
我们来想一下原先的冒泡排序:
int main()
{int arr[10] = { 9,8,7,6,5,4,3,2,1,0 };int sz = sizeof(arr) / sizeof(arr[0]);for (int i = 0; i < sz - 1; i++){for (int 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;}}}for (int i = 0; i < sz; i++){printf("%d " ,arr[i]);}return 0;
}
如果把冒泡排序的这种排序功能做成一个函数是不是应该是如下这样的呢?
void bubble_sort(int arr[], int sz)
{for (int i = 0; i < sz - 1; i++){for (int 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;}}}
}
void Print(int arr[], int sz)
{for (int i = 0; i < sz; i++){printf("%d ", arr[i]);}
}
int main()
{int arr[10] = { 9,8,7,6,5,4,3,2,1,0 };int sz = sizeof(arr) / sizeof(arr[0]);bubble_sort(arr, sz);Print(arr, sz);return 0;
}
那么既然是模拟qsort函数,我们是不是什么都能排序呢?很显然,现在这个冒泡排序是不能的,他只能排序整型数组,那些结构体什么的显然都是不能的,那该怎么办呢?
我们可以从qsort函数去入手,还记得吗?qsort函数需要4个参数,那我们这两个参数是不是不够呀!
void qsort(void* base, //指针,指向的是待排序的数组的第一个元素size_t num, //是base指向的待排序数组的元素个数size_t size, //base指向的待排序数组的元素的大小int (*compar)(const void*, const void*)//函数指针 I 指向的就是两个元素的比较函数
我们模仿qsort函数是不是应该也具备以上参数呀,首先就是得找到要排序的地方对不对,那后还需要知道排序多少个元素,元素大小我们都要知道,对吧。最后一个参数就是要告诉我们指向比较函数的指针,该函数用来比较两个元素。当这些我们知道后是不是可以尝试模拟qsort函数了呢?让我们来试一试:
void Print(int arr[],int sz)
{for (int i = 0; i < sz; i++){printf("%d ", arr[i]);}
}
void Swap(char* p1, char* p2,size_t size)
{for (int i = 0; i < size; i++){char tmp = *p1;*p1 = *p2;*p2 = tmp;p1++;p2++;}
}
int cmp(const void* p1, const void* p2)
{return (*(int*)p1 - *(int*)p2);
}
void simulate_qsort(void* base,size_t num,size_t size, int (*cmp)(const void*, const void*))
{for (int i = 0; i < num - 1; i++){for (int 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);}}}
}
int main()
{int arr[10] = { 1,3,5,7,9,2,4,6,8,10 };simulate_qsort(arr, sizeof(arr) / sizeof(arr[0]), sizeof(arr[0]), cmp);Print(arr, sizeof(arr) / sizeof(arr[0]));
}

可以看到,数组中的元素被很好的排序了,说明我们的模仿比较成功,那么我们的模仿是怎么实现的呢?让我们来分析一下:
首先就是qsort函数的那四个参数对吧,然后比较函数cmp,
(char*)base + j * size:计算第 j 个元素的地址
(char*)base:将基地址转换为字节指针
j * size:第 j 个元素的偏移量(字节数)
(char*)base + (j + 1) * size:计算第 j+1 个元素的地址,
cmp(...) > 0:如果前一个元素大于后一个元素,需要交换
为什么是char类型呢,因为若是int类型或是其他类型不能符合通用情况,而char是一个字节,可以满足所有通用情况,到最后交换时为什么又写了一个Swap函数呢?就是为了把大的模块给拆分成小部分去慢慢交换,一个一个交换,可以处理任何数据类型,适用于所有情况。
实现原理:
将数据视为字节序列,逐个字节交换,使用 char* 指针因为 char 是1字节,可以处理任何数据类型,通过指针自增遍历每个字节。
