前面一篇我们介绍了指针,相信大家对指针不再是那么陌生,虽然在一些大胆的指针强制类型转换上有的读者还不习惯。但是至少大家心里有个数,指针式如此的灵活,以至于你可以操作得比较底层或者根本越过一些语法的限制。这可能也是众多程序员抨击CC++不安全的因素之一。安不安全不是本文想要表达的,这里只需要记住一点,如果你有足够把握,那么你绝对可以毫不犹豫的运用。
本文依然不会离开指针的影子,前面一篇还有没介绍完的,之前本来想在前面一篇介绍,但是发现在本篇介绍更适合一些。数组和指针可以说是两家亲,很多初学的读者对这两者的概念模棱两可。他们之间有什么联系和区别也是很多初学的读者最希望明白的,本文就为解决这个困扰,让指针和数组进一步加深。还是记住我们的出发点,以发散的思维去理解去联想。注重思考过程,这个过程最终只需要用程序来表达而已。
首先还是看看什么是数组,数组即是一段连续的空间(内存空间)。从这句话中,我们可以注意到数组其实就是一段空间,而且还是连续的。好了,此时对数组的基本特征就有个大致的了解了。那么内存空间是怎么样表达出来的呢?很简单:
int a[ 100 ];
char szName[ 16 ];
这两句即为数组了,在这里a为一个拥有100个int类型元素的数组。在这里我们也可以理解int并不是数组a的类型,而是数组内部元素的类型。它表示数组内部每个元素都是32有符号整数。这样想来便联系到了指针,int* p; p代表它指向的内存地址里存放的数据是int型的。第二个szName同理也表示其每个元素的类型就是char型。这样理解对指针数组和数组指针有帮助,先放这里容后介绍。
这里a和szName并没有被初始化,那么它们里面每个元素的值我们可以认为是乱码。也就是说是随便填充的一些值。当然为什么填充这些值也是有道理的,在不同的平台可能填充的值不一样。在windows下通常被填充成类似0xcdcdcdcd或者0xcccccccc之类的。这些值在汇编层面上去理解会更直接,在这里我们就认为它是随便填充的一些值吧。就认为这些值对于我们正常的程序是没有什么用处的。
从程序表现上我们已经知道数组的声明,那么怎么跟指针联系和区别呢?先贴代码:
int* p = a; // 这里a使用上面的a[ 100 ]数组。
我们从前一篇只可以知道,这时指针p指向了数组a的首地址,这里直接将a赋值给了p。那么可以断定这个数组名a即是代表数组的首地址。既然代表的是首地址,那么a可以看成是一个指向这100个int类型元素中首元素所在的内存地址。CC++直接规定数组名字将作为指向数组的首地址的指针:
元素1 <---- a
元素2
元素3
元素4
...
因此,将a直接赋值给p是完全合法可行的。当然也可以不仗着这个规定,我们可以使用:
int* p = &a[ 0 ];
这样也一样可以取得数组的首地址,也就是第一个元素的内存地址。
到这里,我们该思考一下。数组就其本质来讲它就是一段连续的内存空间,哪怕只有一个元素它也是数组,因为这些元素都是放在内存里面的,它肯定就有自己的内存地址,一牵涉到内存地址,那么它就能用一个指针指向它。因此就联系上了指针。而为何要将数组的名作为一个指针看待,其实数组名并不能等同于指针,因为数组名还具有比指针更多的信息,例如:
int array[ 10 ];
int size_a = sizeof( array );
int* pArray = array;
int size_p = sizeof( pArray );
从这两段代码来看,大家应该都知道size_a和size_b的值分别为40和4。这里区别就明显出来了吧。array如果看成是指针,那么size_a就应该为4长度。前面一篇我们介绍了指针变量即是存放了一个32位无符号的整数。因此指针在32位平台下长度可以直接认为是4。那么这里为什么刚好是10个元素的byte数呢?原因很简单,编译器在处理数组名的时候便知道该数组声明了多大空间,就会求出这个数组实际占用了多少字节的内存。而当我们将数组首地址赋值给了pArray指针后,pArray将丢失了数组array的大小。这个丢失现象在比如函数传参数的时候被体现得淋漓尽致,以后咱们讲到函数时再谈。编译器将不会去追根究底也不可能去追究被赋值成了什么。因此,要说数组名完全等同于指针也是不准确的。就其本质可以说是等同的,都是存放的数组的首地址。
在前面指针篇我们并没有谈指针++、--操作。在这里结合数组来阐述一下。指针累加或累减同样跟类型有关,比如:
int* p = a; // a is a[ 100 ]
p++;
p--;
这里累加和累减,说是跟类型有关。那么通过观察或者猜想,我们可以知道p执行一次累加后所保存的内存地址值是向后4个字节。因为类型是int。这里p指向的是数组a的第0个元素的地址,累加后将指向第一个元素。之间间隔4个字节。累减同样也是间隔4个字节。同样类似:
int* p = a; // a is a[ 100 ]
p = p + 2;
p = p + 1;
这些不使用累加的操作,也是要根据类型计算实际的地址应该加多少字节,在这里,+1就等于地址加4,+2就等于加8。如果是其他类型比如结构体类型,累加等操作换算成实际地址的话将一次跳跃结构体大小那么多个字节。以后讲结构体时再一一说明。
问题一: int dis = &a[0] - &a[1]; dis的值是什么?为什么?
从累加和累减这个特性,我们可以猜想设计指针之初,应该是考虑到了指针指向数组这一特性,因此累加和累减就变成了数组访问某个元素的特征。因此这里假如要访问a的某个元素,我们将可以使用:
p[ 0 ] p[ 1 ] ... p[ n ]这种形式。当然用指针操作一定得把握好数组的大小,否则就会读写越界。后果是很严重的。
到这里又有我们值得比较和联想的一点了,比如有这样的代码:
int a[ 100 ];
a++;
( ( int* )a )++;
int var = a[ 0 ];
问题二:上面两个累加这样的代码合法吗?由此是否可以得出数组名和指针的什么差异?
int* p = a; // a[100]
问题三:*p++和(*p)++的区别?
从上面反映的每一个细节,告诉我们应该习惯去思考。去总结让我们模棱两个的东西具有的真实联系和区别。我相信这样对你很有好处。
下面再谈谈指针数组,所谓指针数组就是一个数组里面存放的全是指针,本质其实就是所有元素都是内存地址。也就是32位无符号整数。完全可以当成是unsigned int数组。
int main( void )
{
int a[ 10 ];
int* ap[ 10 ];
int i;
for ( i = 0; i < 10; ++i )
{
ap[ i ] = &a[ i ];
}
return 0;
}
上面的代码中ap就是一个指针数组,每个元素类型为int型数据所在的内存地址(可谓:int型指针)。然后一个循环将a数组的所有元素的内存地址复制给指针数组的每个元素,a数组虽然没有初始化也没有给定元素的值,但是数组一旦声明就已经分配了内存空间。这里是局部数组在栈空间上。因此循环里面取a的每个元素的地址是绝对合法的。ap在声明的时候也没有初始化,里面的每个指针元素的初始值也跟普通数组一样是乱的。假如我们在循环之前有此操作:
int a_var = *( ap[ 0 ] ); // 此句的目的是想去ap第0个元素(指针)所指向的数据。这里不打括号一个效果。
这样将会报错说指针未初始化。假如初始化了,但是初始化成了非法的内存地址,那么这样间接访问也是相当危险的。
上面的循环将每一个a数组元素的地址都赋值给了ap的对应元素。那么:
int* p = a; // a[ 100 ]
p每累加1,p保存的内存地址,能够准确对应于ap中的每个元素的值(内存地址)。这也说明了数组和指针的一个联系。这里又可以总结了:
如果指针指向的是一块合法连续的空间,那么此指针可以当着数组一样来看到,只需注意上限和下限就可以了。有此也可以认为任何指针随便赋值(指向)任何内存地址,都可以用数组下标访问或者累加累减然后间接访问,只要这块内存你需要且合法。
再看看前面所说的void类型的指针。
void* ppp = ( void* )a; // a[ 100 ]
ppp++;
问题四:这句是否合法?为什么?(这点在前面一篇已经说明了原因)
最后再看看数组指针,还是先看代码:
int main( void )
{
int a[ 2 ][ 10 ];
int ( *p )[ 10 ]; // 数组指针
p = a;
p = &a[ 0 ];
p = a[ 1 ];
return 0;
}
在上面的程序中,p为数组指针。顾名思义就是这个指针存放的就是一个数组。从此说法可以推出这个指针指向的就是数组的首地址。这里的p就是指向的一个拥有10个元素的整型数组。中括号里面的10就代表它指向的数组是10元素的。这里我们定义了一个二维数组a,p = a;就将a赋值给了p,咱们再想想其内存关系。
a[ 0 ]: { a[ 0 ][ 1 ], a[ 0 ][ 1 ], ..., a[ 0 ][ 9 ] } <------p[ 0 ]
a[ 1 ]: { a[ 1 ][ 1 ], a[ 1 ][ 1 ], ..., a[ 1 ][ 9 ] } <------p[ 1 ]
因此,p跟a的内存关系就清晰了。p指向的即是一维数组,这里是二维数组a。二维数组可以看成几行几列,每一行就是一个一维数组。p就可以称作是行指针。大家应该明白了吧!那么p++将会跳跃一行那么多字节,这里一行是10个int元素,那么就是40个字节。大家可以自己写程序观察。p = &a[ 0 ]; 即表示将二维数组的第1行赋值给p。同样可以将第一行赋值给p, p = &a[ 1 ]; 由此更形象说明p乃行指针了。
问题五:上面的程序中:p = a[ 1 ];这句合法吗?为什么?
有的读者又会提出一个疑问了,有没有指向二维数组的指针呢?这个大家可以在空间中想象一下,假如指向二维数组,那么指针累加将是增加立方体的厚度(三维空间,三维数组)。就好比我们吃的三明治,三片中的一片就代表我们的二维数组指针。这里我们不做讨论。
同样在这里还得说明一下二级指针和二维数组的联系和区别。同样区别指针和数组名在前面已经说过。我们这里只看看二维数组到二级指针后的访问取值及内存关系。
如果你尝试:
int a[ 2 ][ 10 ];
int** p = ( int** )a;
这样将带来灾难,语法是没有问题,本来我们的a数组里面是整数,被强制转换成指针后。p[ 0 ]就是a的第一行第一个元素的值,然后再p[0][0]就是取p[ 0 ]的值作为内存地址存放在里面的那个值。因此除非你的这个a[0][0]是你掌握的数据,否则你这样用将带来毁灭性的后果——崩溃。然而在这里p[0]给了我们一个启示,我们可以再写一段代码:
int main( void )
{
char* ap[ 3 ] = { "hello", "happy", "good" };
char** app1 = ap;
return 0;
}
这里定义了一个指针数组ap,然后转换成char**二级指针。我们通过下标运算来看关系。
首先ap也可以看成是一个3行6列的二维数组。因此:
ap[ 0 ][ 0 ]的值就为'h'字符。
app[ 0 ][ 0 ]的值也是'h'字符。
相信你已经明白了二维数组和二级指针的区别和关系了。
将一个二维数组直接强制类型转换成二级指针,这样会出问题,因为这样一转。你用二级指针下标访问第一行数据时,第一行第一个元素的地址将被认为是当前存放的值作为下一级的指针值(地址)。也就是说:
char ap[ 3 ][ 6 ] = { "hello", "happy", "good" };
char** app1 = ( char** )ap;
那么,app1[ 0 ](也就是*app1, *(*app1)表示app1[0][0])的值(内存地址)就变成了"hello"字符串的前4字节"hell"逐字符对应的ASCII码了:0x6c6c6568。
68 65 6c 6c
h e l l
至于0x6c6c6568为什么给倒过来了,是因为我的CPU是小端存储,高字节被认为是整数的高位,低字节被认为是整数的低位。这下大家知道二维数组和二级指针的关系了吧。
这里举例是一个char数组,大家也可以举一个int型的数组。这里就不写了!
好了,本篇就到此吧!又写了4个多小时了。已经是第二天0点多了。
上面提的几个问题还是比较值得初学的读者思考的,加油!
【C/C++入门篇系列】