【C语言】指针深度剖析(2)(内含qsort函数的基本使用、模拟实现qsort函数)

1 数组参数、指针参数

在实际应用中,经常会遇到把数组或指针传给函数的情况,那函数的参数应该如何设计呢?


1.1 一维数组传参

首先要知道一点,在数组传参中,无论是一维数组传参还是二维数组传参,形参部分都可以设置成数组或者指针来接收。

来看如下参数设置是否正确

#include <stdio.h>
void test(int arr[])//ok?
{}//可以,因为虽然传递过来的是一个数组,但是函数不会真正的创建一个数组,所以数组大小可以不写
void test(int arr[10])//ok?
{}//可以,道理同上
void test(int *arr)//ok?
{}//可以,这是传递数组首元素的地址
void test2(int *arr[20])//ok?
{} //可以,传递数组为arr2,大小为二十个元素,参数类型为int *(20同样可以省略)
void test(int **arr)//ok?
{}//可以,二级指针用来存放一级指针的地址,这里的二级指针存放了arr2的首元素地址
int main()
{
    int arr[10]={0};
    int *arr2[20]={0};
    test(arr);
    test(arr2);
}
一维数组传参,形参可以是数组,也可以是指针
当形参是指针的时候,要注意类型相匹配

1.2 二维数组传参

void test(int arr[3][5])//ok?
{}//可以,道理同一维数组
void test(int arr[][])//ok?
{}
//不可以,原因如下
void test(int arr[][5])//ok?
{}//可以,原因如下
//总结:二维数组传参时,形参设计只能省略第一个[]中的数字
//即可以不知道该二维数组有多少行,但一定要知道该二维数组有多少列,这样才方便运算


void test(int *arr)//ok?
{}//不可以,二维数组的首元素地址应该是第一行的地址,与形参类型不匹配,在这里强行使用该写法编译器会报警告
void test(int * arr[5])//ok?
{}//不可以,形参部分是指针数组形式,与实参类型不匹配
void test(int (*arr)[5])//ok?
{}
//可以,二维数组首元素地址是第一行的地址,该二维数组每一行有五个元素,
//形参是一个指向拥有五个元素的数组的数组指针,在这里指向arr的第一行
void test(int **arr)//ok?
{}//不可以,二级指针接受的是一级指针的地址,但实参传过去的并不是一级指针
int main()
{
    int arr[3][5] = { 0 };
    test(arr);
}
二级数组传参
参数可以是指针,也可以是数组
如果是数组,行可以省略,列不可以省略
如果是指针,传过去的是第一行的地址,而第一行相当于一个一维数组,形参应该是一个数组指针

1.3 一级指针传参

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;
}

显而易见,将一级指针传递给函数,函数形参部分用一级指针接受即可。

思考:当一个函数的参数部分是一级指针的时候,函数能接受什么参数?

比如:

void test1(int *p)
{}
//test1能够接收什么参数?

//int a;
//test1(&a);
//int *p=&a;
//test1(p);
//int arr[10];
//test1(arr);

1.4 二级指针传参

当函数的参数为二级指针的时候,可以接收什么参数?
void test(char **p)
{}
int main()
{
    char c = 'b';
    char* pc = &c;
    char** ppc = &pc;
    char* arr[10];
    test(&pc);
    test(ppc);
    test(arr);
    return 0;
}

2 函数指针

函数指针,顾名思义就是存放函数地址的指针变量。

而函数指针跟其他指针有一个非常重要的区别。先来看如下代码:

void test()
{}

int main()
{
    printf("%p\n", &test);
    printf("%p\n", test);
}

x64下运行结果:

x86下运行结果:

可以发现,输出的两个地址,都是函数的地址,而&函数名跟函数名,都是函数的地址。因此在通过函数指针指向函数的过程中,可以不用解引用操作。

函数指针的存储方式:

void test()
{}

int main()
{
    void (*pf)();
    //pf先和*结合,说明pf是指针。最后一个括号内应写的是指向函数的参数类型。在本例为空
}

3 函数指针数组

将函数的地址存放在一个数组中,这个数组就叫做函数指针数组。函数指针数组应该如何定义呢?

以下哪一条关于函数指针数组的定义是正确的?
int (*pf1[10])();
int *pf2[10]();
int (*)()pf3[10];
答案是第一条
pf先跟[]结合,表明pf是一个数组
将pf[10]除去,剩下部分为int (*)(),表明该数组中的数据是int (*)()类型的函数指针

以下是函数指针的一个用与简单计算器的例子:

#include <stdio.h>

void menu()
{
    printf("***************************\n");
    printf("****  1.加法    2.减法  ****\n");
    printf("****  3.乘法    4.除法  ****\n");
    printf("********   0.退出   ******** \n");
    printf("请选择:");
}

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;
}
int main()
{
    int x, y;
    int input = 1;
    int ret = 0;
    int (*p[5])(int, int) = { 0,Add,Sub,Mul,Div };
    //该函数指针数组存放了5个函数指针
    while (input)
    {
        menu();
        scanf("%d", &input);
        if (input <= 4 && input >= 1)
        {
            printf("请输入两个操作数");
            scanf("%d %d", &x, &y);
            ret = (p[input])(x, y);
        }
        else if (input == 0)
            break;
        else 
            printf("输入有误\n");
        printf("结果是:%d\n", ret);
    }    
    return 0;
}

函数指针数组在解决一些选择性问题的时候可以极大简化代码,同时也提高了代码的可维护性。


4 指向函数指针数组的指针

指向函数指针数组的指针是一个指针

该指针指向了一个数组,数组的每个元素是函数指针

应该如何定义呢?

void test(const char* str)
{}
int main()
{
    //函数指针pf
    void (*pf)(const char* str) = test;

    //函数指针数组
    void (*pfArr[5])(const char* str);
    pfArr[0] = test;

    //指向函数指针数组的指针
    void (*(*pf2)[5])(const char* str)=&pfArr;
    return 0;
}
如果继续往下延伸,还可以延伸出指向函数指针数组的指针数组,指向指向函数指针函数数组的指针数组的指针... ...理论上来说可以持续下套
但是在实际应用中,过多次的“套娃”并没有实际意义,所以关于这一块知识点到为止即可,过度深究反而走了弯路

5 回调函数

回调函数,是指一个通过函数指针调用的函数。如果将一个函数的指针(地址)作为参数传递给另一个函数,当这个指针用来调用其所指向的函数时,我们就说这是回调函数。
回调函数不是由该函数的实现方直接调用,而是在特定的事件或条件发生时由另一方(被调函数)所调用的,用于对该事件或条件进行相应。

5.1 qsort函数

被调函数一个很典型的例子就是qsort函数:

qsort函数是用来 对数组的元素进行排序的。使用时需要引用头文件<stdlib.h>
对数组中由指向的元素进行排序,每个元素字节长,使用该函数确定顺序。
此函数使用的排序算法通过调用指定的函数来比较元素对,并将指向它们的指针作为参数。
该函数不返回任何值,但通过重新排序数组的元素(如 所定义)来修改指向的数组的内容。
qsort函数的底层采用的是 快速排序算法

这是cplusplus上对qsort的注释:

以下是对该注释的具体介绍:

void qsort(void* base,
//指向待排序数组的第一个元素
size_t count,
// 待排序的元素个数
size_t size,
//每个元素的大小,单位是字节
int(*cmp)(const void*, const void*));
//指向比较元素大小的函数(比较函数),告诉qsort函数比较的规则,由使用者提供

为什么这里使用了void*的指针?

是因为设计者在设计qsort函数时,并不知道在将来使用者会用qsort排序哪种类型的数据。为了实现库函数的通用性,将参数设置为void*类型(这里写成const void*类型是为了使代码更加规范)
因此,使用者在设计比较函数时也要将比较函数的形参部分设计成void*(const void*)类型,在根据所比较数据的类型,在函数内部将指针 强制类型转换来实现对不同数据类型的比较

下面是具体使用:

#include <stdio.h>
#include <stdlib.h>

int int_cmp(const void* p1, const void* p2)
{
    //这里实现的是升序排序
    //如果要改成降序,只需要将语句改为return *(int*)p2 - *(int*)p1;
    return *(int*)p1 - *(int*)p2;
    //返回值>0,p1指向的对象大于p2指向的对象
    //返回值=0,p1指向的对象等于p2指向的对象
    //返回值<0,p1指向的对象小于p2指向的对象
}

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

5.2 qsort函数的模拟实现

在这里,我们基于冒泡排序,来模拟实现qsort

在这里采用模块化开发的思想,将项目拆分为cmp.h, cmp.c, test.c三个部分:

cmp.h用来包含项目中涉及到的头文件;

cmp.c是函数的具体实现过程;

test.c用来测试

cmp.h:

#pragma once
#include <stdio.h>
#include <string.h>
#include <stdlib.h>

struct Stu
{
    char name[20];
    int age;
};
//整形升序排序(函数使用者提供)
int int_cmp(const void* p1, const void* p2);

//字符升序排序
int cmp_strcmp(const void* p1, const void* p2);

//结构体成员排序(按照年龄)
int cmp_stu_by_age(const void* p1, const void* p2);

//结构体成员排序(按照姓名)
int cmp_stu_by_name(const void* p1, const void* p2);

//换位
void Swap(char* p1, char* p2, size_t size);

//模拟实现qsort(采用冒泡方式)
void bubble_sort(void* base, size_t count, size_t size, int(*cmp)(const void*, const void*));

void test1();

void test2();

void test3();

void test4();

cmp.c:

#include"cmp.h"

int int_cmp(const void* p1, const void* p2)
{
    //这里实现的是升序排序,以下同理
    //如果要改成降序,只需要将语句改为return *(int*)p2 - *(int*)p1;
    return *(int*)p1 - *(int*)p2;
}

int cmp_strcmp(const void* p1, const void* p2)
{
    return *(char*)p1 - *(char*)p2;
}

int cmp_stu_by_age(const void* p1, const void* p2)
{
    return ((struct Stu*)p1)->age - ((struct Stu*)p2)->age;
}

int cmp_stu_by_name(const void* p1, const void* p2)
{
    return strcmp(((struct Stu*)p1)->name, ((struct Stu*)p2)->name);
}

void Swap(char* p1, char* p2, size_t size)
{
    //将指针强制类型转换为char *类型,这是因为站在设计者的角度,并不知道将来使用者会使用该函数排序怎样的数据类型
    //将指针强制类型转化为char *,同时传递目标数据所占内存的大小,就可以逐个字节地交换数据
    int i = 0;
    for (i = 0; i < size; i++)
    {
        char tmp = *(p1 + i);
        *(p1 + i) = *(p2 + i);
        *(p2 + i) = tmp;
    }

}

void bubble_sort(void* base, size_t count, size_t size, int(*cmp)(const void*, const void*))
//bubble_sort就是我们自己编写的“qsort”,形参类型应该与qsort一致
{
    size_t i, j = 0;

    for (i = 0; i < count - 1; i++)
    {
        for (j = 0; j < count - 1 - i; j++)
        {
            if (cmp((char*)base + j * size, (char*)base + (j + 1) * size) > 0)
            {
                //每多加一个size,就跳过大小为size的字节
                Swap((char*)base + j * size, (char*)base + (j + 1) * size, size);
            }
        }
    }
}

void test1()
{
    int arr1[] = { 8,7,6,5,2,4,1,3,9,0 };
    int i = 0;
    bubble_sort(arr1, sizeof(arr1) / sizeof(arr1[0]), sizeof(arr1[0]), int_cmp);
    for (i = 0; i < sizeof(arr1) / sizeof(arr1[0]); i++)
    {
        printf("%d ", arr1[i]);
    }
    printf("\n\n");
}

void test2()
{
    //按年龄排序
    struct Stu s[] = { {"zhangsan",20},{"lisi",15},{"wangwu",40} };
    bubble_sort(s, sizeof(s) / sizeof(s[0]), sizeof(s[0]), cmp_stu_by_age);
}

void test3()
{
    //按名字排序(一般按照首字符排序,如果首字符一样再根据第二个字符,以此类推)
    struct Stu s[] = { {"zhangsan",20},{"lisi",15},{"wangwu",40} };
    bubble_sort(s, sizeof(s) / sizeof(s[0]), sizeof(s[0]), cmp_stu_by_name);
}

void test4()
{
    char arr3[] = "zxbgadefasw";
    bubble_sort(arr3, sizeof(arr3) / sizeof(arr3[0]), sizeof(arr3[0]), cmp_strcmp);
    int i = 0;
    for (i = 0; i < sizeof(arr3) / sizeof(arr3[0]); i++)
    {
        printf("%c ", arr3[i]);
    }
    printf("\n");
}

test.c:

#include "cmp.h"

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

运行结果:

test2运行结果(在监视中显示):

test3运行结果(在监视中显示):

下次将为大家带来指针和数组典型笔试题解析

(本篇完)

猜你喜欢

转载自blog.csdn.net/fbzhl/article/details/129335948