数组名在除了定义数组,使用sizeof()函数和加&之外均代表数组首元素的地址
int (*p2)[10] = &arr;
我们首先来看看这个代码,*p2是一个指针,在代码中p2外的括号不可缺少的,因为[]的优先级更高所以如果不加括号,这行代码就会变成一个指针数组的定义
int Add(int x, int y)
{
return x + y;
}
假设我们要用指针来存储这个加法函数的地址,首先我们要知道函数名就代表函数的地址
int (*pf)(int, int) = Add;//pf是指针名,最前面的int代表函数的返回类型,后面的两个int表示函数的参数类型
值得注意的是,代表函数的参数类型的int后面加上x或y或者任意字符都是可以的,只要能明确其类型就行,但这没有必要
因为函数名就代表函数的地址,所以事实上函数的指针与函数名是等价的,所以我们在调用函数指针时既可以将函数指针解引用为原函数名来使用函数,也可以使用指针来直接调用,如
// 先解引用为原函数名
int sum = (*pf)(2,3);
// 直接使用指针
sum = pf(2,3);
我们可以发现在第一个调用过程中,*其实只是一个摆设
顾名思义,该数组就是用来存放函数指针的数组(也可以说数组中存的就是函数名),那么我们要如何来使用这个东西呢?现在咱们再增加一个减法函数
int Sub(int x, int y)
{
return x-y;
}
那么比方说我们想要来完成一个能实现加减法的计算器功能,那么我们可以先进行如下代码
int (*pA)(int, int) = Add;
int (*pS)(int, int) = Sub;
我们会发现这两个函数无论是参数还是返回值的类型都是相同的,所以我们可以考虑使用一个指针数组来实现代码,让我们这样定义函数指针数组
int (*pArr[5])(int, int) = {Add, Sub};//因为[]的优先级高于*,所以当指针名不加括号时pArr就代表一个函数指针数组
更为深入地来看,我们可不可以获取并存储pArr的地址呢?答案是可以的
int (*(*p)[5])(int, int) = &pArr;//其中p是一个指针指向了一个元素为函数指针的数组
到这里套娃的工作我们就不继续了,让我们再来看看回调函数
回调函数就是用一个函数来调用另一个函数,以实现在特定条件下调用某个函数的目的。现在我们有一个Calc()函数,我们要求当我们把Add函数传过去时就完成加法,将Sub传过去时就完成减法,
void Calc(int (*pf)(int, int))
{
int ret = pf(5,3);
printf("%d", ret);
}
int main()
{
Calc(Add);//既然我们传输了函数的地址,我们就要用一个函数指针来接收
Calc(Sub);
return 0;
}
这就是一个简单的回调函数,当把Add传参给Calc时,pf就接收了Add的地址,并用Add来计算5+3的值,Sub同理来计算5-3的值
我们拥有很多种排序方法,如冒泡排序,选择排序,插入排序等,这次我想主要来讲qsort快速排序,并在简单说完冒泡排序后用冒泡排序来模拟qsort函数
我们先来简单地讲讲冒泡排序,冒泡排序的基本思想无非是两两比较大小并排序,,比方说我想把{9,8,7,6,5,4,3,2,1,0}这10个数字改为升序应该怎么做呢
void BubbleSort(int arr[], int sz)
{
int i = 0;
for(i = 0; i < sz - 1; i++)//我们每轮会改变一个数字的位置,因为当我们把之前的9个数字排列完之后最后一个数字的位置也必然已经正确了,故只需排列比数组大小少一轮
{
int j = 0;
for(j = 0; j <= sz - i -1; j++);//因为每轮过后都会有一个数字已经排序完成且位于数列后方,所以每轮之后需要排列的数字都可以减1
{
if(arr[j] > arr[j+1])
{
int tmp = arr[j+1];
arr[j+1] = arr[j];
arr[j] = tmp;
}
}
}
}
int main()
{
int arr[10] = {9,8,7,6,5,4,3,2,1,0};
BubbleSort(arr,sz);//当我们要进行排序的时候一般会考虑将数组的大小也传送过去
}
这就是一个冒泡排序,如果想看它的结果,我们也可以再添加一个打印程序
PrintArr(int arr, int sz)
{
int i = 0;
for(i = 0; i < sz; i++)
{
printf("%d ", arr[i]);
}
}
对于冒泡排序,我们会发现它只能用于对整型的排序,那么如果我们想让一个程序来排列任意类型时,应该怎么办呢,qsort函数能帮我们解决这个难题,下面我们来讲讲qsort函数
void qsort( void *base, size_t num, size_t width, int (__cdecl *compare )(const void *elem1, const void *elem2 ) );
这是qsort函数的格式规范,看起来很长很复杂,但当我们把它给细分出来时就显得很简单了,让我们把它分行写出来,
void qsort( void base,
size_t num,
size_t width,
int (cmp)(const void e1, const void e2 ) );
之前说过,我们排序时除了传送数组名外我们还会把数组大小也给传送,但在这个函数中,我们可以发现这个函数第一个参数是数组,第二个参数是数组大小(元素个数),那么之后两个参数是什么东西?
要回答这个问题,我们首先要注意一下base前的类型是void,void是一种无具体类型的指针,可以理解为通用指针,因为我们希望qsort函数能够排序任意类型的数组,所以我们就需要void的帮助
重点来了!
void能接收任意类型的地址,但它有一个缺点:不能进行运算,不能解引用,因为指针进行运算时依赖指针类型来明确指针所指向类型的内存大小,比如若p是一个整型指针,那么p++增加的就是4bit,而当p是字符指针时,p++增加的就只是1bit,而void*未明确指针指向的类型,自然无法进行运算了,所以我们就能理解第三个参数存在的意义了,它代表的是每个元素类型所占的内存大小,即宽度,这样qsort函数就可以知道每个元素的内存大小以便排序的进行了
现在我们为qsort函数传输了数组,元素个数和元素大小,按道理来说这已经足以让qsort进行排序了,那第四个参数有什么作用呢?你一定想到了,我们当初使用qsort的目的之一就是为了排序任意类型的元素,所以我们必须对种类型的元素定义一个特定的排序规则(先提一嘴,第四个参数就是一个函数指针,也就是说我们需要创建一个函数并传递它的指针),比方说我现在想要给一个结构体排序,
Struct Stu
{
char name[20];
int age;
float score;
};
struct Stu s[3] = { {"张三",15,70.0},{"李四",35,72.0},{"王五",55,14.0} };
显然我们不能简单地比较三个结构体的大小,所以我们必须确立一个规则,确定应该使用名字,年龄还是分数来对三位学生进行排序,我们先来看看qsort对这个排序规则的要求
返回值 | 描述 |
---|---|
返回值小于0 | e1小于e2 |
返回值等于0 | e1等于e2 |
返回值大于0 | e1大于e2 |
也就是说当e1小于e2时,cmp需要返回一个负数作为qsort的参数,等于时返回0,大于时返回正数,这个很简单,只需要将e1-e2即可,现在让我们看看如果想用名字来进行排序应该怎么操作
#define _CRT_SECURE_NO_WARNINGS
#include
#include
#include
struct Stu
{
char name[20];
int age;
float score;
};
int cmp(const void* e1, const void* e2)
{
return strcmp(((struct Stu*)e1)->name,((struct Stu*)e2)->name);//因为void*类型不能进行运算,所以我们把它强制转换为struct Stu*类型,取其中的name,并用strcmp函数来比较字符串大小
}
int main()
{
struct Stu s[3] = { {"张三",15,70.0},{"李四",35,72.0},{"王五",55,14.0} };
int sz = sizeof(s) / sizeof(s[0]);
qsort(s, sz, sizeof(s[0]), cmp);//这里实际上就是一个回调函数,qsort回调了cmp,cmp接收了类型为const void*的指针e1和e2
return 0;
}
同理,如果我们想用年龄来进行排序,我们只需将cmp函数修改为
return ((struct Stu*)e1)->age-((struct Stu*)e2)->age;
而如果我们想实现倒叙则只需要把cmp中前后两个参数调换位置即可。
现在我们已经可以理解冒泡排序和qsort的用法,那么如何使用冒泡排序来模拟qsort的功能呢?
现在笔者先把整体代码展现一下,接着再对每块代码细细解释
void print_arr(int arr[], int sz)
{
int i = 0;
for (i = 0; i < sz; i++)
{
printf("%d ", arr[i]);
}
printf("\n");
}
int cmp_int(const void* e1, const void* e2)
{
return((*(int*)e1) - (*(int*)e2));
}
void swap(char* buf1, char* buf2, int width)
{
int i = 0;
for (i = 0; i < width; i++)
{
char tmp = *buf1;
*buf1 = *buf2;
*buf2 = tmp;
buf1++;
buf2++;
}
}
void BubbleSort(void* base, size_t num, size_t width, int (*cmp_int)(const void* e1, const void* e2))
{
size_t i = 0;
for (i = 0; i < num - 1; i++)
{
size_t j = 0;
for (j = 0; j < num - 1 - i; j++)
{
if (cmp_int((char*)base + j * width, (char*)base + (j + 1) * width) > 0)
{
swap((char*)base + j * width, (char*)base + (j + 1) * width, width);
}
}
}
}
int main()
{
int arr[10] = { 9,8,7,6,5,4,3,2,1,0 };
int sz = sizeof(arr) / sizeof(arr[0]);
BubbleSort(arr, sz, sizeof(arr[0]), cmp_int);
print_arr(arr, sz);
return 0;
}
现在让我们来理一下这些代码的思路。
这里的BubbleSort()其实就相当于qsort(),和qsort一样,我们也传入了4个参数,分别是数组,元素个数,元素大小和排序函数,他们的作用也和qsort函数中的一样,我就不一一赘述了。让我们来看一下代码,首先我们还是冒泡排序一样,进行了两次循环,第一次(i)代表排序趟数,第二次(j)代表每趟的排序次数。(这块的详细介绍在本篇文章稍前的冒泡排序中也有详细介绍。)所以让我们马上来看看这个函数的不同之处,让我们接着看代码,
if (cmp_int((char*)base + j * width, (char*)base + (j + 1) * width) > 0)
{
swap((char*)base + j * width, (char*)base + (j + 1) * width, width);
}
在这里我们使用了两个函数,cmp_int和swap,其中cmp_int的功能是对数列前后两个元素进行大小比较,swap则负责对降序的元素位置进行交换。
类似于qsort函数,在这个函数中我们也希望这个比较函数能够在升序时返回负数,降序时返回一个正数,所以我们只需要将数组中的前后两个元素依次传参,并用前一个元素减去后一个元素,并将该值作为函数的返回值。而其中我们为了能够使这个比较函数适用于各种类型的数组,我们在传参时将参数类型设成了char*,所以在这个为int类型比较大小的函数里,我们可以把它强制类型转换为int*(当然在字符比较函数里我们可以将它强制转换为char*)
如果知道这个函数中有两个元素是降序,我们就要将其调换位置,在传参时,为了函数的通用性,我们依然传递了char类型的参数,并用char进行接收,所以我们绝对就不能直接调换元素的位置,而具体的调换方法呢,我们就用9和8这两个元素代替,我们知道在小端存储中,9和8的内存分别为09 00 00 00和08 00 00 00且他们的内存是连续的
我们想要把09和08,09后的00和08后的00,09后的00后的00······(此处省略一万字)交换位置,所以现在思路就有了,我们首先获得09和08元素的第一个字节的地址,交换位置后向后调整一个字节,直到这个元素的所有字节被调换完,对于int类型,它的大小是四个字节,所以我们需要对每个元素调换四次位置
void swap(char* buf1, char* buf2, int width)
{
int i = 0;//i代表调换次数,这里我们希望他循环4次
for (i = 0; i < width; i++)
{
//调换
char tmp = *buf1;
*buf1 = *buf2;
*buf2 = tmp;
buf1++;
buf2++;
}
}