C语言:数组指针 函数指针
C语言:数组指针 & 函数指针
- 数组指针
- 数组名
- 数组访问
- 二维数组
- 函数指针
- 函数指针使用
- 回调函数
- typedef关键字
数组指针
数组本质上也是一个变量,那么数组也有自己的地址,指向整个数组的指针,就叫做数组指针。
我先为大家展示一个数组指针,再做数组指针的语法解析。
数组int arr[10]
的指针:
int (*p)[10]
(*p)
代表p
是一个指针[10]
代表这个指针指向的数组有10个元素int
代表这个指针指向的数组元素类型为int
不能写成 int *p[10]
:
[]
的优先级高于 *
,所以p
会先和 []
结合,此时p
就是一个数组变量了,而指向的元素类型为 int*
。所以需要一个 ()
来改变操作符的结合顺序,让 p
和 *
先结合,代表p
是一个指针。
数组指针的类型就是去掉指针名剩下的部分,比如:
int (*p1)[10] = &arr1;
//p1的类型为:int (*)[10]char (*p2)[3] = &arr2;
//p2的类型为:char (*)[3]
数组名
学习指针后,其实我们的数组名就已经不单纯是一个数组名了,我们先来观察现象:
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
printf("&arr[0] = %p\n", &arr[0]);
printf("arr = %p\n", arr);
printf("&arr = %p\n", &arr);
输出结果:
&arr[0] = 009BFEB0
arr = 009BFEB0
&arr = 009BFEB0
可以看到,数组名
本质上是地址,&数组名
也是地址,而且arr == &arr == &arr[0]
,也就是说它们都是首元素的地址。
数组名的本质是首元素的地址
那么 数组名
与 &数组名
有什么区别吗?
我们再看一段代码:
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };printf("&arr[0] = %p\n", &arr[0]);
printf("&arr[0]+1 = %p\n", &arr[0]+1);printf("arr = %p\n", arr);
printf("arr+1 = %p\n", arr+1);printf("&arr = %p\n", &arr);
printf("&arr+1 = %p\n", &arr+1);
输出结果:
&arr[0] = 0077F820
&arr[0]+1 = 0077F824
arr = 0077F820
arr+1 = 0077F824
&arr = 0077F820
&arr+1 = 0077F848
这⾥我们发现&arr[0]
和&arr[0]+1
相差4个字节,arr
和arr+1
相差4个字节,是因为&arr[0]
和arr
都是⾸元素的地址,+1
就是跳过⼀个元素。
但是&arr
和&arr+1
相差40个字节,这是因为&arr
是数组的地址,+1
操作是跳过整个数组的。
也就是说:
arr 本质是数组首元素的指针
&arr 本质是整个数组指针
但是有关数组名,也有特例:
sizeof(arr),sizeof内单独放数组名,此时数组名表示整个数组,得到整个数组的大小
数组访问
现有如下数组:
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
现在我们以一般的方式来遍历这个数组:
for (int i = 0; i < 10; i++)
{printf("%d ", arr[i]);
}
由于数组的内存是连续存储的,我们也可以通过指针的方式来遍历这个数组:
int* p = arr;for (int i = 0; i < 10; i++)
{printf("%d ", *(p + i));
}
我们刚刚辨析过,arr
本质就是首个元素的地址,所以int* p = arr;
就是把第一个元素的地址交给指针p
。在循环内部,我们通过指针偏移量i
与首元素的指针p
来定位元素,再解引用访问*(p + i)
。这样就可以完成数组的遍历。
有没有发现,arr[i]
与*(p + i)
非常像,它们之间有没有什么联系?
arr的本质是首元素的指针,而p也是arr,那么我们可不可以用p代替arr进行下标访问?
比如这样:
int* p = arr;for (int i = 0; i < 10; i++)
{printf("%d ", p[i]);
}
答案是可以的,这就要讲一讲下标访问的本质了。
arr数组本质上是首元素的地址,通过第一个地址与偏移量,我们就可以访问到所有数组元素。而数组下标的本质就是指针偏移量。
而数组的下标访问,本质上也是指针的访问:
arr[i] == *(arr + i)
这条规则并不局限于数组名,任何指针都可以使用p[i]
来替代*(p + i)
的效果。
此外,由于加法支持交换律,所以 *(arr + i)
与*(i + arr)
是等效的,故有以下代码:
arr[i] == *(arr + i) == *(i + arr) == i[arr]
因为i[arr]
最后会被解析为*(i + arr)
,所以这个写法也是可以的。
二维数组
了解数组指针后,我们就可以深入理解二维数组的底层实现了。
其实二维数组的本质是存储一维数组的数组,二维数组的每一个元素都是一维数组。
接下来我们模拟实现一个二维数组:
int arr1[5] = { 1,2,3,4,5 };
int arr2[5] = { 2,3,4,5,6 };
int arr3[5] = { 3,4,5,6,7 };int* parr[3] = { arr1,arr2,arr3 };
我们一开始创建了三个一维数组arr1
,arr2
,arr3
,然后创建了一个parr
把前三个一维数组放进去了,此时我们就模拟实现了一个二维数组。即将三个一维数组放进了另外一个数组中,接下来我们用访问二维数组的方式parr[i][j]
来进行访问:
for (int i = 0; i < 3; i++)
{for (int j = 0; j < 5; j++){printf("%d ", parr[i][j]);}printf("\n");
}
输出结果:
1 2 3 4 5
2 3 4 5 6
3 4 5 6 7
可以看到,我们确实可以用二维数组的方式去访问这个数组。
那么为什么可以这样操作呢?
重点在以下过程:
int* parr[3] = { arr1,arr2,arr3 };
我们真的把三个数组放在了这个parr
数组里面吗?
我们先前讲过,数组名的本质是首元素的地址,也就是说这里的arr1
,arr2
,arr3
只是三个地址而已,我们只是把三个地址存进了parr
里面。
如图:
接下来我们解析一下parr[i][j]
是如何定位到指定位置的。
对第一个索引值i:
通过前面的学习,我们知道parr
的本质是外层数组的第一个元素,那么parr[i]
就是*(parr + i)
,此时就得到了下标为i的元素。而parr里存储的是数组指针,比如parr[0]
得到第一个数组的指针,也就是arr1
,parr[1]
得到第一个数组的指针,也就是arr2
。
所以我们可以通过第一个索引值i
来定位数组。
对第二个索引值j:
既然parr[i]
得到的是内部一维数组的指针,那parr[0][j]
其实就是arr1[j]
,parr[1][j]
其实就是arr2[j]
。这样事情就简单了,我们通过第一个索引值拿到了小数组的指针,接着再用一个索引值j
来定位这个一维数组中的具体哪一个元素,就可以得到目标元素了。
以上只是一个模拟的二维数组,但是真实的二维数组还要复杂一些,接下来我们看看真实的二维数组是如何运作的:
看到以下代码:
int arr[3][5] = { {1,2,3,4,5}, {2,3,4,5,6}, {3,4,5,6,7} };for (int i = 0; i < 3; i++)
{for (int j = 0; j < 5; j++){printf("%p\n", &arr[i][j]);}printf("\n");
}
输出结果:
007AFAB8
007AFABC
007AFAC0
007AFAC4
007AFAC8007AFACC
007AFAD0
007AFAD4
007AFAD8
007AFADC007AFAE0
007AFAE4
007AFAE8
007AFAEC
007AFAF0
可以看到,这个二维数组的地址是完全连续的,不存在每一行之间存在间隔,两行之间也是紧密挨着的。
这是你想象的二维数组:
但是其在内存中是这样的:
那么其是如何运作的呢?
对于arrr[3][5]
这个数组,其内部存储了三个数组(而不是三个数组的指针!),每个数组中存储了五个元素。
在刚刚的模拟实现中,我们是用外层parr
数组存储了3个指针,这里存的就是真真正正的数组。这就是两者的区别。
那么我们这里的arr
是什么类型?
arr
是外层数组的数组名,数组名代表了第一个元素的指针,这里arr
的第一个元素是一个数组:
这三个数组的类型是:int (*)[5]
,所以二维数组的数组名arr
的指针类型就是int (*)[5]
。
接下来我们再对arr[i][j]
这样的下标访问进行分析:
对第一个索引值i:
arr
作为数组名,本身是一个指针,此指针的类型为int(*)[5]
,类型决定步长,于是步长为int [5]
类型数组的大小:20字节。在面对一个步长为20字节的指针,i
的偏移量也就变成了20字节。
对第二个索引值j:
arr
内存储的是步长为4字节的数组指针,解引用后,*(arr+i)
这个整体就变成了一维数组的指针,其类型为int *
,类型决定步长,于是步长为4字节,在面对一个步长为4字节的指针,j的偏移量就变成了4字节。
所以可以看到,此时的i
和j
是通过指针类型不同,进而影响指针的偏移量大小,对于i
每个单位跳过二十字节,也就是一个数组的大小;对于j
,每个单位跳过四字节,也就是一个元素的大小。先用i
来确定元素在第几个数组,再用j
来确定元素在这个数组的第几位,从而确定元素的位置。
函数指针
么是函数指针变量呢?
根据前⾯学习整型指针,数组指针的时候,我们的类⽐关系,我们不难得出结论:
函数指针变量应该是⽤来存放函数地址的,未来通过地址能够调⽤函数的。
那么函数是否有地址呢?
做一个测试:
void test()
{printf("hehe\n");
}
int main()
{printf("test: %p\n", test);printf("&test: %p\n", &test);return 0;
}
输出结果:
test: 005913CA
&test: 005913CA
确实打印出来了地址,所以函数是有地址的,函数名就是函数的地址,当然也可以通过 &函数名 的⽅式获得函数的地址。
如果我们要将函数的地址存放起来,就得创建函数指针变量咯,函数指针变量的写法其实和数组指针⾮常类似。
我先为大家展示一个函数指针,再做指针的语法解析。
函数void Add(int x, int y)
的指针:
void (*p) (int, int)
(*p)
代表p
是一个指针(int , int y)
代表这个指针指向的函数有两个int
类型的参数void
代表这个指针指向的函数返回值为void
函数指针有以下注意点:
函数的参数名可有可无:
void (*p) (int, int);
void (*p) (int x, int y);
两者效果是一致的
函数名 与 &函数名 没有区别
在数组中,arr
与&arr
是有区别的,但是函数中,两者效果一致。
函数指针使用
想要使用函数的指针,那就是:先解引用指针,再调用函数。
void (*p) (int, int) = &Add;(*p)(2, 3);//完成2 + 3 的加法
void (*p) (int, int) = &Add;
首先定义了一个指向Add
函数的指针p
。
我们获得指针后,要先解引用(*p)
,然后调用函数(*p)()
,再传入参数(*p)(2, 3)
。
这样我们就完成了函数的调用。
但是,Add
函数名本质上也是一个函数指针,为什么Add(2, 3)
可以直接调用函数,而不用解引用呢?
ANSIC
标准规定:函数指针中,p()
是(*p)()
的简写。
也就是说在调用函数时,可以减少解引用这个步骤。
因此以上代码也可以写成:
void (*p) (int, int) = &Add;p(2, 3);//完成2 + 3 的加法
另外的,对于函数指针,解引用*
是没有意义的,所以我们有以下通过指针调用函数的方法:
p(2, 3);//省略*
(*p)(2, 3);//不省略*
(**p)(2, 3);//有多余的*
(***p)(2, 3);//有多余的*
//......
(**********p)(2, 3);//有多余的*
你不论解引用多少次,最后都可以正常调用函数。
回调函数
回调函数就是⼀个通过函数指针调⽤的函数。
如果你把函数的指针(地址)作为参数传递给另⼀个函数,当这个指针被⽤来调⽤其所指向的函数时,被调⽤的函数就是回调函数。
下面是一个简单的例子来说明回调函数的使用:
// 回调函数的定义
void callback(int num)
{printf("回调函数被调用,传递的参数为: %d\n", num);
}// 接受回调函数作为参数的函数
void performCallback(void (*func)(int)){printf("执行回调函数之前的操作\n");func(10);
}int main(){// 主函数中调用接受回调函数的函数performCallback(callback);return 0;
}
在上面的例子中,我们定义了一个名为callback
的回调函数,它接受一个整数作为参数,并在函数体内输出这个参数的值。然后我们定义了一个名为performCallback
的函数,它接受一个函数指针作为参数。在performCallback
函数中,我们先输出一些操作,然后调用传递进来的函数指针,并传递一个整数参数10,最后再输出一些操作。在主函数中,我们调用performCallback
函数,并将callback
函数作为参数传递进去。
运行这个程序,输出如下:
执行回调函数之前的操作
回调函数被调用,传递的参数为: 10
可以看到,performCallback
函数在执行之前和之后都执行了一些操作,并在中间调用了传递进来的回调函数callback
,并将参数10传递给它。
typedef关键字
typedef
关键字用于对变量重命名。
用法如下:
typedef unsigned int uint;
将unsigned int
重命名为 uint
,后续可以使用uint
代替 unsigned int
,比如这样:
uint x = 5;
此时的x
变量就是unsigned int
类型了。
那为什么我要在指针这里讲typedef
关键字呢?
因为对于指针,其有不太一样的语法。
对于一般的指针,直接重命名即可
将 int*
的指针重命名为 pint
:
typedef int* pint;
普通指针的语法命名与一般的类型没有区别。
对于数组指针:
对于数组指针
如果根据一般的语法,重命名是:
typedef int (*) [5] parr;
将 int (*) [5]
这个数组指针重命名为 parr
但是数组指针不允许这样命名,必须把新的名称放在
*
的旁边
typedef int (*parr) [5];
这样才算把 int (*) [5]
这个数组指针重命名为 parr
。
对于函数指针:
和数组指针同理,不允许按照一般的语法重命名,要把名称放在*
旁边:
将void (*) (int)
类型的指针重命名为pfunc
错误案例:
typedef void (*) (int) pfunc;
正确示范:
typedef void (*pfunc) (int);
相关文章:

C语言:数组指针 函数指针
C语言:数组指针 & 函数指针 数组指针数组名 数组访问二维数组 函数指针函数指针使用回调函数 typedef关键字 数组指针 数组本质上也是一个变量,那么数组也有自己的地址,指向整个数组的指针,就叫做数组指针。 我先为大家展示…...

全面介绍HTML的语法!轻松写出网页
文章目录 heading(标题)paragraph(段落)link(超链接)imagemap(映射)table(表格)list(列表)layout(分块)form(表单)更多输入:datalistautocompleteautofocusmultiplenovalidatepatternplaceholderrequired head(首部)titlebaselinkstylemetascriptnoscript iframe HTMLÿ…...

数学建模【相关性模型】
一、相关性模型简介 相关性模型并不是指一个具体的模型,而是一类模型,这一类模型用来判断变量之间是否具有相关性。一般来说,分析两个变量之间是否具有相关性,我们根据数据服从的分布和数据所具有的特点选择使用pearsonÿ…...

「优选算法刷题」:字母异位词分组
一、题目 给你一个字符串数组,请你将 字母异位词 组合在一起。可以按任意顺序返回结果列表。 字母异位词 是由重新排列源单词的所有字母得到的一个新单词。 示例 1: 输入: strs ["eat", "tea", "tan", "ate", "na…...

【教程】 iOS混淆加固原理篇
目录 摘要 引言 正文 1. 加固的缘由 2. 编译过程 3. 加固类型 1) 字符串混淆 2) 类名、方法名混淆 3) 程序结构混淆加密 4) 反调试、反注入等一些主动保护策略 4. 逆向工具 5. OLLVM 6. IPA guard 7. 代码虚拟化 总结 摘要 本文介绍了iOS应用程序混淆加固的缘由…...

《银幕上的编码传奇:计算机科学与科技精神的光影盛宴》
目录 1.在电影的世界里,计算机科学不仅是一门严谨的学科,更是一种富有戏剧张力和人文思考的艺术载体。 2.电影作为现代文化的重要载体,常常以其丰富的想象力和视觉表现力来探讨计算机科学和技术的各种前沿主题。 3.电影中的程序员角色往往…...

linux提权之sudo风暴
🍬 博主介绍👨🎓 博主介绍:大家好,我是 hacker-routing ,很高兴认识大家~ ✨主攻领域:【渗透领域】【应急响应】 【Java】 【VulnHub靶场复现】【面试分析】 🎉点赞➕评论➕收藏 …...

数据结构之:跳表
跳表(Skip List)是一种概率性数据结构,它通过在普通有序链表的基础上增加多级索引层来实现快速的查找、插入和删除操作。跳表的效率可以与平衡树相媲美,其操作的时间复杂度也是O(log n),但跳表的结构更简单,…...

matlab 线性四分之一车体模型
1、内容简介 略 57-可以交流、咨询、答疑 路面采用公式积分来获得,计算了车体位移、非悬架位移、动载荷等参数 2、内容说明 略 3、仿真分析 略 线性四分之一车体模型_哔哩哔哩_bilibili 4、参考论文 略...

LeetCode第二题: 两数相加
文章目录 题目描述示例 解题思路 - 迭代法Go语言实现 - 迭代法算法分析 解题思路 - 模拟法Go语言实现 - 模拟法算法分析 解题思路 - 优化模拟法主要方法其他方法的考虑 题目描述 给出两个非空的链表用来表示两个非负的整数。其中,它们各自的位数是按照逆序的方…...

web组态插件
插件演示地址:http://www.byzt.net 关于组态软件,首先要从组态的概念开始说起。 什么是组态 组态(Configure)的概念来自于20世纪70年代中期出现的第一代集散控制系统(Distributed Control System)…...

Android14 InputManager-InputManagerService环境的构造
IMS分为Java层与Native层两个部分,其启动过程是从Java部分的初始化开始,进而完成Native部分的初始化。 □创建新的IMS对象。 □调用IMS对象的start()函数完成启动 同其他系统服务一样,IMS在SystemServer中的ServerT…...

搜维尔科技:【周刊】适用于虚拟现实VR中的OptiTrack
适用于 VR 的 OptiTrack 我们通过优化对虚拟现实跟踪最重要的性能指标,打造世界上最准确、最易于使用的广域 VR 跟踪器。其结果是为任何头戴式显示器 (HMD) 或洞穴自动沉浸式环境提供超低延迟、极其流畅的跟踪。 OptiTrack 主动式 OptiTrack 世界领先的跟踪精度和…...

matlab倒立摆小车LQR控制动画
1、内容简介 略 54-可以交流、咨询、答疑 2、内容说明 略 摆杆长度为 L,质量为 m 的单级倒立摆(摆杆的质心在杆的中心处),小车的质量为 M。在水平方向施加控制力 u,相对参考系产生位移为 y。为了简化问题并且保其实质不变,忽…...

【C++】类和对象(2)
目录 1. 初始化列表 2.explicit关键字 3. Static成员 3. 友元 3.1友元函数 3.2友元类 4. 内部类 5.匿名对象 1. 初始化列表 在创建对象时,编译器通过调用构造函数,给对象中各个成员变量一个合适的初始值,但是这个过程并不能称为对对…...

用Python实现创建十二星座数据分析图表
下面小编提供的代码中,您已经将pie.render()注释掉,并使用了pie.render_to_file(十二星座.svg)来将饼状图渲染到一个名为十二星座.svg的文件中。这是一个正确的做法,如果您想在文件中保存图表而不是在浏览器中显示它。 成功创建图表…...

备战蓝桥杯————递归反转单链表的一部分
递归反转单链表已经明白了,递归反转单链表的一部分你知道怎么做吗? 一、反转链表Ⅱ 题目描述 给你单链表的头指针 head 和两个整数 left 和 right ,其中 left < right 。请你反转从位置 left 到位置 right 的链表节点,返回 反…...

rabbitmq知识梳理
一.WorkQueues模型 Work queues,任务模型。简单来说就是让多个消费者绑定到一个队列,共同消费队列中的消息。 当消息处理比较耗时的时候,可能生产消息的速度会远远大于消息的消费速度。长此以往,消息就会堆积越来越多,…...

【数据结构与算法】动态规划法解题20240227
动态规划法 一、什么是动态规划二、动态规划的解题步骤三、509. 斐波那契数1、动规五部曲: 四、70. 爬楼梯1、动规五部曲: 五、746. 使用最小花费爬楼梯1、动规五部曲: 一、什么是动态规划 动态规划,英文:Dynamic Pro…...

备战蓝桥杯—— 双指针技巧巧答链表2
对于单链表相关的问题,双指针技巧是一种非常广泛且有效的解决方法。以下是一些常见问题以及使用双指针技巧解决: 合并两个有序链表: 使用两个指针分别指向两个链表的头部,逐一比较节点的值,将较小的节点链接到结果链表…...

半监督节点分类-graph learning
半监督节点分类相当于在一个图当中,用一部分节点的类别上已知的,有另外一部分节点的类别是未知的,目标是使用有标签的节点来推断没有标签的节点 注意 半监督节点分类属于直推式学习,直推式学习相当于出现新节点后,需要…...

软件文档-运维-开发-管理-资质-评审-招投标-验收
开发文档:这类文档主要用于记录软件的开发过程和细节,包括: 《功能要求》:描述了软件应具备的功能,是软件开发的基础。《投标方案》:向潜在的客户或招标方展示公司的技术和项目实施能力。《需求分析》&…...

猫头虎分享已解决Bug || Vue中的TypeError: Cannot read property ‘name‘ of undefined 错误
博主猫头虎的技术世界 🌟 欢迎来到猫头虎的博客 — 探索技术的无限可能! 专栏链接: 🔗 精选专栏: 《面试题大全》 — 面试准备的宝典!《IDEA开发秘籍》 — 提升你的IDEA技能!《100天精通鸿蒙》 …...

技术应用:使用Spring Boot、MyBatis Plus和Dynamic DataSource实现多数据源
引言 在现代的软件开发中,许多应用程序需要同时访问多个数据库。例如,一个电子商务平台可能需要访问多个数据库来存储用户信息、产品信息和订单信息等。在这种情况下,使用多数据源是一种常见的解决方案,它允许我们在一个应用程序…...

C# Onnx 使用onnxruntime部署实时视频帧插值
目录 介绍 效果 模型信息 项目 代码 下载 C# Onnx 使用onnxruntime部署实时视频帧插值 介绍 github地址:https://github.com/google-research/frame-interpolation FILM: Frame Interpolation for Large Motion, In ECCV 2022. The official Tensorflow 2…...

编程笔记 Golang基础 016 数据类型:数字类型
编程笔记 Golang基础 016 数据类型:数字类型 1. 整数类型(Integer Types)a) 固定长度整数:b) 变长整数: 2. 浮点数类型(Floating-Point Types)3. 复数类型(Complex Number Types&…...

一周学会Django5 Python Web开发-会话管理(CookiesSession)
锋哥原创的Python Web开发 Django5视频教程: 2024版 Django5 Python web开发 视频教程(无废话版) 玩命更新中~_哔哩哔哩_bilibili2024版 Django5 Python web开发 视频教程(无废话版) 玩命更新中~共计26条视频,包括:2024版 Django5 Python we…...

QT之QString.arg输出固定位数
问题描述 我需要用QString输出一个固定位数的数字字符串。起初我的代码是这样: int img_num 1 auto new_name QString("%1.png").arg((int)img_num, 3, 10, 0); //最后一个参数用u0也是一样的 qDebug() << "new_name:" << new…...

Linux下各种压缩包的压缩与解压
tar 归档,不压缩,常见后缀 .tar # 将文件夹归档成为一个包 tar cf rootfs.tar rootfs # 将归档包还原为文件夹 tar xf rootfs.tar # 将归档包还原到路径 a/b/c tar xf rootfs.tar -C a/b/cgzip压缩, 常见后缀 .tar.gz .tgz # 压缩 tar czf …...

【ctfshow—web】——信息搜集篇1(web1~20详解)
ctfshow—web题解 web1web2web3web4web5web6web7web8web9web10web11web12web13web14web15web16web17web18web19web20 web1 题目提示 开发注释未及时删除 那就找开发注释咯,可以用F12来查看,也可以CtrlU直接查看源代码呢 就拿到flag了 web2 题目提示 j…...