深⼊理解指针(5)

1. 回调函数详解

回调函数就是⼀个通过函数指针调⽤的函数。

如果你把函数的指针(地址)作为参数传递给另⼀个函数,当这个指针被⽤来调⽤其所指向的函数时,被调⽤的函数就是回调函数。回调函数不是由该函数的实现⽅直接调⽤,⽽是在特定的事件或条件发⽣时由另外的⼀⽅调⽤的,⽤于对该事件或条件进⾏响应。

回调函数是通过函数指针实现的机制,其核心特点如下:

  1. 函数指针作为参数传递
  2. 在特定条件触发时被调用
  3. 实现调用方与被调用方的解耦

数学表示:

设存在函数 f:R×R→Rf: \mathbb{R} \times \mathbb{R} \rightarrow \mathbb{R}f:R×R→R

回调机制可描述为:
g(f,x,y)=f(x,y) g(f, x, y) = f(x, y) g(f,x,y)=f(x,y)

其中 ggg 是调用函数,fff 是通过指针传递的回调函数


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("1.add    2.sub\n");
	printf("3.mul    4.div\n");
	printf("0.eixt\n");
}
int main()
{
	int input = 0;
	int x = 0;
	int y = 0;
	int r = 0;
	do
	{
		menu();
		printf("请输入:");
		scanf("%d", &input);
		switch(input)
		{
		case 1:
			printf("请输入2个数字:");
			scanf("%d %d", &x, &y);
			r = Add(x, y);
			printf("结果是:%d\n", r);
			break;
		case 2:
			printf("请输入2个数字:");
			scanf("%d %d", &x, &y);
			r = Sub(x, y);
			printf("结果是:%d\n", r);
			break;
		case 3:
			printf("请输入2个数字:");
			scanf("%d %d", &x, &y);
			r = Mul(x, y);
			printf("结果是:%d\n", r);
			break;
		case 4:
			printf("请输入2个数字:");
			scanf("%d %d", &x, &y);
			r = Div(x, y);
			printf("结果是:%d\n", r);
			break;
		case 0:
			printf("推出计算器\n");
			break;
		default :
			printf("请输入0------4\n");
			break;

		}
	} while (input);
	return 0;
}
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 calc(int (*pf)(int, int))
{
	int x = 0;
	int y = 0;
	int r = 0;
	printf("请输入2个数字:");
	scanf("%d %d", &x, &y);
	r = pf(x, y);
	printf("结果是:%d\n", r);
}
void menu()
{
	printf("1.add    2.sub\n");
	printf("3.mul    4.div\n");
	printf("0.eixt\n");
}
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("请输入0------4\n");
			break;

		}
	} while (input);
	return 0;
}

代码对比分析

原始代码结构

c 复制代码
switch(input) {
case 1:
    // 重复输入输出逻辑
    r = Add(x, y);
    break;
// 类似重复代码...
}

问题

  1. 代码冗余:每个分支重复输入/输出逻辑
  2. 耦合度高:业务逻辑与界面操作强耦合
  3. 扩展困难:新增功能需修改主流程
改进后代码
c 复制代码
void calc(int (*pf)(int, int)) {
    int x, y, r;
    scanf("%d %d", &x, &y);
    r = pf(x, y); // 回调点
    printf("结果是:%d\n", r);
}

switch(input) {
case 1:
    calc(Add); // 传递函数指针
    break;
//...
}

优化点

  1. 抽象公共逻辑:输入输出封装在calc()
  2. 函数指针应用:int (*pf)(int,int)声明回调接口
  3. 解耦实现:业务逻辑与界面操作分离

回调机制图解

复制代码
主流程 → calc() → 通过函数指针调用
              ↑
          (Add/Sub/Mul/Div)
  1. calc() 作为通用框架
  2. 具体运算函数作为回调注入
  3. 框架控制流程,回调实现具体功能

关键语法解析

c 复制代码
int (*pf)(int, int) // 函数指针声明
  1. *pf:指针变量
  2. (int, int):参数类型
  3. int:返回值类型
  4. calc(Add):传递函数地址

性能对比

指标 原始代码 改进代码
代码行数 48 36
分支复杂度
维护成本
扩展性

改进代码通过回调机制减少30%重复代码,显著提升可维护性和扩展性,完美演示了回调函数在实际工程中的应用价值。


2. qsort使用举例

2.1 使用qsort函数排序整型数据

qsort 是 C 标准库 <stdlib.h> 中提供的快速排序函数,其函数原型如下:

c 复制代码
void qsort(void *base, size_t num, size_t size,
           int (*compar)(const void *, const void *));
  • base:待排序数组的首元素地址
  • num:数组元素个数
  • size:每个元素的大小(字节数)
  • compar:比较函数的函数指针(回调函数),用于定义排序规则
示例:排序整型数组
c 复制代码
#include <stdio.h>
#include <stdlib.h>

// 比较函数:升序排列
int cmp_int(const void *e1, const void *e2)
{
    return *(int *)e1 - *(int *)e2;
}

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

输出结果1 2 3 4 5 6 7 8 9

关键点 :比较函数接收 const void * 指针,需要先强制类型转换为实际类型再解引用。e1 - e2 返回正数表示 e1 > e2(升序),返回负数表示 e1 < e2,返回 0 表示相等。


2.2 使用qsort排序结构数据

当排序结构体数据时,比较函数需要访问结构体的成员。这里就需要用到结构指针结构体成员访问操作符 ->

结构指针与 -> 操作符详解

结构指针是指向结构体变量的指针,其定义方式为:

c 复制代码
struct Student stu;          // 定义结构体变量
struct Student *p = &stu;    // 定义结构指针并指向stu

-> 操作符(箭头操作符)用于通过结构指针访问结构体成员:

c 复制代码
p->name    // 等价于 (*p).name
p->age     // 等价于 (*p).age
p->score   // 等价于 (*p).score

重点p->member(*p).member 的语法糖,两者完全等价。箭头操作符更简洁直观,是 C 语言中通过指针访问结构体成员的标准写法。

示例:按成绩排序学生信息
c 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

// 定义学生结构体
typedef struct Student
{
    char name[20];
    int age;
    float score;
} Student;

// 按成绩升序排序的比较函数
int cmp_by_score(const void *e1, const void *e2)
{
    // e1、e2 是 void* 指针,先强转为结构指针
    Student *s1 = (Student *)e1;
    Student *s2 = (Student *)e2;
    
    // 使用 -> 操作符访问结构体成员
    if (s1->score > s2->score)
        return 1;
    else if (s1->score < s2->score)
        return -1;
    else
        return 0;
}

// 按年龄升序排序的比较函数
int cmp_by_age(const void *e1, const void *e2)
{
    // 直接通过强制类型转换 + 箭头操作符访问成员
    return ((Student *)e1)->age - ((Student *)e2)->age;
}

// 按姓名排序(字典序)
int cmp_by_name(const void *e1, const void *e2)
{
    return strcmp(((Student *)e1)->name, ((Student *)e2)->name);
}

int main()
{
    Student students[] = {
        {"张三", 20, 88.5},
        {"李四", 19, 92.0},
        {"王五", 21, 76.5},
        {"赵六", 20, 95.5}
    };
    int sz = sizeof(students) / sizeof(students[0]);
    
    // 按成绩排序
    qsort(students, sz, sizeof(students[0]), cmp_by_score);
    
    printf("按成绩升序排列:\n");
    for (int i = 0; i < sz; i++)
    {
        // 结构体变量用 . 访问成员,结构指针用 -> 访问成员
        printf("姓名:%s  年龄:%d  成绩:%.1f\n",
               students[i].name,
               students[i].age,
               students[i].score);
    }
    
    return 0;
}

输出结果

复制代码
按成绩升序排列:
姓名:王五  年龄:21  成绩:76.5
姓名:张三  年龄:20  成绩:88.5
姓名:李四  年龄:19  成绩:92.0
姓名:赵六  年龄:20  成绩:95.5
结构指针与 -> 操作符要点总结
概念 说明 示例
结构指针 指向结构体变量的指针 Student *p = &stu;
-> 操作符 通过指针访问成员 p->score
. 操作符 通过变量直接访问成员 stu.score
等价写法 p->member = (*p).member 两者完全等价
在 qsort 中的应用 void* 强转为结构指针后,用 -> 访问待比较的成员 ((Student *)e1)->score

重点强调

  1. qsort 的比较函数参数是 const void *,必须先强制类型转换为实际的结构指针类型才能访问成员。
  2. 访问结构体成员时:结构变量用 .,结构指针用 ->
  3. -> 操作符的优先级高于强制类型转换,因此 (Student *)e1->score 是错误的写法,必须加括号:((Student *)e1)->score

2.3 综合对比:排序整型 vs 排序结构

对比项 排序整型数据 排序结构数据
比较函数参数 const void * const void *
类型转换 *(int *)e1 ((Student *)e1)->score
访问方式 直接解引用 通过 -> 访问成员
比较依据 元素本身的值 结构体中的某个成员
典型场景 数组排序 学生成绩、员工信息等结构化数据排序

3. qsort函数的模拟实现

理解了 qsort 的使用方法后,我们来深入其底层原理------使用回调函数 机制,模拟实现一个通用的冒泡排序版本 my_qsort

3.1 设计思路

qsort 的核心在于通用性 :它不知道要排序的数据类型,只通过 void* 指针操作内存。其设计思想如下:

  1. 参数通用化 :接收 void* base(起始地址)、size_t num(元素个数)、size_t size(元素大小)

  2. 比较回调化 :通过函数指针 int (*cmp)(const void*, const void*) 让用户自定义比较规则

  3. 交换字节化:由于不知道具体类型,交换元素时按字节逐个交换

    流程图:
    my_qsort(base, num, size, cmp)

    ├─ 外层循环 i = 0 → num-1
    │ └─ 内层循环 j = 0 → num-1-i
    │ ├─ 计算第j个元素地址:base + j*size
    │ ├─ 计算第j+1个元素地址:base + (j+1)*size
    │ ├─ 调用 cmp(elem_j, elem_j+1) 比较
    │ └─ 若返回值 > 0,则交换两个元素(按字节交换)
    └─ 排序完成


3.2 核心代码实现

c 复制代码
#include <stdio.h>
#include <string.h>

// 按字节交换两个内存块
// 因为不知道具体类型,所以逐字节交换
void Swap(char *buf1, char *buf2, size_t size)
{
    size_t i = 0;
    for (i = 0; i < size; i++)
    {
        char tmp = *buf1;
        *buf1 = *buf2;
        *buf2 = tmp;
        buf1++;
        buf2++;
    }
}

// 模拟实现qsort(基于冒泡排序)
// base:待排序数组首地址
// num:元素个数
// size:每个元素的大小(字节数)
// cmp:比较函数的函数指针(回调函数)
void my_qsort(void *base, size_t num, size_t size,
              int (*cmp)(const void *, const void *))
{
    size_t i = 0;
    size_t j = 0;

    // 外层循环:排序趟数
    for (i = 0; i < num - 1; i++)
    {
        // 内层循环:每趟比较次数
        for (j = 0; j < num - 1 - i; j++)
        {
            // 计算第j个元素的地址
            // 将 base 强转为 char*,按字节偏移
            char *elem_j = (char *)base + j * size;
            // 计算第j+1个元素的地址
            char *elem_j1 = (char *)base + (j + 1) * size;

            // 调用回调函数比较两个元素
            // 如果返回值 > 0,说明 elem_j > elem_j1,需要交换
            if (cmp(elem_j, elem_j1) > 0)
            {
                Swap(elem_j, elem_j1, size);
            }
        }
    }
}

重点解析

  1. (char *)base + j * size:将 void* 转为 char*,因为 char 占 1 字节,这样指针加 j * size 就能精确跳到第 j 个元素的起始地址。
  2. Swap 函数按字节交换,保证了无论什么类型的数据都能正确交换。
  3. cmp 是回调函数,由调用者提供,my_qsort 只负责调用它来判断大小关系。

3.3 测试:排序整型数据

c 复制代码
// 比较整型(升序)
int cmp_int(const void *e1, const void *e2)
{
    return *(int *)e1 - *(int *)e2;
}

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

    printf("排序前:");
    for (i = 0; i < sz; i++)
        printf("%d ", arr[i]);
    printf("\n");

    // 使用我们模拟的 my_qsort 排序
    my_qsort(arr, sz, sizeof(arr[0]), cmp_int);

    printf("排序后:");
    for (i = 0; i < sz; i++)
        printf("%d ", arr[i]);
    printf("\n");

    return 0;
}

输出结果

复制代码
排序前:3 1 5 7 2 4 9 6 8
排序后:1 2 3 4 5 6 7 8 9

3.4 测试:排序结构数据

c 复制代码
#include <string.h>

typedef struct Student
{
    char name[20];
    int age;
    float score;
} Student;

// 按成绩升序
int cmp_by_score(const void *e1, const void *e2)
{
    // 先强转为结构指针,再用 -> 访问成员
    float s1 = ((Student *)e1)->score;
    float s2 = ((Student *)e2)->score;

    if (s1 > s2) return 1;
    else if (s1 < s2) return -1;
    else return 0;
}

int main()
{
    Student students[] = {
        {"张三", 20, 88.5},
        {"李四", 19, 92.0},
        {"王五", 21, 76.5},
        {"赵六", 20, 95.5}
    };
    int sz = sizeof(students) / sizeof(students[0]);
    int i = 0;

    printf("排序前:\n");
    for (i = 0; i < sz; i++)
        printf("姓名:%s  年龄:%d  成绩:%.1f\n",
               students[i].name, students[i].age, students[i].score);

    // 使用 my_qsort 按成绩排序
    my_qsort(students, sz, sizeof(students[0]), cmp_by_score);

    printf("\n按成绩升序排列:\n");
    for (i = 0; i < sz; i++)
        printf("姓名:%s  年龄:%d  成绩:%.1f\n",
               students[i].name, students[i].age, students[i].score);

    return 0;
}

输出结果

复制代码
排序前:
姓名:张三  年龄:20  成绩:88.5
姓名:李四  年龄:19  成绩:92.0
姓名:王五  年龄:21  成绩:76.5
姓名:赵六  年龄:20  成绩:95.5

按成绩升序排列:
姓名:王五  年龄:21  成绩:76.5
姓名:张三  年龄:20  成绩:88.5
姓名:李四  年龄:19  成绩:92.0
姓名:赵六  年龄:20  成绩:95.5

3.5 关键知识点总结

知识点 说明 代码示例
void* 指针运算 void* 不能直接加减,需强转为 char* 再按字节偏移 (char*)base + j * size
按字节交换 通过 char* 逐字节交换,实现通用数据交换 Swap(char *buf1, char *buf2, size_t size)
回调函数 比较函数由调用者提供,my_qsort 通过函数指针调用 cmp(elem_j, elem_j1)
函数指针参数 int (*cmp)(const void*, const void*) 声明回调接口 my_qsort(arr, sz, size, cmp_int)
类型无关性 同一套排序逻辑可排序任意类型数据 整型、结构体均可

3.6 与标准库 qsort 的对比

对比项 标准库 qsort 模拟实现 my_qsort
算法 快速排序(平均 O(n log n)) 冒泡排序(O(n²))
参数 完全相同 完全相同
回调机制 使用函数指针 使用函数指针
通用性 任意类型 任意类型
性能 高效 教学演示用

核心思想 :虽然算法不同,但回调函数的设计模式 完全一致------框架(排序函数)负责流程控制,回调(比较函数)负责具体规则。这就是 qsort 能够排序任意类型数据的根本原因。


3.7 完整代码汇总

c 复制代码
#include <stdio.h>
#include <string.h>

// 按字节交换
void Swap(char *buf1, char *buf2, size_t size)
{
    size_t i = 0;
    for (i = 0; i < size; i++)
    {
        char tmp = *buf1;
        *buf1 = *buf2;
        *buf2 = tmp;
        buf1++;
        buf2++;
    }
}

// 模拟qsort(冒泡排序版)
void my_qsort(void *base, size_t num, size_t size,
              int (*cmp)(const void *, const void *))
{
    size_t i = 0, j = 0;
    for (i = 0; i < num - 1; i++)
    {
        for (j = 0; j < num - 1 - i; j++)
        {
            char *elem_j = (char *)base + j * size;
            char *elem_j1 = (char *)base + (j + 1) * size;

            if (cmp(elem_j, elem_j1) > 0)
            {
                Swap(elem_j, elem_j1, size);
            }
        }
    }
}

// 比较整型
int cmp_int(const void *e1, const void *e2)
{
    return *(int *)e1 - *(int *)e2;
}

// 测试整型排序
void test_int()
{
    int arr[] = {3, 1, 5, 7, 2, 4, 9, 6, 8};
    int sz = sizeof(arr) / sizeof(arr[0]);
    int i = 0;

    printf("排序前:");
    for (i = 0; i < sz; i++) printf("%d ", arr[i]);
    printf("\n");

    my_qsort(arr, sz, sizeof(arr[0]), cmp_int);

    printf("排序后:");
    for (i = 0; i < sz; i++) printf("%d ", arr[i]);
    printf("\n\n");
}

// 学生结构体
typedef struct Student
{
    char name[20];
    int age;
    float score;
} Student;

// 比较成绩
int cmp_by_score(const void *e1, const void *e2)
{
    float s1 = ((Student *)e1)->score;
    float s2 = ((Student *)e2)->score;
    if (s1 > s2) return 1;
    else if (s1 < s2) return -1;
    else return 0;
}

// 测试结构体排序
void test_struct()
{
    Student students[] = {
        {"张三", 20, 88.5},
        {"李四", 19, 92.0},
        {"王五", 21, 76.5},
        {"赵六", 20, 95.5}
    };
    int sz = sizeof(students) / sizeof(students[0]);
    int i = 0;

    printf("排序前:\n");
    for (i = 0; i < sz; i++)
        printf("姓名:%s  年龄:%d  成绩:%.1f\n",
               students[i].name, students[i].age, students[i].score);

    my_qsort(students, sz, sizeof(students[0]), cmp_by_score);

    printf("\n按成绩升序排列:\n");
    for (i = 0; i < sz; i++)
        printf("姓名:%s  年龄:%d  成绩:%.1f\n",
               students[i].name, students[i].age, students[i].score);
}

int main()
{
    test_int();
    test_struct();
    return 0;
}

3.8 总结

通过模拟实现 qsort,我们深刻理解了回调函数的强大之处:

  1. void* 指针 + 按字节操作 实现了类型无关性
  2. 回调函数(函数指针) 实现了比较规则的解耦
  3. 同一套排序逻辑 可以排序整型、结构体等任意数据类型
  4. 这种框架 + 回调的设计模式在 C 语言中广泛应用(如信号处理、线程创建等)
相关推荐
哩哩橙1 小时前
分支电路对限时电流速断保护的影响
人工智能·笔记·数据挖掘
AI科技星1 小时前
全域数学:从理论到现实的终极落地全记录 光速不变公理(v=c)+ 可见派维度常数公理(D_v=3)统一广义相对论与量子力学,解决物理学百年难题
c语言·开发语言
lwf0061641 小时前
顺序模型学习日记
算法
_日拱一卒1 小时前
LeetCode:199二叉树的右视图
算法·leetcode·职场和发展
祁白_1 小时前
跨平台通用危险函数深度解析
linux·windows·笔记·安全·系统命令
The Chosen One9851 小时前
分享对dp题目的理解-不断更新ing
笔记·算法·深度优先·动态规划·dp
有时间要学习1 小时前
【无标题】
算法
Chloeis Syntax1 小时前
JavaEE学习日记(2)---文件操作和IO
java·笔记·学习·java-ee
re林檎1 小时前
算法札记——5.15
算法