ps:点进来的小伙伴如果能通过下面的小测验并且熟悉原因,那么右上角xx慢走不送,如果你不清楚,那么请你留下来好好看看这篇博客所需能帮助你学会什么- -!

前言

实际上相信大家对于二维数组并不是很陌生,我们每天在写程序时可能都会使用到二维数组,大多数人对于二维数组的使用都非常的熟悉,但是,仅仅局限于使用它的使用远远不够,我们得真正了解他的含义,遇到一些复杂的笔试题时迎刃而解。

小测试

这是笔者初始c语言时老师给的一组题,当时真的相当懵逼,如果你能完全做对,这篇博客对你来说或许毫无意义,但是如果你不明白这里的意思,那你接着往下看,听我好好给你娓娓道来。

int a[3][4] = {0};
	printf("%d\n",sizeof(a));
	printf("%d\n",sizeof(a[0][0]));
    printf("%d\n",sizeof(a[0]));
    printf("%d\n",sizeof(a[0]+1));
    printf("%d\n",sizeof(*(a[0]+1)));
    printf("%d\n",sizeof(a+1));
    printf("%d\n",sizeof(*(a+1)));
	printf("%d\n",sizeof(&a[0]+1));
    printf("%d\n",sizeof(*(&a[0]+1)));
    printf("%d\n",sizeof(*a));
    printf("%d\n",sizeof(a[3]));

解析:

int a[3][4] = {0};
	printf("%d\n",sizeof(a));//48//输出这个二位数组的大小
	printf("%d\n",sizeof(a[0][0]));//4//第一个元素的大小(int)
    printf("%d\n",sizeof(a[0]));//16//第一行的数组的大小
    printf("%d\n",sizeof(a[0]+1));//4//第一行第二个数字的地址(指针的大小)
    printf("%d\n",sizeof(*(a[0]+1)));//4//第一行第二个数字的大小
    printf("%d\n",sizeof(a+1));//4//第二行的地址(指针的大小)
    printf("%d\n",sizeof(*(a+1)));//16//第二行的大小
	printf("%d\n",sizeof(&a[0]+1));//4//第二行的地址(指针的大小)
    printf("%d\n",sizeof(*(&a[0]+1)));//16//第二行的大小
    printf("%d\n",sizeof(*a));//16//第一行的大小
    printf("%d\n",sizeof(a[3]));//16//虽然越界了但是sizeof只是一个关键字他不关心这块空间是否真的存在

二维数组

二维数组的概念我们就不在做过多的讲解,我们直接从他的存储顺序入手或许能让大家更明了

int arr[3];

这个数组包含3个元素,就像下面的图一样:
[c语言]——深入剖析二维数组
接着我们希望他的每个元素又包含了6个元素,有了下面的书写方式

int arr[3][6];

[c语言]——深入剖析二维数组
新的理解:我们可以这样认为,arr可以看作是一个一维数组,包含3个元素,每个元素恰好是包含6个整形的元素,arr这个名字的值成了一个指向一个包含6个整形元素的数组的指针(你学过数组指针就该知道,他们是一个道理,后面我会讲解数组指针)

1.1二维数组的下标

接着我们在看看二维数组的下标,他是我们访问二维数组最常用的方式:
上图中数组的下标可以编码为

(0,0)(0,1)(0,2)(0,3)(0,4)(0,5)
(1,0)(1,1)(1,2)(1,3)(1,4)(1,5)
(2,0)(2,1)(2,2)(2,3)(2,4)(2,5)
printf("%d\n",sizeof(a[0][0]));//所以我们这里就是计算他的成员的大小

其实我们发现在对数组的元素进行下标编码时,逻辑结构上我们分了三行,但是实际上二维数组在物理上是一行存储的,而多位数组的元素存储顺序按照最右下标先变化的原则,称为行主序。已经知道了二维数组的存储方式和下标的编码有助于我们的硬菜理解,如果你已经明白了这些,可以直接跳到下一部分。

1.2二维数组数组结合下标以及指针的理解(重点)

接着二维数组,读者先记住二维数组名就是一个数组指针(不管你有没有理解先记在心里)

int arr[3][6];

我们现在希望访问arr[1][3]这个元素,如下图:
[c语言]——深入剖析二维数组
但是,下标访问数组实际上是一种访问表达式的伪装形式,为什么这么讲,我们来接着看:

arr

arr是数组的名字,他的类型是“指向包含6个整型数组的指针”(他实际上是一个数组指针),现在他默认指向第一行,他如下图:
[c语言]——深入剖析二维数组

arr+1

紧接着我们让他加一,现在的他指向了第二行
[c语言]——深入剖析二维数组

这里如果不是很清楚的同学笔者可以分享一下自己的理解,笔者认为,当一个int*的指针指向一个数组的某一个int时,++他便指向了后一个int,同理,这里的数组指针指向了一个数组,所以++他就要指向下一个数组

不要眨眼我们进行下一个有趣的操作:

*(arr+1)

问读者一个问题,int*解引用后我们拿到了int类型数据,那数组指针呢?没错我们对一个数组的指针解引用,我们现在拿到了一个数组。
问题好像变得越来越有趣了

*(arr+1) == arr[1]

你没猜错,解引用指针数组后我们拿到了一个数组,而这个*(arr+1)就是第二行的数组名,如果你觉得这样表达很奇怪,那么arr[1]和他意义相同,所以我们回头来看。

现在这个问题是不是非常明了呢,相信你认真看了上面的讲解你会觉得他很简单。

printf("%d\n",sizeof(*a));//16//对p解引用你得到了一个一维数组名
printf("%d\n",sizeof(a[0]));//16//arr[0]是第一行数组名,所以求得第一行的大小
printf("%d\n",sizeof(*(a+1)));//16//*(arr+1)== arr[1],求得第二行的大小
ps:之前有小姐姐问笔者,数组名不是个指针么?sizeof()应该是指针的大小啊,实际sizeof(数组名)求得的是整个数组的大小
,牢记这一点这对后面的问题有帮助

现在回到我们的问题:

我们现在拿到这个二维数组的第二行,第二行实际上就是一个一维数组,所以或许我们所有的处理都可以使用一维数组的方式

int arr[6];

还记得一维数组数组名是什么么,你已经想到了,他是一个指向int类型的常量指针,所以之前没有理解二维数组名的同学现在应该明白了,二维数组的元素实际上就是一个一个的数组,所以二维数组名就是数组指针。

*(arr+1)来看下图

[c语言]——深入剖析二维数组
你现在所有的操作都是在操作一个一维数组!!!!!!!!
戴好帽子,接着看:

*(arr+1)+ 3

你好像让他指向了这个数组中的第4个元素。
[c语言]——深入剖析二维数组
看看这些题眼熟么:

printf("%d\n",sizeof(a[0]+1));//4//第一行第二个数字的地址(指针的大小)

最后一步你可能拿到了你想要的东西

*(*(arr+1)+ 3)

[c语言]——深入剖析二维数组
来看看之前我们遇到的问题

printf("%d\n",sizeof(*(a[0]+1)));//4//第一行第二个数字的大小

我们来看看一些关于取地址的问题:

printf("%d\n",sizeof(a+1));//4//第二行的地址(指针的大小)
printf("%d\n",sizeof(&a[0]+1));//4
printf("%d\n",sizeof(*(&a[0]+1)));//16

第一题:
指针的大小在32位机器下为4字节,所以a是一个数组指针加一后他还是指针,所以是4,切记笔者在之前强调过的a单独放在sizeof中是一个数组的大小,但是他的本质还是一个指针,+1后再次放在sizeof中就是指针的大小了。
第二题:
给a[0]取地址?a[0]是一个一维数组的名字,也就是一个指针,再次取地址后的结果就是一个二级指针,我们想想之前我们对数组指针解引用得到数组名这不刚好相反吗?所以&arr[0]你可以理解为,我们现在拿到了一个数组指针,对数组指针+1就是让他指向第二行,sizeof(指针) == 4.(之前有同学不知道为什么数组指针也叫二级指针,现在应该明了了)
第三题:
如果你认真读了文章,这个问题你现在早已胸有成竹,没错你拿到了第二行数组的名字,sizeof他就是第二行大小

printf("%d\n",sizeof(a[3]));//假设下一行存在,第4行的大小

有同学问,这个a[3]不是越界访问了么怎么不报错,事实上sizeof只是一个操作符,他不关心你这块空间是否存在,所以也是可以求出结果的

到这里,我们所有关于小测验的题以及二维数组的相关的理解全部讲解完,但是别急,这或许才刚刚开始。

数组指针

1.为什么要有数组指针

来看简单的两句代码

int arr[10], *p = arr;
int array[3][10], *q = array;

第一句代码没有问题,arr这个数组存储的是int类型的元素,所以交给一个int*的指针管理
第二局代码报错类型不匹配,还记得二维数组的里的元素么,他们是一个一个的数组(上面忘记说了,学了cpp的同学应该知道cpp二维数组其实就是vector里的vector),所以用int *类型指针接受当然不对了

所以我们有了指向数组的指针,如下

int (*p)[10]

切记那个圆括号,如果不小心写成int* p[10],很遗憾你得到了一个指针数组,因为[]的优先级高于*

1.1指向数组的指针

这个看起来好像有点抽象,你还记得int指针怎么来的么, *代表p是一个指针, 而他之前则是指向的数据类型

int* p;

所以同理,*代表p是一个指针,它指向的类型为int [10]

int (*p)[10]

ps:笔者一直不理解按理来说数组指针应该是int(*)[10] p啊(类型+变量名字),不知道为啥有这么古怪的写法,emmm。。

1.2初始化数组指针

现在你的指针指向了二维数组的第一行

int array[3][10];
int (*p)[10] = array;

就像这样:
他与数组名使用方式相同
[c语言]——深入剖析二维数组
这里加粗使用方式,因为数组名和指针是有区别的:

  1. 数组名代表了一个指向数组首元素的常量指针,一经定义,不可更改,数组名作为常量指针,其类型与数组元素类型相同。指针是变量指针,定义之后仍可更改,其类型在定义时确定。
  2. 当出现sizeof,和&操作符时,数组名不再当成指向一个元素的常量指针来使用,而指针仍当成指向一个元素的变量指针来使用

错误初始化

数组指针现在不明确自己可以执行的长度,所以应该避免这种错误,有的编译器并不会报错

int array[3][10];
int (*p)[] = array;

他们类型都会报错无法转换,意味着,定义一个数组指针时[]里的长度一定要和数组的列长度相同

int array[3][10];
int (*p)[8] = array;err
int  (*p) [10] = array;err

小练习

我们已经介绍完了二维数组和二维数组指针,下面的题相信不会难住你了

int main()
{
	int arr[][3] = { 10, 20, 30, 40, 50, 60 };
	int(*p)[3];
	p = arr;
	cout << p[0][0] << " " << *(p[0] + 1) << " " << (*p)[2] << endl;
	return 0;
}

1.p[0][0]按照下标访问第一行第一个元素,输出10
2.*(p[0] + 1)这个让第一行指针指向第二个元素然后解引用,输出20
3.(*p)[2] 等同于 * ((解引用p)+2),先解引用数组指针拿到第一行数组名,然后+到第3个位置解引用,一定注意p[2]和前面表达式不同,p先与[]结合,这里相当于解引用第三行的数组名,你得到第三行第一个元素,而事实上第三行不存在。

关于数组的经典笔试题

这一部分笔者打算将遇到有趣难理解的笔试题持续不断跟新于这一部分,如果对上文有什么不理解觉得写的不好的的地方还请提出宝贵的意和建议

输出的结果为2,5。(int * )(&a + 1)这里将&a+1强制类型转换为int
首先跳过了一个数组然后–刚好指向元素5,*(a + 1)指针指向第二个元素解引用

int main()
{
    int a[5] = { 1, 2, 3, 4, 5 };
    int *ptr = (int *)(&a + 1);
    printf( "%d,%d", *(a + 1), *(ptr - 1));
    return 0;
}*/   

ptr1:将&a+1 的值强制转换成 int类型,赋值给 int 类型的变量 ptr,ptr1 肯定指到数组 a 的下一个 int 类型数据了。ptr1[-1]被解析成*(ptr1-1),即 ptr1 往后退 4 个 byte。所以其 值为 0x4。
ptr2:按照上面的讲解,(int)a+1 的值是元素 a[0]的第二个字节的地址。然后把这个地址 强制转换成 int类型的值赋给 ptr2,就是说ptr2 的值应该为元素 a[0]的第二个字节开始的 连续 4 个 byte 的内容。 最后参考内存发现结果为2000000.
[c语言]——深入剖析二维数组

int main()
{
    int a[4]={1,2,3,4};
    int *ptr1=(int *)(&a+1);
    int *ptr2=(int *)((int)a+1);
    printf("%x,%x",ptr1[-1],*ptr2);
    return 0;
}
	
    

花括号里面嵌套的是小括号,而不是花括号!这里是花括号里面嵌套了逗号表达式!其实这个赋值就相当于 int a [3][2]={ 1, 3,
/5}; 所以最后的答案应该是1

int main(int argc,char * argv[])
{
     int a [3][2]={(0,1),(2,3),(4,5)};
     int *p;
     p=a [0];
     printf("%d",p[0]);
	
}

这个我的编译器已经编不过了

int main()
{
     int a[5][5];
     int (*p)[4];
     p = a;
     printf("a_ptr=%#p,p_ptr=%#p\n",&a[4][2],&p[4][2]);
     printf("%p,%d\n",&p[4][2] - &a[4][2],&p[4][2] - &a[4][2]);
     return 0;
}
	 //这道题的结果为-4,为什么呢?
	 这里的 a 为二     维数组,我们把数组 a 看作是包含 5int 类型元素的一维数组,里面再存储了一个一维数组。
     如此,则 a 在这里代表的是 a[0]的首地址。a+1 表示的是一维数组 a 的第二个元素。a[4]表
     示的是一维数组 a 的第 5 个元素,而这个元素里又存了一个一维数组。所以&a[4][2]表示的
     是&a[0][0]+4*5*sizeof(int) + 2*sizeof(int)。
     根据定义,p 是指向一个包含 4 个元素的数组的指针。也就是说 p+1 表示的是指针 p 向
     后移动了一个“包含 4int 类型元素的数组”。这里 1 的单位是 p 所指向的空间,即
     4*sizeof(int)。所以,p[4]相对于 p[0]来说是向后移动了 4 个“包含 4int 类型元素的数组”,
     即&p[4]表示的是&p[0]+4*4*sizeof(int)。由于 p 被初始化为&a[0],那么&p[4][2]表示的是
     &a[0][0]+4*4*sizeof(int)+2* sizeof(int)。现在就不难理解为啥我们得到的结果为-4

	return 0;
}

int main()
{
	int aa[2][5] = {1,2,3,4,5,6,7,8,9,10};
    int* ptr1 = (int* )(&aa+1);
	int* ptr2 = (int*)(aa+1);
	printf("%d %d",*(ptr1-1),*(ptr2-1));
	return 0;
}
//结果为10,5
//第一个为10不难理解与上面的题类似
//第二个结果为5是因为,aa相当于第一行的地址,加一后相当于第二行的首地址,减一指针指向了第一行的最后一个元素

持续跟新中。。。

相关文章: