C语言笔记归纳13:指针(4)

指针(4)

目录

指针(4)

1. 🎯回调函数:代码复用的 “魔法调料包”

1.1 回调函数的定义(原码 + 通俗解读)

1.2 为什么需要回调函数?

2. 🛠️qsort 库函数:C 语言的 “万能排序神器”

2.1 qsort 函数原型:4 个参数的 “分工协作”

2.2 qsort 实战 1:排序整型数据(避坑 void*)

2.3 qsort 实战 2:排序结构体(按名字 / 年龄)

3. ✍️手写通用冒泡排序:复刻 qsort 的核心逻辑

3.1 void * 指针:“万能地址接收器”

3.2 Swap 函数:逐字节 “搬家” 的交换技巧

3.3 偏移计算:找到任意元素的 “精准地址”

完整通用冒泡排序实现(原码 + 标注)

4. 📝核心知识点总结:通用排序的 3 个关键

🎉最后:从 qsort 到手写排序,我们学会了什么?


✨引言:

普通排序只能排整型?太局限了!C 语言的回调函数 + 通用指针能让排序 “通吃” 任意数据 —— 整型、字符串、结构体全搞定!今天从 qsort 库函数拆解到手写通用冒泡排序,用 “调料包 + 搬家公司” 的通俗比喻,把 “万能排序” 的底层逻辑扒得明明白白~

1. 🎯回调函数:代码复用的 “魔法调料包”

回调函数不是 “直接调用”,而是 “通过指针间接调用”—— 主逻辑像 “餐厅后厨”,回调函数像 “不同口味的调料包”,后厨不变,换调料包就能做出不同菜!

1.1 回调函数的定义(原码 + 通俗解读)

回调函数 = 由函数指针调用的函数(如calc里的add)  
主调函数 = 负责框架的函数(如calc)  

💡举例:

  • 排序函数(主调)负责 “两两比较 + 交换” 的框架;
  • 比较函数(回调)负责 “怎么比”(整型比大小 / 字符串比 ASCII / 结构体比年龄)。

1.2 为什么需要回调函数?

如果没有回调函数,排序整型要写一个冒泡,排序结构体又要写一个冒泡 —— 代码冗余到爆炸!有了回调函数,一个排序框架适配所有数据类型,这就是 “抽象” 的魅力~

2. 🛠️qsort 库函数:C 语言的 “万能排序神器”

qsort 是 C 标准库的 “瑞士军刀”,底层用快速排序实现,能排序任意类型数据 —— 核心是靠你提供的 “比较回调函数”!

2.1 qsort 函数原型:4 个参数的 “分工协作”

void qsort(
    void* base,        // 待排序数组首地址(万能指针:能接int/char/结构体地址)
    size_t num,        // 数组元素个数
    size_t size,       // 单个元素的字节大小(int占4,结构体占24...)
    int(*compar)(const void*, const void*) // 比较回调函数指针
);

📌compar 函数规则(必须记!):

  • 返回 > 0 → p1 元素 > p2 元素
  • 返回 < 0 → p1 元素 < p2 元素
  • 返回 0 → 两元素相等

2.2 qsort 实战 1:排序整型数据(避坑 void*)

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* p1, const void* p2)
{
	// ❌易错点:void*是“无类型指针”,不能直接解引用/±整数!
	// return *p1 - *p2; 
	return *(int*)p1 - *(int*)p2; // ✅先转成int*再解引用
}

void test1()
{
	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); // 输出:0 1 2 3 4 5 6 7 8 9
}

💡void * 指针的 “万能插座” 特性:能接任意类型的地址,但必须 “转换插头”(强制类型转换)才能用 —— 就像万能插座能插国标 / 美标插头,但要适配才能通电!

2.3 qsort 实战 2:排序结构体(按名字 / 年龄)

结构体比较需要 “指定维度”,比如按名字(字符串)或年龄(整型):

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

// 按名字比较(字符串用strcmp)
int cmp_stu_by_name(const void* p1, const void* p2)
{
	// ❌易错点:原代码漏写return!strcmp的返回值要返回才生效!
	return strcmp(((struct Stu*)p1)->name, ((struct Stu*)p2)->name);
}

// 按年龄比较(整型直接减)
int cmp_stu_by_age(const void* p1, const void* p2)
{
	return ((struct Stu*)p1)->age - ((struct Stu*)p2)->age;
}

// 打印结构体数组
void print_stu(struct Stu arr[], int sz)
{
	int i = 0;
	for (i = 0; i < sz; i++)
	{
		printf("姓名:%s 年龄:%d\n", arr[i].name, arr[i].age);
	}
}

void test2()
{
	struct Stu arr[3] = { {"zhangsan",20},{"lisi",35},{"wangwu",18} };
	int sz = sizeof(arr) / sizeof(arr[0]);
	
	// 按年龄排序(结果:wangwu(18)→zhangsan(20)→lisi(35))
	qsort(arr, sz, sizeof(arr[0]), cmp_stu_by_age);
	print_stu(arr, sz);
	
	// 按名字排序(注释上面,打开下面)
	// qsort(arr, sz, sizeof(arr[0]), cmp_stu_by_name);
	// print_stu(arr, sz); // 结果:lisi→wangwu→zhangsan(按ASCII)
}

📌结构体访问小贴士:

  • 直接访问:结构体变量.成员名(如arr[i].name
  • 间接访问:结构体指针->成员名(如((struct Stu*)p1)->name)👉 字符串比较必须用strcmp,不能用>/<—— 因为字符串名是地址,比的是地址不是内容!

3. ✍️手写通用冒泡排序:复刻 qsort 的核心逻辑

普通冒泡排序只能排整型?我们改造它,让它变成 “万能排序”—— 思路和 qsort 一致:框架通用 + 细节回调

3.1 void * 指针:“万能地址接收器”

void* base接收任意数组的首地址,不管是 int 数组还是结构体数组,都能接下~

3.2 Swap 函数:逐字节 “搬家” 的交换技巧

不同类型的元素大小不同(int 占 4 字节,结构体占 24 字节),交换要像 “搬家公司”—— 不管房子多大,按 “单个箱子(字节)” 搬:

void Swap(char* buf1, char* buf2, size_t width)
{
	int i = 0;
	for (i = 0; i < width; i++) // width是元素的字节大小
	{
		char tmp = *buf1;
		*buf1 = *buf2;
		*buf2 = tmp;
		buf1++; // 逐个字节交换,搬完width个字节就搞定
		buf2++;
	}
}

💡为什么用 char*?因为 char 是 1 字节,是 C 语言最小的内存单位,能精准操作每一个字节!

3.3 偏移计算:找到任意元素的 “精准地址”

要访问arr[j],需要用(char*)base + width*j计算地址:

  • (char*)base:把 void转成 char,才能计算偏移(void * 不能 ± 整数);
  • width*j:j 个元素的总字节数(比如 int 占 4 字节,j=2 就是 8 字节偏移)。

完整通用冒泡排序实现(原码 + 标注)

// 通用冒泡排序框架
void bubble_sort(void* base, size_t sz, size_t width, int (*cmp)(const void* p1, const void* p2))
{
	int i = 0;
	for (i = 0; i < sz - 1; i++) // 趟数:n个元素n-1趟
	{
		int j = 0;
		for (j = 0; j < sz - 1 - i; j++) // 每趟比较次数递减
		{
			// 计算arr[j]和arr[j+1]的地址
			char* elem1 = (char*)base + width * j;
			char* elem2 = (char*)base + width * (j + 1);
			
			// 调用回调函数比较,需要交换则调用Swap
			if (cmp(elem1, elem2) > 0)
			{
				Swap(elem1, elem2, width);
			}
		}
	}
}

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

// 测试:排序结构体(按年龄)
void test4()
{
	struct Stu arr[3] = { {"zhangsan",20},{"lisi",35},{"wangwu",18} };
	int sz = sizeof(arr) / sizeof(arr[0]);
	bubble_sort(arr, sz, sizeof(arr[0]), cmp_stu_by_age);
	print_stu(arr, sz);
}

👉 测试结果和 qsort 完全一致!这就是 “通用排序” 的魔力 —— 框架不变,换个比较回调函数就能排任意数据~

4. 📝核心知识点总结:通用排序的 3 个关键

  1. void * 指针:万能地址接收器,适配任意数据类型(但要转成 char * 才能计算偏移);
  2. 回调函数:把 “怎么比较” 的细节交给用户,实现框架通用;
  3. 逐字节操作:用 char*+width 实现任意大小元素的交换(Swap 函数)。

💡一句话记住:框架通用靠 void,细节定制靠回调,内存操作靠 char*!

🎉最后:从 qsort 到手写排序,我们学会了什么?

C 语言的 “通用” 思想 —— 不是写死逻辑,而是把变化的部分(比如比较规则)抽象成回调函数,固定的部分(比如排序框架)写成通用代码。这种思想不仅用在排序,还能用在遍历、查找等场景,是 C 语言进阶的关键!如果这篇内容帮你打通了 “通用编程” 的任督二脉,欢迎点赞收藏🌟~

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值