让我们用C/C++的术语来讨论这个问题;虽然有一些关于C#数组的额外知识,但它并不是真正相关的重点。
给定一个16位整数值的数组:
short[5] myArray = {1,2,3,4,5};
实际发生的是计算机在内存中分配了一块空间。这个内存块为该数组保留,恰好足够容纳整个数组(在我们的例子中为16*5 == 80位 == 10字节),并且是连续的。这些事实是确定的;如果在任何给定时间您的环境中有任何一个或多个不正确,您的程序通常会因访问冲突而崩溃。
因此,考虑到这个结构,变量myArray
背后的真正含义是内存块的起始地址。这也是第一个元素的起始位置。每个额外的元素都按顺序排列在第一个元素之后的内存中。为myArray
分配的内存块可能如下所示:
00000000000000010000000000000010000000000000001100000000000001000000000000000101
^ ^ ^ ^ ^
myArray([0]) myArray[1] myArray[2] myArray[3] myArray[4]
访问内存地址并读取固定数量的字节被认为是一个常数时间操作。如上图所示,如果您知道三件事情:内存块的起始位置、每个元素的内存大小和您想要的元素的索引,您可以获取每个元素的内存地址。因此,当您在代码中请求myArray[3]
时,该请求将通过以下方程式转换为内存地址:
myArray[3] == &myArray+sizeof(short)*3;
因此,通过常数时间计算,您已经找到了第四个元素(索引3)的内存地址,并且通过另一个常数时间操作(或者至少被认为是这样;实际访问复杂度是硬件细节,足够快,您不应该关心),您可以读取该内存。如果您曾经想过,这就是为什么大多数C风格语言中集合的索引从零开始的原因;数组的第一个元素从数组本身的位置开始,没有偏移量(sizeof(anything)* 0 == 0)
在C#中,有两个显着的区别。 C#数组具有对CLR有用的一些标头信息。标头首先出现在内存块中,其大小是恒定且已知的,因此寻址方程只有一个关键差异:
myArray[3] == &myArray+headerSize+sizeof(short)*3;
C#不允许在托管环境中直接引用内存,但是运行时本身将使用类似于这样的东西来执行堆外存储器访问。
第二个常见的问题,对于大多数C/C++的变体也是普遍存在的,就是某些类型总是按引用处理。必须使用new
关键字创建的所有内容都是引用类型(还有一些对象,如字符串,在代码中看起来像值类型,但实际上也是引用类型)。当实例化引用类型时,它被放置在内存中,不会移动,并且通常不会被复制。表示该对象的任何变量因此在幕后只是内存中对象的内存地址。数组是引用类型(记得myArray只是一个内存地址)。引用类型的数组是这些内存地址的数组,因此访问作为数组元素的对象是一个两步过程;首先计算数组元素的内存地址,然后获取它。那是另一个内存地址,是实际对象的位置(或者至少是可变数据的位置;如何在内存中构造复合类型是一个完全不同的问题)。这仍然是一个常数时间操作;只是比一步多了两步。