【C语言进阶技巧】指针掌握之道:深入挖掘指针的无尽潜力(第二部)


【C语言进阶技巧】指针掌握之道:深入挖掘指针的无尽潜力(第二部))

❤️博客主页: 小镇敲码人
🍏 欢迎关注:👍点赞 👂🏽留言 😍收藏
🌞回来5天了,加油!!!🍎🍎🍎
💗疲倦的生活里,总要有些温柔的梦想,愿一切真心不被辜负,愿一切努力终有收获,愿一切如你所愿。✡️✡️✡️

1. 函数指针数组

函数指针数组,顾名思义,即存放函数的地址的数组,这个数组的类型是函数指针,在指针的进阶(一)中我们学习了函数指针,它就是让*号和变量名先去结合,使变量变为指针类型,而如果[]和变量名先结合,从而这就是一个数组了。

1.1 函数指针数组的创建和初始化

下面是一个函数

 int Add(int x, int y)
{
	return x + y;
}

这是函数指针:

  int (*pf)(int,int) = Add; 

这是函数指针数组:

int (*ppf[])(int,int) = {Add};

我们可以通过类比其他类型的数组来理解,比如int型指针数组int* arr[] = {NULL};我们可以知道指针数组去掉[]和数组名就是就是数组每个元素的类型,去掉之后就是int*,所以这是一个int*类型的数组。
上述函数指针数组,去掉[]和数组名为,int (*)(int,int)所以上述数组每个元素的类型是一个函数指针,它有两个int型的参数,返回值也是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;
}
 
int main() {
    int (*pf1)(int, int) = Add;  // 定义函数指针并初始化为 Add 函数的地址
    int (*pf2)(int, int) = Sub;  // 定义函数指针并初始化为 Sub 函数的地址
    int (*pf3)(int, int) = Mul;  // 定义函数指针并初始化为 Mul 函数的地址
    int (*pf4)(int, int) = Div;  // 定义函数指针并初始化为 Div 函数的地址
    
    // 定义函数指针数组,存储四个函数的地址
    int (*pfArr[4])(int, int) = { pf1, pf2, pf3, pf4 };
    // 或者 int (*pfArr[4])(int, int) = { Add, Sub, Mul, Div };
    
    return 0;
}
  • 注意:通过指针的进阶(一)我们已经知道&函数名和函数名,%p打印出来的地址是一样的,所以函数指针数组进行初始化这两种编译器都是允许的,但是数组指针不行,会发生类型不匹配的问题,像vs2019的编译器下会报出警告,如图:
    在这里插入图片描述
    而函数指针数组的初始化,不管是否在函数名前加&,vs2019编译器下都是不报警告的。
  1. 不加&符号:
    在这里插入图片描述
  2. &符号:
    在这里插入图片描述
    总结:1. 函数指针数组是数组,数组每个元素类型是函数指针类型。
               2.*号的优先级没有[]高,所以没有()[]先和变量名结合,该变量就成了数组类型。

1.2 函数指针数组的应用

如果我们想用C语言实现一个简单的整数计算器(加减乘除)在不了解函数指针数组前,可能我们会这样去实现:

#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() {
    int x = 0;
    int y = 0;
    int ret = 0;
    int input = 0;

    do {
        menu();
        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); // 当 input 不为 0 时继续循环

    return 0;
}

通过测试这段代码实现加减乘除功能是可行的,但是存在一定的问题:

  1. 代码冗余
  2. 后续想增加功能,会出现很多的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() {
    int x = 0;
    int y = 0;
    int ret = 0;
    int input = 0;
    
    // 函数指针数组的使用 -> 转移表
    int (*pfArr[5])(int, int) = {NULL, Add, Sub, Mul, Div};
    //                           0    1    2    3    4

    do {
        menu();
        printf("请选择:>");
        scanf("%d", &input);

        if (input >= 1 && input <= 4) {
            printf("请输入两个操作数:");
            scanf("%d %d", &x, &y);
            ret = pfArr[input](x, y); // 使用函数指针调用对应的函数
            printf("ret = %d\n", ret);
        }
        else if (input == 0) {
            printf("退出计算器\n");
        }
        else {
            printf("选择错误,重新选择\n");
        }
    } while (input);
    return 0;
}
  • 使用函数指针数组就省了很多不必要的冗余代码,后续想要添加其它的计算功能,直接添加函数定义,修改函数指针数组的初始化部分和input的范围就行。

2. 指向函数指针数组的指针

有没有感觉像是套娃呢?没错,你感觉对了🤪回到正题,指向函数指针数组的指针,它是一个指针,指向的对象的类型函数指针数组,可类比int []型的数组指针理解。

这是一个int型的数组:

int a[10] = {0};

a的类型为:int [10]
这是指向这个数组的指针:

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

同理,这是一个函数指针类型的数组:

int (* pf[4])(int,int);

pf的类型为int (* [4])(int,int)
这是指向这个函数指针数组的指针:

int(*(*ppf)[4])(int,int) = &pf;
  • 细心的小伙伴已经发现,不管是定义指向整数数组的指针还是定义指向函数指针数组的指针,都只需要在原先数组类型的基础上加上一个(*指针的变量名)就行了。

3. 回调函数

当我们在定义函数时,如果其中一个参数是函数指针的类型时,实参传的是函数的地址,当通过函数指针调用其指向的函数,那个被调用的函数,我们就称之为回调函数。回调函数不是该函数的实现方直接调用,而是在特定的事件或条件发生时由另外一方调用,用于对该事件的或条件进行响应。

下面通过qsort函数的例子,来帮助我们更好理解回调函数

3.1 qsort函数的定义及参数说明

3.1.1 qsort函数定义

qsort函数是C语言里面的一种排序函数,它是标准库函数'stdlib.h'的一部分,提供了一种通用的排序机制,可以用于各种类型的排序。

3.1.2 参数说明

这里引用cplusplus.com网站上对qsort函数的声明,来让读者了解我们在使用qsort函数时应该传什么参数。在这里插入图片描述

  • 参数一:base代表要排序的那个数组。
  • 参数二 :num代表要排序的元素的个数。
  • 参数三:size代表排序的每个元素的内存大小(以字节为单位)。
  • 参数四:compar是一个指向函数的指针。由于类型不同,比较方式也不同(例如,整数的大小比较与字符串的大小比较),所以比较函数需要程序员自己实现,标准函数不提供比较函数,我们把这种专门用于比较的函数又称为比较器,这里的compar指向的函数就是我们所说的回调函数。

3.1.3 利用标准库函数qsort排序函数

这里先对cmpar所指向的函数做一下阐述,因为这是需要我们自己设计的比较器,下面是cplusplus文档的一些阐述:

下面我们将使用qsort标准函数对指针和结构体进行排序,我们对一些地方做一下阐释:

3.1.3.1 比较器的实现
  • void*指针强制转换为用户需要比较的元素指针类型后,要先强制转换为相应元素的类型才可以使用,我们这里是升序所以是前者减后者(qsort函数内部所决定),如果大于0就交换。
  • 如果想要设计降序的比较器将两个变量的顺序交换就可以了,同样是返回大于0就交换。
3.1.3.1.1 整数比较器
int cmp_int(const void* a,const void* b)
{
	return *(int*)a - *(int*)b;
}
3.1.3.1.2 结构体字符串比较器
  • 字符串比较在我们的标准库'string.h'中有专门的比较函数strcmp
    在这里插入图片描述
  • strcmp字符串比较函数有两个参数,类型都是常量字符串,返回值是int类型。
    在这里插入图片描述
    简单点来说就是:
  1. 当字符串1和字符串2相等时,返回值为0。
  2. 当字符串1与字符串2开始出现不相等字符时,将字符转换为ASCII码值,如果字符串1更大返回大于0的数,反之返回小于0的数。
  • 细心的佬可能会发现这和我们整数的比较规则是相似的,即a如果大于b,返回一个大于0的数,当得到的数大于0时,qsort函数内部就会交换,下面代码采用升序的方式:
int cmp_stu_by_name(const void* a, const void* b) 
{
  return strcmp(((struct Stu*)a)->name, ((struct Stu*)b)->name);
}

完整代码:

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

// 定义结构体 Stu
struct Stu {
    char name[20];
    int score;
};

// 按照 score 升序排序的比较函数
int cmp_stu_by_score(const void* a, const void* b) {
    return ((struct Stu*)a)->score - ((struct Stu*)b)->score;
}

// 打印按照 score 升序排序的结构体数组
void print1(struct Stu arr1[], int sz) {
    printf("按照成绩升序排序为:\n");
    for (int i = 0; i < sz; i++) {
        printf("%s %d\n", arr1[i].name, arr1[i].score);
    }
}

// 测试排序结构体按照 score 升序排序
void test1() {
    struct Stu arr1[] = { {"zhangsan", 86}, {"lisi", 90}, {"wangwu", 66} };
    int sz = sizeof(arr1) / sizeof(arr1[0]);
    qsort(arr1, sz, sizeof(arr1[0]), cmp_stu_by_score);
    print1(arr1, sz);
}

// 按照 name 升序排序的比较函数
int cmp_stu_by_name(const void* a, const void* b) {
    return strcmp(((struct Stu*)a)->name, ((struct Stu*)b)->name);
}

// 打印按照 name 升序排序的结构体数组
void print2(struct Stu arr2[], int sz) {
    printf("按照名称升序排序为:\n");
    for (int i = 0; i < sz; i++) {
        printf("%s %d\n", arr2[i].name, arr2[i].score);
    }
}

// 测试排序结构体按照 name 升序排序
void test2() {
    struct Stu arr2[] = { {"zhangsan", 86}, {"lisi", 90}, {"wangwu", 66} };
    int sz = sizeof(arr2) / sizeof(arr2[0]);
    qsort(arr2, sz, sizeof(arr2[0]), cmp_stu_by_name);
    print2(arr2, sz);
}

// 比较函数,用于整型数组的升序排序
int cmp_int(const void* a, const void* b) {
    return *(int*)a - *(int*)b;
}

// 打印升序排序的整型数组
void print3(int* arr3, int sz) {
    printf("数组升序排序:\n");
    for (int i = 0; i < sz; i++) {
        printf("%d ", *(arr3 + i));
    }
}

// 测试整型数组升序排序
void test3() {
    int arr3[] = { 10, 9, 8, 3, 6, 5, 4, 7, 1, 2 };
    int sz = sizeof(arr3) / sizeof(arr3[0]);
    qsort(arr3, sz, sizeof(arr3[0]), cmp_int);
    print3(arr3, sz);
}

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

运行的结果:
在这里插入图片描述

3.1.4 使用回调函数模拟实现qsort(采用冒泡的方式)

对核心代码做一下阐述:

  1. my_qsort函数的实现:
    在这里插入图片描述
  • 首先要明白为什么basevoid*类型的指针,因为qsort函数可以实现各种排序,而我不知道用户究竟要传一个什么类型的指针,所以只能以用万能指针void*来接受,后续利用强制转换和一个元素的大小(跨度)等信息找到元素的地址来进行比较和交换。
  • (void*)指针不能直接进行指针的加减运算,进行加减运算的目的是得到不同元素的地址,如果使用(void*)指针进行加减运算编译器就不知道要跳几个字节,所以使用前要强制类型转换为元素相应的类型,而且(void*)类型的指针在使用前也需要进行强制类型转换,因为不管是整数的加减法和还是结构体的->操作符的使用前提是给出这个变量的类型,才能实现相应变量的功能。
  1. 我们按照cplusplus文档对qsort函数的声明,设计函数的参数。
  2. 冒泡排序的思路很简单,下次再一起总结,现在假设读者都已经会了冒泡排序,关键是回调函数cmpar的实参如何传?通过上述对比较器的介绍,我们可以知道这个函数的两个参数都是void*类型,我们只需要传不同元素的地址过去就行了,想得到不同元素的地址,我们就要对指针进行加法(减法)运算,但是我们知道void *类型的指针不能直接进行加减运算,需要强制转换base为一个具体的指针类型,那么问题来了,应该强制转换为哪一种类型呢?char*类型的指针加减1跳的字节数最少只有1个字节,如果我们强制转换的类型比这个大,那么在进行指针的加减时就无法准确得到每个字符元素的地址了(这里涉及指针初阶的内容后续会写,所以我们应该将base强制转换为char*类型,然后通过宽度sizej的值确定每个元素的地址,比如((char*)base+1*size)就是第二个元素的地址,通过它可以找到第二个元素。在弄明白这个之后我们swap函数的实现也应该用char*的指针来传参,以满足各种类型的需求,然后再传一个元素类型的大小就行了。

代码如下:

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

void swap(char* a, char* b, int size) {
    for (int i = 0; i < size; i++) {
        char temp = *a;
        *a = *b;
        *b = temp;
        a++;
        b++;
    }
}

void my_qsort(void* base, size_t num, size_t size, int (*cmpar)(void*, void*)) {
    for (int i = 0; i < num - 1; i++) {
        for (int j = 0; j < num - i - 1; j++) {
            if (cmpar((char*)base + j * size, (char*)base + (j + 1) * size) > 0) {
                swap((char*)base + j * size, (char*)base + (j + 1) * size, size);
            }
        }
    }
}

// 测试 qsort 排序的结构体数据
struct Stu {
    char name[20];
    int score;
};

// 按照 score 升序排序的比较函数
int cmp_stu_by_score(const void* a, const void* b) {
    return ((struct Stu*)a)->score - ((struct Stu*)b)->score;
}

// 打印按照 score 升序排序的结构体数组
void print1(struct Stu* arr1, int sz) {
    printf("结构体按照分数升序排序为:\n");
    for (int i = 0; i < sz; i++) {
        printf("%s %d\n", (arr1 + i)->name, (arr1 + i)->score);
    }
}

// 测试排序结构体数据按 score 升序排序
void test1() {
    struct Stu arr1[] = { {"zhangsan", 86}, {"lisi", 90}, {"wangwu", 66} };
    int sz = sizeof(arr1) / sizeof(arr1[0]);
    my_qsort(arr1, sz, sizeof(arr1[0]), cmp_stu_by_score);
    print1(arr1, sz);
}

// 按照 name 升序排序的比较函数
int cmp_stu_by_name(void* a, void* b) {
    return strcmp(((struct Stu*)a)->name, ((struct Stu*)b)->name);
}

// 打印按照 name 升序排序的结构体数组
void print2(struct Stu arr2[], int sz) {
    printf("结构体按照名称升序排序为:\n");
    for (int i = 0; i < sz; i++) {
        printf("%s %d\n", arr2[i].name, arr2[i].score);
    }
}

// 测试排序结构体按 name 升序排序
void test2() {
    struct Stu arr2[] = { {"zhangsan", 86}, {"lisi", 90}, {"wangwu", 66} };
    int sz = sizeof(arr2) / sizeof(arr2[0]);
    my_qsort(arr2, sz, sizeof(arr2[0]), cmp_stu_by_name);
    print2(arr2, sz);
}

// 比较函数,用于整型数组的升序排序
int cmp_int(const void* a, const void* b) {
    return *(int*)a - *(int*)b;
}

// 打印升序排序的整型数组
void print3(int* arr3, int sz) {
    printf("数组升序排序:\n");
    for (int i = 0; i < sz; i++) {
        printf("%d ", *(arr3 + i));
    }
}

// 测试整型数组升序排序
void test3() {
    int arr3[] = { 10, 9, 8, 3, 6, 5, 4, 7, 1, 2 };
    int sz = sizeof(arr3) / sizeof(arr3[0]);
    my_qsort(arr3, sz, sizeof(arr3[0]), cmp_int);
    print3(arr3, sz);
}

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

运行结果如下:
在这里插入图片描述

  • 42
    点赞
  • 30
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 59
    评论
嵌入式C语言之道,可以涉及以下几个方面: 1. 熟悉硬件平台:嵌入式开发需要对目标硬件平台有深入了解,包括处理器架构、寄存器、外设等。学习并理解硬件的特性和工作原理,能够更好地行底层编程和优化。 2. 掌握低级编程技巧:嵌入式开发常常需要行底层编程,因此需要熟悉C语言的底层特性,如指针操作、位操作、内存管理等。掌握这些技巧可以提高代码的效率和可靠性。 3. 理解中断和定时器:在嵌入式系统中,中断和定时器是常用的机制,用于实时响应和时间触发的任务。学习如何使用中断和定时器,并了解其原理和应用场景,可以实现更加高效和可靠的嵌入式系统。 4. 学习RTOS:实际的嵌入式系统往往需要处理多个任务,并具有实时性要求。学习实时操作系统(RTOS)的使用和原理,可以帮助开发者更好地管理任务、调度和资源。 5. 行性能优化:嵌入式系统通常资源有限,需要行性能优化以提高系统的效率和响应速度。了解编译器优化、代码压缩、数据结构选择等技术,可以帮助优化嵌入式系统的性能。 总之,嵌入式C语言之道包括对硬件平台的深入了解、掌握底层编程技巧、理解中断和定时器、学习RTOS以及行性能优化。通过不断学习和实践,可以提升嵌入式开发的能力和水平。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 59
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小镇敲码人

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值