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.mzph.cn/news/884769.shtml

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

相关文章

Ceisum无人机巡检视频投放

公司投标内容有个视频投放的功能动画&#xff0c;原本想实现这么一个效果&#xff1a; 案例效果来自别人的展示作品&#xff0c;Leader一眼就相中了这个效果&#xff0c;可惜别人的终究是别人的&#xff0c;又不会白白给你&#xff0c;终究是要自己动手尝试。 动画方面的展示…

Redis为什么用跳表实现有序集合

Redis为什么用跳表实现有序集合 手写一个跳表 为了更好的回答上述问题以及更好的理解和掌握跳表&#xff0c;这里可以通过手写一个简单的跳表的形式来帮助读者理解跳表这个数据结构。 我们都知道有序链表在添加、查询、删除的平均时间复杂都都是 O(n) 即线性增长&#xff0c…

ubuntu双屏只显示一个屏幕另一个黑屏

简洁的结论&#xff1a; 系统环境 ubuntu22.04 nvidia-535解决方案 删除/etc/X11/xorg.conf 文件 记录一下折腾大半天的问题。 ubuntu系统是22.04,之前使用的时候更新驱动导致桌面崩溃&#xff0c;重新安装桌面安装不上&#xff0c;请IT帮忙&#xff0c;IT一番操作过后也表示…

Docker可视化管理面板DPanel的安装

本文软件由网友 rui 推荐&#xff1b; 什么是 DPanel &#xff1f; DPanel 是一款 Docker 可视化管理面板&#xff0c;旨在简化 Docker 容器、镜像和文件的管理。它提供了一系列功能&#xff0c;使用户能够更轻松地管理和部署 Docker 环境。 软件特点&#xff1a; 可视化管理&…

学习threejs,使用对象组合

&#x1f468;‍⚕️ 主页&#xff1a; gis分享者 &#x1f468;‍⚕️ 感谢各位大佬 点赞&#x1f44d; 收藏⭐ 留言&#x1f4dd; 加关注✅! &#x1f468;‍⚕️ 收录于专栏&#xff1a;threejs gis工程师 文章目录 一、&#x1f340;前言1.1 ☘️THREE.Object3D 三维物体 二…

企业必备:一合通电子合同

一合通&#xff0c;让合同签署更简单。加密技术保障安全&#xff0c;随时随地可签。助力企业加速业务流程&#xff0c;提高办公效率。 在数字化转型的浪潮中&#xff0c;企业面临着前所未有的机遇与挑战。特别是在人力资源管理和合同签署方面&#xff0c;传统的纸质合同已经难以…

二十二、MySQL 8.0 主从复制原理分析与实战

文章目录 一、复制&#xff08;Replication&#xff09;1、什么是复制2、复制的方式3、复制的数据同步类型3.1、异步复制3.2、半同步复制3.3、设计理念&#xff1a;复制状态机——几乎所有的分布式存储都是这么复制数据的 4、基于binlog位点同步的主从复制原理4.1、异步复制示例…

AAA 数据库事务隔离级别及死锁

目录 一、事务的四大特性&#xff08;ACID&#xff09; 1. 原子性(atomicity)&#xff1a; 2. 一致性(consistency)&#xff1a; 3. 隔离性(isolation)&#xff1a; 4. 持久性(durability)&#xff1a; 二、死锁的产生及解决方法 三、事务的四种隔离级别 0 .封锁协议 …

w~自动驾驶~合集4

我自己的原文哦~ https://blog.51cto.com/whaosoft/12451789 #基于深度学习的端到端自动驾驶 最新的端到端自动驾驶综述刚刚出炉&#xff0c;话说论文一作卡内基梅隆大学的Apoorv Singh今年产出了七篇综述&#xff0c;都和自动驾驶相关&#xff0c;推荐给大家。就一个字&…

【6G 需求与定义】ITU(国际电联)对全球6G标准的愿景

博主未授权任何人或组织机构转载博主任何原创文章&#xff0c;感谢各位对原创的支持&#xff01; 博主链接 本人就职于国际知名终端厂商&#xff0c;负责modem芯片研发。 在5G早期负责终端数据业务层、核心网相关的开发工作&#xff0c;目前牵头6G技术研究。 博客内容主要围绕…

JVM 类加载器

字节码的结构 魔数u4 cafe babe 版本u4 52 java8 常量池计数器u2 从1开始&#xff0c;0索引留给不需要的情况 常量池 表 #1 -> #计数器-1 类标识符 u2 public final abstrat class annotion interface 之类 类索引u2 名字 父类索引u2 父类名字 接口计数器 u2 接口数…

Sentieon软件快速入门指南

Sentieon软件为完整的纯软件基因变异检测二级分析方案&#xff0c;其分析流程完全忠于BWA、GATK、MuTect2、STAR、Minimap2、Fgbio、picard等金标准的数学模型。在匹配开源流程分析结果的前提下&#xff0c;大幅提升WGS、WES、Panel、UMI、ctDNA、RNA等测序数据的分析效率和检出…

数字信号处理:自动增益控制(AGC)

自动增益控制&#xff1a; &#xff1a;自动增益控制&#xff08;Automatic Gain Control, AGC&#xff09;是一种信号处理技术&#xff0c;用于在接收端调整输入信号的增益&#xff08;或放大系数&#xff09;&#xff0c;以保持信号在一个合适的强度范围内&#xff0c;从而防…

RAG中的代表性上下文压缩方案总结:从RECOMP、CompAct到COCOM

今天是2024年11月5日&#xff0c;星期二&#xff0c;北京&#xff0c;天气晴 昨天有说到RAG中的长文本压缩&#xff0c;现有的上下文压缩方法主要分为基于词汇的压缩&#xff08;硬提示&#xff0c;如LLMLingua和RECOMP&#xff09;和基于嵌入的压缩(软提示&#xff0c;如Gist…

创新材料科技:铜冷却壁助力高炉节能降耗

高炉用铜冷却壁是高炉内部的一种构件&#xff0c;通常用于高炉的炉身部分。它的主要功能是在高炉冶炼过程中冷却炉壁&#xff0c;以防止炉壁过热。铜冷却壁通常由铜制成&#xff0c;因为铜具有良好的导热性和耐腐蚀性&#xff0c;能够有效地将热量从高炉内部传导到外部&#xf…

免费送源码:Java+ssm+MySQL ssm小区车辆信息管理系统的设计与实现 计算机毕业设计原创定制

摘 要 科技进步的飞速发展引起人们日常生活的巨大变化&#xff0c;电子信息技术的飞速发展使得电子信息技术的各个领域的应用水平得到普及和应用。信息时代的到来已成为不可阻挡的时尚潮流&#xff0c;人类发展的历史正进入一个新时代。在现实运用中&#xff0c;应用软件的工作…

云轴科技ZStack在CID大会上分享VF网卡热迁移技术

近日&#xff0c;2024中国云计算基础架构开发者大会&#xff08;以下简称CID大会&#xff09;在北京举行。此次大会集中展示了云计算基础架构技术领域最前沿的科创成果&#xff0c;汇聚众多的技术专家和行业先锋&#xff0c;共同探讨云计算基础设施的最新发展和未来趋势。云轴科…

ES6中数组新增了哪些扩展?

ES6中数组新增了哪些扩展&#xff1f; 1、扩展运算符的应⽤ ES6通过扩展元素符 … &#xff0c;好⽐ rest 参数的逆运算&#xff0c;将⼀个数组转为⽤逗号分隔的参数序列 console.log(...[1, 2, 3]) // 1 2 3 3 console.log(1, ...[2, 3, 4], 5) // 1 2 3 4 5 [...documen…

「Mac畅玩鸿蒙与硬件15」鸿蒙UI组件篇5 - Slider 和 Progress 组件

Slider 和 Progress 是鸿蒙系统中的常用 UI 组件。Slider 控制数值输入&#xff0c;如音量调节&#xff1b;Progress 显示任务的完成状态&#xff0c;如下载进度。本文通过代码示例展示如何使用这些组件&#xff0c;并涵盖 进度条类型介绍、节流优化、状态同步 和 定时器动态更…

GitHub个人主页美化

效果展示 展示为静态效果&#xff0c;动态效果请查看我的GitHub页面 创建GitHub仓库 创建与GitHub用户名相同的仓库&#xff0c;当仓库名与用户名相同时&#xff0c;此仓库会被视作特殊仓库&#xff0c;其README.md&#xff08;自述文件&#xff09;会展示在GitHub个人主页…