数组里面存储的元素是数组的话得到的就是二维数组,采用如下方式定义:
int a[2][3] = { {1, 2, 3}, {4, 5, 6} }; |
该数组里面有两个元素,每个元素是一个含有三个int型成员的数组。以下是二维数组的内存图:
还有一种虚拟的二维数组内存图,它将二维数组理解成二维矩阵,通过行与列的方式,有助于对二维数组的记忆,如下:
如果我们换一种方式来思考一维数组这种数据类型的话,那么对于二维数组的理解将会非常简单。以整型一维数组举例,我们通过int a[3]
定义了一个一维数组a,它有三个int类型的成员。而我们知道,数组也是一种数据类型,那么,它到底是什么类型呢,也就是说,我们的数组a到底是什么类型? 答案是,每一个一维数组都是一种单独的数据类型,以a为例,它的类型可以理解为int[3],表示的是含有3个整型元素的数组类型。同理,int b[5],b表示的类型是int[5],表示的是含有5个整型元素的数组类型。那么我们的数组或许可以这么定义:
int[3] a; //通过int[3]这种数据类型定义一个变量a,它有三个int型元素 int[3] a[2]; //通过int[3]这种数据类型定义一个数组a,它的每个元素都是int[3]类型 |
通过这种理解方式可以发现,二维数组本质上还是一维数组,可以将二维数组甚至是多维数组拆解成一层一层的一维数组,按照一维数组的性质进行分析(所有关于一维数组地址运算法则都适用,比如地址偏移计算,取地址,解地址)。
在二维数组中,关于数组名与数组首地址的讨论将更加复杂一些,我们来看一下int a[2][3]
这个数组的数组名有哪些情况:
注意:
&a, a, a[0]的关系就好比浙江省省政府,杭州市市政府,滨江区区政府,虽然这三个政府都在杭州,但是代表的身份完全不一样。
以下这些二维数组初始化都是合法的:
int a[3][3] = { {1,2,3}, {4,5,6}, {7,8,9} }; // 分行初始化 int a[3][3] = { 1, 2, 3, 4, 5, 6, 7, 8, 9 }; // 顺序初始化 int a[][3] = {1, 2, 3, 4, 5, 6, 7 , 8 , 9 }; // 省略第一维的长度,建议不要采用这种做法 int a[3][3] = { {2}, {5, 6}, {7, 8, 9} }; // 按行部分元素初始化 |
指针变量也有地址,它的地址可以用一个二级指针来存放,如下:
int a = 3; int* p = &a; //一级指针 int** pp = &p; //二级指针 |
二级指针内存图:
通过二级指针访问变量:
*pp ==> p **pp ==> a |
数组指针本质上还是一个指针,只不过它指向的是整个数组。按照我们刚才对数组的理解,那么,要定义一个指向整个数组的指针,或许可以这么定义:
int[3] a; int[3] *p = &a; //定义一个指针p,它指向a这个变量的地址 |
可以用这种方式去理解,但形式上还是要遵从C语言的语法规定,C语言中,定义数组指针的方式如下:
int a[3]; int (*p)[3]; //定义一个数组指针p,它希望存储一个含有三成员的整型数组地址 p = &a; //让p指向数组a |
以下是数组指针的内存图:
既然p指向的是含有三个元素的整型数组类型(int[3]类型),那么,就不可以把除此之外的类型赋值给它,哪怕同样是整型,类似下面的写法是错误(Warning)的:
int a[5]; int (*p)[3] = &a; //类型不匹配,报Warning错误,由编译器进行强制类型转换 |
由于p指向了整个数组,那么,对p的算术运算将以整个数组大小为单位进行偏移。
通过数组指针获取指向的数组成员的过程:
p ==> &a p[0] ==> a p[0][0] ==> a[0] |
可以通过数组指针保存二维数组的数组名,因为二维数组的数组名代表的就是一个数组的地址,如下:
int a[2][3]; int (*p)[3] = a; //数组指针p指向二维数组首地址 |
那么,在往后要使用a的地方都可以用p来代替:
p[1] ==> a[1] p[1][0] ==> a[1][0] |
注意落脚点,数组指针的本质是一个指针,在32位系统下永远占用4个字节,而指针数组本质是一个数组,代表的是一串连续的内存空间,只不过由于它是一个指针数组,所以它里面存储的是地址值。它们的定义如下,注意区分有括号和没括号:
int *a[3]; //指针数组,整体占用12字节,可以存储三个整型地址 int (*a)[3]; //数组指针,占用4个字节,希望存储一个int[3]类型的地址 |