详解多维数组与指针之间的关系

先介绍一下简单的一维数组:

列如:

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)[4] = a; //一个指向有7个整型一维数组的指针即int *a[7];

如果不相信的话,我们来修改一下代码看看:

 
 
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个整型数组的指针;

解引用方法:

最简单的就是我们可以直接将该指针当做数组来使用,因为:二维数组实则上并没有并转换成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


堆栈下标是从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分配内存可以节省编译后内存,并且栈也是有限的!

猜你喜欢

转载自blog.csdn.net/bjbz_cxy/article/details/79962349