详解多维数组与指针之间的关系
一维数组
先介绍一下简单的一维数组:
列如:
int a[3] = {0,1,2};
[3]和类型int则明确表示编译器应该为这个栈分配多大的内存,也就是三个int大小!
在内存中示意图是:
在CPU看来内存是一组连续的地址空间,所以当我们对一维数组进行操作时只需要知道数组首地址,就可以通过地址偏移加减运算方式来求得每个元素位于内存中的文件映射出来的数据段虚拟地址!
不过要注意不要越界,其实你在自己的地址空间内访问超出数组大小的空间也不会有问题,因为你是在自己的地址下进行访问的,不会被内核卡擦掉,只不过那一段内存可能被对齐了,是未知不被使用的数据!
详细内存对齐参见:详解C语言内存对齐
使用方法也非常简单:
int a[3] = { 0, 1, 2 };printf("%d", a[0]); //打印第0个数据
打印结果:
使用指针方式:
注意数组即本身就是一个地址,而指针可以直接操作地址,所以指针可以使用数组的方式来表示:
int a[3] = { 0, 1, 2 };int *p = a;printf("%d", p[0]); //打印第0个数据
编译器会自动根据表达式所使用的运算符来自动汇编解引用代码!
打印结果:
也可以显示的使用指针解引用:
int a[3] = { 0, 1, 2 };int *p = a;printf("%d", *p+1); //打印第1个数据
这里p已经指向了a数组的首地址,也就是a[0],想打印第一个元素的值,只需要对其*解引用并+1让其进行地址偏移一个类型单位(int,编译器会根据表达式在结合类型自动进行汇编上的地址偏移量add)!
二维数组
二维数组:
int a[3][3] = {{0,1,2},{3,4,5}}; //定义一个二维数组
上面的表达方式即:定义一个有3列且每列有3行数据的一个二维数组!
上面只是抽象的表达方式,其实底层就是一个一维数组:
长度是每行的集合,只是C语言上对其更加抽象的区分开了,根据第一个[]操作符里的值将其分成多少个段!根据[]后的[]确定每段内存能存下多少字节,根据类型来确定该内存用来存储什么样的类型数据运算时调用alu(整数运算器)还是fpu(浮点数运算器)
浮点数是由单独的存储方式的,所以需要严格的区分开!
而且在底层是没有类型这一区分的全部都是二进制数,所以在编译阶段编译器就会检查类型之间的读写,所以类型限制是由编译器来维护的!
使用方法:
int a[3][3] = { { 0, 1, 2 }, {3,4,5} };printf("%d", a[0][1]); //打印第0行第1个数据
打印结果:
下面来介绍一下使用指针的方法和问题:
首先先来看一下:
下面代码为什么会报错?
int a[3][8] = {{1,2,3,4,5,6,7,8},{1,2,3,4,5,6,7,8},{1,2,3,4,5,6,7,8}};
int **p = a;
原因很简单,二级指针只能用来指向int*指针,而int a是一个二维数组,两个类型一开始指向的实际类型就不对,在其次,双方占用的内存大小也不同!
列如
int a[3][8]占用的是int*3*8个字节大小
而*p仅占用4个字节(取决于编译器位数)
那问题又来了,为什么一维数组就可以?
其原因如下:
在C/C++编译器当中一维数组隐式是一个指针,换句话来说,数组就是指针,数组本身就是一个地址,无需二次寻址,指针和数组区别在于数组不用解引用,而且数组名也是一个常量,无法直接赋值!
最经典的列子就是当你将数组作为参数时,编译器会自动将数组转化为指针,其原因是为了剩内存!
而二维数组则被隐式的声明成:int *a[8];
所以我们如果想指向一个二维数组时候就要声明成int (*p)[8] = a; // 一个指向有8个整型数组的指针;
如果不相信的话,我们来修改一下代码看看:
int a[3][8] = {{1,2,3,4,5,6,7,8},{1,2,3,4,5,6,7,8},{1,2,3,4,5,6,7,8}};
int (*p)[5] = a; //这里将int (*p)[4]改成int (*p)[5]看看会报什么错误
报如下错误:
可以看到:int a[3][8]被隐式的转换成:int(*)[8]了!
修改一下:
int (*p)[8] = a; //一个指向有8个整型数组的指针;
解引用方法:
补充2022.3.9:
int (*p)[8],(*p)代表这是个指针,因为[]的优先级大于*,如果不加(),int *p[8];会首先被识别为p[8]的int数组,然后在与int *结合则认为数组里元素类型int *,C语言编译器在运算符结合时是根据运算符优先级进行先左后右或先右后左,取决于左边以及右边两个运算级的优先级,如上右边的[]大于*所以结合类型是从右往左,加了括号之后则是从左往右,因为()是用来告诉编译器先结合哪个的,控制结合优先级。
如果加了(),int (*p)[10];则先*p,是一个指针,然后在与[10]结合,意味着指向一个int [10]的数组,代表这是一个指针,指向一个拥有10个元素的int数组
int (*a)[8],意味着每次(*p+1)偏移时递增多少个type(int),这个type属于你当前指针数组的类型size,[8]意味着每次偏移递增8个int。
最简单的就是我们可以直接将该指针当做数组来使用,因为:二维数组实则上并没有并转换成int (*)[8]只是隐式的类型转换,实际内存还是位于栈中!(*p)指向的是:一个隐式的int *a,而int *a指向a[3]这个第一维数组的首元素也就是首地址a[0],要知道数组地址是连续的可以通过解引用隐式的*a+1得到下一个元素的地址!而后面的[8]则表示每个一维数组中有多少个元素!
也就是说说int a[3][8]被隐式的转换成int *a[8],*a指向原本的a[3]的首地址,而后面的[8]则是告诉*a每个元素的偏移量是多少!
则也就是说[8]为8个int!
其实更为明确的表示方法就是 int a[3][8] = 3个int[8]
其实我们也不需要对其进行解引用,因为使用[]括号编译器会把该指针作为数组一样使用,而数组自己就是一个地址,所以编译器会自动将该指针转化为地址!
printf("%d", p[1][1]); //打印第一维第一个数据
上面这张方法是最为简单的,
还有一种方法:
printf("%d", *(*(p+1)+1)); //打印第一维第一个数据
下面来详细的分解一下上面的解引用过程
首先第一步需要对p进行解引用,这里不在当做数组使用所以需要显示的对其进行解引用,上面说过*p指向隐式的*a,这里对其解引用实则上是对找到了*a的地址并对其进行+1
*(p+1)
这里加上括号是因为*取值运算符优先级要高于+号运算符,注意乘法*不高于+号运算符,而取值*会高于+号运算符,编译器会根据表达式来确定*号的用途。
下面再在来看p+1,上面说过(*p)指向的是隐式的*a地址,而*a指向数组的首地址也就是a[0],这里p+1也就是让*a+1,加上括号()让其优先对地址进行+1在解引用,否则会直接对*a解引用然后在对该元素值+1!即操作*a栈地址里存储的地址+1而非真正的数组地址,如果不解引用的话那就是p本身地址+1了!
补充一个小知识:
指针也是有自己的地址的,指针存在于栈中,一般指针的栈内存存储的是堆或栈地址!
然后又在*(p+1)的外面加了一个括号(*(p+1)),最后并让其+1再次解引用:*(*(p+1)+1)
下面来详细解释一下:
第一,当我们通过*(p+1)找到了隐式*a的地址,注意只是找到了隐式*a的地址而非数组的地址,需要再次对*a解引用找到*a栈内存存储的数组地址:
**p 这样的写法才是真正以指针的形式找到二维数组的写法!
不信我们试一下:
printf("%d", **p);
打印结果为:1
而**p+1就是对a指向的数组地址+1,要知道二维实则上也是一维数组,都是地址都是线性排序的所有**p+1,就是指向第二个元素,不需要加括号是因为**优先级高于+,按照这个优先级来算表达式,会先对p解引用找到隐式的*a,在对*a解引用找到数组地址+1则下一个元素的地址:
printf("%d", **p+1);
打印结果:2
通过上面的介绍,就应该很容易理解这段代码了:
*(*(p+1)+1)
首先对*(p+1)解引用找到也就是隐式的*a并对其地址进行解引用,然后在对其+1(这里+1加的是int*字节大小的偏移地址)也就是找到指向a[1]的*a偏移地址,在对其进行+1,也就是找到数组里的元素,然后在对其进行解引用,在解引用之前要加上括号,上面也说了,优先级的原因,否则会找到a[1]首元素然后对该值+1
所以正确的指针引用写法是:
*(*(p+1)+1)
三维数组
下面说说三维数组应该怎样使用:
列如:
int nA[2][2][2];
对于这样的三维数组并不难理解:
int nA[2][2][2];
实则上就是
在每行多增加了一个行宽
列如:
int nA[2][2] = { { 1, 2 }, { 3, 4 }};
更改为三维数组之后:
int nA[2][2][2] = { { { 1, 2 }, { 3, 4 } }, { { 5, 6 }, { 7, 8 } } }; // 三维数组
三维数组可以被认为是二维数组,而二维数组也可以被认为是一维数组,因为在计算机当中数组的地址是连续的,只有行没有列,维数组只是一种抽象的表达方式!
三维则是给每行增加额外的行宽
更明确的表达方式就是:int a[2][2][2] = 2个int[2][2]
更加明确的表达方式其实就是:int a[2][2][2] = 有列,每个列上有两行,每行可以放2个数据!
注意这里不是画画,没有高度,所以在更底层的表达方式三维实则上是给每行增加行宽!
使用方法:
int nA[2][2][2] = { { { 1, 2 }, { 3, 4 } }, { { 5, 6 }, { 7, 8 } } }; // 三维数组 int(*p)[2][2] = nA;printf("%d\n", p[0][1][1]); //打印第0列第一行第1个行宽
注意三维的初始化必须用{}括起来!
即表示每行宽
打印结果:
可以看到打印出第打印第0列第一行第1个行宽第1个元素数据:4
更新于2020.12.25,修正之前博客里的一个错误写法
如果想访问第1行第一列正确的写法应当是:
int nA[2][2][2] = { { { 1, 2 }, { 3, 4 } }, { { 5, 6 }, { 7, 8 } } }; // 三维数组 int(*p)[2][2] = nA;printf("%d\n", *(*(*p+0+0)+1)); //打印第0列第0行第1个行宽
(p+0+0)+1 其中第一个0是第几行,第二个0是第几列
(p+1+0)+1 则是第一行,第0列 对应的是{{5,6},{7,8}}中的{5,6}这个列,后面的+1则是对应6这个列元素
建议:
数组的话,空间想象力要强,把他们拆分成平面二维图然后排列会方便理解。
堆栈下标是从0开始的所以索引是1!
下面介绍如何使用指针的形式来访问:
int nA[2][2][2] = { { { 1, 2 }, { 3, 4 } }, { { 5, 6 }, { 7, 8 } } }; // 三维数组 int(*p)[2][2] = nA;printf("%d\n", *(*(*p)+1)); //打印第0列第0行第1个行宽
打印结果:
下面来解释一下上面的指针解引用过程:
*(*(*p)+1)
*p首先解引用是对p指向的*nA指针解引用找到*nA指针,在*解引用是找到*nA指向的指向的nA[2]的首地址解引用,注意这个时候必须再次解引用,因为行宽已经被分成了两个,nA[2][2]也已经被隐式的声明成一个指针**nA指向该数组的首地址也就是nA[2][2]的首地址,我们要对其解引用确定要对哪个地址进行访问***p 这种解引用方式则是对nA元素第0行第0列第0个元素进行访问,如果+1则是对第0行第1列第0个元素访问***p+1,如果想访问其中的每个元素需要进行括号优先级运算,上面也说过:
(*p)解引用*nA
*(*p)解引用*nA指向的数组元素首地址
*(*(*p)) 上面说过nA[2][2]已经被隐式的声明成了一个指针指向每个行宽,所以这步操作是对该指针进行解引用则每行的首地址
*(*(*p)+1) 对指针进行加减运算,让指针向下个地址偏移一个指针变量单位也就是一个int的大小,指向下一个元素
所以打印的是:
第0行第0列第1个元素:2
如果想打印第0行第1列第0个元素只需要对*p+1即可
*(*(*p+1))
其指针概念较多,容易混淆,下面是几种指针的声明方式:
1、 一个整型数;int a;2、 一个指向整型数的指针;int *a;3、 一个指向指针的指针,它指向的指针是指向一个整型数;int **a;4、 一个有10个整型数的数组;int a[10];5、 一个有10个指针的数组,该指针是指向一个整型数的;int *a[10];6、 一个指向有10个整型数组的指针;int (*a)[10];7、 一个指向函数的指针,该函数有一个整型参数并返回一个整型数;int (*a)(int);8、 一个指向数组的指针,该数组有10个指针,每个指针指向一个整型数;int *(*a)[10];9、 一个有10个指针的数组,给指针指向一个函数,该函数有一个整型参数并返回一个整型数;int (*a[10])(int);10、 一个指向函数的指针,该函数有一个整型参数并返回一个指向函数的指针,返回的函数指针指向有一个整型参数且返回一个整型数的函数;int (*(*a)(int))(int);
其实指针和数组并没有本质上的区别,区别在于数组在初始化之后就不能作为右值进行运算修改数组大小或指向其它数组地址,所以数组为什么被称为数组就是地址?因为数组在声明之后就是一个常量,其地址就是整个数组的起始地址,而指针则可以随意指向,当然除了被const修饰符修饰的指针!
而且数组名是不能参与运算的,必须通过下标显示指明要参与运算的元素!
那么又来了一个问题,上面说的数组名就是数组的首地址那为何还要用[]来指明下标才能运算?
答:因为在C/C++编译器规定数组名虽然是首地址,但是只能被作为右值运算,如果想要被作为左值参与运算必须显示指定下标确定操作哪个元素,而数组名则对应整个数组的首地址,如果对数组名操作不指明对哪个元素操作,即对整个数组操作,那么对于编译器来说如果这个数组大于CPU位数那么会造成硬件中断!
关于CPU寻址详解: 深度理解“CPU内部寻址方式”
最后在补充一点为什么说要经常使用指针?
答:指针节省内存,使用指针并通过malloc分配内存可以节省编译后内存,并且栈也是有限的!
四维数组
博客更新2022年1月6号
回顾自己曾经写的博客,多维非常容易理解,理解方式有抽象与实际两种,实际上多维在计算机里就是一个线性的内存空间,元素指示符来访问不同的空间。
如下代码是四维数组:
int nA[2][2][2][2] = {{{{1,2},{3,4}},{{5,6},{7,8}}},{{{9,10},{11,12}},{{13,14},{15,16}}}};
如果你已经理解了我上面说的三维数组的含义,那么四维也非常简单,就是在行宽里加了一组元素,扩充了一下行宽,仅此而已。
用指针访问方式也很简单:
解引用与三维不同的是多了一层解引用,因为每列又多了一组元素
int (*p)[2][2][2] = nA;
printf("%d\n",*(*(*(*p+1)+1)+1));
就像洋葱一样,一层一层的剥开它,这段代码输出的值是:8
其实说白话,就是给每个元素组里在额外增加一个元素组而已,然后通过下标去访问它。
如下代码,这是二维:
int nA[2][2] = {{2,2},{2,2}
};
三维就是扩充每行
int nA[2][2] = {{{2,2},{2,2}},{{2,2},{2,2},}
};
四维也是:
int nA[2][2] = {{{{2,2},{2,2}},{{2,2},{2,2},}},{{{2,2},{2,2}},{{2,2},{2,2}}}
};
这样一看是不是就一目了然了,二维是每行每列,多维就是在每列里扩充额外一列出来
然后通过下标访问它们也非常简单:
printf("%d\n",nA[0][1][0][1]);
输出为:6,即第0行,第1列里的第0列的第一个元素
详解多维数组与指针之间的关系相关推荐
- 二维数组和指针之间的关系详解
一.引言 说起二维数组可能首先想到的是各种嵌套的for循环,二维数组的初始化,二维数组的赋值,二维数组的输出等各种问题,当然了,数组的问题永远离不开指针,而二维数组所能联系到的就是二维指针了,此文则是 ...
- 多维数组与指针之间的关系详解
先介绍一下简单的一维数组: 列如: int a[3] = {0,1,2}; [3]和类型int则明确表示编译器应该为这个栈分配多大的内存,也就是三个int大小! 在内存中示意图是: 在CPU看来内存是 ...
- 零基础学习PHP编程——详解Apache、PHP和Mysql之间的关系
详解Apache.PHP和Mysql之间的关系 注意: 本文主要写给基础薄弱的同学, 如有不当之处,还请指正. 访问源站 原创不易,转载请注明 欢迎交流: 640765823 学习方法 弄清楚Apa ...
- 一文详解像素、DPI、分辨率之间的关系
1.像素 像素:是指在由一个数字序列表示的图像中的一个最小单位,称为像素. 像素可以用一个数表示,比如一个"0.3兆像素"数码相机,它有额定30万像素:也可以用一对数字表示,例如& ...
- 【Python】Numpy数组的切片、索引详解:取数组的特定行列
[Python]Numpy数组的切片.索引详解:取数组的特定行列 文章目录 [Python]Numpy数组的切片.索引详解:取数组的特定行列 1. 介绍 2. 切片索引 2.1 切片索引先验知识 2. ...
- C语言 多维数组和指针
右图中圆圈代表指针,箭头代表它指向某个元素. 定义多维数组 int a[3][2]; 它的逻辑结构可以理解为右图. 图中上层数组存储的是3个指向二维数组的指针. 所以如果我们做如下操作: int *p ...
- 二维数组及其指针基础
编程学习-二维字符串数组的初始化-动态内存分配 动态内存分配 1.堆内存分配 : C/C++定义了4个内存区间:代码区,全局变量与静态变量区,局部变量区即栈区,动态存储区,即堆 (heap)区或自由存 ...
- c语言多维数组指针地址讲解,C语言入门之多维数组的指针变量
一.多维数组地址的表示方法 设有整型二维数组a[3][4]如下: 0 1 2 3 4 5 6 7 8 9 10 11 设数组a的首地址为1000,各下标变量的首地址及其值如图所示. 在前面曾经介绍过, ...
- C++ 二维数组和指针数组
C++ 二维数组和指针数组 开发工具与关键技术:C++.VisualStudio 作者:何任贤 撰写时间:2019年04月10日 二维数组大家都很清楚,就是该数组包含的元素是一个数组,那么和指针数组又 ...
最新文章
- slam for dummies
- 深度学习多变量时间序列预测:卷积神经网络(CNN)算法构建时间序列多变量模型预测交通流量+代码实战
- adpcm 解码音量小_Oriolus 1795解码耳放评测:仅仅蓝牙还不够,我要的是“真无线”...
- 使用react、antd组件报错TypeError: _this.formRef.current.validateFields is not a function
- C++set容器-查找和统计
- python和rpa_什么是RPA
- vue2中的keep-alive使用总结及注意事项
- Mybatis下collections使用pageHelper进行分页
- 2019款新iPhone发布时间曝光:依旧9月12日亮相?
- 真传x深度学习第一课:环境配置搭建
- git21天打卡Day2-注册账号
- web前端开发和java后端_web前端开发和后端开发的区别是什么
- 公差基本偏差代号_基本偏差代号公差等级代号.ppt
- 【OpenCV】图像进行数字化操作:像素确定位置、获取像素BGR值、修改像素BGR值、修改指定区域内像素
- Poco库学习——1
- 你不知道的VLC播放器常用痛点功能——快进、快捷键、剪切视频、旋转画面、视频提取声音等
- 新增数学与人工智能学部,考数据结构!齐鲁工业大学(山东省科学院)计算机考研...
- layui table 改变列表字体颜色
- Win10自动更新永久关闭,有效的Win10强制更新关闭方法,禁止windows10自动更新,禁止update medic service ,win10显示更新并关机没有单独的关机按钮
- H3C安全技术高级工程师H3CSE Security GB0-551
热门文章
- 低秩矩阵完备_矩阵之芯 SVD: 基本应用以及与其他分解的关系
- 电脑怎么卸载软件干净_电脑卸载软件怎么卸载?
- 宏定义处理特殊字符 -_c语言编译与预处理命令
- plsql怎么导出几十w的数据到csv_Greenplum数据库使用总结(干货满满)初级使用
- python new方法_Python中的__new__()方法的使用
- c语言输入的成绩由高到低该怎么,c语言编程:输入学生信息(姓名年龄分数)并按照分数由高到低输出...
- 苹果怎么删除通讯录联系人_苹果手机通讯录怎么恢复?这才是正确的打开方式!...
- 禁用计算机服务,适当禁用系统服务 提升计算机运行速度
- sqlplus 执行sql文件_详解sqlplus设定行大小、页大小、字符列格式、数字列格式、清屏...
- 通用 字符串工具类