C语言:指针详解(4)

作者本人由于大一下学期事情繁多,大部分时间都在备赛,没有时间进行博客撰写,如今已经到了暑假时间,作者将抓紧每一天的时间进行编程语言的学习,由于目前作者已经进行到了C++的学习,C语言阶段的学习与初阶数据结构的学习的任务都已完成,所以每天会进行一到两篇博客的发布来抓回来之前放飞的鸽子(视情况而定,本人没有一个既定的计划,一切随缘,但是大致就是早上起来听课,听完课开始敲代码做题写博客,完成编程学习后会进行全国大学生数学竞赛以及全国大学生数学建模比赛的备赛,作者尽量,呜呜),同时也会努力勤奋地维护gitte(gitte地址会在本文结尾放出,有兴趣的同学可以点开看一下,主要是作者本人假期间写的一些练习~),希望本人能在这个暑假大大提高编程能力,以便参战明年的蓝桥杯等计算机编程相关赛事,同时也会在明年暑假考虑线上/线下的计算机岗位实习/打工。也希望大家不要在假期摆烂~表面上说大学生在暑假要么很闲在家里躺着,要么就外出打工,但是作为以后要进行技(码)术(农)开(探)发(花)(bushi)的我们应该共勉,不要在暑假这个关键时期摆烂~好好学习技术,以后进入大厂拿到高薪工作~~

后续我也会考虑将自己在家所学习的MATLAB的相关知识进行总结并以博客的形式发出~以便对建模比赛有意向的同学学习!

话不多说,我们接着很久以前的内容继续来带大家探讨C语言中的指针~

目录

一、回调函数

二、qsort函数的使用举例

1.使用qsort函数排序整型数据

2.使用qosrt函数排序字符串类型的数据

3.使用qsort函数排序结构体类型的数据

4.使用qsort函数排序结构数据

三、冒泡排序(完全版)

正文开始

一、回调函数

回调函数就是一个通过函数指针 调用的函数

如果你把函数的指针(地址)作为参数传递给另一个函数,当这个指针被用来调用其所指向的函数 时,被调用的函数就是回调函数。回调函数不是由该函数直接调用,而是在特定的事件或条件发生时由另外一方调用的,用于对该事件或条件进行响应。在指针详解(3)中我们写的简易计算器的实现代码中,红色框中的代码是重复出现的,其中虽然执行计算的逻辑是有区别的,但是输入输出操作是冗余的。有没有办法简化一些呢?

因为红色框中的代码只有调用函数的逻辑是有差异的,我们可以把调用的函数的地址以参数的形式 传递过去,使用函数指针接收,函数指针指向什么函数就调用什么函数,这里使用的就是回调函 数的功能。

cpp 复制代码
//使⽤回调函数改造后
#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 calc(int (*pf)(int,int))
{
    int x = 0,y = 0,z = 0;
    printf("输入操作数:");
    scanf("%d %d", &x, &y);
    z = pf(x, y);
    printf("ret = %d\n", ret);
}
 
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:
            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;
}

虽然这样不能显著减少代码量,但是当我们一眼看上去时,代码就会显得简洁很多,在未来写代码时,如果遇到例如实现简易计算器的代码中有高度相似的内容,我们就可以利用回调函数来将这些高度相似的内容进行封装,封装到一个函数内,然后通过这个函数来间接调用原本要调用的函数。

二、qsort函数的使用举例

接下来我们再讲解一个与回调函数相关的一个函数------qsort函数

在C库中,qsort函数其实是用来对某个数组的元素进行排序的。我们先来观察一下qsort函数的原型:

void qsort (void* base, size_t num, size_t size, int (*compar)(const void*,const void*));

qsort函数一共有四个形参,它们分别是base、num、size以及一个函数指针。

base:指向要排序数组的第一个元素的指针

num:数组中元素的个数

size:数组中每个元素的大小(单位为字节)

compar:一个比较函数指针,用于定义两个元素之间的排序关系。compar函数接收两个const void*类型的参数,并返回一个整数,指示第一个参数所指向的元素与第二个参数所指向的元素的相对顺序。

|-----|-----------------------|
| 返回值 | 意义 |
| <0 | 第一个被指向的元素在第二个被指向的元素之前 |
| >0 | 第一个被指向的元素在第二个被指向的元素之后 |
| =0 | 第一个被指向的元素与第二个被指向的元素相等 |

我们已经知晓的排序有这些:希尔排序、选择排序、冒泡排序、归并排序、快速排序、堆排序等。而qsort函数的底层逻辑就是快速排序。为了便于理解qsort函数的逻辑以及后面对qsort的函数进行模拟实现,我们这里需要着重理解冒泡排序。由于在之前的文章中已经提及并写出冒泡排序的代码,所以这里不再做过多的讲解:C语言:指针详解(2)-CSDN博客。冒泡排序(整型版)的代码如下:

cpp 复制代码
#include <stdio.h>
 
void bubble_sort(int arr[], int sz)//参数接收数组元素个数
{
    int i = 0;
    for(i=0; i<sz-1; i++)
    {
        int j = 0;
        for(j=0; j<sz-i-1; j++)
        {
            if(arr[j] > arr[j+1])
            {
                int tmp = arr[j];
                arr[j] = arr[j+1];
                arr[j+1] = tmp;
            }
        }
    }
}
 
int main()
{
    int arr[] = {3,1,7,5,8,9,0,2,4,6};
    int sz = sizeof(arr)/sizeof(arr[0]);
    bubble_sort(arr, sz);
    for(int i=0; i<sz; i++)
    printf("%d ", arr[i]);
    return 0;
}

我们不难发现,我们前面实现的冒泡排序只能对整型数组进行排序,而不能对其他类型的数组进行排序。经过上面我们对qsort函数的理解,qsort函数是能够对所有元素进行排序的,不光光是整型。所以为了模拟实现qsort函数,我们需要对前面我们所写出的冒泡排序的代码进行改进,我们要让冒泡排序不光能对整型数组进行排序,也能对如char、double等等类型进行排序,这样才能基于该冒泡排序来模拟实现qsort函数。

1.使用qsort函数排序整型数据

在实现冒泡排序(完全版)之前,我们先要理解qsort函数是怎样运作的。通过对qsort函数形参的观察,我们发现有一个形参我们无法直接进行传参------函数指针。所以我们需要自己实现一个函数后再进行传参。由上面分析可得,我们要实现的函数返回类型是int,因为是要获取两个元素的相对位置,不难得出函数的原型:

int compar (const void* p1, const void* p2);

实现该函数的目的是为了实现某个数组元素的排序,我们当然不希望数组中的元素被改变,所以要在形参void*之前加上const来防止一些操作对数组元素进行改变的操作,来提高程序的安全性。有了该函数的原型,我们就不难写出qsort函数的传参以及调用了:

cpp 复制代码
#include <stdio.h>

//qosrt函数使用前得实现一个比较函数
int compare_int(const void * p1, const void * p2)
{
    return (*( int *)p1 - *(int *) p2);
}

int main()
{
    int arr[] = { 1, 3, 5, 7, 9, 2, 4, 6, 8, 0 };
    int i = 0;
    qsort(arr, sizeof(arr) / sizeof(arr[0]), sizeof (int), compare_int);
    for (i = 0; i< sizeof(arr) / sizeof(arr[0]); i++)
    {
        printf( "%d ", arr[i]);
    }
    printf("\n");
    return 0;
}

由于int_cmp函数的两个形参均是void*类型,所以在获取两个元素之前我们需要先将这两个元素的地址再强转为int*类型来获取它们真正的地址,然后再分别进行解引用操作来获取原本数组中的元素,最后再进行相减操作,然后再根据上面表格来判断是否需要排序,如何进行排序。

实现了比较函数后,我们在main函数随机创建一个乱序数组,然后将该传入的参数传入到qsort函数当中,qsort函数就会帮我们将数组按升序排好,随后打印出来就是排序后的结果。

在上面我们对qsort函数进行了调用之后,我们可以感受到qsort函数的精髓所在,那就是其函数指针的运用,qsort根据比较函数的返回数据来判断是否需要排序以及如何进行排序,那么我们根据这点就可以来实现一个对所有数据都起效果的冒泡排序。这个冒泡排序不光光是用到了qsort函数中所运用到的比较函数,还用到了在之前我们所提到的回调函数。有了比较函数,我们可以轻松地将所有元素类型的数组用冒泡排序来进行排序;有了回调函数,我们大可将比较不同类型元素的比较函数进行封装,使代码变得更加简洁。

在正式地实现冒泡排序(完全版)之前,我们需要再了解两个特殊的例子后才能实现。

2.使用qosrt函数排序字符串类型的数据

如果我们需要使用qsort函数来对一个元素类型为字符串的数组,那我们该如何实现呢?

我们在对整型数据进行大小比较时,可以直接使用">" "<" "=="来直接进行大小比较。但是字符串不像整型数据那样方便,我们不能直接将两个字符串直接进行比较大小。想象一下,这里有若干个字符串,我想让你随机挑出两个字符串进行比较大小,你知道该如何进行比较吗?我们不妨细心分析一下,假设有以下两个字符串:

abcdef;

abedew;

我们不难发现,这两个字符串只有最后一个字符是不同的。虽然我们不能直接将两个字符串进行大小比较,但是我们可以逐位比较,就是将两个字符串的对应位置进行比较。比如这两个字符串,第一个位置都是字符a,因此是对应相等的,直到最后一个字符时我们发现字符不一样了,这时候我们就要依靠ascll码值来进行比较,不难得出,字符w明显是大于字符f的,又由于这两个字符串前面五个位置都相等,只有最后一个位置是不相等的,所以最后一个位置起到了决定性作用。所以上面两个字符串中abcdew是比较大的。

再给出两个字符串:

abcdef;

aby;

我们发现,这两个字符串的长度不相等,那我们该如何比较呢?还是一样,我们先对这两个字符串进行逐位比较,当我们比较到第三个字符的时候,发现字符y是大于字符c的,到这里我们就无需再进行比较了(无论长度较长的字符串后面还有多少个字符)因为y是大于c的,所以上面两个字符串中aby是比较大的。

以上两个例子就是我们在比较字符串时两个常见的例子,总结成一句话就是:逐位比较,别忘了长度

但实际上呢,我们在进行代码编写的时候是不会进行这样繁琐的大小比较的,以上的字符串比较方法只适合我们在分析某些题目的思路时考虑用到的,但在编程时,创始人已经考虑到了这点,并在C库中为我们提供了这么一个专门比较字符串的函数------**strcmp()**函数。在这里只简单介绍strcmp函数的用法,后续文章会详细介绍strcmp()函数的用法

strcmp()函数的原型如下:

int strcmp ( const char * str1, const char * str2 );

它的返回类型是int类型,形参有两个,分别代表着要进行比较大小的两个字符串。

|-----|------------------------|
| 返回值 | 表明 |
| >0 | 字符串str1的长度大于字符串str2的长度 |
| =0 | 字符串str1的长度等于字符串str2的长度 |
| <0 | 字符串str1的长度小于字符串str2的长度 |

用法如下:

既然我们知道了如何在编写代码中对两个字符串进行大小比较,回归正题,我们就来通过qsort函数来实现元素为字符串的数组的排序。由上面可以非常简单得出,如果我们要编写一个比较函数来专门对字符串的大小进行比较,我们需要先将传入的参数强转为char*类型,然后才能将正常使用strcmp函数来进行字符串的大小比较,因此不难得出以下函数:

cpp 复制代码
//用来比较字符串大小的比较函数
int compare_str(const void *a, const void *b) 
{
     return strcmp((const char *)a, (const char *)b);
}

3.使用qsort函数排序结构体类型的数据

结构体类型我们会在后面进行完整详细的介绍,在前面的文章我们进行一个大概的介绍,如果有对结构体相关知识不熟悉的同学可以看:C语言:操作符详解-CSDN博客

接下来我们来分析一下如何对元素为结构体的数组进行排序。结构体虽然可以直接进行大小的计算,但是当我们要实现比较函数时,只计算结构体的大小然后来进行结构体大小的比较通常是没有意义的,因为在大多数情景中,不同结构体的成员类型和数量多多少少会有一定的差异。所以我们在对元素为结构体的数组进行排序时,通常会先指定这些结构体中的共有 元素类型的、共有 的有实际意义的成员等为指定标准进行大小比较。比如我们在对一个班的学生进行排序时,通常有多种排序方法,最常见的就是根据学号的大小、根据某一科目的成绩或所有科目的总成绩、根据姓名等等来进行排序。在C语言中的结构体排序亦是同理。如果我们要进行学号的大小来进行排序,我们大可以直接使用上面的整型类型的比较函数来进行排序;如果我们要进行姓名来进行排序,我们也可以通过字符串的比较来进行排序。但是具体的代码实现肯定是不能直接照搬的,结构体的成员需要访问之后才能进行一定的操作。

我们在C语言:操作符详解-CSDN博客中已经提及过结构体的成员如和进行访问,一种是通过操作符点(.)来进行直接访问,另一种则是通过操作符箭头(->)来进行间接访问。两者的区别就在于我们可以不用知道结构体的地址来进行直接访问,而知道了结构体的地址后我们才可以通过(->)来进行间接访问。这里我们采取间接访问的方式来为大家讲解。

假如我们要对一个元素类型为结构体的数组进行排序,我们需要先选定一个所有元素都共有的一个标准。

cpp 复制代码
struct Stu
{
    char name[15];
    int age;
    int id;
    int grade;
};

上述结构体是学生的个人信息表。就拿对学生进行排序的例子来说,假如我们要以姓名为标准来对数组进行排序,首先我们需要先知道每个学生的姓名后再进行排序。由于学生的姓名是字符串,所以我们可以通过strcmp()函数来进行学生姓名的比较,进而来对数组进行排序。还是同样的,在对整型数据和字符串数据进行大小比较时我们需要先对形参进行强制类型转换对应的数据类型。结构体大小的比较也是一样的,我们需要先对形参强转为结构体指针类型,然后再根据标准选择合适的比较大小方式,以姓名为标准我们大可以这样来写:

cpp 复制代码
int compare_struct(const void *a, const void *b) 
{
     return strcmp((const struct MyStruct*)a->name,(const struct MyStruct*)pb->name);
}

我们将形参强转为结构体指针后,形参就变为了指向学生个人信息的结构体指针,然后我们就可以通过间接访问操作符来访问其中的姓名成员再利用strcmp()函数进行大小比较,进而达到排序的效果。如果我们要根据每个学生的id来进行排序,同样的道理,先强转再比较,我们就可以写出如下函数:

cpp 复制代码
int compare_struct(const void *a, const void *b) 
{
     return ((const struct MyStruct*)a->id - (const struct MyStruct*)pb->id);
}

这里我所定义的成员id由于是int类型而不是数组,所以可以直接通过比较整型大小的方式来进行id大小的比较。

4.使用qsort函数排序结构数据

我们将如上三个比较函数结合起来使用后就可以写一个简便的学生排序程序。这里不再作过多解释,需自行消化和理解:

cpp 复制代码
struct Stu//学⽣的个人信息样例
{
    char name[20];//名字
    int age;//年龄
};

//按照年龄来⽐较
int cmp_stu_by_age(const void* e1, const void* e2)
{
    return ((struct Stu*)e1)->age - ((struct Stu*)e2)->age;
}

//按照名字来⽐较
int cmp_stu_by_name(const void* e1, const void* e2)
{
    return strcmp(((struct Stu*)e1)->name, ((struct Stu*)e2)->name);
}

//按照年龄来排序
void test2()
{
    struct Stu s[] = { {"zhangsan", 20}, {"lisi", 30}, {"wangwu", 15} };
    int sz = sizeof(s) / sizeof(s[0]);
    qsort(s, sz, sizeof(s[0]), cmp_stu_by_age);
}

//按照名字来排序
void test3()
{
    struct Stu s[] = { {"zhangsan", 20}, {"lisi", 30}, {"wangwu", 15} };
    int sz = sizeof(s) / sizeof(s[0]);
    qsort(s, sz, sizeof(s[0]), cmp_stu_by_name);
}

int main()
{
    test2();
    test3();
    return 0;
}

三、冒泡排序(完全版)

在学习了足够多的知识之后,我们就需要对前面的知识的不足进行改进和补充。由于知识几类的局限性,我们在前面实现的冒泡排序只能支持整型类型的排序,而无法支持更多类型的排序。接下来我将带着大家进行对冒泡排序的升级。

首先我们来分析一下前面我们写的冒泡排序的代码来看看有哪些不足:

cpp 复制代码
​#include <stdio.h>
 
void bubble_sort(int arr[], int sz)//参数接收数组元素个数
{
    int i = 0;
    for(i=0; i<sz-1; i++)
    {
        int j = 0;
        for(j=0; j<sz-i-1; j++)
        {
            if(arr[j] > arr[j+1])
            {
                int tmp = arr[j];
                arr[j] = arr[j+1];
                arr[j+1] = tmp;
            }
        }
    }
}
 
int main()
{
    int arr[] = {3,1,7,5,8,9,0,2,4,6};
    int sz = sizeof(arr)/sizeof(arr[0]);
    bubble_sort(arr, sz);
    for(int i=0; i<sz; i++)
    printf("%d ", arr[i]);
    return 0;
}

​

既然我们要让冒泡排序对所有类型的元素都起到效果,我们肯定是要从函数的形参以及数组元素大小的比较来进行改动。

对冒泡排序进行升级,我们可以仿照C库为我们提供的qsort函数为模板来对冒泡排序进行升级。首先我们可以将冒泡排序的原型写成如下形式:

void bubble_sort(void* base, size_t sz, size_t width, int(*cmp)(const void* p1,const void* p2));

//

base 要求排序不同类型的数组,void*恰好能接收任意类型

sz 元素个数

width 一个元素的大小

int (*p)(const void*, const void*) 函数传参函数指针接收

也就是将qsort函数的形参拿来归冒泡排序使用(因为qsort函数实在是太好用了)。在冒泡排序中,我们对元素的排序是先对下标为j和下标为j+1的两个元素进行比较大小后再对两个元素进行交换,但是这里不一样,这里我们不能直接使两个元素直接进行大小比较和交换(本质上是为了通用性)。我们在对一个数组进行排序时,我们通常是不会提前知道这个数组的元素的类型是什么,所以在进行元素大小的比较时,我们通常需要获取到每个元素(也就是要通过字节去一个个访问),因为不知道是什么类型,所以要一个字节一个字节交换。如果是使用int*或者short*的类型来去访问字节的话会访问不完全导致数据的丢失。所以这里我们要将base强制类型转换为char*类型(char*类型)以便我们来进行元素的识别和查找。依据这个原理,我们可以得出以下访问元素原理图:

既然我们是以字节为单位来一个个地去访问数组中的元素,在进行元素的交换时,我们也需要去一个个字节地去交换。但是这里进行交换的时候是比较简单的,我们不妨将一个元素分为若干个小段,将每个小段视为一个小元素,要将整个元素与另一个元素进行交换,就是将这两个元素的小元素依次对应地进行交换即可,交换完所有的小元素后,整个元素也就交换完毕了。为了代码的简洁性,我们可以将这个代码封装在一个函数内,在实现时调用这个函数即可,我们可以定义一个Swap()函数:

cpp 复制代码
void Swap(char* x1, char* x2, size_t width)
{
	for (int i = 0; i < width; i++)
	{
		char tmp = *x1;
		*x1 = *x2;
		*x2 = tmp;
		x1++;
		x2++;
	}
}

其中呢,x1为数组中第j个元素的地址,x2为数组中第j+1个元素的地址,width为数组元素类型的大小,根据实际情况进行传参。在这里我们已经知道了base已经被我们强转为了char*类型,这里的形参我们不必再设置为void*类型,直接设置为char*类型即可。

随后我们将参数传入到Swap()函数内部即可达到交换元素的目的,然后我们就可以写出冒泡排序(升级版)的代码:

cpp 复制代码
​void Swap(char* x1, char* x2, size_t width)
{
	for (int i = 0; i < width; i++)
	{
		char tmp = *x1;
		*x1 = *x2;
		*x2 = tmp;
		x1++;
		x2++;
	}
}

void Sort(void* base, size_t sz, size_t width, int (*p)(const void*, const void*))
{
	for (size_t i = 0; i < sz - 1; i++)
	{
		for (size_t j = 0; j < sz - 1 - i; j++)
		{
			if (p((char*)base + j * width,(char*)base + (j + 1) * width) > 0)
			{
				Swap((char*)base + j * width, (char*)base + (j + 1) * width, width);
			}
		}
	}
}

为了起到升级版冒泡排序的作用,我们大可在Swap()函数的前面声明我们需要的比较函数即可

以上就是冒泡排序的升级版。


作者已经放假了!后续每天都会持续更新博客,请多多关注!谢谢!

相关推荐
秃头佛爷1 小时前
Python学习大纲总结及注意事项
开发语言·python·学习
待磨的钝刨1 小时前
【格式化查看JSON文件】coco的json文件内容都在一行如何按照json格式查看
开发语言·javascript·json
XiaoLeisj3 小时前
【JavaEE初阶 — 多线程】单例模式 & 指令重排序问题
java·开发语言·java-ee
励志成为嵌入式工程师4 小时前
c语言简单编程练习9
c语言·开发语言·算法·vim
捕鲸叉4 小时前
创建线程时传递参数给线程
开发语言·c++·算法
A charmer4 小时前
【C++】vector 类深度解析:探索动态数组的奥秘
开发语言·c++·算法
Peter_chq4 小时前
【操作系统】基于环形队列的生产消费模型
linux·c语言·开发语言·c++·后端
记录成长java6 小时前
ServletContext,Cookie,HttpSession的使用
java·开发语言·servlet
前端青山6 小时前
Node.js-增强 API 安全性和性能优化
开发语言·前端·javascript·性能优化·前端框架·node.js
hikktn6 小时前
如何在 Rust 中实现内存安全:与 C/C++ 的对比分析
c语言·安全·rust