Bootstrap

C语言指针超详解——最终篇一

C语言指针系列文章目录

入门篇
强化篇
进阶篇
最终篇一


由于最终篇内容较多,所以拆分成两篇。

1. 回调函数是什么

回调函数就是一个通过函数指针调用的函数。
如果你把函数的指针(地址)作为参数传递给另个函数,当这个指针被用来调用其所指向的函数时,被调用的函数就是回调函数

上一篇博客我们写的计算器的实现的代码中,main 函数的许多代码是重复出现的,其中虽然执行计算的逻辑是有一些区别的,但是输入输出操作是冗余的,然后我们利用转移表进行了简化。
有没有其他的简化思路呢?

因为红色框中的代码,只有调用函数的逻辑是有差异的,我们可以把调用的函数的地址以参数的形式传递过去,使用函数指针接收,函数指针指向什么函数就调用什么函数,这里其实使用的就是回调函数的功能。

未改造的计算器
(这个代码太过臃肿,所以图片展示,如果需要这段代码可以在我的指针系列的上一篇博客中获取)

//使用回到函数改造后
#include <stdio.h>
int add(int a, int b)
{
	return a + b;
}
int sub(int a, int b)
{
	return a - b;
}
int mul(int a, int b)
{
	return a * b;
}
int div(int a, int b)
{
	return a / b;
}
void calc(int(*pf)(int, int))//看一看这个函数,它的参数是一个函数指针,而且这个函数指针的		
{							 //类型恰好和上面的4个计算用的函数类型相同
	int ret = 0;
	int x, y;
	printf("输入操作数:");
	scanf("%d %d", &x, &y);
	ret = pf(x, y);	//调用 calc 函数,会调用传参过来的函数,这就是回调函数
	printf("ret = %d\n", ret);
}
int main()
{
	int input = 1;
	do
	{
		printf("*************************\n");
		printf("	1:add 2:sub \n");
		printf("	3:mul 4:div \n");
		printf("*************************\n");
		printf("请选择:");
		scanf("%d", &input);
		switch (input)
		{
		case 1:
			calc(add);//这里调用 calc 函数实际上是是在通过 calc 函数调用其他的函数
			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;
}

像上面这样的给一个函数传过去一个函数指针的地址来进行函数实现的,就叫做回调函数

2. qsort 函数

2.1 概念

qsort 函数是一个用来排序的库函数,我们来看看cplusplus上对于这个函数的介绍:
参数部分

可以看到

void qsort(void* base, size_t num, size_t size,
    int (*compar)(const void*, const void*));

qosrt 是一个没有返回类型的函数,它有4个参数,分别是:

  void* base  ---- 需要排序的数据的首地址,注意需要被排序的元素的地址必须是连续的
  size_t num  ---- 需要排序的数据个数
  size_t size ---- 需要排序的数据每个数据的大小,单位为字节
  int(*compare)(const void*,const void*) ---
  		一个返回类型为 int 的函数指针,两个参数的类型都是 const void*

我想你可能已经猜出来 qosrt 排序的原理了,没错,就是根据 size 的大小去解引用 base 指向的数据,然后调用 compare 函数比较两个数据的大小,根据其返回结果按照字节依次将两个数据中的数据进行交换(也就是交换内存中这两个数据的每个字节存储的数据)

有了这样的推理,我们就只需要再关心一下 compare 函数怎么构建就可以了,我们再来看cplusplus上对于 compare 函数的介绍:
compare
返回值

如果前面的元素比后面的元素大,返回一个大于0的数字
如果前面的元素比后面的元素小,返回一个小于0的数字
如果前面的元素和后面的元素相等,返回0

参数
参数为两个 void* 的指针,值得注意的是 void* 类型的指针是无法直接解引用的,所以在函数内部对 p1,p2 进行解引用操作之前,需要先进行强制类型转换

2.2 qsort 排序 int 类型数据

代码示例:

#include<stdio.h>
#include<stdlib.h>//注意 qsort 函数包含在这个头文件中

int int_cmp(const void* p1, const void* p2)
{
	return *(int*)p1 - *(int*)p2;//这个代码首先对 p1 和 p2 进行了强制类型转换,然后解引用
}

int main()
{
	int arr[] = { 9,8,7,6,5,4,3,2,1 };		 //注意,qsort 排序的结果默认是升序的,
	size_t sz = sizeof(arr) / sizeof(arr[0]);//所以我们先用一个降序的数组做测试

	qsort(arr, sz, sizeof(arr[0]), int_cmp);//使用 qsort 进行排序
	//这里的 sz 就是数据的个数, sizeof(arr[0]) 就是每个数据的大小
	for (int i = 0; i < sz;i++)//打印一下
		printf("%d ", arr[i]);
	printf("\n");
	return 0;
}

输出结果:
qsort

我们再来看看 int_cmp 这个函数:
这个函数接受了两个 void* 的指针,但是我们在设计这个函数的时候已经知道了这个函数会接受的指针实际指向的类型,所以我们可以直接将这两个参数强制类型转换为 int* 类型的变量,然后按照要求设计返回值,在返回时,除了上面的做法,还有一种写法:

int int_cmp(const void* p1, const void* p2)
{
	if (*(int*)p1 > *(int*)p2)
		return 1;
	else if (*(int*)p1 < *(int*)p2)
		return -1;
	else
		return 0;
}

也就是说,这个函数只要符合要求,怎么写都是可以的。

注释中提到: qsort 函数排序默认是升序,那么有没有办法让它排成降序的呢?当然有,只需要修改一下 compare 函数的返回值的正负就可以了:

int int_cmp(const void* p1, const void* p2)
{
	return *(int*)p2 - *(int*)p1;//这里 p1 和 p2 调换了位置
}

int_cmp 函数修改为这样,那么 qsort 函数排出来的就是降序的了。
降序

2.3 使用 qsort 排序结构体数据

qosrt 并不只能用来排序 int , char 类型的数据,它还能用来排序结构数据。

不过在测试之前,我们先来了解一个库函数:strcmp

int strcmp(const char* str1, const char* str2);

这是一个包含在 <string.h>库中的库函数,用来比较两个字符串是否相同
如果相同,它将返回0,
如果不相同,它将返回两个字符串中第一个不相同的位置的两个数据的差(str1 - str2)(当然一些编译器并不是这么实现的,以后的博客会对这个库函数进行详细的介绍,现在这么理解就可以了)。

有了这个函数,我们就来尝试一下实现 qsort 排序结构体数据吧:

#include<stdio.h>
#include<stdlib.h>//注意 qsort 函数包含在这个头文件中
#include<string.h>// strcmp 包含在这个头文件中
typedef struct stu
{
	char name[10];
	int age;
}stu;//想一想,stu 结构体中有两个成员,我们要用哪一个来排序呢?

int age_cmp(const void* p1, const void* p2)
{	//按年龄排序
	return ((stu*)p1)->age - ((stu*)p2)->age;
}

int name_cmp(const void* p1, const void* p2)
{	//按名字排序
	return strcmp(((stu*)p1)->name, ((stu*)p1)->name);
}	

void Print(stu* s,size_t sz)
{	//这个函数用来打印结构体,方便我们观察
	static int time = 1;//还记得 static 修饰局部变量吗,不记得的话可以看一下我的函数基础知识的博客
	printf("第%d次\n", time++);
	for (int i = 0; i < sz; i++)
	{
		printf("%s %d\n", s[i].name, s[i].age);
	}
}

int main()
{
	stu s[] = { {"zhangsan", 15}, {"lisi", 30}, {"wangwu", 20} };//创建一个 stu 数组并初始化
	size_t sz = sizeof(s) / sizeof(s[0]);
	Print(s, sz);

	qsort(s, sz, sizeof(s[0]), age_cmp);//先根据年龄排序一下
	Print(s, sz);

	qsort(s, sz, sizeof(s[0]), name_cmp);//再根据名字排序一下
	Print(s, sz);

	return 0;
}

输出结果为:
输出结果
这就是 qsort 函数的一个优势,无论存储的是什么类型的元素,只要给出它的大小,数量,再设计一个比大小的函数,就能实现排序

3. 模拟实现 qsort 函数

为了简单起见,我们使用冒泡排序模拟实现这个 qsort 函数。
(如果你不了解冒泡排序,可以看看指针强化篇这篇博客)

想一想我们需要什么?
我们再来看一看 qsort 的声明:

void qsort(void* base, size_t num, size_t size,
    int (*compar)(const void*, const void*));

还有我们前面分析出来的 qsort 的原理:根据 size 的大小去解引用 base 后的数据,然后调用 compare 函数比较两个数据的大小,根据其返回结果按照字节依次将两个数据中的数据进行交换(也就是交换内存中这两个数据的每个字节存储的数据)
那么我们需要做的就是实现遍历,比较,交换这三个过程,我们来试一试:

void my_qsort(void* arr, size_t ElementNum, size_t ElementSize, int (*cmp)(const void*, const void*))
{
	for (int i = 0; i < ElementNum - 1; i ++)
	{
		for (int j = 0; j < ElementNum - i - 1; j ++)
		{	//使用冒泡排序算法的思路进行数据遍历
			if (cmp((char*)arr + j * ElementSize, (char*)arr + (j + 1) * ElementSize) > 0)
			{	//如果 cmp 函数的返回值大于 0 ,就说明这两个数据的顺序与最终结果不匹配,进行交换
				for (int k = 0; k < ElementSize; k++)
				{	//在内存中进行每个字节的数据的交换,那么 char 类型就正好可以满足这个任务
					char tmp = *((char*)arr +  j * ElementSize + k);
					*((char*)arr + j * ElementSize + k) = *((char*)arr + j * ElementSize + ElementSize + k);
					*((char*)arr + j * ElementSize + ElementSize + k) = tmp;
					//上面这三行代码实际上就是交换两个 char 类型的数据,只不过由于是 void* 类型,需要不断地强制转换,所以显得麻烦  
					//如果你不希望这个函数看起来这么臃肿,可以把交换数据的这段代码封装为函数
				}
			}
		}
	}
}

这个代码就可以完成 qsort 函数的工作,但是要注意,由于我们使用的是冒泡排序,时间复杂度为O(n2),所以在处理特别多的数据时可能会耗费大量的时间。

4. sizeof 与 strlen 的对比

4.1 sizeof

在操作符的博客中,我们学习了 sizeof 这个操作符,sizeof 计算变量所占内存内存空间大小的,单位是字节,如果操作数是类型的话,计算的是使用类型创建的变量所占内存空间的大小。
sizeof 只关注占用内存空间的大小,不在乎内存中存放什么数据。
比如:

#include <stdio.h>
int main()
{
	int a = 10;
	printf("%d\n", sizeof(a));//40
	printf("%d\n", sizeof a);// 40 ,sizeof 计算变量大小时,可以省略括号
	printf("%d\n", sizeof(int));//4

	return 0;
}

4.2 strlen

strlen 是<string.h>中的库函数,功能是求字符串长度。函数原型如下:

size_t strlen ( const char * str );

统计的是从 strlen 函数的参数 str 中这个地址开始向后,\0 之前字符串中字符的个数。
strlen 函数会一直向后找 \0 字符,直到找到为止,所以可能存在越界查找

#include <stdio.h>
int main()
{
	char arr1[3] = { 'a', 'b', 'c' };//arr1 后面没有 \0
	char arr2[] = "abc";			 //arr2 后面有 \0
	printf("%d\n", strlen(arr1));//随机
	printf("%d\n", strlen(arr2));//3

	printf("%d\n", sizeof(arr1));//3
	printf("%d\n", sizeof(arr2));//4
	return 0;
}

4.3 sizeof 与 strlen 的对比

sizeofstrlen
sizeof是操作符strlen是库函数
sizeof计算操作数所占内存的大小,单位是字节srtlen是求字符串长度的,统计的是、0之前字符的个数
不关注内存中存放什么数据关注内存中是否有\0,如果没有\0,就会持续往后找,可能会越界

5. 数组和指针笔试题解析

注:以下分析均以 x64 环境分析

5.1 一维数组

#include<stdio.h>
int main()
{
	int a[] = { 1,2,3,4 };
	printf("%zd\n", sizeof(a));			//这里 a 代表数组					16
	printf("%zd\n", sizeof(a + 0));		// a+0 是一个指针,指向数组首元素		8
	printf("%zd\n", sizeof(*a));		//解引用 a 得到数组首元素				4
	printf("%zd\n", sizeof(a + 1)); 	// a+0 是一个指针,指向数组第二个元素	8
	printf("%zd\n", sizeof(a[1]));  	//数组的第二个元素					4
	printf("%zd\n", sizeof(&a));		//数组的地址,也是一个指针			8
	printf("%zd\n", sizeof(*&a));   	//数组的地址解引用,得到数组			16
	printf("%zd\n", sizeof(&a + 1));	//跳过整个数组,但还是一个指针			8
	printf("%zd\n", sizeof(&a[0])); 	//取出第一个元素的地址,是一个指针		8
	printf("%zd\n", sizeof(&a[0] + 1));	//第二个元素的地址,指针				8
	return 0;
}

5.2 字符数组

代码一:

#include<stdio.h>
int main()
{
	char arr[] = { 'a','b','c','d','e','f' };//注意后面没有 \0
	printf("%zd\n", sizeof(arr));		//数组大小					6
	printf("%zd\n", sizeof(arr + 0));	//指向第一个元素的指针		8
	printf("%zd\n", sizeof(*arr));		//第一个元素的大小			1
	printf("%zd\n", sizeof(arr[1]));		//第一个元素的大小			1
	printf("%zd\n", sizeof(&arr));		//指向整个数组的指针		8
	printf("%zd\n", sizeof(&arr + 1));	//跳过整个数组,但还是指针	8
	printf("%zd\n", sizeof(&arr[0] + 1));//指向第二个元素的指针		8
	return 0;
}

代码二:

#include<stdio.h>
int main()
{
	char arr[] = { 'a','b','c','d','e','f' };
	printf("%d\n", strlen(arr));		//从 arr 向后找 \0,字符串中没有	随机值
	printf("%d\n", strlen(arr + 0));	//从 arr 向后找 \0,字符串中没有	随机值
	printf("%d\n", strlen(*arr));		//传递给 strlen 一个字符			报错
	printf("%d\n", strlen(arr[1]));		//传递给 strlen 一个字符			报错
	printf("%d\n", strlen(&arr));		//从 arr 向后找 \0,字符串中没有	随机值
	printf("%d\n", strlen(&arr + 1));	//跳过整个数组后开始找 \0			随机值
	printf("%d\n", strlen(&arr[0] + 1));//从第二个元素开始找 \0			随机值
	return 0;
}

未完待续……
由于最终篇内容较多,所以拆分成两篇。
谢谢你的阅读,喜欢的话来个点赞收藏评论关注吧!

;