【维生素C语言】第十章 - 指针的进阶(下)

 🔥 CSDN 累计订阅量破千的火爆 C/C++ 教程的 2023 重制版,C 语言入门到实践的精品级趣味教程。
了解更多: 👉 "不太正经" 的专栏介绍 试读第一章
订阅链接: 🔗《C语言趣味教程》 ← 猛戳订阅!

前言:

 🚪 传送门:【维生素C语言】第十章 - 指针的进阶(上)

本章将继续对继续讲解指针的进阶部分,并对指针知识进行一个总结。并且介绍qsort函数的用法以及模拟实现qsort函数。本章学习完毕后C语言指针专题就结束了,配备了相应的练习和讲解,强烈推荐做一做。另外,C语言的指针靠这个专题并不能完全讲完,还有更多指针的用法需要通过书籍、实战进行学习,不断地积累才能学好C语言最具代表性的东西——指针。


知识梳理:

int main()
{
    int a = 10;
    int* pa = &a;

    char ch = 'w';
    char* pc = &ch;

    int arr[10] = {0};
    int (*parr)[10] = &arr; // 取出数组的地址

    return 0;
}

 一、函数指针

0x00 函数指针介绍

📚 指针数组:存放指针的数组。数组指针:指向数组的指针,

函数指针:指向函数的指针,存放函数地址的指针。

0x01 取函数指针地址

📚 函数也是有地址的,取函数地址可以通过 &函数名 或者 函数名 实现。

📌 注意事项:

      ①  函数名  ==  &函数名 (这两种写法只是形式上的区别而已,意义是一模一样的)

      ②  数组名  !=  &数组名

💬 取函数地址:

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

int main()
{
    // 函数指针 - 存放函数地址的指针
    // &函数名  - 取到的就是函数的地址
    printf("%p\n", &Add);
    printf("%p\n", Add);

    return 0;
}

🚩 运行结果如下:


 

0x02 函数指针的定义

📚 函数返回类型( * 指针变量名 )( 函数参数类型... )  =  &函数名;

💬 创建函数指针变量:

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

int main()
{
    int (*pf)(int, int) = &Add;
//        👆 pf 就是一个函数指针变量

    return 0;
}

🔑 解析:

 💬 函数指针定义练习:

请完成下面函数指针的定义。

void test(char* str)
{
    ;
}

int main()
{
    pt = &test;

    return 0;
}

🚩 参考答案:

void (*pt)(char*) = &test

0x03 函数指针的调用

📚  ( *指针变量名 )( 传递的参数... );

💬 函数指针的调用:

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

int main()
{
    int (*pf)(int, int) = &Add;
    
    int ret = (*pf)(3, 5);
//              👆 对 pf 进行解引用操作,找到它所指向的函数,然后对其传参
    printf("%d\n", ret);

    return 0;
}

❓ 那能不能把 (*pf) (3,5) 写成 *pf (3,5) 呢?

💡 答:不行,这么写会导致星号对函数返回值进行解引用操作,这合理吗?这不合理!所以如果你要加星号,一定要用括号括起来。当然你可以选择不加,因为不加也可以:

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

int main()
{
    int (*pf)(int, int) = &Add;

    // int ret = Add(3, 5);
    int ret = pf(3, 5);

    printf("%d\n", ret);

    return 0;
}

🔑 解析:结果是一样的,说明 (*pf)只是摆设,没有实际的运算意义,所以 pf(3, 5) 也可以。

🔺 总结:

Add(3, 5);   // ✅
(*pf)(3, 5); // ✅
pf(3, 5);    // ✅

*pf(3, 5);   // ❌

💬 字符指针 char* 型函数指针:

void Print(char*str)
{
    printf("%s\n", str);
}

int main()
{
    void (*p)(char*) = Print; // p先和*结合,是指针
    (*p)("hello wrold"); // 调用这个函数
    
    return 0;
}

🚩  hello world


0x04 分析下列代码

📚 《C陷阱与缺陷》中提到了这两个代码。

💬 代码1:

(*(void (*)())0)();

🔑 解析:这段代码的作用其实是调用 0 地址处的函数,该函数无参,返回类型是 void

💬 代码2:

void (*signal(int, void(*)(int)))(int);

🔑 解析:

⚡ 简化代码:

int main()
{
    void (* signal(int, void(*)(int)) )(int);

    // typedef void(*)(int) pfunc_t; ❌ 不能这么写,编译器读不出
    typedef void(*pfun_t)(int); // 对void(*)(int)的函数指针类型重命名为pfun_t

    pfun_t signal(int, pfun_t); // 和上面的写法完全等价

    return 0;
}

二、函数指针数组

数组是一个存放相同类型数据的存储空间,我们已经学习了指针数组,比如:

int *arr[10]; // 函数的每个元素都是 *int 指针数组

0x00 函数指针数组的介绍

📚 如果要把函数的地址存到一个数组中,那这个数组就叫 函数指针数组

0x01 函数指针数组的定义

💬 函数指针数组的定义:

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

int Sub(int x, int y) {
    return x - y;
}

int main()
{
    int (*pf)(int, int) = Add;
    int (*pf2)(int, int) = Sub;

    int (*pfArr[2])(int, int) = {Add, Sub};
//         👆 pfArr 就是函数指针数组

    return 0;
}

0x02 函数指针数组的应用

📚 实现一个计算器,可以进行简单的加减乘除运算。

💬 代码1:

#include <stdio.h>

void menu()
{
    printf("*****************************\n");
    printf("**    1. add     2. sub    **\n");
    printf("**    3. mul     4. div    **\n");
    printf("**         0. exit         **\n");
    printf("*****************************\n");
}

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 input = 0;
    do {
        menu();
        int x = 0;
        int y = 0;
        int ret = 0;
        printf("请选择:> ");
        scanf("%d", &input);
        printf("请输入2个操作数:> ");
        scanf("%d %d", &x, &y);
        switch(input) {
            case 1:
                ret = Add(x, y);
                break;
            case 2:
                ret = Div(x, y);
                break;
            case 3:
                ret = Mul(x, y);
                break;
            case 4:
                ret = Div(x, y);
                break;
            case 0:
                printf("退出程序\n");
                break;
            default:
                printf("重新选择\n");
                break;
        }
        printf("ret = %d\n", ret);
    } while(input);
    
    return 0;
}

🚩 让我们来测试一下代码:

❗ 此时我们发现了问题点,即使选择0或选择错误,程序也依然要求你输入2个操作数。这合理吗?这不合理!所以我们需要对代码进行修改:

      ① 需要计算才让用户输入2个操作数

      ② 计算完之后有结果才打印

💬 修改:

#include <stdio.h>

void menu()
{
    printf("*****************************\n");
    printf("**    1. add     2. sub    **\n");
    printf("**    3. mul     4. div    **\n");
    printf("**         0. exit         **\n");
    printf("*****************************\n");
}

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 input = 0;
    do {
        menu();
        int x = 0;
        int y = 0;
        int ret = 0;
        printf("请选择:> ");
        scanf("%d", &input);
        switch(input) {
            case 1:
                printf("请输入2个操作数:> ");
                scanf("%d %d", &x, &y);
                ret = Add(x, y);
                printf("ret = %d\n", ret);
                break;
            case 2:
                printf("请输入2个操作数:> ");
                scanf("%d %d", &x, &y);
                ret = Div(x, y);
                printf("ret = %d\n", ret);
                break;
            case 3:
                printf("请输入2个操作数:> ");
                scanf("%d %d", &x, &y);
                ret = Mul(x, y);
                printf("ret = %d\n", ret);
                break;
            case 4:
                printf("请输入2个操作数:> ");
                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);
    
    return 0;
}

🚩  让我们来测试一下代码:

❗  修改之后代码合理多了,虽然功能都实现了,但是存在可以优化的地方:

      ① 当前代码比较冗余,存在大量重复出现的语句。

      ② 添加计算器的功能(比如 a & b)时每加一个功能都要写一段case,能否更方便地增加?

⚡ 使用函数指针数组改进代码:

#include <stdio.h>

void menu()
{
    printf("*****************************\n");
    printf("**    1. add     2. sub    **\n");
    printf("**    3. mul     4. div    **\n");
    printf("**         0. exit         **\n");
    printf("*****************************\n");
}

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 input = 0;
    do {
        menu();

        // pfArr 就是函数指针数组
        int (*pfArr[5])(int, int) = {NULL, Add, Sub, Mul, Div};
        int x = 0;
        int y = 0;
        int ret = 0;
        printf("请选择:> ");
        scanf("%d", &input);

        if(input >= 1 && input <= 4) {
            printf("请输入2个操作数:> ");
            scanf("%d %d", &x, &y);
            ret = (pfArr[input])(x, y);
            printf("ret = %d\n", ret);  
        }
        else if(input == 0) {
            printf("退出程序\n");
            break;
        } else {
            printf("选择错误\n");
        }

    } while(input);
    
    return 0;
}

🚩 让我们来测试一下代码:

🔑 解析:这就是函数指针数组的应用。接收一个下标,通过下标找到数组里的某个元素,这个元素如果恰好是一个函数的地址,然后去调用那个函数。它做到了一个 "跳板" 的作用,所以我们通常称这种数组叫做 转移表(转移表在《C和指针》这本书中有所提及)。

三、指向函数指针数组的指针

0x00 函数指针数组的指针定义

📚 定义:指向函数指针数组的指针是一个指针,指针指向一个数组,数组的元素是函数指针。

0x01 函数指针数组的例子

💬 ppfArr 就是一个函数指针数组:

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

int main()
{
    int arr[10] = {0};
    int (*p)[10] = &arr; // 取出数组的地址

    int (*pfArr[4])(int, int); // pfArr是一个数组 - 函数指针的数组
    // ppfArr是一个指向[函数指针数组]的指针
    int (* (*ppfArr)[4])(int, int) = &pfArr;
    // ppfArr 是一个数组指针,指针指向的数组有4个元素
    // 指向的数组的每个元素的类型是一个函数指针 int(*)(int, int)

    return 0;
}

0x02 指针的总结

🔺 指针:

四、回调函数(call back)

0x00 回调函数的概念

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

0x01 回调函数的例子

💬 用刚才的 switch 版本的计算器为例:

#include <stdio.h>

void menu()
{
    printf("*****************************\n");
    printf("**    1. add     2. sub    **\n");
    printf("**    3. mul     4. div    **\n");
    printf("**         0. exit         **\n");
    printf("*****************************\n");
}

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 Calc(int (*pf)(int, int))
{
    int x = 0;
    int y = 0;
    printf("请输入2个操作数:>");
    scanf("%d %d", &x, &y);
    printf("%d\n", pf(x, y));
}

int main()
{
    int input = 0;

    do {    
        menu();
        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;
}

🔑 解析:上面的代码做到了想要做什么计算就做什么计算的目的,这就是函数指针能够做到的事。一个 Calc 函数就可以做很多的功能,给它传递不同的参数,它就可以做不同的事情。

0x02 无指针类型 void*

void*

0x03 qsort 函数

📚 说明:qsort 函数是C语言编译器函数库自带的排序函数( 需引入头文件 stdlib.h

💬 回顾冒泡排序:

【维生素C语言】第四章 - 数组 ( 3 - 0x01 )

#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-1-i; j++) {
            if(arr[j] > arr[j + 1]) {
                // 交换
                int tmp = arr[j];
                arr[j] = arr[j + 1];
                arr[j + 1] = tmp;
            }
        }
    }
}

void print_arr(int arr[], int sz)
{
    int i = 0;
    for (i = 0; i < sz; i++) {
        printf("%d ", arr[i]);
    }
    printf("\n");
}

int main()
{
    int arr[10] = {9,8,7,6,5,4,3,2,1,0};
    int sz = sizeof(arr) / sizeof(arr[0]);

    print_arr(arr, sz);
    bubble_sort(arr, sz);
    print_arr(arr, sz);

    return 0;
}

❓ 问题点:我们自己实现的冒泡排序函数只能排序整型顺序,如果我们要排序字符串或者一个结构体,我们是不是要单独重新实现这个函数呢?而 qsort 函数可以帮我们排任意想排的数据类型。

📚 qsort 函数的四个参数:

💬 qsort 整型数据排序(升序):

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

/*
void qsort (
    void* base,
    size_t num,
    size_t size,
    int (*cmp_int)(const void* e1, const void* e2)
    );
*/

int cmp_int(const void* e1, const void* e2)
{
    // 升序: e1 - e2
    return *(int*)e1 - *(int*)e2;
}


void print_arr(int arr[], int sz)
{
    int i = 0;
    for (i = 0; i < sz; i++) {
        printf("%d ", arr[i]);
    }
    printf("\n");
}

void int_sort()
{
    int arr[] = {9,8,7,6,5,4,3,2,1,0};
    int sz = sizeof(arr) / sizeof(arr[0]);
    // 排序(分别填上四个参数)
    qsort(arr, sz, sizeof(arr[0]), cmp_int);

    // 打印
    print_arr(arr, sz);
}

int main()
{
    int_sort();

    return 0;
}

🚩  0 1 2 3 4 5 6 7 8 9

❓ 如果我想测试一个结构体数据呢?

💬 那我们就写结构体的cmp函数(升序):

( 需求:结构体内容为 " 姓名 + 年龄 ",使用qsort,实现按年龄排序和按姓名排序 )

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

struct Stu
{
    char name[20];
    int age;
};

/*
void qsort (
    void* base,
    size_t num,
    size_t size,
    int (*cmp_int)(const void* e1, const void* e2)
    );
*/

int cmp_struct_age(const void* e1, const void* e2)
{
    return ((struct Stu*)e1)->age - ((struct Stu*)e2)->age;
}

int cmp_struct_name(const void* e1, const void* e2)
{
    return strcmp(((struct Stu*)e1)->name, ((struct Stu*)e2)->name);
}

void struct_sort()
{
    // 使用qsort函数排序结构体数据
    struct Stu s[3] = { 
        {"Ashe", 39},
        {"Hanzo", 38},
        {"Ana", 60}
    };
    int sz = sizeof(s) / sizeof(s[0]);
    // 按照年龄排序
    qsort(s, sz, sizeof(s[0]), cmp_struct_age);
    // 按照名字来排序
    qsort(s, sz, sizeof(s[0]), cmp_struct_name);
}

int main()
{
    struct_sort();

    return 0;
}

🔑 解析:按照年龄排序则比较年龄的大小,按照名字排序本质上是比较Ascii码的大小。

❓ 现在是升序,如果我想实现降序呢?

💡 很简单,只需要把 e1 - e2 换为 e2 - e1 即可:

int cmp_int(const void* e1, const void* e2)
{
    // 降序: e2 - e1
    return( *(int*)e2 - *(int*)e1 );
}

int cmp_struct_age(const void* e1, const void* e2)
{
    return( ((struct Stu*)e2)->age - ((struct Stu*)e1)->age );
}

int cmp_struct_name(const void* e1, const void* e2)
{
    return( strcmp(((struct Stu*)e2)->name, ((struct Stu*)e1)->name) );
}

0x04 模拟实现 qsort 函数

📚 模仿 qsort 实现一个冒泡排序的通用算法

💬 完整代码(升序):

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

struct Stu 
{
    char name[20];
    char age;
};

// 模仿qsort实现一个冒泡排序的通用算法
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 bubble_sort_q (
    void* base, // 首元素地址
    int sz, // 元素总个数
    int width, // 每个元素的大小
    int (*cmp)(const void*e1, const void*e2) // 两个元素的函数
    )
{
    // 确认趟数
    int i = 0;
    for(i=0; i<sz-1; i++) {
        // 一趟排序
        int j = 0;
        for(j=0; j<sz-1-i; j++) {
            // 两个元素比较   arr[i] arr[j+i]
            if(cmp( (char*)base+j*width, (char*)base+(j+1)*width ) > 0) {
                //交换
                Swap((char*)base+j*width, (char*)base+(j+1)*width, width);     
            }
        }
    }
}

int cmp_struct_age(const void* e1, const void* e2) {
    return ((struct Stu*)e1)->age - ((struct Stu*)e2)->age;
}
int cmp_struct_name(const void* e1, const void* e2) {
    return strcmp( ((struct Stu*)e1)->name, ((struct Stu*)e2)->name );
}
void struct_sort()
{
    // 使用qsort排序结构体数据
    struct Stu s[] = {"Ashe", 39, "Hanzo", 38, "Ana", 60};
    int sz = sizeof(s) / sizeof(s[0]);
    // 按照年龄排序
    bubble_sort_q(s, sz, sizeof(s[0]), cmp_struct_age);
    // 按照名字排序
    bubble_sort_q(s, sz, sizeof(s[0]), cmp_struct_name);
}

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) {
    // 升序: e1 - e2
    return *(int*)e1 - *(int*)e2;
}
void int_sort()
{
    int arr[] = {9,8,7,6,5,4,3,2,1,0};
    int sz = sizeof(arr) / sizeof(arr[0]);
    // 排序
    bubble_sort_q(arr, sz, sizeof(arr[0]), cmp_int);
    // 打印
    print_arr(arr, sz);
}


int main()
{
    int_sort();
    // struct_sort();

    return 0;
}

🚩 0 1 2 3 4 5 6 7 8 9


参考资料:

Microsoft. MSDN(Microsoft Developer Network)[EB/OL]. []. .

比特科技. C语言进阶[EB/OL]. 2021[2021.8.31]. .

📌 本文作者: 王亦优

📃 更新记录: 2021.6.22

勘误记录:

📜 本文声明: 由于作者水平有限,本文有错误和不准确之处在所难免,本人也很想知道这些错误,恳望读者批评指正!

本章完。

  • 16
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 6
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

平渊道人

喜欢的话可以支持下我的付费专栏

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

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

打赏作者

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

抵扣说明:

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

余额充值