C语言指针详解-包过系列(四)目录版
一、冒泡排序
冒泡排序是一种数据排序方式,其核心思想就是将两两相邻的元素进行比较,下面我们来构造主函数部分
int main()
{
int arr[] = {
3,1,5,8,7,9,6,2,4,0 };
int sz = sizeof(arr) / sizeof(arr[0]);//计算数组长度
bubble_sort(arr, sz);//调用冒泡排序函数
sort_print(arr, sz);//调用打印函数
return 0;
}
下面来对两个自定义函数进行构造
1、冒泡排序函数
泡排序的核心思想既然是将两两相邻的元素进行比较,那么也就意味着我们要进行两次循环。每个数字彻底排序完一回称为一趟,第一个循环是针对排序的趟数,也就是彻底比较了几回。第二个循环是针对有几对元素还需要比较。如下面所示:
void bubble_sort(int arr[], int sz)
{
int i = 0;
for (i = 0; i < sz - 1; i++)//数组长度减 1 就是需要排序的趟数
{
int j = 0;
for (j = 0; j < sz - 1 - i; j++)
//每循环完一趟就有一个元素不再需要排序,故数组长度减 1 再减去 i (已循环的趟数)就是还需比较的元素个数
{
}
}
}
接下来在第二个循环中,还要设置相邻两元素比较后如何交换。我们可以在定义一个局部变量,然后通过三个变量之间的交换赋值来实现两元素数值交换。如下:
void bubble_sort(int arr[], int sz)
{
int i = 0;
for (i = 0; i < sz - 1; i++)//数组长度减 1 就是需要排序的趟数
{
int j = 0;
for (j = 0; j < sz - 1 - i; j++)
//每循环完一趟就有一个元素不再需要排序,故数组长度减 1 再减去 i (已循环的趟数)就是还需比较的元素个数
{
if (arr[j] > arr[j + 1])
{
int tmp = arr[j];//定义局部变量tmp,将arr[j]的值赋给tmp
arr[j] = arr[j + 1];//将arr[j + 1]的值赋给arr[j]
arr[j + 1] = tmp;//将tmp中存储的最早arr[j]的值赋给arr[j + 1]完成数值交换
}
}
}
}
2、打印函数
void sort_print(int last_arr[], int sz)
{
int i = 0;
for (i = 0; i < sz; i++)
{
printf("%d", last_arr[i]);
}
}
因为数组传参时,数组的地址是一样的,所以冒泡排序更改的数组就是最早的数组,打印函数所打印的数组亦然。
冒泡排序的全部代码及运行效果图如下:
#include<stdio.h>
void bubble_sort(int arr[], int sz)
{
int i = 0;
for (i = 0; i < sz - 1; i++)//数组长度减 1 就是需要排序的趟数
{
int j = 0;
for (j = 0; j < sz - 1 - i; j++)
//每循环完一趟就有一个元素不再需要排序,故数组长度减 1 再减去 i (已循环的趟数)就是还需比较的元素个数
{
if (arr[j] > arr[j + 1])
{
int tmp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = tmp;
}
}
}
}
void sort_print(int last_arr[], int sz)
{
int i = 0;
for (i = 0; i < sz; i++)
{
printf("%d", last_arr[i]);
}
}
int main()
{
int arr[] = {
3,1,5,8,7,9,6,2,4,0 };
int sz = sizeof(arr) / sizeof(arr[0]);//计算数组长度
bubble_sort(arr, sz);//调用冒泡排序函数
sort_print(arr, sz);//调用打印函数
return 0;
}
二、回调函数
回调函数就是一个通过函数指针调用的函数
若函数A的指针(地址)被当做参数传递给另一个函数B,当这个函数A的指针被用来调用其所指向的函数A时,被调用的函数A就是回调函数。

我们通过对简易计算器的改造来体现回调函数的效果,下面是改在前的简易计算器代码:
//改造前的计算器
#include<stdio.h>
int add(int a, int b)
{
return a + b;
}
int sub(int a, int b)
{
return a - b;
}
int mul(int a, int b)
{
return a * b;
}
int div(int a, int b)
{
return a / b;
}
int main()
{
int x, y;
int input = 1;
int ret = 0;
do
{
printf("**************\n");
printf("1:add 2:sub\n");
printf("3:mul 4:div\n");
printf("0:exit \n");
printf("**************\n");
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);
return 0;
}
通过观察上面代码,我们不难发现,在条件分支的每种 case 情况下均有重复的代码部分,如下图:
这样就出现两个问题:
1、将这样重复的代码实现成函数。 2、这个函数又能完成不同的任务
如此我们有两种思路:
1、设计回调函数
2、引入函数指针数组来设计转移表。
在此我们以思路一进行改造,首先编写主调函数,并将回调函数的地址传递给主调函数。如下:
#include<stdio.h>
//各个回调函数
int add(int a, int b)
{
return a + b;
}
int sub(int a, int b)
{
return a - b;
}
int mul(int a, int b)
{
return a * b;
}
int div(int a, int b)
{
return a / b;
}
//将回调函数传址给主调函数
void cal(int (*p)(int a, int b))
{
int x, y;
int ret = 0;
printf("请输入操作数:");
scanf("%d %d", &x, &y);
ret = p(x, y);
printf("ret = %d\n", ret);
}
//主调函数部分
int main()
{
int input = 1;
do
{
printf("**************\n");
printf("1:add 2:sub\n");
printf("3:mul 4:div\n");
printf("0:exit \n");
printf("**************\n");
printf("请选择: ");
scanf("%d", &input);
switch (input)
{
case 1:
cal(add);
break;
case 2:
cal(sub);
break;
case 3:
cal(mul);
break;
case 4:
cal(div);
break;
case 0:
printf("退出程序\n");
break;
default:
printf("选择错误\n");
break;
}
} while (input);
return 0;
}
下面我们用画图的方式将回调的过程明晰化:
在上面的程序中每种case
下的分支是0-4,可读性并不是很高,我们还可以使用枚举来进行优化(相当于给对应常量值起了个名字)
#include<stdio.h>
//使用枚举来定义选项
enum Option
{
EXIT,//0
ADD,//1
SUB, //2
MUL, //3
DIV, //4
};
//各个回调函数
int add(int a, int b)
{
return a + b;
}
int sub(int a, int b)
{
return a - b;
}
int mul(int a, int b)
{
return a * b;
}
int div(int a, int b)
{
return a / b;
}
//将回调函数传址给主调函数
void cal(int (*p)(int a, int b))
{
int x, y;
int ret = 0;
printf("请输入操作数:");
scanf("%d %d", &x, &y);
ret = p(x, y);
printf("ret = %d\n", ret);
}
//主调函数部分
int main()
{
int input = 1;
do
{
printf("**************\n");
printf("1:add 2:sub\n");
printf("3:mul 4:div\n");
printf("0:exit \n");
printf("**************\n");
printf("请选择: ");
scanf("%d", &input);
switch (input)
{
case ADD://等同于case 1
cal(add);
break;
case SUB://等同于case 2
cal(sub);
break;
case MUL://等同于case 3
cal(mul);
break;
case DIV://等同于case 4
cal(div);
break;
case EXIT://等同于case 0
printf("退出程序\n");
break;
default:
printf("选择错误\n");
break;
}
} while (input);
return 0;
}
这样修改后,在阅读代码时就方便知道每种分支情况是何作用,增强了代码的可读性与可维护性。
三、qsort函数使用举例
1、qsort介绍
qsort是C标准库中的一个函数,全称快速排序(Quicksort)。它是一种高效的通用排序算法。qsort函数通常用于对数组进行就地排序,即直接修改输入数组而不是创建新的数组。且qsort可以排序任意类型数据。其函数原型如下:
void qsort(void* base, //指针,指向的是待排序的数组的第一个元素
size_t num, //是base指向的待排序数组的元素个数
size_t size, //base指向的待排序数组的元素的大小
int (*compar)(const void*, const void*)//函数指针 - 指向的就是两个元素的比较函数
);
2、qsort使用举例
(1)使用qsort函数排序整型数据
首先编写主函数部分,调用qsort函数。
int main()
{
int arr[] = {
1 , 5 , 4 , 7 , 6 , 2 , 8 , 9 , 0 , 3 };
int i = 0;
size_t sz = sizeof(arr) / sizeof(arr[0]);
qsort(arr, //排序数组的首元素地址
sz, // 待排序数组元素个数
sizeof(int),//待排序数组元素大小
cmp_arr);//比较数组元素的比较函数
for (i = 0; i < sz; i++)
{
printf("%d", arr[i]);
}
printf("\n");
return 0;
}
下面对qsort中的比较函数进行编写。如下:
int cmp_arr(const void* p1, const void* p2)
{
return(* (int*) p1 - * (int*) p2);
}
注意,在这里我们对比较函数内形参的设置为 void* 类型,如此是为了便于接受各种类型的数据,而不是仅限于整型 int 这么单一的一种。
在C++官网中对比较函数的定义如下:
如此我们便需要将两元素相减的结果作为返回值传给qsort。这里还有一坑,就是我们的形参类型设置的是 void* 类型,而 void* 类型不能直接进行解引用与加减运算,所以需要进行强制类型转换,将变量转换为 int* 类型在进行运算。
最后全代码与运行结果如下:
#include<stdio.h>
int cmp_arr(const void* p1, const void* p2)
{
return(* (int*) p1 - * (int*) p2);
}
int main()
{
int arr[] = {
1 , 5 , 4 , 7 , 6 , 2 , 8 , 9 , 0 , 3 };
int i = 0;
size_t sz = sizeof(arr) / sizeof(arr[0]);
qsort(arr, sz, sizeof(int), cmp_arr);
for (i = 0; i < sz; i++)
{
printf("%d", arr[i]);
}
printf("\n");
return 0;
}
(2)qsort排序结构体数据
首先我们要先定义一个结构体类型,我们就以学生的姓名和年龄为例
struct student
{
char name[20];
int age;
};
然后我们来编写主函数部分,定义一个结构体变量,然后调用排序函数 qsort 和打印函数 print_arr
int main()
{
struct student arr[3] = {
{
"xiaoming", 18} , {
"zhangsan" , 19} , {
"lisi" , 17} };
int sz = sizeof(arr) / sizeof(arr[0]);
qsort(arr, sz, sizeof(arr[0]), arr_cmp_name);//需要再去定义比较函数arr_cmp_name
//注意这里我们使用的是比较字符串的大小的方式来定义比较函数
print_arr(arr, sz);
return 0;
}
定义比较函数arr_cmp_name
int arr_cmp_name(const void* p1, const void* p2)
{
return strcmp(((struct student*)p1) -> name , ((struct student*)p2) -> name);
}
在上述代码中,首先我们依旧沿用将形参类型定义为 void* 类型,以便于我们接受不同类型的数据,只是要注意在计算时要对其进行强制类型转换。其次由于我们的比较方式是比较字符串的大小,所以我们需要使用 strcmp 函数。
C++官网上对于strcmp的定义如上,我们不难发现,其参数设置和返回值的大小设置于 qsort 函数中关于比较函数 compare 的设置是一样的,如此我们在比较函数的返回值中就可以直接使用strcmp 函数,而不需要再做其余对返回值的设定。
在这里拓展一点,我们同时也是可以使用年龄来作为排序比较的依据,我们将以年龄为标准进行比较的函数命名为 arr_cmp_age 函数,其具体定义如下:
int arr_cmp_age(const void* p1, const void* p2)
{
return ((struct student*)p1) -> age - ((struct student*)p2) -> age;
}
比较函数定义完毕后再来定义打印函数 print_arr
void print_arr(const void* arr, int sz)
{
int i = 0;
for (i = 0; i < sz; i++)
{
printf("%s %d \n", ((struct student*)arr + i)->name, ((struct student*)arr + i)->age);
}
}
注意,这里我们使用的是指针变量,所以应使用结构体间接访问操作符 -> 来访问结构体数组中对应元素内的内容。
拓展:上述代码访问结构体数组的一步中我们也可以使用直接访问操作符 . 来访问结构体数组中对应元素内的内容。
思路一:在定义一个结构体变量来做强制类型转换
思路二:直接进行强制类型转换,然后再解引用在进行访问
思路一程序实现:
void print_arr(const void* arr, int sz)
{
int i;
const struct student* students = (const struct student*)arr;
//进行强制类型转换,将结果赋给students
for (i = 0; i < sz; i++)
{
printf("%s %d\n", students[i].name, students[i].age);
//进行直接访问
}
}
思路二程序实现:
void print_arr(const void* arr, int sz)
{
int i = 0;
for (i = 0; i < sz; i++)
{
printf("%s %d \n", (*((const struct student*)arr + i)).name, (*((const struct student*)arr + i)).age);
//直接进行强制类型转换再解引用再进行结构体数组内容直接访问
}
思路二的实现过程虽然省去了再多定义变量的步骤,但降低了可读性,仍应尽量避免
下面为整体代码以及以 arr_cmp_name 作为比较函数的运行结果
#include<stdio.h>
#include<string.h>
struct student //定义结构体变量
{
char name[20];//学生姓名
int age;//学生年龄
};
//定义qsort中比较函数
int arr_cmp_name(const void* p1, const void* p2)
{
return strcmp(((struct student*)p1) -> name , ((struct student*)p2) -> name);
//返回值中使用强制类型转换后再进行计算
}
//定义打印函数
void print_arr(const void* arr, int sz)
{
int i = 0;
for (i = 0; i < sz; i++)
{
printf("%s %d \n", ((struct student*)arr + i)->name, ((struct student*)arr + i)->age);
/*printf("%s %d \n", (*((const struct student*)arr + i)).name, (*((const struct student*)arr + i)).age);*/
//上面这段注释内容为使用直接访问操作符的方式访问结构体数组内容
}
}
int main()
{
//定义结构体数组
struct student arr[3] = {
{
"xiaoming", 20} , {
"zhangsan" , 19} , {
"lisi" , 17} };
int sz = sizeof(arr) / sizeof(arr[0]);//计算结构体数组长度
qsort(arr, sz, sizeof(arr[0]), arr_cmp_name);//调用qsort排序函数
print_arr(arr, sz);//调用打印函数
return 0;
}
四、qsort函数模拟实现
我们利用回调函数并采取冒泡排序的方式模拟实现qsort函数
首先我们对冒泡排序中为贴合 qsort ,哪些部分应当发生改变进行分析
在上图冒泡排序程序中红色与绿色所标注的循环所对应的趟数与剩余需排序的元素个数不需要改变,而在条件分支的比较判断中,若变量是整型则可以直接比较,若不是整型,比如是字符串类型就需要使用strcmp函数进行比较。其次内部的数据交换方式也是仅限于整型。故二者均需要做出改变
解决办法如下:
要比较数值大小我们就定义一个用于比较各类数值大小的函数;要交换数值我们就定义一个用于交换数值的函数
首先编写主函数部分,因为采取的是冒泡排序的方式,我们就将 qsort 的模拟函数命名为 bubble
排序的数组我们就以最简单的整型数组为例
int main()
{
int arr[] = {
0,2,5,1,8,6,9,7,6,3,4,};
int i = 0;
int sz = sizeof(arr) / sizeof(arr[0]);//计算数组长度
//调用qsort模拟函数 bubble
bubble(arr, sz, sizeof(arr[0]), int_cmp);
for (i = 0; i < sz; i++)
{
printf("%d", arr[i]);
}
printf("\n");
return 0;
}
下面我们需要对bubble函数进行详细定义,在此之前,我们要先定义两个函数,第一是比较函数,第二是交换函数。
比较函数:
与上文的编写方式大致相同,形参设置为 void* 类型用于接受各类数据,因为 qsort 函数的比较函数需要与0做比较,所以我们需要强制转换为整型再进行减法运算。具体如下:
void int_cmp(const void* p1, const void* p2)
//形式参数设置为 void* 类型来接收各类数据
{
return (*(int*)p1 - *(int*)p2);
//因为 void* 类型不能直接进行运算,所以要进行强制类型转换
}
交换函数:
为进行各类数据的交换,我们以数据的底层指针(地址)为单位,进行交换操作。那么就会有另一个问题,每次交换要交换多少字节,为便于计算通用表达式,我们采取字符的大小进行交换,即每次交换1字节。具体见下:
void swapp(const void* p1, const void* p2, int size)
//size是传过来的数据大小
{
int i = 0;
for (i = 0; i < size; i++)
{
//一字节一字节的进行交换
char tmp = *((char*)p1 + i);
*((char*)p1 + i) = *((char*)p2 + i);
*((char*)p2 + i) = tmp;
}
}
定义 bubble 函数:
void bubble(void* base, size_t count, size_t size, int(*cmp)(void*, void*))
//模拟qsort的参数,分别为首元素地址,数组长度,数组元素大小,比较函数
{
int i = 0;
for (i = 0; i < count - 1; i++)
{
int j = 0;
for (j = 0; j < count - 1 - i; j++)
{
if (cmp((char* )base + j * size, (char* )base + (j + 1) * size) > 0)
//此处判断中比较函数的比较也是以一字节为单位进行比较
{
swapp((char*)base + j * size, (char*)base + (j + 1) * size, size);
//此处交换也是以一字节为单位进行交换
}
}
}
}
在此解析一下对于 参数 (char* )base + j * size 与 (char* )base + (j + 1) * size 的设置。若我们不知道数组内元素类型,但是所有类型大小都是以一字节为基本单位,所以我们将其转换为 char 类型。而我们只知道数组首元素地址且冒泡排序需要前一个元素与后一个元素相比较,所以(char* )base + jsize就确定了需排序的元素地址,(char )base + (j+1)* size就确定了需排序的元素后一个元素的地址。如此在每交换一字节的情况下实现整体元素的跨越。
qsort模拟函数全部代码与运行效果如下:
#include<stdio.h>
void int_cmp(const void* p1, const void* p2)
{
return (*(int*)p1 - *(int*)p2);
}
void swapp(const void* p1, const void* p2, int size)
{
int i = 0;
for (i = 0; i < size; i++)
{
char tmp = *((char*)p1 + i);
*((char*)p1 + i) = *((char*)p2 + i);
*((char*)p2 + i) = tmp;
}
}
void bubble(void* base, size_t count, size_t size, int(*cmp)(void*, void*))
{
int i = 0;
for (i = 0; i < count - 1; i++)
{
int j = 0;
for (j = 0; j < count - 1 - i; j++)
{
if (cmp((char* )base + j * size, (char* )base + (j + 1) * size) > 0)
{
swapp((char*)base + j * size, (char*)base + (j + 1) * size, size);
}
}
}
}
int main()
{
int arr[] = {
0,2,5,1,8,6,9,7,6,3,4,};
int i = 0;
int sz = sizeof(arr) / sizeof(arr[0]);
bubble(arr, sz, sizeof(arr[0]), int_cmp);
for (i = 0; i < sz; i++)
{
printf("%d", arr[i]);
}
printf("\n");
return 0;
}
全文到此结束!!!
写作不易,不知各位老板能否给个一键三连或是一个免费的赞呢(▽)(▽),这将是对我最大的肯定与支持!!!谢谢!!!(▽)(▽)