深入理解指针(4)
1回调函数
回调函数就是一个通过函数指针调用的函数
如果我们把函数的地址作为参数传递给另外一个函数, 当这个指针被用来调用其所指向的函数时,被调用的函数就是回调函数。回调函数不是由该函数的实现方直接调用,而是在特定事件或条件发生时由另外一方调用的,用于对该时间或条件进行响应。
我们之前学的用函数指针 解决计算器的问题 实际上就是回调函数的应用
void menu()
{
printf("**************************************\n");
printf("**************简单计算器***************\n");
printf("******1.Add 2.Sub 3.Mid 4.Div******\n");
printf("****************0.exit****************\n");
}
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 Calc(int (*p)(int, int))
{
int x, y;
int ret = 0;
printf("请输入两个操作数\n");
scanf("%d %d", &x, &y);
ret = p(x, y); // 这里通过函数指针来实现对函数的调用
printf("%d\n", ret);
}
int main()
{
int input = 0;
int x, y;
int ret = 0;
do
{
menu();
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;
}
2.qsort 函数 (有点难)
qsort 函数 – 用来排序的 来自库函数
底层使用的是快速排序
在讲述qsort函数之前 我们先来回忆一下我们之前所学过得冒泡排序
// 冒泡排序
void bubble_sort(int arr[],int sz)
{
int flag = 0;
for (int i = 0; i < sz - 1; i++)
{
for (int j = 0; j < sz - 1; j++)
{
//if (arr[j] > arr[j + 1])
//{
// int tmp = arr[j];
// arr[j] = arr[j + 1];
// arr[j + 1] = tmp;
// flag = 1;
//}
// 也可以改成指针的形式
if (*(arr + j) > *(arr + j + 1))
{
int tmp = *(arr+j);
*(arr + j) = *(arr + j + 1);
*(arr + j + 1) = tmp;
flag = 1;
}
}
if (flag == 0)
break;
}
}
void Print(int* p, int sz)
{
for (int i = 0; i < sz; i++)
{
printf("%d ", *(p + i));
}
}
int main()
{
int arr[] = { 1,3,2,4,6,7,5,8,9,10 };
int sz = sizeof(arr) / sizeof(arr[0]);
bubble_sort(arr, sz);
Print(arr, sz);
return 0;
}
我们现在来思考一下冒泡排序的逻辑是怎么样的
首先它会两两比较 直到比完最后一个元素 不满足顺序就交换 而一次这个过程叫做一趟冒泡排序
而如果需要排序的元素有n个的话 它就需要n-1 躺冒泡排序
但是这个冒泡排序是有点问题的
因为它只能排序整形数据
这个时候我们需要学习的qsort函数 它可以排序任意数据
那它是如何做到的呢 这个时候我们就来学习它
我们首先来看 qsort 函数
void qsort( void *ptr, // 指针,指向的是待排序数组的首元素地址 为什么是void* 是因为不知道传进来的 参数是什么类型的
size_t count, // 是ptr所指向的的待排序数组的元素个数
size_t size, // ptr所指向的待排序数组的元素大小
int (*compar)(const void*, const void*) // 函数指针变量
// 指向的就是对两个元素进行比较的函数
// 为什么这里是一个函数指针变量呢?
);
那么为什么qsort函数的形参有一个函数指针变量呢
从前面的冒泡排序中我们就可以看到原因
我们发现 在冒泡排序中
/if (arr[j] > arr[j + 1])
//{
// int tmp = arr[j];
// arr[j] = arr[j + 1];
// arr[j + 1] = tmp;
// }
我们发现其对两个元素的比较是直接使用 > 去比较的
而要想实现对任意元素都能进行排序的话 这里就会出现问题 比如字符串类型 和结构体都 无法使用 这个方式
因此我们可以联想到 在if语句那边 采用回调函数的思想 对不同类型的元素 采用相应的函数去进行比较
要想实现回调函数 那么接受的参数内 自然就需要函数指针类型了
经过这样的思考我们就可以知道为何qsort函数需要一个函数指针变量的形参了
实际上qsort函数 就是像我们上面说的这么做的
由于我们是qsort的使用者 我们清楚我们需要比较的两个类型 是什么类型 。 应该如何去比较
因此这个比较的函数是修需要我们自己写的并把函数指针传给sqort
// 在qsort函数内部调用的函数 是有要求的
// 如果比较的两个元素 :
// p1 > p2 要返回一个大于0的数
// p1 = p2 要返回0
// p1 < p2 要返回一个小于0的数
我们来看使用qsort函数的实例:
// 在qsort函数内部调用的函数 是有要求的
// 如果比较的两个元素 :
// p1 > p2 要返回一个大于0的数
// p1 = p2 要返回0
// p1 < p2 要返回一个小于0的数
int cmp_int(const void* p1, const void* p2) //p1 p2 分别放着第一个和第二个要做比较的元素的地址
// 这里我们需要注意一个细节
//void*类型的指针 是无具体类型的指针,这种类型的指针无法直接解引用 也不能进行+-整数得到运算
{
//if (*(int*)p1 >*(int*) p2) // 我们自己是清楚这个函数内部比较的两个元素是int型的 因此我们直接采取强制转换
// return 1;
//else if (*(int*)p1 == *(int*)p2)
// return 0;
//else
// return -1;
这里其实可以简化代码
return *(int*)p1 - *(int*)p2;
并且如果我们想要实现降序的话
//return *(int*)p2 - *(int*)p1; // 这个就可以实现降序
}
void Print(int* p, int sz)
{
for (int i = 0; i < sz; i++)
{
printf("%d ", *(p + i));
}
}
int main()
{
// 写一段代码使用sqort排序整型数据
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, sz); // 0 1 2 3 4 5 6 7 8 9
return 0;
}
学了如何使用qsort函数排序整型数据
那我们来试一下qsort 排序结构体数据
//写一段代码 用sqort函数排序 结构体数据
struct Stu
{
char name[20];
int age;
};
// 有个问题 两个结构体元素怎么比较大小?
// 1.按照名字比较 - 字符串比较 - strcmp
// 2.按照年龄比较 - 整形比较
int cmp_stu_by_name(const void* p1, const void* p2)
{
// 一样的需要对p1进行强制转换 变成结构体类型
//if (strcmp(((struct Stu*)p1), ((struct Stu*)p1)) == 0)
// return 0;
//...... // 用条件判断可以 这里直接可以简化代码
//strcmp(((struct Stu*)p1), ((struct Stu*)p2)); // strcmp 是按照对应字符串中的字符ASCII码值比较的
// strcmp 自己在比较字符串大小的 时候 也是
// p1 > p2 要返回一个大于0的数
// p1 = p2 要返回0
// p1 < p2 要返回一个小于0的数
// 那我们这边直接返回一个 strcmp 函数
return strcmp(((struct Stu*)p1)->name, ((struct Stu*)p2)->name);
}
int cmp_stu_by_age(const void* p1, const void* p2)
{
return ((struct Stu*)p1)->age - ((struct Stu*)p2)->age;
}
void test2()
{
struct Stu arr[3] = { {"zhangsan",20},{"lisi",30},{"wangwu",40} };
int sz = sizeof(arr) / sizeof(arr[0]);
//qsort(arr, sz, sizeof(arr[0]), cmp_stu_by_name); // 按照名字来排序
qsort(arr, sz, sizeof(arr[0]), cmp_stu_by_age); // 按照年龄来排序
for (int i = 0; i < sz; i++)
{
printf("%s %d\n", arr[i].name, arr[i].age);
}
}
int main()
{
test2();
return 0;
}
// 我们学会了如何使用qsort函数后
// 那我们能不能改造我们之前的冒泡排序
// 让其能对任意类型排序
struct Stu
{
char name[20];
int age;
};
int cmp_int(const void* p1, const void* p2) // 在前面已经写过了
{
return *(int*)p1 - *(int*)p2;
}
int cmp_stu_by_name(const void* p1, const void* p2) // 在前面已经写过并且分析过了
{
return strcmp(((struct Stu*)p1)->name, ((struct Stu*)p2)->name);
}
int cmp_stu_by_age(const void* p1, const void* p2)
{
return ((struct Stu*)p1)->age - ((struct Stu*)p2)->age;
}
void Swap(char* p1, char* p2, size_t width)// 为什么需要width呢 因为我们只知道了两个要交换元素的地址 并不知道他们的类型
// 也就是不知道步长 那么这个时候就需要传入一个width来告知这个步长是多少
// 由于我们知道了地址和一个元素的字节大小
// 我们就可以新建一个空间 把第一个元素放进去 再把第二个元素放到 第一个元素的空间 再把新建空间内的第一个元素放到第二个元素的空间
// 这样就完成了一次交换
{
for (int i = 0; i < width; i++)// 传进来的数据 类型的大小是多少字节 那么就需要交换多少次
// 因为一次只交换一个字节大小的数据 如果是int类型的数据 一个内存空间有四个字节 那么就需要交换四次
{
// 先让p1 p2 拿到各自一个字节的数据
// 创建一个字节大小的tmp
char tmp = *p1;
*p1 = *p2;
*p2 = tmp;
p1++;
p2++;
}
}
// 冒泡函数内部的思想不变
void bubble_sort(void*base, size_t sz,size_t width,int (*cmp)(const void*p1, const void*p2))
{
for (int i = 0; i < sz - 1; i++)
{
for (int j = 0; j < sz - 1; j++)
{
// 那我们如何比较传进来参数的大小呢
// 通过cmp比较函数去比较 将两个要比较的数传给cmp函数去比较
// // 让cmp函数去比较两个数的大小 返回来>0就说明第一个参数大于第二个参数
// 因此我们需要算出arr[i] 和 arr[j + 1] 的地址 去传给cmp函数让它去比较
// 关键是如何获取到arr[j] 和 arr[j + 1] 的地址
if (cmp((char*)base+ j * width, (char*)base + (j + 1) * width)>0) // 将传入进来的地址强制转化成char*类型
// j * width 是为了刚好跳过一个步长 这样刚好跳过一个元素的字节大小
{
// 交换两个元素
Swap((char*)base + j * width, (char*)base + (j + 1) * width,width);
}
}
}
}
void Print(int* p, int sz)
{
for (int i = 0; i < sz; i++)
{
printf("%d ", *(p + i));
}
}
int test1()
{
int arr[] = { 1,3,2,4,6,7,5,8,9,10 };
int sz = sizeof(arr) / sizeof(arr[0]);
bubble_sort(arr, sz, sizeof(arr[0]), cmp_int);
Print(arr, sz);
return 0;
}
void test2()
{
struct Stu arr[3] = { {"zhangsan",20},{"lisi",30},{"wangwu",40} };
int sz = sizeof(arr) / sizeof(arr[0]);
//bubble_sort(arr, sz, sizeof(arr[0]), cmp_stu_by_name); // 按照名字来排序
bubble_sort(arr, sz, sizeof(arr[0]), cmp_stu_by_age); // 按照年龄来排序
for (int i = 0; i < sz; i++)
{
printf("%s %d\n", arr[i].name, arr[i].age);
}
}
int main()
{
test2();
return 0;
}
总结 : 正式因为函数指针的存在我们才能实现想传入什么类型就传入什么类型
这正是回调函数的应用。
zhangsan",20},{“lisi”,30},{“wangwu”,40} };
int sz = sizeof(arr) / sizeof(arr[0]);
//bubble_sort(arr, sz, sizeof(arr[0]), cmp_stu_by_name); // 按照名字来排序
bubble_sort(arr, sz, sizeof(arr[0]), cmp_stu_by_age); // 按照年龄来排序
for (int i = 0; i < sz; i++)
{
printf("%s %d\n", arr[i].name, arr[i].age);
}
}
int main()
{
test2();
return 0;
}
总结 : 正式因为函数指针的存在我们才能实现想传入什么类型就传入什么类型
这正是回调函数的应用。