静态二维数组与指针
我们定义一个二维数组int a[4][3]。
二维数组实际是由多个一维数组组成,在这里,a[3][4]就是由3个长度为4的一维数组组成的二维数组。并且它们在空间上是连续的,相当于一个长为12的一维数组。
这里a,a[i]全部为指针,a为指向整个数组第一个一维数组的指针,a+1指向第二个一维数组,a[0]则指向第一个一维数组的第一个元素,a[1]指向第二个一维数组的第一个元素。
类型说明
a的类型为int(*)[4],数组指针(后面动态数组还会出现指针数组,注意区别),是指向第一个一维数组的指针,即a所指向的类型为一个一维数组。
a[0]的类型为int*,是指向第一个一维数组第一个元素的指针,即a[0]指向的类型为一个元素,这里是int。
数组与指针的关系
要弄清楚数组与指针的关系,我们需要弄清楚*a,a,&a,a[0],a[0][0]之间的关系。
首先我们来看一下它们的值。a[0][0]自然不用说,就是数组第一个值。
剩下的我们来猜一猜值:
std::cout << *a << std::endl;std::cout << &a << std::endl;std::cout << a << std::endl;std::cout << a[0] << std::endl;std::cout << &a[0] << std::endl;
它们输出的值都相同,都是同一个地址。
那它们之间有什么差异呢。这里我们使用+1来判断。
首先我们要知道,一个指针+1就是指针向后移动它所指向类型的字节数。假如指向的是int类型,就会向后移动4位(不同环境int字节可能不同),如果指向的是一个长为4的int类型的数组,将会后移4×4=16位。这里的位是指地址的位,地址是以字节位单位编号的,所以地址里的的每一个数就对应一个字节的数据。
我们从新重新使用以下代码:
std::cout <<"a:\t\t" <<a << std::endl;//没+1前都一样所以其他的省略std::cout << "*a+1:\t\t"<<*a+1 << std::endl;std::cout << "&a+1:\t\t"<<&a + 1 << std::endl;std::cout << "a+1:\t\t"<<a + 1 << std::endl;std::cout << "a[0]+1:\t\t"<<a[0] + 1 << std::endl;std::cout << "&a[0] + 1:\t"<<&a[0] + 1 << std::endl; std::cout << "&a[0][0] + 1:\t"<<&a[0][0] + 1 << std::endl;
结果如下:
我们发现
*a+1和a[0]+1,&a[0][0]一样,移动了4位(变大了4)。因为它们本质上来说是一样的。a指向第一个一维数组,也就指向a[0],a[0]指向第一个数组的第一个元素,也就是指向a[0][0]。
&a+1指针后移了48位,因为它指向的是a[3][4]这个数组,这里int型数据占4个字节,+1则后移3×4×4位。
a+1 指针后移了16位,因为它指向的是第一个[4]的一维数组,即a[0],+1后移4×4位。
&a[0]等同于a,因为a就是a[3][4]中第一个数组的地址。
元素的获取
要获得数组的元素,m行n列元素,可以用三种方法。
1.直接用下标访问:a[m][n];
2.用第m行指针位移:*(a[m]+n);
3.用第一个元素位移:*(*(a+m)+n);
长度计算
从上面我们可以知道,a,a[0]这些都是地址,那么它们的长度是多少呢。
int a[3][4];
std::cout << sizeof(a[0]) << std::endl;
std::cout << sizeof(a) << std::endl;
结果为 16 48。分别为一维数组长度和二维数组长度。但是这里就有疑问了,它们不是指针吗,它们的长度不该是指针本身的长度吗,这是因为编译器在将 C 代码转换成汇编代码时,自动将其替换成了实际的数值。但是它们如果作参数传入函数里,则只会传入指针,比如运行以下代码:
void test(int b[3][4]) {std::cout << sizeof(b) << std::endl;
}int main()
{int a[3][4];test(a);
}
这时候我们会得到a的长度为4(x86)或8(x64)
这个时候指针b的长度就为指针本身的长度。因为b本来就是指针,我们经常在数组作参数传入函数时,会多加一个参数记录其长度一样,因为数组指针并没有整个数组的长度信息,只能将首地址传入函数。
如果没搞清楚可以看我的另一篇博客:点这里。
动态二维数组与指针
这里我们只讨论new这种方式创建的二维数组。
int** a = new int*[3];//创建3×4的数组for (int i = 0; i < 3; i++){a[i] = new int[4];}
其存储结构如下:
这里可以看到new创建的动态二维数组也是由多个一维数组组成的。不同的是,动态二维数组的中这些一维数组并没有相连,而是每个数组对应一个指向其第一个数的指针。再将这些指针组合成数组(静态数组没有这个指针数组)。a就是指向这些数组指针组成的数组的第一个数的地址。之所以有空白是因为动态数组长度可以增加,并没有静态数组那样的限制,因此声明时数组的长度可以是参数。
注意:指针数组长度可以增加,指针数组每一个指针对应的一维数组可以增加,并不代表二维数组x,y坐标没有限制,比如上面数组就不能给a[3][1]赋值,一维就是有了a[3]这个指针,也没有对其指向的数组声明,即没有a[3] = new int[4]这样一个过程。
我们用之前的代码测试:
std::cout << a << std::endl;std::cout << a[0] << std::endl;std::cout << &a[0] << std::endl;std::cout << &a[0][0] << std::endl;
得到如下结构:
从结果中我们发现,和静态二维数组不一样,a和a[0]的值不同。因为a[0]指向第一个一维数组的指针,a为指向指针构成的数组的第一个元素的指针。a的类型为int**,a[0]的类型为int*。这里a和a[0]都是指向的一维数组,只不过a[0]指向的元素是int,a指向的元素是指针。
+1后结果如下(代码中数组用的c表示,这里和a不加区别):
可以看到,无论是c还是c[0],+1后都是移动4位,因为它们都是指向一维数组第一个元素的指针,元素类型分别为指针类型和int类型,int类型占4个字节,指针类型也占4个字节。*c=c[0]因为c指向的是指针数组的第一个位置。
虽然在地址+1操作做动态二维数组和静态二维数组所得到的地址值变换不一样,但是所对应到数组的值都一样。所以上面这些计算元素的方法,动态数组和静态数组所得结果都是一样的。
要获得数组的元素,m行n列元素,可以用三种方法。
1.直接用下标访问:a[m][n];
2.用第m行指针位移:*(a[m]+n);
3.用第一个元素位移:*(*(a+m)+n);
半截子动态二维数组
这里稍微介绍一下,并不常用
int(*array)[3] = new int[4][3];
它的存储结构和静态数组很像,因为列是静态的,所以还是一维的存储方式,不同的是它还能对a[4][2],a[5][1]等元素进行操作,因为它的行是动态的。
为什么我把它叫半截子的动态二维数组?因为它只有行具有动态功能。
还有个有趣的地方就是,比如a[0][4]这个元素,列超出了范围,会向下一行继续移动,即a[0][2]向后移动两位,也就是说它完全等于 a[1][1],地址一样,就是同一个数。
如有错误,还望指正。