把Array说透(续一)

1. 写在前面的

前文中,我主要介绍了数组的一些相关知识,希望加深各位对Array的理解,不过,看过Ivony…同学的回复,我发觉自己离说透还有很大的距离,于是就有了下面的文章。在本文中,我也主要来围绕Ivony…同学提出的几点问题来作以说明,问题如下:

A、数组在托管堆内部是怎么存放的?数组元素的位置是连续的么?
B、非零基数组可以和零基数组转换么?
C、int[]与System.Array的关系到底是同一类型?还是基类与派生类的关系?
D、ldelem不检查下标越界么?
E、多维数组可以和零基数组转换么?
F、Array.Copy和CopyTo与手动拷贝性能有多大差距?
G、数组的协变是怎么做到的?
H、数组是如何实现泛型接口(如IList<T>)的?
I、多维数组每一维度长度必须相等么?必须零基么?
J、数组的Length属性到底指示的是什么?

2. 数组内存详解

在这里,我们依然把数组分为零基数组和非零基数组来讨论。

首先来看零基数组的内存分配,废话少说,我们先来看测试代码:

static unsafe void Main(string[] args)
{
    int[] intArr = new int[3];
    intArr[0] = 1;
    intArr[1] = 2;
    intArr[2] = 3;
}

代码本身很简单,接下来单步执行向下看,首先我们来查看一下源代码的汇编代码:

            int[] intArr = new int[3];
00000035  mov         edx,3 
0000003a  mov         ecx,61CD4192h 
0000003f  call        FFFB2140 
00000044  mov         dword ptr [ebp-44h],eax 
00000047  mov         eax,dword ptr [ebp-44h] 
0000004a  mov         dword ptr [ebp-40h],eax 
            intArr[0] = 1;
0000004d  mov         eax,dword ptr [ebp-40h] 
00000050  cmp         dword ptr [eax+4],0 
00000054  ja          0000005B 
00000056  call        624B6B29 
0000005b  mov         dword ptr [eax+8],1 
            intArr[1] = 2;
00000062  mov         eax,dword ptr [ebp-40h] 
00000065  cmp         dword ptr [eax+4],1 
00000069  ja          00000070 
0000006b  call        624B6B29 
00000070  mov         dword ptr [eax+0Ch],2 
            intArr[2] = 3;
00000077  mov         eax,dword ptr [ebp-40h] 
0000007a  cmp         dword ptr [eax+4],2 
0000007e  ja          00000085 
00000080  call        624B6B29 
00000085  mov         dword ptr [eax+10h],3 
        }

在这里,我们就可以清晰地发现,在0x0000005b,0x00000070和0x00000085中,mov操作的目标地址之间是相隔4个Bytes的,也就是一个整数位。接下来我们来进一步证实。

当我们为数组分配过内存地址后,打开即使窗口查看数组所在的内存地址。

image

接下来打开内存窗口还查看0x015cc790内存块的数据:

image

以上是对数组赋值前的情况,赋值后的内存数据如下:

image

在这里可以更清晰地看出,数组元素之间差的正好是4个Bytes,也就是一个整数位。由此,我们可以得出结论。零基数组的元素在内存中是连续排布的。

接下来我们来看一下非零基数组:

由于空间所限,过程如上,就不再发,截图证明:

image

总之,当我们在托管堆中为数组分配内存时,数组占据一段连续的内存空间。

我们知道,当我们在托管堆中初始化一个对象时,每个对象都需要维护一个指针,该指针的作用是指向下一块空闲内存空间,由于对数组的操作经常是循环遍历等操作,这样如果把数组分配到一个连续的内存空间有一下两个好处:

A. 减少内存碎片

B. 节省内存,不需要维护指针

C. 基地址不需要发生变化,只需要改变偏移量即可,在一定程度上也提高了访问的效率。

接下来,我们还需要来补充一下数组在栈上分配内存的情况:

还记得上文中提到的这个关键字吧,stackalloc,就是他了。补充一下,在上文的回复中,有人问到说栈空间上分配的内存是不是也被垃圾回收器回收?这里的栈空间和C语言中的栈一样,没有垃圾回收器,每个变量都有他自己的作用域,当出了作用域后,变量自动销毁,具体的函数执行过程,请参看《深入理解计算机系统》

3. 再论零基数组和非零基数组

我们先来看这样一段代码:

static void Main(string[] args)
{
    int[,] intArr = (int[,])(Array.CreateInstance(typeof(Int32), new int[] { 3,4 }, new int[] { 1,1 }));
    intArr[2, 3] = 1;
}

这段代码没有问题,我们将Array显式地转换成了强类型的二维数组,然后直接访问索引对其复制。

但是我们知道,对弱类型的Array而言,我们不能通过其下标访问他的元素,而只能通过SetValue和GetValue来获得值,但是我们看到SetValue和GetValue访问和设置的值的类型都是Object,这就意味着我们需要对其进行一次装箱或者拆箱。那么我们有没有办法也生成一个强类型的非零基数组呢?

在上文中,我们提到过,.NET Framework的几种数组类型:

一维零基数组:System.Int32[]。一维非零基数组:System.Int32[*]。多维数组:System.Int32[,]。

那么也就是说,我们是否能通过这样的代码来把Array转换成一维非零基数组呢?

static void Main(string[] args)
{
    int[*] intArr = (int[*])(Array.CreateInstance(typeof(Int32), new int[] { 3}, new int[] { 1 }));
}

事实证明是错误的。在CLR via C#中Jeffery有这样一段话:

“C# does not allow you to declare a variable of type string[*],and therefore it is not possible to user C# syntax to access a single-dimensional ,non-zero-based array.”

这段话翻译成中文的意思就是:C#不允许声明一个string[*]类型的变量,因此,我们能够使用C#语法来访问一个非零基一维数组。

通过以上的解释,我们也许又额外明白了一点,Array究竟是数组类型,还是数组类型的基类?

通过上面的一些代码,我们不妨又把数组重新分类为“强类型数组”和“弱类型数组”。而Array就属于弱类型数组。为什么Array是所有数组类型的基类,我没想出办法来如何证明,只是看到Jeffery说了这样一句话:

“All Arrays are Implicitly Derived from System.Array”。

我想这句话可以说明问题了,不过还是希望各位大侠指点如果证明这一点。

未完持续…………

 

posted @ 2009-10-09 23:59  飞林沙  阅读(2944)  评论(18编辑  收藏  举报