数组与运算符*、&和[],行指针和列指针的概念
一、一维数组
#include<iostream>
int main(int argc, char *argv[])
{
int a[5] = { 1, 2, 3, 4, 5 };
std::cout << "a : " << a << "\t\t&a[0]: " << &a[0] << "\t\t*a\t: " << *a << "\ta[0]: " << a[0] << std::endl;
std::cout << "a + 1: " << a + 1 << "\t\t&a[1]: " << &a[1] << "\t\t*(a + 1): " << *(a + 1) << "\ta[1]: " << a[1] << std::endl;
std::cout << "a + 2: " << a + 2 << "\t\t&a[2]: " << &a[2] << "\t\t*(a + 2): " << *(a + 2) << "\ta[2]: " << a[2] << std::endl;
std::cout << "a + 3: " << a + 3 << "\t\t&a[3]: " << &a[3] << "\t\t*(a + 3): " << *(a + 3) << "\ta[3]: " << a[3] << std::endl;
std::cout << "a + 4: " << a + 4 << "\t\t&a[4]: " << &a[4] << "\t\t*(a + 4): " << *(a + 4) << "\ta[4]: " << a[4] << std::endl;
return 0;
}
对于一维数组typename arrayname[n],数组名arrayname指向该数组中首元素的起始地址,其类型为typename*(列指针)
经验1:arrayname+i(i<n)指向该数组的第i个元素,对arrayname+i解引用即*(arrayname+i)得到数组中第i个元素的值,即*(arrayname+i)等价于arrayname[i]。
经验2:对于一维数组来说,重载运算符[i]作用于数组名arrayname上的含义是将指向数组首元素地址的指针向后移动i位,然后解引用。
经验:3:对数组名arrayname取地址即&arrayname得到的指针将指向整个一维数组,其类型为typename(*)[](行指针)。
有趣的是:虽然有些指针指向相同的地址,但其类型却完全不同。
对于其他类型,虽然类型大小不同,但行为完全相同。
二、二维数组
#include<iostream>
int main(int argc, char *argv[])
{
int a[2][5] = {
{ 1, 2, 3, 4, 5 },
{ 6, 7, 8, 9, 10 }
};
std::cout << "打印每个元素的指针\t对数组名解引用\t 对数组名进行重载运算符[]\t对数组名解引用\t\t对数组名进行重载运算符[]" << std::endl;
for(int i= 0; i < 2; ++i)
for (int j = 0; j < 5; ++j)
{
if (i == 0)
{
std::cout << "&a[" << i << "][" << j << "]: " << &a[i][j] << "\t(*a)+" << j << ": " << (*a) + j << "\ta[0]+" << j << ": " << a[0] + j << "\t*(a+" << i << ")+" << j << ": " << *(a + i) + j << "\ta[1]-" << 5 - j << ": " << a[1] - 5 + j << std::endl;
}
else if (i == 1)
{
std::cout << "&a[" << i << "][" << j << "]: " << &a[i][j] << "\t(*a)+" << 5 + j << ": " << (*a) + 5 + j << "\ta[0]+" << 5 + j << ": " << a[0] + 5 + j << "\t*(a+1)+" << j << ": " << *(a + 1) + j << "\ta[1]+" << j << ": " << a[i] + j << std::endl;
}
}
std::cout <<"以上都是列指针,其类型为int*\n\n";
std::cout << "a指向行\t\t对a[0]取地址即&a[0]也指向行\t对a[1]取地址即&a[1]也指向行\n";
for (int i = 0; i < 10; ++i)
{
std::cout << "a+" << i << ": " << a + i << "\t&(a[0])+" << i << ": " << &(a[0]) + i;
if (i < 1)
{
std::cout << "\t\t&(a[1])-" << 1 << ": " << &(a[1]) - 1 << std::endl;
}
else
{
std::cout << "\t\t&(a[1])+" << i - 1 << ": " << &(a[1]) + i - 1 << std::endl;
}
}
std::cout << "以上都是行指针,其类型为int(*)[5]\n\n";
std::cout << "对数组名取地址即&a,指向整个数组\n";
std::cout << "(&a)-1: " << (&a) - 1 << std::endl;
std::cout << "&a : " << &a << std::endl;
std::cout << "(&a)+1: " << (&a) + 1 << std::endl;
std::cout << "以上都是数组指针,其类型为int(*)[2][5]\n\n";
return 0;
}
对于二维数组typename arrayname[i][j],数组名arrayname指向该二维数组中第一个一维数组的起始地址或者说指向该二维数组中第一个整个一维数组,其类型为typename(*)[](行指针)。
类似于经验1, 对数组名取地址,即&arrayname,指向整个二维数组,其类型为typename(*)[][](数组指针)。
对数组名解引用,即*arrayname,指向二维数组中首个元素的地址,其类型为typename*(列指针)。
arrayname+i(i<n)指向该二维数组的第i个整个一维数组(行指针),对arrayname+i解引用即*(arrayname+i)指向该二维数组的第i个一维数组的首元素即arrayname[i][0],其类型为typename*(列指针)等价于&arrayname[i][0]。
类似于经验2,对于二维数组来说,重载运算符[i]作用于数组名arrayname上的含义是将指向该二维数组中首个一维数组的指针(typename(*)[](行指针))向后移动i位,然后解引用(typename*(列指针))。
对于其他类型,虽然类型大小不同,但行为完全相同。
三、三维数组
在头文件中定义一个三维数组
//header.h
int a[3][3][3] =
{
{
{ 1,2,3 },
{ 4,5,6 },
{ 7,8,9 }
},
{
{ 10,11,12 },
{ 13,14,15 },
{ 16,17,18 }
},
{
{ 19,20,21 },
{ 22,23,24 },
{ 25,26,27 }
}
};
首先,对数组名取地址
#include"header.h"
int main(int argc, char *argv[])
{
std::cout << "&a: " << &a << "\t&a+1:" << &a + 1 << "\t&a即" << &a << "移动了" << (int)(&a + 1) - (int)(&a) << "个字节后到达&a+1即" << &a + 1 << std::endl;
return 0;
}
输出结果表明对数组名a取地址,从&a+1移动的长度来看,它指向a[3][3][3]整个数组,类型是int(*)[][][]。这表明数组名携带了整个数组的信息。所以,&a的类型是int(*)[3][3][3]。
然后对数组名做算术运算
#include"header.h"
int main(int argc, char *argv[])
{
std::cout << "该数组元素a[0][0][0]的地址&a[0][0][0]:" << &a[0][0][0] << "\n该数组元素a[2][0][0]的地址&a[2][0][0]:" << &a[2][0][0] << std::endl << std::endl;
for (int i = 0; i < 3; ++i)
{
std::cout << "a+" << i << ":" << a + i;
if (i > 0)
{
std::cout << "\ta+" << i - 1 << "即" << a + i - 1 << "移动了" << (int)(a + i) - (int)(a + i - 1) << "个字节后到达a+" << i << "即" << a + i;
}
std::cout << std::endl;
std::cout << "a[" << i << "]:" << a[i];
if (i > 0)
{
std::cout << "\ta[" << i - 1 << "]即" << a[i - 1] << "移动了" << (int)(a[i]) - (int)(a[i - 1]) << "个字节后到达a[" << i << "]即" << a[i];
}
std::cout << std::endl;
std::cout << "&a[" << i << "]:" << &a[i];
if (i > 0)
{
std::cout << "\t&a[" << i - 1 << "]即" << &a[i - 1] << "移动了" << (int)(&a[i]) - (int)(&a[i - 1]) << "个字节后到达&a[" << i << "]即" << &a[i];
}
std::cout << std::endl << std::endl;
}
return 0;
}
从输出结果来看,数组名a指向的是该数组中第一个元素,可是第一个元素是一个二维数组,所以a+1移动了36个字节。a[i]表示取数组a中第i个元素,a[1]取数组a中的第二个元素,可是第二个元素也是一个二维数组,所以a[1]+1也移动了36个字节。对a[1]取地址后,从&a[1]+1移动36个字节来看,&a[1]指向第二个二维数组整个数组。与数组名a不同的是,a[1]只携带了一个二维数组的信息,但是类似于数组名a,a[1]相当于数组
{
{ 10,11,12 },
{ 13,14,15 },
{ 16,17,18 }
}
的数组名,它指向这个数组中的第一个元素即一维数组{ 10,11,12 }(这个紧接着就会讨论到),对它取地址即&a[1]就指向整个数组。
综上所述,a和&a[0]具有共同的类型int(*)[3][3],而a[0]的类型是int(*)[3](这个紧接着就会讨论到)。
接着,进入第一个二维数组
#include"header.h"
int main(int argc, char *argv[])
{
std::cout << "该数组元素a[0][0][0]即" << a[0][0][0] << "的地址&a[0][0][0]:" << &a[0][0][0]
<< "\n该数组元素a[0][1][0]即" << a[0][1][0] << "的地址&a[0][1][0]:" << &a[0][1][0]
<< "\n该数组元素a[0][2][0]即" << a[0][2][0] << "的地址&a[0][2][0]:" << &a[0][2][0] << std::endl << std::endl;
for (int i = 0; i < 3; ++i)
{
std::cout << "a[0][" << i << "]:" << a[0][i] << "\t对a[0][" << i << "]解引用即*a[0][" << i << "]得到" << *a[0][i];
std::cout << std::endl;
std::cout << "a[0]+" << i << ":" << a[0] + i;
if (i > 0)
{
std::cout << "\t\t\t\t\t\ta[0]+" << i - 1 << "即" << a[0] + i - 1 << "移动了" << (int)(a[0] + i) - (int)(a[0] + i - 1) << "个字节后到达a[0]+" << i << "即" << a[0] + i;
}
std::cout << std::endl;
std::cout << "*(a+0)+" << i << ":" << *(a + 0) + i;
if (i > 0)
{
std::cout << "\t\t\t\t\t*(a+0)+" << i - 1 << "即" << *(a + 0) + i - 1 << "移动了" << (int)(*(a + 0) + i) - (int)(*(a + 0) + i - 1) << "个字节后到达*(a+0)+" << i << "即" << *(a + 0) + i;
}
std::cout << std::endl << std::endl;
}
return 0;
}
注意,a[0]相当于第一个二维数组的数组名,对数组名取地址指向整个二维数组,但是它本身却指向数组内的第一个一维数组,从a[0]+1移动12个字节就可以验证这一点。
那么取a[0]这个二维数组中的第一个元素a[0][0],可是第一个元素是一个一维数组 {1,2,3 },类似地,a[0][i](0<i<3)相当于一维数组的数组名,它只携带了这个一维数组的信息,&a[0][i](0<i<3)指向整个一维数组,但是它本身指向一维数组中的第一个元素,所以对a[0][i](0<i<3)解引用即*a[0][i](0<i<3),分别取值1、4、7,得证。
既然a+0指向第一个二维数组,那么对a+0解引用即*(a+0)+0它就指向第一个二维数组中的第一个一维数组{ 1,2,3 },从*(a+0)+0移动了12个字节到*(a+0)+1可以验证这一点。
综上所述,a[0]、*a和&a[0][i](0<i<3)具有相同的类型int(*)[3],而a[0][i]的类型是int(*)。
接着进入这个三维数组中的第一个二维数组中的第一个一维数组
#include"header.h"
int main(int argc, char *argv[])
{
std::cout << "该数组元素a[0][0][0]即" << a[0][0][0] << "的地址&a[0][0][0]:" << &a[0][0][0]
<< "\n该数组元素a[0][0][2]即" << a[0][0][2] << "的地址&a[0][0][2]:" << &a[0][0][2] << std::endl << std::endl;
for (int i = 0; i < 3; ++i)
{
std::cout << "a[0][0][" << i << "]:\t" << a[0][0][i] ;
std::cout << std::endl;
std::cout << "a[0][0]+" << i << ":\t" << a[0][0] + i;
if (i > 0)
{
std::cout << "\ta[0][0]+" << i - 1 << "即" << a[0][0] + i - 1 << "移动了" << (int)(a[0][0] + i) - (int)(a[0][0] + i - 1) << "个字节后到达a[0][0]+" << i << "即" << a[0][0] + i;
}
std::cout << std::endl;
std::cout << "*(a[0][0]+" << i << "):\t" << *(a[0][0] + i);
std::cout << std::endl;
std::cout << "*(*(a+0)+0)+" << i << ":\t" << *(*(a + 0) + 0) + i;
if (i > 0)
{
std::cout << "\t*(*(a+0)+0)+" << i - 1 << "即" << *(*(a + 0) + 0) + i - 1 << "移动了" << (int)(*(*(a + 0) + 0) + i) - (int)(*(*(a + 0) + 0) + i - 1) << "个字节后到达*(*(a+0)+0)" << i << "即" << *(*(a + 0) + 0) + i;
}
std::cout << std::endl;
std::cout << "*(*(*(a+0)+0)+" << i << "):" << *(*(*(a + 0) + 0) + i);
std::cout << std::endl << std::endl;
}
}
a[0][0]相当于这个三维数组中的第一个二维数组中的第一个一维数组的数组名,&a[0][0]即指向这个一维数组整个数组(读者可以自行验证),但是a[0][0]却指向一维数组中的第一个元素,而这个第一个元素是一个整型数值1,对a[0][0]进行移动并且解引用即可验证它的指向。直接取值即a[0][0][0]、a[0][0][1]、a[0][0][1]分别得到1、2、3,这符合通常的取值。
既然*(a+0)+0指向第一个二维数组中的第一个一维数组的第一个元素,而这个元素是一个整型值,那么对(*(a+0)+0)进行移动并且解引用也能验证它的指向。
综上所述,&a[0][0][i](0<i<3)与(*(a+0)+0)+i(0<i<3)具有相同的类型int(*)。
四、四维数组
现在,只要对[]、*和&熟练地运用,就能很快速地解析出指针的类型与它的指向。
首先在头文件中定义一个四维数组
//header.h
int a[2][4][3][3] =
{
{
{
{ 1,2,3 },
{ 4,5,6 },
{ 7,8,9 }
},
{
{ 10,11,12 },
{ 13,14,15 },
{ 16,17,18 }
},
{
{ 19,20,21 },
{ 22,23,24 },
{ 25,26,27 }
},
{
{ 28,29,30 },
{ 31,32,33 },
{ 34,35,36 }
}
},
{
{
{ 51,52,53 },
{ 54,55,56 },
{ 57,58,59 }
},
{
{ 60,61,62 },
{ 63,64,65 },
{ 66,67,68 }
},
{
{ 69,70,71 },
{ 72,73,74 },
{ 75,76,77 }
},
{
{ 78,79,80 },
{ 81,82,83 },
{ 84,85,86 }
}
}
};
例如
*(*(*(*(a + 0) + 1) + 1) + 2),由内向外解析:(a + 0)表示指向这个四维数组的第一个三维数组,类型为int(*)[3][3][3],对它解引用后即*(a + 0)就指向三维数组中的第一个二维数组,类型为int(*)[3][3],+1表示移动“一位”,即*(a + 0) + 1指向这个三维数组中的第二个二维数组,类型为int(*)[3][3],解引用后即*(*(a + 0) + 1) 就指向这个二维数组中的第一个一维数组,类型为int(*)[3],+1表示移动“一位”,即(*(*(a + 0) + 1) + 1指向这个二维数组中的第二个一维数组,类型仍然为int(*)[3],解引用后即*(*(*(a + 0) + 1) + 1)指向这个一维数组中的第一个元素,类型为int(*),+2表示移动“两位”,即*(*(*(a + 0) + 1) + 1) + 2指向这个一维数组中的第三个元素即15。
验证:
#include"header.h"
int main(int argc, char *argv[])
{
std::cout << *(*(*(*(a + 0) + 1) + 1) + 2) << std::endl;
return 0;
}
注意数组的表示形式,再例如:
(*(*(a[1]+2)+2))[2],
a[1]指向这个四维数组中的第二个三维数组的第一个二维数组,类型为int(*)[3][3]。
a[1]+2指向这个四维数组中的第二个三维数组中的第三个二维数组,类型仍为int(*)[3][3]。
*(a[1]+2)指向这个四维数组中的第二个三维数组中的第三个二维数组的第一个一维数组,
类型为int(*)[3]。
*(a[1]+2)+2指向这个四维数组中的第二个三维数组中的第三个二维数组的第三个一维数组,
类型仍为int(*)[3]。
*(*(a[1]+2)+2)指向这个四维数组中的第二个三维数组中的第三个二维数组的第三个一维数组的第一个元素,类型为int(*)。
(*(*(a[1]+2)+2))[2]取这个四维数组中的第二个三维数组中的第三个二维数组的第三个一维数组的第三个元素的值即77。
验证:
#include"header.h"
int main(int argc, char *argv[])
{
std::cout << (*(*(a[1] + 2) + 2))[2] << std::endl;
return 0;
}
五、n维数组
对于n维数组的情形:
第一点是对数组名取地址,类型是typename(*)[]…(总共n个[])它指向这个n维数组整个数组。数组名携带了整个数组的全部信息,数组名指向这个n维数组中的第一个(n-1)维数组,(数组名+i)指向这个n维数组中的第i个(n-1)维数组。
第二点是重载运算符[],传入参数i即[i],它的含义是取n维数组中的第i个元素(不妨设这个元素是m维数组),相当于指向这个m维数组中的第一个(m-1)维数组。(数组名[i])的类型是typename(*)[]…(总共m-1个[]),(数组名[i])也可以理解成是这个n维数组中第i个(n-1)维数组的数组名,类型是typename(*)[]…(总共n-2个[])
第三点是要牢记运算符*和运算符&互为逆操作。*解引用当然是根据地址取值,&取地址当然是根据变量的值取址。
第四点是优先级与结合性,当分不清优先级时一个有用的技巧就是加小括号。
参考
《C语言程序设计》(第3版)(谭浩强,清华大学出版社)
《C++程序设计教程》(第3版)(王珊珊,臧洌,张志航,机械工业出版社)
《C++ Primer Plus》(Six Edition)(Stephen Prata)
《C和指针》(Kenneth A. Reek)
广大的CSDN社友们的文章