原文 https://zhuanlan.zhihu.com/p/174091195
1、int[3][4]的各种地址与指针
从前有座山,山里有这么一个二维数组:int a[3][4]
然后我们看这个二维数组,这里很明显可以有一个问题,就是a,*a,&a等等是什么类型?它们有什么区别吗?
具体可以直接看下面的表格,左列为变量名,右列为该变量的类型:
测试代码如下,主要用了typeinfo头文件里的typeid函数,这个函数或者说操作符是C++为支持RTTI(Run-Time Type Identification)-运行时类型识别而提供的操作符之一,RTTI使程序能够获取由基指针或引用所指向的对象的实际派生类型,即允许“用指向基类的指针或引用来操作对象”的程序能够获取到“这些指针或引用所指对象”的实际派生类型。另外的操作符主要还有dynamic_cast和static_cast,但本文就不展开了:
#include<iostream>
#include <typeinfo>
using namespace std;
int main(){
int a[3][4] ={{1,2,3,4},
{1,2,3,4},
{1,2,3,4},};
int (*b)[3][4];
int (*c)[4];
int *d;
int e;
cout<<"&a: "<<&a<<"\tsizeof(&a): "<<sizeof(&a)<<"\ttypeof(&a): "<<typeid(&a).name()<<endl;
cout<<"a: "<<a<<"\tsizeof(a): "<<sizeof(a)<<"\ttypeof(a): "<<typeid(a).name()<<endl;
cout<<"&a[0]: "<<&a[0]<<"\tsizeof(&a[0]): "<<sizeof(&a[0])<<"\ttypeof(&a[0]): "<<typeid(&a[0]).name()<<endl;
cout<<"a[0]: "<<a[0]<<"\tsizeof(a[0]): "<<sizeof(a[0])<<"\ttypeof(a[0]): "<<typeid(a[0]).name()<<endl;
cout<<"&a[0][0]: "<<&a[0][0]<<"\tsizeof(&a[0][0]): "<<sizeof(&a[0][0])<<"\ttypeof(&a[0][0]): "<<typeid(&a[0][0]).name()<<endl;
cout<<"a[0][0]: "<<a[0][0]<<"\tsizeof(a[0][0]): "<<sizeof(a[0][0])<<"\ttypeof(a[0][0]): "<<typeid(a[0][0]).name()<<endl;
system("pause");
return 0;
}
执行结果:
A4_i的意思就是类型为含有4个int元素的array,也就是数组类型;
PA4_i的意思就是类型为含有4个int元素的array pointer,也就是数组指针类型;
同理A3_A4_i的意思就是类型为含有3x4个int元素的整形数组类型;
而PA3_A4_i的意思就是类型为含有3x4个int元素的整形数组指针类型。
可以看到,该测试结果是和表格相符的。
但是,这里就有一个新的问题了,一般不是说数组名是该数组首元素的地址吗,怎么这里 int a[3][4] 的 a 不是一个地址(指针)呢?
2、数组的隐式指针转换
先看两行代码:
int (*p)[4] = a;//这里符合对a是数组首元素地址的理解
p = &a[0]; // 但是这里为什么需要取地址符?
在第一行代码中,变量a的类型是3x4的数组,但是却可以被用来赋值给p,原因就是因为c++内部自定的隐式转换规则:
这个隐式转换直观一点理解就是,把数组类型转换为数组首元素指针类型,所以第一行的代码中,a在被用来赋值时,从int [3][4]类型隐式转换成了 int (*)[4] 类型,赋值正确。
而如果在第二行中,如果把代码改为:
p = a[0];//报错:不能将 "int *" 类型的值分配到 "int (*)[4]" 类型的实体
则会报错:不能将 “int " 类型的值分配到 "int ()[4]” 类型的实体
原因也是因为隐式指针转换,重复一次,这个隐式转换直观一点理解就是,把数组类型转换为数组首元素指针类型,所以这回 a[0] 本来是 int[4] 类型(查第一部分的表可知),在被用来赋值时,从 int[4] 类型隐式转换成了 int * 类型,赋值失败。
而用了取地址符 & 之后,从第一部分内容的表可知,&a[0] 的类型就是 int (*)[4] ,赋值正确。
因此就可以回答第一部分最后的问题了,一开始a[3][4] 的 a 确实不是一个地址(指针),而是在被用来赋值时,编译器内部会做相应的指针隐式转换,转换后的 a 才是一个指向数组首元素的指针。
3、a[1]是下级数组名字吗?
在很多时候,你可以这样认为,但我想不仅仅如此。
有数组 a[3][4] ,使用a[1]时,运算符 [ ] 相当于对数组名做了偏移操作,将指针(在完成前面所述的隐式转换之后 a 就是一个指针)移到了第二个元素的开头,实际上并没有什么所谓的下级数组名,这不过,为了方便学习,一般可以从这个角度理解。
当然本质上在内存中,也并没有所谓的多维数组,所有变量都只是存在于一个有序编址的内存上,这个多维只是我们将地址一次偏移多少,将这个偏移中取出来的数据看做成了一个一维数组,多个这样的偏移成了二维数组,要继续将其分成更小的偏移量存取,那就是更多维了。在底层硬件实现上,机器才不管你一维还是二维呢。
所以,我并不是想否定 a[1] 是下级数组名这种认知,而是要在这基础至上,更加强调“偏移量”这个观点。
4、char型数组指针
前面讲完了int类型数组的一些指针类型问题,放到char等其它基本类型也是适用的。
还是看代码:
char(*aa)[6];
char b[6] = "abcde";
aa = &b;//这个时候aa只能用 char(*)[6]类型的进行赋值,不然会报错
上面这段代码定义了一个含有六个 char 元素的数组指针 aa ,然后用 同类型的 &b对其赋值,注意不能直接用 aa = b ,因为这样 b 就会被隐式转换为 char * 类型的了,而 aa 是 char(*)[6]类型的,所以会赋值失败。
最后还有一段代码:
char a[4] = {'k','k','k','k'};
char b[4] = {'k','k','k','k'};
char c[4] = {'k','k','k','k'};
char *A[3] ;
A[0] = a;
A[1] = b;
A[2] = c;//A[3] = {a,b,c};
让我们来回顾一下,这时候 A[3] 里面的 a,b,c 的数据类型已经从 char [4] 被隐式转换为 char *了。
(本文完,部分内容参考自网络资料,若有不足与错误,望指正)