C语言 — 指针的进阶

文章目录

  • 前言
  • 一、字符指针
  • 二、指针数组
  • 三、数组指针
    • 数组名 与 &数组名
  • 四、指针传参
    • 二维数组传参
  • 五、函数指针
  • 结语

前言

通过前面的关于指针的学习,我们了解了指针的一些个特性。本篇文章我们将深入指针,挖掘指针更深处的知识。
在开始之前,我们先来复习一下指针:

  1. 指针就是变量,是用来存放地址的。地址是用来标识数据在内存中存储的唯一内存空间
  2. 指针变量的大小为4/8个字节。取决是32位的还是64位的平台
  3. 指针是有类型的,不同的指针类型决定了指针+ - 整数的步长,指针解引用的权限

一、字符指针

顾名思义,就是存放字符的指针。例子如下:

char c = ‘a’;
char* pc = &c;

第一行代码就是把 a 赋值给变量c
第二行代码就是取地址c,得到c的地址,而c的地址指向字符a,所以打印的话就会打印 a。

二、指针数组

数组指针是数组,是用来存放指针的数组
我们来举点例子,帮助下理解:

一、 int arr[4];
二、 char ch[3];
三、 int* arr2[4];
四、 char* ch2[3];

“一、”,这里的代码代表了arr这个数组的每个元素的类型都是int
“二、”,同理,ch这个数组里存放的类型都是char类型
“三、”,这里面数组arr2里面存放的内容均是地址,其指针变量均是int*
“四、”,同“三、”。ch2存放的均是char* 类型的指针

那么,我们来看个指针数组模拟二维数组的例子:

int arr1[3] = {1, 2, 3};
int arr2[3] = {2, 3, 4};
int arr3[3] = {3, 4, 5};int* parr[3] = {arr1, arr2, arr3};

这里 ,我们定义了三个数组,并且分别存放了不同的数据。
然后,我们使用指针数组存放这三个数组。

注意:这里,指针数组parr[3]里,存放的每个数组都是其首元素的地址。
那么,我们只需要遍历一遍这个指针数组,就能得到一个二维数组

int i = 0, j = 0;
for(i = 0; i < 3; i++){for(j = 0; j < 3; j++){printf(%d “, *(parr[i] + j));}printf(“\n”);
}

这里解释一下 *(parr[i] + j) 这个操作
首先parr[i]取到的是首元素地址,这里假设i = 0,那么取到的就是arr1这个数据的首元素地址,我们把首元素地址 + j,得到的地址就能得到arr1中的任何一个元素的值,arr2 与 arr3 同理

三、数组指针

数组指针,其实本质上是一个指针,就好比好孩子,本质上是一个孩子一样。
这里我们来看两个不同的例子:

  1. int *p1[3];
  2. int(*p2)[3];

第一个表示的是p1是一个int类型的指针(int *),指向这个数组的首元素的地址。所以这是一个指针数组
第二个就有点区别了。首先,这里的p2被括号括起来了,没法与前面的int *
相结合,所以这里的 p2 是一个数组指针,表示的是 p2 可以指向一个数组,而该数组有 3 个元素,每个元素都是int类型的。

int *p1的意思是:能够指向整形数据的指针
所以数组指针是:能够指向一个数组数组的指针
是不是要被绕进去了?那么我就再来说个结论:
指针数组指向的一个数组的首元素地址,而数组指针指向的就是一整个数组。意味着如果 * p1 + 1指向的是下一个数据的话,那么(*p1)+1指向的就是这个数组的末尾的下一位,也就是直接移动了3格

要理解数组指针,我们不得不来看看 arr 与 &arr 的区别。

数组名 与 &数组名

int arr[10] = 0;
int *p = arr;printf(%p\n”, arr);
printf(%p\n”, arr+1);printf(%p\n”, &arr[0]);
printf(%p\n”, &arr[0] +1);printf(%p\n”, &arr);
printf(%p\n”, &arr + 1);

以上代码都会输出什么呢?
为了方便理解,我将它们分为三组,以换行进行区分。
首先第一组的第一个arr。前面我们不是说过指针数组指向的是数组的首元素地址吗,所以这里的arr输出的是arr这个数组的首元素的地址。
而arr + 1 就是输出arr这个数组的首元素地址往后+1,移动一位,也就是输出第二个元素的地址。

第二组的&arr[0] 跟第一组的 arr 虽然写法不同,但这俩是一个意思。第一组敢直接写arr实际上也是因为使用了第二组的这个写法的简化版。所以第二组第一行输出的也是首元素地址,&arr[0] + 1同理,也是将首元素+1后得到的第二个元素的地址并输出。

第三组的 &arr 也是一样的道理,也是输出首元素的地址。
但是,第二行就有点不一样了。&arr取的确实是arr这个元素的地址,但是这是一个数组指针,它 + 1 并不是首元素地址往后 + 1位,而是直接将arr看做一个整体,+1就是跳过这个整体,而不是上面两组的跳过这个整体中的其中一个元素。所以输出会是这样的
在这里插入图片描述
之前的都是001CFB88 - 001CFB84 = 4,也就是跳过了一个位
而(&arr + 1 )- &arr 也就是001CFBAC - 001CFB84 =40,也就是跳过了一整个数组。
下面我画了一张图方便大家理解

在这里插入图片描述
所以:

整形指针是用来存放整形的地址
字符指针是用来存放字符的地址
数组指针是用来存放数组的地址

四、指针传参

讲完了指针数组与数组指针,我们下载来学习下指针的传参
先来看看数组的传参:

void test(int arr[]) {}			//第一组
void test(int arr[10]) {}		//第二组
void test(int *arr[]) {}		//第三组int main() {int arr[10] = { 0 };test(arr);return 0;
}

第一组传参:实参arr在被传递时实际上传递的是一个指向arr这个数组的首元素地址的指针。在传递过去后形参arr[]接受了这个地址,所以是可以的。
第二组传参:虽然形参变成了arr[10],但是实际上这个 10 是可以忽略的。数组大小在这里不会影响函数参数,所以可行。
第三组传参:这个其实就是第一组传参更为标准的写法,前面说到过传递的值是一个指针,所以我们这里可以使用 * 号解引用这个指针,得到地址。

看完了数组,再来看指针数组:

void test(int *arr[10]) {}		//第一组
void test(int* *arr) {}		//第二组int main() {int *arr[10] = { 0 };test(arr);return 0;
}

这是一个指针数组,我们定义了一个数组arr[10],并且把他变成了指针数组。
第一组:指针数组里存储的是指针,所以在传递后也是需要通过解引用操作来得到首元素地址对应的值。而10不会影响这个函数的参数
第二组:int *arr[10] = { 0 };这段代码里的 *arr[10] 里面存放的其实是 10 个 int *,arr是数组名, int *在被传递过后首元素地址就是是int * 的地址。而又因为二级指针是用来存放一级指针变量的地址,所以也是没有问题。

二维数组传参

说完了一维数组,我们来说下二维数组

void test(int arr[3][4]) {};	//第一组
void test(int arr[][]) {};		//第二组
void test(int arr[][4]) {};		//第三组
int main() {int arr[3][4] = {0};test(arr);return 0;
}

记住二维数组传参的一句话就行:可以省略行,但是不能省略列!
也可以理解为多维数组可以省略低纬,但是不能省略高纬。二维数组可以省略第一维(行),但是不能省略第二维(列)
所以第一第三组可以,第二组不行。

指针传参宗旨:形参与实参类型需一致

五、函数指针

数组指针是指向数组的指针
那么显而易见,函数指针就是指向函数的指针

int main() {int arr[5] = { 0 };int (*p)[5] = &arr;return 0;
}

前面我们说了数组指针(如上所示)那么函数指针其实也是一样的

int Add(int x, int y) {return x + y;
}int main() {int arr[5] = { 0 };int (*p)[5] = &arr;printf("%p\n", &Add);return 0;
}

我们这里打印出这个函数的地址,发现是可以打印出来的
在这里插入图片描述

而且对于函数来说,函数名与取地址函数名取出来的地址没有区别
再来看看如何搞个指针给函数
在这里插入图片描述

我们只需要参照数组指针的形式就能写出函数指针:
这里我的写法为:int (*pf)(int, int) = &Add;
解释一下,(int, int)对应的是函数的两个参数类型,然后在函数前面的int对应函数的返回类型,然后让 *号与要定义的函数变量相结合即可。
那么,可能有的小伙伴就会好奇了。诶,函数的指针有什么意义呢?
我们知道,定义一个指针变量在后期就可以通过指针变量找到地址然后通过地址来修改内容,就比如

int a = 10;
int* pa = &a;
*pa = 20;

通过*pa我们可以找到a然后再修改a的值。
那么这里也是一样的
在这里插入图片描述

我们可以通过跟前面学习的方式一样,直接修改函数的参数。
并且,这里的:(*pa)(3, 5) 其实等价于 pa(3, 5)
这个 * 号其实是为了让我们更好的理解罢了,不写也是可以的。甚至你可以写好几个星星,都没有问题。

学完了函数指针,我们就来看一段特别特别“有意思”的代码:
在这里插入图片描述

可能看到这串代码会让你不寒而栗。不要怕,我们来简化一下(虽然我已经简化过一次了)
在这里插入图片描述

现在再看这串代码你会发现好像应该能看懂了。
我来解释一下这串“有趣”的代码:
首先我们把关注点看向 “0” ,0是一个 int 类型的数据。他前面用了红色的括号给扩了起来,说明 “0” 将要被强制类型转换。
然后我们来看红色括号里的内容。红色括号里又有两个绿色的小括号,绿色小括号前还有一个 void。那么显而易见,这跟我们刚学的函数指针很像…好吧这就是一个函数指针,返回值类型为void的函数指针。函数变量有两个。所以这串代码就是把 int 类型的 “0” 强制类型转换成函数指针。
最后我们来看粉色括号,粉色括号有两个,这时候我们就要警觉了,有没有可能是一个函数指针。显而易见这可不就还是函数指针嘛,第一个粉色括号是某个函数的第一个形参,并且跟第一个绿色括号一样,都是解引用后的数据,而第二个粉色括号就是这个函数的第二个形参。并且前面我们说过函数指针不需要特别声明(不像数组指针一样要用括号和*号来标识),所以至此,解读完毕。


我这里再简单解释一下函数指针的妙用。(这部分比较抽象,慎看)
假设我们要定义一个计算器,然后我们使用while来进行选择(看你要选加法还是减法),
case1意味着加法,然后以此类推。
然后我们再写四个函数(加减乘除),然后在case1里调用加法,case2里调减法(以此类推)
我们会发现,每个case里的内容清一色都是调用某个函数再printf输出
代码会有冗余。
这时候我们就可以考虑封装一下代码,用一串一样的代码完美判断我们是否进行加减乘除,并且代码简单不冗余。
方法是这样的:

void calc(int (*pf)(int, int))
{int x = 0, y = 0;int ret = 0;printf(“请输入两个数:>);scanf(%d %d”, &x, &y);ret(x, y);printf(&d”, ret);
}

这里我们的杀招就是ret(x, y)这一串代码
因为上面的pf会完美的接受传递过来的函数地址,通过函数地址可以找到函数的名称还有实现方法,我们再通过ret就可以直接输出了。
所以下面的每个case都直接调用calc这个函数就能直接实现计算器功能,并且没有冗余。
(调用函数例子:calc(Add))这里就会直接把Add的地址传递给函数calc
通过*pf的解析就能得到所传递的函数的内容,再通过两个int 就能实现函数调用函数,函数化作参数互相传递。


结语

以上就是本次指针进阶的内容了。文字虽多希望可以帮到你理解。我们下篇文章再见~

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

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

相关文章

【机器学习导引】ch4-决策树

基本流程 两个需要解决的问题 属性顺序&#xff1a; 问题&#xff1a;哪些属性在前面&#xff0c;哪些属性在后面&#xff1f;这个问题指的是在处理数据或进行排序时&#xff0c;需要确定属性的排列顺序&#xff0c;以便更好地进行数据处理或分析。 属性选择&#xff1a; 问题…

[ DOS 命令基础 4 ] DOS 命令命令详解-端口进程相关命令

&#x1f36c; 博主介绍 &#x1f468;‍&#x1f393; 博主介绍&#xff1a;大家好&#xff0c;我是 _PowerShell &#xff0c;很高兴认识大家~ ✨主攻领域&#xff1a;【渗透领域】【数据通信】 【通讯安全】 【web安全】【面试分析】 &#x1f389;点赞➕评论➕收藏 养成习…

博客系统(SpringBoot项目)

文章目录 一、项目开发的流程二、项目开发2.1 准备工作2.2 开发公共模块&#xff1a;把能写的先写了什么是公共模块model层mapper层定义统一返回结果统一异常处理 2.2 博客列表页2.3 更改显示的时间2.4 博客详情页2.5 登录Session式登录方法分析使用Token来实现登录 2.6 强制登…

软件设计师笔记-数据结构

数据结构 数据元素的集合及元素间的相互关系和构造方法。 线性表的存储结构 顺序存储链式存储 单链表节点 typedef struct node { int data; struct node *link; }NODE, *LinkList; 双向链表 每个节点有两个指针&#xff0c;分别指出直接前驱和直接后继。 循环链表 尾…

LangChain Ollama实战文献检索助手(一)环境配置和输入输出解析

挑选合适的模型 调用API需要花钱&#xff0c;因此在搭建阶段最佳的方法是利用Ollama部署本地CPU推理的轻量化大模型。大模型选择可以参照hugging face的榜单open-llm-leaderboard。 这里对我来说&#xff0c;要选择的模型需要满足 1.ollama上有的模型。 2.推理速度快&#xff…

在docker中搭建redis哨兵环境

文章目录 一、引言二、环境准备前提条件目录结构 三、配置文件1. 主节点配置文件 sentinel-master.conf2. 从节点配置文件3. 哨兵配置文件 sentinel.conf4. Docker Compose 文件 四、启动 Docker Compose五、验证哨兵机制1. 检查主节点状态2. 检查从节点状态3. 检查哨兵状态4. …

上线不出网机器

不出网机器介绍 上线不出网机器是我们常见的问题&#xff0c;如何在内网中实现不出网机器的上线呢&#xff0c;我们分为了如下的形式&#xff0c;根据之前所学的内容我们开始进行实验&#xff0c;常见的网络拓扑如下 情况分类 上线不出网机器一般是指B区域的电脑上线到CS工具或…

Modbus解析流程全面升级:体验全新核心与终极优化!

01 前言 本文章原文发表于我的微信公众号&#xff0c;请大家关注阅读&#xff0c;涉及的源代码等都在公众号&#xff0c;请搜索公众号&#xff1a; 智能家居NodeRed和HomeAssistant 即可关注。 02 全面改进的解析流程 前面发布过的Modbus解析流程在经过多个设备测试后发现存…

Python邮差:如何用代码精确投递商品快递费用的密信

目录 一、准备工作 二、编写API请求脚本 三、解析与处理快递费用数据 四、案例应用&#xff1a;模拟电商平台的快递费用计算 五、自动化邮件通知 六、总结 在电子商务的广阔天地里&#xff0c;精确计算并快速传递商品快递费用是一项至关重要的任务。作为Python邮差&#…

修改sql server 数据库的排序规则Chinese_PRC_CI_AS(字符集+排序)

文章目录 引言I 解决方案案例II 知识扩展排序规则SQL SERVER支持的所有排序规则引言 新增sql server 数据库实例的默认排序规则不支持中文存储,导致乱码 解决方案: 修改排序规则为Chinese_PRC_CI_AS 或者 Chinese_PRC_Stroke_CI_AS_WS或者Chinese_PRC_CI_AI_KS_WS 仅对新增…

七十页PPT展示智驾时代来临,国产汽车零部件厂商准备几何?

u 智能汽车车身架构主要可分为感知、决策控制、执行及通信四大板块&#xff0c;目前国产汽车零部件供应商在感知系统已取得较强的话语权&#xff0c;在决策控制系统、执行系统领域亦取得一定竞争力。 u 感知系统主要硬件包括激光雷达、毫米波雷达、摄像头等&#xff1b;其中&a…

Springboot 整合 Java DL4J 打造自然语言处理之智能写作助手

&#x1f9d1; 博主简介&#xff1a;CSDN博客专家&#xff0c;历代文学网&#xff08;PC端可以访问&#xff1a;https://literature.sinhy.com/#/literature?__c1000&#xff0c;移动端可微信小程序搜索“历代文学”&#xff09;总架构师&#xff0c;15年工作经验&#xff0c;…

moffee

https://github.com/BMPixel/moffee Moffee&#xff1a;一键将Markdown转换为专业PPT&#xff0c;支持多主题与实时预览 文章目录 1-安装1.1-环境1.2-编码 2-使用2.1-语法 moffee 演示让 Markdown 准备好演示为什么选择 moffee&#xff1f;展示用 Markdown 设置样式媒体布局 1-…

玩转「HF/魔搭/魔乐」平台

模型下载 Hugging Face 下载到 GitHub CodeSpace CodeSpace创建环境&#xff1a; # 安装transformers pip install transformers4.38 pip install sentencepiece0.1.99 pip install einops0.8.0 pip install protobuf5.27.2 pip install accelerate0.33.0下载internlm2_5-7b…

运维高可用架构设计

一、硬件 1、服务器 2、网络架构 二、软件 1、基础组件 组件名称 高可用方式 最少节点数 负载均衡(Tenginx) corsyncpacemaker互为主备 多组集群通过DNS轮循实现一个大集群 2DNS主从集群2RabbitMQ原生HA镜像集群3Zookeeper原生分布式集群3Kafka原生分布式集群3ES原生分布式集…

DICOM标准:MR图像模块属性详解——磁共振成像(MR)在DICOM中的应用

目录 引言 磁共振成像&#xff08;MR&#xff09; 一、MR图像模块 二、MR图像属性描述 1、图像类型 (Image Type) 2、抽样每个象素 (Sampling per Pixel) 3、光度插值 (Photometric Interpretation) 4、位分配 (Bits Allocated) 结论 引言 数字成像和通信在医学&#xff08…

SpringBoot在线教育系统:多语言支持

5系统详细实现 5.1 普通管理员管理 管理员可以对普通管理员账号信息进行添加修改删除操作。具体界面的展示如图5.1所示。 图5.1 普通管理员管理界面 5.2 课程管理员管理 管理员可以对课程管理员进行添加修改删除操作。具体界面如图5.2所示。 图5.2 课程管理员管理界面 5.3 …

Cursor和GitHub Copilot之间的竞争

大家好&#xff0c;今天我们要聊聊一个在开发者圈子里引起热议的话题&#xff1a;GitHub Copilot和Cursor之间的竞争&#xff0c;以及Copilot最近宣布的新功能&#xff0c;这可能会改变我们对编程辅助工具的看法。 GitHub Copilot将支持来自Anthropic、Google和OpenAI的模型&am…

Python酷库之旅-第三方库Pandas(181)

目录 一、用法精讲 836、pandas.api.types.is_file_like函数 836-1、语法 836-2、参数 836-3、功能 836-4、返回值 836-5、说明 836-6、用法 836-6-1、数据准备 836-6-2、代码示例 836-6-3、结果输出 837、pandas.api.types.is_list_like函数 837-1、语法 837-2、…

软件测试必会:cookie、session和token的区别~

今天就来说说session、cookie、token这三者之间的关系&#xff01;最近这仨玩意搞得头有点大&#x1f923; 01、为什么会有它们三个 我们都知道 HTTP 协议是无状态的&#xff0c;所谓的无状态就是客户端每次想要与服务端通信&#xff0c;都必须重新与服务端链接&#xff0c;意…