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

news2024/12/26 21:25:22

C语言指针系列文章目录

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

文章目录

  • C语言指针系列文章目录
  • 1. 回调函数是什么
  • 2. qsort 函数
    • 2.1 概念
    • 2.2 qsort 排序 int 类型数据
    • 2.3 使用 qsort 排序结构体数据
  • 3. 模拟实现 qsort 函数
  • 4. sizeof 与 strlen 的对比
    • 4.1 sizeof
    • 4.2 strlen
    • 4.3 sizeof 与 strlen 的对比
  • 5. 数组和指针笔试题解析
    • 5.1 一维数组
    • 5.2 字符数组


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

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;
}

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1939316.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

ctf中php反序列化汇总

序列化与反序列化的概念 序列化就是将对象转换成字符串。字符串包括 属性名 属性值 属性类型和该对象对应的类名。 反序列化则相反将字符串重新恢复成对象。 对象的序列化利于对象的保存和传输,也可以让多个文件共享对象。 序列化举例&#xff1a;一般ctf题目中我们就是要将对…

02设置burpsuite代理

在日常工作之中&#xff0c;我们最常用的web客服端就是web浏览器&#xff0c;我们可以通过代理的设置&#xff0c;做到web浏览器的流量拦截&#xff0c;并且经过burpsuite代理的数据流量进行处理。 在火狐浏览器中安装foxyporxy

哥德尔不完备定理(Godel‘s Incompleteness Theorem) —— 奠定了计算机与 AI 的理论基础

哥德尔不完备定理 在数理逻辑中&#xff0c;哥德尔不完备定理是指库尔特・哥德尔于 1931 年证明并发表的两条定理。简单地说&#xff0c;第一条定理指出&#xff1a;任何相容的形式系统&#xff0c;只要蕴涵皮亚诺算术公理&#xff0c;就可以在其中构造在体系中既不能证明也不…

Java GC(垃圾回收)机制详解

Java GC&#xff08;垃圾回收&#xff09;机制详解 1、GC触发的条件2、GCRoots的对象类型 &#x1f496;The Begin&#x1f496;点点关注&#xff0c;收藏不迷路&#x1f496; 在Java的世界里&#xff0c;内存管理是自动进行的&#xff0c;其中垃圾回收&#xff08;Garbage Col…

WDL(Wide Deep Learning for Recommender Systems)——Google经典CTR预估模型

一、文章简介 Wide & Deep Learning for Recommender Systems这篇文章介绍了一种结合宽线性模型和深度神经网络的方法&#xff0c;以实现推荐系统中的记忆和泛化。这种方法在Google Play商店的应用推荐系统中进行了评估&#xff0c;展示了其显著的性能提升。 推荐系统中的…

解决使用腾讯地图没超过额度却一直报“此key每日调用量已达到上限”

1、个人开发者配额说明 2、需要在 配额管理 的 账户额度 中进行配额的分配即可。 3、开发工具接口的调用就不会出现该报错了

【专项刷题】— 快排

1、颜色分类 - 力扣&#xff08;LeetCode&#xff09; 思路&#xff1a; 创建三个指针&#xff0c;然后把数组分为三个区域遍历代码&#xff1a; class Solution {public void swap(int[] nums, int i, int j){int t nums[i];nums[i] nums[j];nums[j] t;}public void sortCo…

如何快速开发一个简单的企业信息系统?O2OA手把手带你,高效开发!(附源码)

前言 想象一下&#xff0c;如果你的企业能够通过一个系统快速发布企业信息&#xff0c;员工们无论身在何处都能即时获取新信息&#xff0c;那该多好&#xff01;告别email轰炸和口头传达的低效&#xff0c;O2OA企业应用开发平台让这一切变得简单。 今天&#xff0c;就让我们一…

【2024最新华为OD-C/D卷试题汇总】[支持在线评测] 二进制游戏(200分)- 三语言AC题解(Python/Java/Cpp)

🍭 大家好这里是清隆学长 ,一枚热爱算法的程序员 ✨ 本系列打算持续跟新华为OD-C/D卷的三语言AC题解 💻 ACM银牌🥈| 多次AK大厂笔试 | 编程一对一辅导 👏 感谢大家的订阅➕ 和 喜欢💗 🍿 最新华为OD机试D卷目录,全、新、准,题目覆盖率达 95% 以上,支持题目在线…

CCF-Csp算法能力认证, 202312-2因子化简含解析

CCF-Csp算法能力认证&#xff0c; 202312-1仓库规划含解析 前言 推荐书目&#xff0c;在这里推荐那一本《算法笔记》&#xff08;胡明&#xff09;&#xff0c;需要PDF的话&#xff0c;链接如下 「链接&#xff1a;https://pan.xunlei.com/s/VNvz4BUFYqnx8kJ4BI4v1ywPA1?…

Java(九)——抽象类、抽象方法

个人简介 &#x1f440;个人主页&#xff1a; 前端杂货铺 ⚡开源项目&#xff1a; rich-vue3 &#xff08;基于 Vue3 TS Pinia Element Plus Spring全家桶 MySQL&#xff09; &#x1f64b;‍♂️学习方向&#xff1a; 主攻前端方向&#xff0c;正逐渐往全干发展 &#x1…

Synchronized升级到重量级锁会发生什么?

我们从网上看到很多&#xff0c;升级到重量锁的时候不会降级&#xff0c;再来线程都是重量级锁 今天我们来实验一把真的是这样的吗 1.首选导入Java对象内存布局的工具库&#xff1a; <dependency><groupId>org.openjdk.jol</groupId><artifactId>jol-…

django-vue-admin项目运行

文本主要对django-vue-admin项目进行了简要介绍&#xff0c;并且对前后端进行了源码安装和运行。在此基础上可作为管理系统二次开发的基础框架。 一.django-vue-admin简介和安装 1.简介 django-vue-admin项目是基于RBAC模型权限控制的中小型应用的基础开发平台&#xff0c;采…

hung 之 Android llkd

目录 1. llkd 简介 2. 原理 2.1 内核活锁 2.2 检测机制 2.3 为什么 persistent stack signature 检测机制不执行 ABA 检查&#xff1f; 2.4 为什么 kill 进程后&#xff0c;进程还存在就能判定发生了内核 live-lock&#xff1f; 3. 代码 3.1 内核 live-lock 检查 3.2 …

轨道交通全国产工控机:基于飞腾E2000Q的实现AFC系统控制器

AFC系统 提供基于Intel平台、NXP平台、Rockchip平台的核心板、 PICO-ITX板、3.5寸板、Mini-ITX主板以及嵌入式准系统等计算机硬件。产品具有出色的数据传输与处理能力&#xff0c;板载内存&#xff0c;CPU集成图形控制&#xff0c;提供丰富串口、USB、LAN、PCIe扩展接口等I/O接…

学会这个技巧,你的电子画册将秒变专业

在这个数字化的时代&#xff0c;电子画册已经成为展示个人和商业作品的重要方式。然而&#xff0c;许多人虽然拥有出色的内容&#xff0c;却因为缺乏一定的技巧&#xff0c;使得电子画册显得平凡无奇。学会这个技巧&#xff0c;你的电子画册将秒变专业&#xff0c;让你的作品在…

Docker容器下安装Matlab,无需挂载

Matlab的安装需要这些文件 传入ubuntu后&#xff0c;改过相关的文件权限后&#xff0c;发现还是无法挂载 这有可能是docker的安全管理策略导致容器不能挂载&#xff0c;因此采用不挂载形式&#xff0c;直接解压的方式安装Matlab 1.将iso改成zip&#xff0c;并解压 2.解压rar文件…

【深入C++】二叉搜索树

文章目录 什么是二叉搜索树二叉搜索树的接口1.查找操作2.插入操作3.中序遍历4.删除操作 所有代码总结 什么是二叉搜索树 二叉搜索树&#xff08;Binary Search Tree, BST&#xff09;是一种特殊的二叉树&#xff0c;其每个节点最多有两个子节点&#xff0c;分别称为左子节点和…

MySQL 数据库 day 7.16

ok了家人们今天继续记录一下数据库,看看今天学了什么。 一.事物概述 1.1 环境准备 -- 账户表 create table account( id int primary key auto_increment, name varchar(20), money double );insert into account values (null,张三,1000); insert into account values (n…

Matlab Git管理

目录 1、Git配置 1.1 下载 1.2 注册账户 1.3 录入信息 2、matlab配置 2.1 测试git 2.2 创建git 1、Git配置 1.1 下载 使用镜像网站&#xff0c;选择合适的版本download git&#xff0c;一直点next&#xff0c;最后install。 CNPM Binaries Mirror (npmmirror.co…