在之前已经学习了指针初阶相关知识,知道了指针的概念
- 指针就是个变量,用来存放地址,地址唯一标识一块内存空间.
- 指针的大小固定是4/8个字节(32位平台/64位平台)
- 指针是有类型的,指针的类型决定了指针±整数的步长,指针解引用操作的权限.
- 指针的运算
具体可以见我之前写的指针初阶
下面是对指针高阶知识的简单描述
1. 字符指针
用来存放char
字符类型地址的指针类型是char*
字符指针类型
#include <stdio.h>
int main(void)
{
char ch = 'w';
char* pc = &ch;
*pc = 'r';
printf("%c\n", ch);
return 0;
}
可以使用指针存放变量的地址,通过对指针的解引用对变量本身存放的内容进行修改.
#include <stdio.h>
int main(void)
{
char arr[] = "abcdef";
const char* p = "abcdef";
printf("%s\n", p);
return 0;
}
- 之前一直使用字符数组用来存放字符串的,在栈区开辟(字符串长度+1)的空间来存放.
- 同样可以用字符指针来存放常量字符串的首元素地址,与字符数组所存放的字符串不同的是:
*p
是常变量即不可以直接修改,不可以通过对指针p
的解引用进行修改.
-
p
是指针,并不是它存放了这个字符串的所有内容,指针p
所存放的是该字符串首元素的地址.
- 常量字符串也不是在栈区存储的,它是存放在静态区中的只读区(loader section),在源文件编译阶段就已经存放在该区域了.具体在下图的.text段
#include <stdio.h>
int main(void)
{
char* p = "abcdef";
printf("%p\n", p);
printf("%c\n", *p);
printf("%p\n", &(*p));
return 0;
}
通过上述代码,更加证实了指针p
存放的是字符串首元素的地址.
对p
进行解引用得到的是字符串第一个元素'a'
,将'a'
的地址和字符串的地址都打印出来,发现两地址值是一样的.
这个字符串的地址偏低,也可以证实它确实是被存放在低地址上的只读区.
#include <stdio.h>
int main(void)
{
char str1[] = "abcdef";
char str2[] = "abcdef";
const char* str3 = "abcdef";
const char* str4 = "abcdef";
if (str1 == str2)
printf("str1 and str2 are same\n");
else
printf("str1 and str2 are not same\n");
if (str3 == str4)
printf("str3 and str4 are same\n");
else
printf("str3 and str4 are not same\n");
return 0;
}
-
str1
和str2
都是在栈区临时开辟一块空间用来存放这个字符串,在内存中这是两块地方.数组名表示的是数组首元素地址,明显两者是不相等的.
-
str3
和str4
同时存放只读区中的常量字符串"abcdef"
的首元素地址,显然两者相等.
-
程序运行结果如下 [外链图片转存失败,源站可能有防盗链机制,
2. 指针数组
在指针初阶同样已经学习过指针数组了,具体请点链接.
int* arr1[10]; //整型指针的数组
char* arr2[10]; //一级字符指针的数组
char** arr3[10];//二级字符指针的数组,每个元素存放一级指针的地址
通常使用指针数组模拟二维数组,但并不是真正的二维数组.
#include <stdio.h>
int main(void)
{
int arr1[] = { 1,2,3,4,5 }; //整型数组
int arr2[] = { 2,3,4,5,6 }; //整型数组
int arr3[] = { 3,4,5,6,7 }; //整型数组
int* arr[] = { arr1, arr2, arr3 }; //指针数组
int i = 0;
for (i = 0; i < 3; i++)
{
int j = 0;
for (j = 0; j < 5; j++)
{
printf("%d ", arr[i][j]);
}
printf("\n");
}
return 0;
}
3. 数组指针
3.1 数组指针的定义
数组指针是指针
整型指针int* pi
是指向整型数据的指针.
浮点型指针float* pf
是指向浮点型数据的指针.
同理,数组指针:指向数组类型的指针.
int* p1[10];
int (*p2)[10];
- p1是指针数组,能存放十个元素,每个元素是
int*
类型的.
- p2是数组指针,p2先与
*
结合,表示这是一个指针类型,然后指向一个int [10]
类型的数组,数组是构造类型.
-
[]
的优先级是要高于*
的,所以需要使用()
来保证p
先与*
结合.
3.2 &数组名 VS 数组名
对于下面的数组:
int arr[10];
我们都知道arr
是数组名,数组名表示数组首元素的地址,是一个常量.
但是又两个例外,在sizeof
操作符后,得到的是整个数组所占字节的大小,还有一个例外是在&
取地址操作符后,那么&arr
到底是什么意思呢?
首先,我们打印arr
和&arr
的值
#include <stdio.h>
int main(void)
{
int arr[10] = { 0, };
printf("arr = %p\n", arr);
printf("&arr = %p\n", &arr);
return 0;
}
程序运行如下:
可见arr
和&arr
打印的地址是一样的,但两者真的是一样的吗?
#include <stdio.h>
int main(void)
{
int arr[10] = { 0, };
printf("arr = %p\n", arr);
printf("arr + 1 = %p\n", arr + 1);
printf("&arr[0] = %p\n", &arr[0]);
printf("&arr[0] + 1 = %p\n", &arr[0] + 1);
printf("&arr = %p\n", &arr);
printf("&arr + 1 = %p\n", &arr + 1);
return 0;
}
发现结果有点不一样了:
- 将
arr
加1
,步长为4
个字节,正好是一个int
类型元素所占据的内存空间.与&arr[0]
加1
等效.
- 将
&arr
加1
,步长却是0x28 = 40
个字节,这正好是整个数组所占据的内存空间的大小.
- 通过调试,编译器也给我们了答案,
arr
是int*
类型的,这是整型指针类型;而&arr
是int (*)[10]
类型的,这是数组指针类型.
编译器写的int[10] *,只是为了好分辨理解,正确的数组指针写法应该是int (*p)[10]
.
3.3 数组指针的使用
那么数组指针又是如何使用的呢?
传递数组给函数:当数组作为参数传递给函数时,实际上会将数组的首地址传递给函数。函数可以使用数组指针来操作整个数组,而不需要传递数组的长度。
动态内存分配:使用动态内存分配函数(如malloc、calloc等)分配内存时,会返回一个指向分配内存的指针。可以通过将该指针视为数组指针来操作动态分配的内存块。
多维数组:对于多维数组,可以使用数组指针来简化访问。例如,二维数组可以被视为一个指向一维数组的指针数组,可以使用指针运算来遍历或访问元素。
字符串操作:C语言中的字符串实际上是以字符数组的形式存储的。可以使用字符数组指针来进行字符串操作,如拷贝、连接、比较等。
数组的动态管理:通过数组指针,可以方便地动态管理数组的大小和内容。可以使用realloc函数重新调整已分配数组的大小,从而实现动态数组的功能。
本章先重点讲一下使用数组指针对于二维数组中使用,包括作为形参传递.
#include <stdio.h>
int main(void)
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
//使用数组指针来遍历一维数组
int(*p1)[10] = &arr;
int i = 0;
for (i = 0; i < 10; i++)
{
printf("%d ", *(*p1 + i));
//printf("%d ", (*pi)[i]);
}
printf("\n");
//使用整型指针来遍历一维数组
int* p2 = arr;
for (i = 0; i < 10; i++)
{
printf("%d ", *(p2 + i));
}
return 0;
}
-
printf("%d ", *(*p1 + i));
首先*p1
,对p1
解引用得到指针指向的整个数组的首元素的地址,再通过对数组首元素地址偏移解引用遍历到每个元素.*&arr -> arr
.
-
可以发现,这样做的代码易读性明显不如直接使用整型指针来进行遍历
-
程序运行结果如下:
-
所以一般,二维数组以上使用数组指针.
#include <stdio.h>
void print_arr1(int arr[3][5], int row, int col)
{
int i = 0;
for (i = 0; i < row; i++)
{
int j = 0;
for (j = 0; j < col; j++)
{
printf("%d ", arr[i][j]);
}
printf("\n");
}
}
void print_arr2(int(*arr)[5], int row, int col)
{
int i = 0;
for (i = 0; i < row; i++)
{
int j = 0;
for (j = 0; j < col; j++)
{
printf("%d ", arr[i][j]);
}
printf("\n");
}
}
int main(void)
{
int arr[3][5] = { 1,2,3,4,5, 2,3,4,5,6, 3,4,5,6,7 };
print_arr1(arr, 3, 5);
printf("\n");
print_arr2(arr, 3, 5);
return 0;
}
-
定义了一个二维数组,3行5列.我们可以将它看成如下图所示,实际上二维数组在内存中和一维数组一样,是连续存储的.
-
数组名表示数组首元素的地址,那么二维数组的数组名就表示"第一行"的地址,即"第一个一维数组"的地址,而一维数组的地址也是首元素的地址.
即&arr = &arr[0] = &arr[0][0]
.
-
既然传递的arr
表示的是一行一维数组,那么就可以用数组指针来接收,即int (*)[5]
数组指针类型来接收.
-
那么arr
虽然实际上是int [3][5]
类型的,但也可以把它看作int (*)[5]
类型的数组指针的数组,每个指针指向一个int [5]
类型的一维数组.arr[0]
表示{1,2,3,4,5}
这个数组,等等.
-
print_arr1
直接创建了一个二维数组来接收形参.
-
print_arr2
而是创建了一个数组指针来接收形参.
-
本质上都是传递指针
-
程序运行结果如下:
int arr[5]; //整型数组, 能存放5个元素
int *parr1[10]; //整型指针数组, 能存放10个元素
int (*parr2)[10]; //数组指针, 指向一个能存放10个元素的数组
int (*parr3[10])[5]; //数组指针数组, int (*)[10]类型, 等同于二维数组, 5行10列
4.数组参数,指针参数
在写代码的时候难免要把[数组]或者[指针]传给函数,那么函数的参数应该如何设计呢?
4.1 一维数组传参
#include <stdio.h>
void test(int arr[])//ok
{}
void test(int arr[10])//ok 编译器会忽视[]中的10
{}
void test(int *arr)//ok
{}
void test2(int *arr[20])//ok
{}
void test2(int **arr)//ok
{}
int main()
{
int arr[10] = {0};
int *arr2[20] = {0};
test(arr);
test2(arr2);
}
4.2 二维数组传参
void test(int arr[3][5])//ok
{}
void test(int arr[][])//err
{}
void test(int arr[][5])//ok
{}
//总结:二维数组传参,函数形参的设计只能省略第一个[]的数字。
//因为对一个二维数组,可以不知道有多少行,但是必须知道一行多少元素。
//这样才方便运算。
void test(int *arr)//err 传递的是一维数组
{}
void test(int* arr[5])//err 传递的是一维指针数组
{}
void test(int (*arr)[5])//ok
{}
void test(int **arr)//err 传递的是一维指针数组
{}
int main()
{
int arr[3][5] = {0};
test(arr);
}
二维数组在内存中连续存放的,必须要知道一行有多少元素,这样编译器才好分配空间.
4.3 一级指针传参
#include <stdio.h>
void print(int *p, int sz)
{
int i = 0;
for(i=0; i<sz; i++)
{
printf("%d\n", *(p+i));
}
}
int main()
{
int arr[10] = {1,2,3,4,5,6,7,8,9};
int *p = arr;
int sz = sizeof(arr)/sizeof(arr[0]);
//一级指针p,传给函数
print(p, sz);
return 0;
}
形参是一级指针,实参可以是一级指针变量和普通变量地址.
4.4 二级指针传参
#include <stdio.h>
void test(int** ptr)
{
printf("num = %d\n", **ptr);
}
int main()
{
int n = 10;
int*p = &n;
int **pp = &p;
test(pp);
test(&p);
return 0;
}
形参是二级指针,实参可以是二级指针变量和一级指针地址.
5. 函数指针
函数指针是指向函数的指针
数组指针是指向数组的指针,那么函数指针就是指向函数的指针.
#include <stdio.h>
void test()
{
printf("hehe\n");
}
int main()
{
printf("%p\n", test);
printf("%p\n", &test);
return 0;
}
程序运行结果如下:
可以看到函数也是有地址的,&test
和test
的地址都是一样的.
- 那么,怎么保存函数地址呢?这就需要用到函数指针,来保存函数地址.
void test()
{
printf("hehe\n");
}
//下面pfun1和pfun2哪个有能力存放test函数的地址?
void (*pfun1)();
void *pfun2();
- pfun1 可以存放, pfun1先和
*
结合,表明这是一个指针;剩下的void ...()
,表示指针指向一个函数,这个函数没有参数,返回值是void
.
-
void *pfun2();
这只是声明一个函数,函数名叫pfun2,没有参数,返回值是void*
.
#include <stdio.h>
int Add(int a, int b)
{
return a + b;
}
int main(void)
{
int a = 3;
int b = 5;
int (*p)(int, int) = Add; //p是函数指针,指向Add函数
int ret1 = Add(a, b);
int ret2 = (*p)(a, b);
int ret3 = p(a, b);
printf("%d\n", ret1);
printf("%d\n", ret2);
printf("%d\n", ret3);
return 0;
}
- 定义了函数指针
p
指向了Add
函数
-
p = &Add = Add
-> (*p) = (*&Add) = Add = p
- 下面有两行代码,取自《C陷阱与缺陷》,可以加深对函数指针的理解
//代码1
(*(void (*)())0)();
从内向外一步一步理解
-
void (*)()
表示一个函数指针类型,指向一个没有参数,返回值是void
的函数.
-
(void (*)())0
表示将0
强制类型转换为函数指针类型.
-
(*(void (*)())0)()
表示对函数指针变量0
解引用得到0
地址处的函数,同时后面跟上参数()
,总结就是调用了0
地址处的函数.
- 但运行这段代码会崩溃,
0
地址处的代码需经操作系统转换为内核态才可以访问.
//代码2
void (*signal(int , void(*)(int)))(int);
-
void(*)(int)
表示一个函数指针类型,指向一个有一个int
类型参数,返回值是void
的函数.
-
signal(int , void(*)(int))
中signal
没有和*
被括号括上,说明signal
是一个函数,这个函数有两个参数,一个是int
类型,一个是void (*) (int)
函数指针类型
- 观察整体结构,大概是这样
void(* p )(int)
,这是一个函数指针类型,同时也是signal
函数的返回值
- 总结:
signal
函数的声明,有两个参数:int
类型和void(*)(int)
函数指针类型,返回值也是void(*)(int)
类型的
代码2太复杂了,可以使用typedef
进行优化
typedef void(* pfun_t)(int);
pfun_t signal(int, pfun_t);
这样代码可读性就大大提高了.
6. 函数指针数组
函数指针数组是一个数组,存储了多个函数指针的地址
int (*parr[10])()
返回类型 (*函数指针数组名[数组大小])(参数列表)
parr
先和[]
结合,说明parr
是一个数组,数组的类型就是剩余的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;
}
void menu()
{
printf("##########################\n");
printf("###### 1.add 2.sub ######\n");
printf("###### 3.mul 4.div ######\n");
printf("###### 0.exit ######\n");
printf("##########################\n");
}
int main(void)
{
int x = 0;
int y = 0;
int input = 0;
do
{
menu();
printf("请选择:");
scanf("%d", &input);
switch (input)
{
case 1:
printf("请输入操作数:");
scanf("%d %d", &x, &y);
printf("%d\n", add(x, y));
break;
case 2:
printf("请输入操作数:");
scanf("%d %d", &x, &y);
printf("%d\n", sub(x, y));
break;
case 3:
printf("请输入操作数:");
scanf("%d %d", &x, &y);
printf("%d\n", mul(x, y));
break;
case 4:
printf("请输入操作数:");
scanf("%d %d", &x, &y);
printf("%d\n", div(x, y));
break;
case 0:
printf("退出计算器\n");
break;
default:
printf("输入有误,请重新输入\n");
break;
}
} while (input);
return 0;
}
-
上面的程序使用了switch-case
语句用来判断输入,明显代码有冗余
-
若后续还需添加模运算,逻辑运算,则需要更多的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(void)
{
int x = 0;
int y = 0;
int input = 1;
int (*pArr[5])(int, int) = { NULL, add, sub, mul, div };
while (input)
{
menu();
printf("请选择:");
scanf("%d", &input);
if ((input >= 1) && (input <= 4))
{
printf("请输入操作数:");
scanf("%d %d", &x, &y);
printf("%d\n", (*pArr[input])(x, y));
}
else
{
printf("输入有误,请重新输入\n");
}
}
return 0;
}
- 函数指针数组提供了一种灵活的方式来管理和调用多个函数
7. 指向函数指针数组的指针
指向函数指针数组的指针,是一个指针指向一个数组,数组的元素都是函数指针
#include <stdio.h>
void test(const char* str)
{
printf("%s\n", str);
}
int main(void)
{
//函数指针
void (*pFun)(const char*) = test;
//函数指针数组
void (*pFunArr[5])(const char*);
pFunArr[0] = test;
//指向函数指针数组的指针
void (*(*ppFunArr)[5])(const char*) = &pFunArr;
return 0;
}
- 写复杂指针,从简单写到复杂.
- 首先它是一个指针,
(*p)
- 这个指针指向了一个数组,
(*p)[5]
- 数组类型是函数指针类型,
void (*(*p)[5])(const char*)
8.回调函数
回调函数是指作为参数传递给另一个函数的函数,当满足特定条件时,另一个函数会调用该回调函数来执行特定的操作.
回调函数就是一个通过函数指针调用的函数.如果你把函数的指针(地址)作为参数传递给另一个函数,当这个指针被用来调用其所指向的函数时,我们就说这是回调函数.
回调函数不是由该函数的实现方直接调用,而是在特定的事件或条件发生时,由另外一方调用的,用于对该事件或条件进行响应.
8.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 calculate(int (*p)(int, int))
{
int x = 0;
int y = 0;
printf("请输入操作数:");
scanf("%d %d", &x, &y);
printf("%d\n", p(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(void)
{
int input = 1;
while (input)
{
menu();
printf("请选择:");
scanf("%d", &input);
switch (input)
{
case 1:
calculate(add);
break;
case 2:
calculate(sub);
break;
case 3:
calculate(mul);
break;
case 4:
calculate(div);
break;
case 0:
printf("退出计算器.\n");
break;
default:
printf("你的输入有误,请重新输入.\n");
break;
}
}
return 0;
}
- 在
calculate
函数中,传入了函数指针类型的参数,根据用户的不同输入来调用不同的回调函数进行计算
- 在用户输入操作数后,调用传递过来的函数指针,进行计算,并输出结果
- 计算器程序通过回调函数的机制,使得计算逻辑与菜单显示解耦,同时也提高了代码的可扩展性和灵活性.可以根据需求添加更多的运算函数,并在菜单中相应地增加选项
- 回调函数只有在被调用后,才会运行
8.2 库函数qsort
之前已经写过对数组的内容进行冒泡排序,但是只能对整型数据进行排序.
那么如何写一个能对通用(适用于所有类型的)的排序呢?
- 其实,C语言库提供了一个基于快速排序的函数
qsort
#include <stdlib.h>
void qsort(void* base, size_t num, size_t size,
int (*compar)(const void*, const void*));
qsort
函数是对数组的元素进行排序
- 对基于
base
开始的连续num
个size
字节的元素,使用compar
回调函数来进行排序.
- 排序算法,是通过调用传入
qsort
函数的函数指针所指向的函数compar
来实现的.
- 函数没有返回值,直接对数组的内容进行修改,使用
compar
回调函数进行排序.
- 该函数对于两个等价的元素不进行排序.
参数:
-
void*
无类型指针
在传入的函数指针参数中,该函数类型中的参数内容是const void*
,const
是为了不让指针所指向的内容被修改,那void*
究竟是什么呢?
void*
是一种特殊的C语言数据类型,称为"无类型指针".它可以被强制类型转换为任意类型的指针.
可用于
- 函数传参: 当一个函数需要接受不同类型的指针参数时,可以使用
void*
参数,然后在函数内部根据实际需要进行类型转换。
void foo(void *ptr)
{
int *p = (int *)ptr; // 将 void* 转换为 int*
// 对 p 进行操作
}
int main()
{
int num = 10;
foo(&num); // 传递 int* 类型指针的地址给 foo 函数
return 0;
}
- 动态内存分配:在使用
malloc
或 calloc
动态分配内存时,返回的指针类型为 void *
.需要根据实际情况进行类型转换.
int *ptr = (int *)malloc(sizeof(int)); // 将 void* 转换为 int*
- 通用数据结构:可以使用 `void *`` 指针来在数据结构中存储不同类型的元素,然后根据需要进行类型转换.
struct Node
{
void *data;
struct Node *next;
};
需要注意的是:void*
类型的指针,不能直接进行解引用,进行指针运算.需要先将该指针强制类型转换为具体指针类型,才能进行相关指针操作.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
//学生类型结构体
struct Stu
{
char name[20];
int age;
};
//打印学生结构体数组内容
void print_stu(struct Stu* stuArr, size_t num)
{
int i = 0;
for (i = 0; i < num; i++)
{
printf("%s %d\n", stuArr[i].name, stuArr[i].age);
}
}
//对整型数据进行排序 test1
int cmp_int(const void* p1, const void* p2)
{
return *(int*)p1 - *(int*)p2; //先将指针强制转换类型为int*, 再使用*操作符对指针进行解引用得到数据
}
void test1()
{
int arr[10] = { 9,8,7,6,5,4,3,2,1,0 };
size_t num = sizeof(arr) / sizeof(arr[0]);
size_t size = sizeof(arr[0]);
//调用qsort函数
qsort(arr, num, size, cmp_int);
int i = 0;
for (i = 0; i < num; i++)
{
printf("%d ", arr[i]);
}
printf("\n");
}
//对结构体类型进行排序, 基于整型数据 test2
int cmp_stu_age(const void* p1, const void* p2)
{
return ((struct Stu*)p1)->age - ((struct Stu*)p2)->age; //先将指针强制转换类型为struct Stu*, 再使用->操作符访问到指针成员变量age
}
void test2()
{
//创建结构体数组
struct Stu arr[3] = {
{"zhangsan", 20},
{"lisi", 21},
{"wangwu", 19}
};
size_t num = sizeof(arr) / sizeof(arr[0]);
size_t size = sizeof(arr[0]);
//使用qsort进行排序
qsort(arr, num, size, cmp_stu_age);
print_stu(arr, num);
printf("\n");
}
//对结构体类型进行排序, 基于字符类型数据 test3
int cmp_stu_name(const char* p1, const char* p2)
{
return strcmp(((struct Stu*)p1)->name, ((struct Stu*)p2)->name); //使用strcmp 比较字符串
}
void test3()
{
//创建结构体数组
struct Stu arr[3] = {
{"zhangsan", 20},
{"lisi", 21},
{"wangwu", 19}
};
size_t num = sizeof(arr) / sizeof(arr[0]);
size_t size = sizeof(arr[0]);
//使用qsort进行排序
qsort(arr, num, size, cmp_stu_name);
print_stu(arr, num);
printf("\n");
}
//对排序字符类型进行排序 test4
int cmp_char(const char* p1, const char* p2)
{
return *(char*)p1 - *(char*)p2;
}
void test4()
{
char arr[] = { 'b','e','c','a','f','d' };
size_t num = sizeof(arr) / sizeof(arr[0]);
size_t size = sizeof(arr[0]);
qsort(arr, num, size, cmp_char);
int i = 0;
for (i = 0; i < num; i++)
{
printf("%c ", arr[i]);
}
printf("\n");
}
int main(void)
{
//测试排序整型数组的数据
test1();
//测试排序结构体数组的数据, 基于整型数据排序
test2();
//测试排序结构体数组的数据, 基于字符类型数据排序
test3();
//测试排序字符类型数组的数据
test4();
return 0;
}
主要是为了熟悉cmp
函数的写法
程序运行结果如下:
- 熟悉了
qsort
的用法,模拟qsort
的功能,实现通用的冒泡排序
#include <stdio.h>
//交换函数
void swap(void* p1, void* p2, size_t 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_sort(void* base, size_t num, size_t size, int (*cmp)(const char*, const char*))
{
int i = 0;
int j = 0;
for (i = 0; i < num - 1; i++)
{
for (j = 0; j < num - i - 1; j++)
{
if (cmp((unsigned char*)base + j * size, (unsigned char*)base + (j + 1) * size) > 0) //arr[j] > arr[j+1]
{
swap((unsigned char*)base + j * size, (unsigned char*)base + (j + 1) * size, size);
}
}
}
}
- 写出冒泡排序的框架:一共
num - 1
轮, 每轮比较num - 1 - i
次
- 使用比较函数,两个参数相当于
arr[j]
和arr[j+1]
.
-
首先要将void *
类型的数组首地址base
,强制类型转换为unsigned char *
,一则能对base
进行指针运算,二则将base
看成一连串的字符数组;通过对base
地址的加减,得到对应元素的地址.
-
j * size
是元素相对于数组首元素地址的偏移量, 将数组首元素地址加上偏移量,就能得到对应元素的地址
-
cmp
函数的两个参数就是const char*
,直接将偏移后的元素地址传入即可
-
cmp
函数返回值:
- 若第一个元素所指向的值
>
第二个元素所指向的值, 返回1
- 若第一个元素所指向的值
==
第二个元素所指向的值, 返回0
- 若第一个元素所指向的值
<
第二个元素所指向的值, 返回-1
若要求升序排序数组,则如果前一个元素大于后一个元素的值,则判断为真,两元素进行交换.所以需要cmp(....) > 0
- 将两元素进行交换,构造了
swap
函数,参数有三个,前两个为被交换的两元素地址,第三个为两元素所占字节大小
-
本质是将两元素按字节进行交换, 假设元素占据size = 4
个字节,循环则需要重复4
次,每次将一个字节的内容进行交换
-
(char*)p1
将p1
强制转换类型为char*
,方便对指针运算,按字节访问p1
所指向的数据
-
将(char*)p1
加i
,依次按字节访问该地址所指向空间的数据
-
*((char*)p1 + i)
,对指针*
解引用,访问到该地址上占据一个字节的数据
-
交换两个数据,简单设置个中间量tmp
,将两个数据进行交换
对自建的bubble_sort
函数进行测试
#include <stdio.h>
#include <string.h>
//交换函数
void swap(void* p1, void* p2, size_t 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_sort(void* base, size_t num, size_t size, int (*cmp)(const char*, const char*))
{
int i = 0;
int j = 0;
for (i = 0; i < num - 1; i++)
{
for (j = 0; j < num - i - 1; j++)
{
if (cmp((unsigned char*)base + j * size, (unsigned char*)base + (j + 1) * size) > 0) //arr[j] > arr[j+1]
{
swap((unsigned char*)base + j * size, (unsigned char*)base + (j + 1) * size, size);
}
}
}
}
//学生类型结构体
struct Stu
{
char name[20];
int age;
};
//打印学生结构体数组内容
void print_stu(struct Stu* stuArr, size_t num)
{
int i = 0;
for (i = 0; i < num; i++)
{
printf("%s %d\n", stuArr[i].name, stuArr[i].age);
}
}
//对整型数据进行排序 test1
int cmp_int(const void* p1, const void* p2)
{
return *(int*)p1 - *(int*)p2; //先将指针强制转换类型为int*, 再使用*操作符对指针进行解引用得到数据
}
void test1()
{
int arr[10] = { 9,8,7,6,5,4,3,2,1,0 };
size_t num = sizeof(arr) / sizeof(arr[0]);
size_t size = sizeof(arr[0]);
bubble_sort(arr, num, size, cmp_int);
int i = 0;
for (i = 0; i < num; i++)
{
printf("%d ", arr[i]);
}
printf("\n");
}
//对结构体类型进行排序, 基于整型数据 test2
int cmp_stu_age(const void* p1, const void* p2)
{
return ((struct Stu*)p1)->age - ((struct Stu*)p2)->age; //先将指针强制转换类型为struct Stu*, 再使用->操作符访问到指针成员变量age
}
void test2()
{
//创建结构体数组
struct Stu arr[3] = {
{"zhangsan", 20},
{"lisi", 21},
{"wangwu", 19}
};
size_t num = sizeof(arr) / sizeof(arr[0]);
size_t size = sizeof(arr[0]);
bubble_sort(arr, num, size, cmp_stu_age);
print_stu(arr, num);
printf("\n");
}
//对结构体类型进行排序, 基于字符类型数据 test3
int cmp_stu_name(const char* p1, const char* p2)
{
return strcmp(((struct Stu*)p1)->name, ((struct Stu*)p2)->name); //使用strcmp 比较字符串
}
void test3()
{
//创建结构体数组
struct Stu arr[3] = {
{"zhangsan", 20},
{"lisi", 21},
{"wangwu", 19}
};
size_t num = sizeof(arr) / sizeof(arr[0]);
size_t size = sizeof(arr[0]);
bubble_sort(arr, num, size, cmp_stu_name);
print_stu(arr, num);
printf("\n");
}
//对排序字符类型进行排序 test4
int cmp_char(const char* p1, const char* p2)
{
return *(char*)p1 - *(char*)p2;
}
void test4()
{
char arr[] = { 'b','e','c','a','f','d' };
size_t num = sizeof(arr) / sizeof(arr[0]);
size_t size = sizeof(arr[0]);
bubble_sort(arr, num, size, cmp_char);
int i = 0;
for (i = 0; i < num; i++)
{
printf("%c ", arr[i]);
}
printf("\n");
}
int main(void)
{
//测试排序整型数组的数据
test1();
//测试排序结构体数组的数据, 基于整型数据排序
test2();
//测试排序结构体数组的数据, 基于字符类型数据排序
test3();
//测试排序字符类型数组的数据
test4();
return 0;
}
程序运行结果符合预期:
本章完.