`
RednaxelaFX
  • 浏览: 3016993 次
  • 性别: Icon_minigender_1
  • 来自: 海外
社区版块
存档分类
最新评论

数组……Geez,我总是弄混

阅读更多
来来回回已经碰到这问题不知道多少次了,但每过一段时间总得把它弄混。郁闷啊。还是得记下来才行,不然每次都翻规范太痛苦了。

C/C++里的数组是“矩形”(rectangle)的,也就是说数组每个维度中的元素的长度都一样。
#include <stdio.h>

void main() {
    int arr[2][3] = { { 1, 2, 3 }, { 4, 5, 6 } };
    // int arr[][3] = { { 1, 2, 3 }, { 4, 5, 6 } };
    printf("%d", arr[0][1]); // 2
    // printf("%d", *(*(arr + 0) + 1)); // 2
}

下标的顺序是:下标从左向右指定的是数组从外向内的维度的长度。跟有初始化器时,声明的最外层的维度可以留空,也就是说最左边的下标可以留空。没有初始化器时,声明的所有维度都必须指定。其中C89是只允许以常量来指定数组维度的长度,C99和GCC扩展则允许使用变量来指定。至于为什么VC从6到9都无法编译这段代码:
void main() {
    int n = 2;
    int arr[n][3];
}

大概只是因为对C99支持得不好吧。用GCC开-std=c99也照样能编译的。
对多维数组的指针运算跟下标是对应的。
C99对数组声明的规定真是繁琐得不行……在什么范围允许留空,或者允许*,或者允许可变长度,Geez。还是要用到那么麻烦的东西的时候才去查C99规范的6.7.5.2好了。
C/C++的初始化器里元素的个数可以比对应维度的长度要少,此时剩余的元素与静态存储级的变量一样会被默认初始化(例如说算术类型的话会初始化为0)。
#include <stdio.h>

void main() {
    int i, j;
    int arr[2][3] = { { 1, 2 }, { 4 } };
    for (i = 0; i < 2; ++i) {
        for (j = 0; j < 3; ++j) {
            printf("%d\n", arr[i][j]);
        }
    }
}

// 1
// 2
// 0
// 4
// 0
// 0


C#和Java的初始化器就不能省略任何元素,必须与new表达式里指定的维度/长度匹配。

C#同样支持矩形数组。
using System;

static class Program {
    static void Main( string[ ] args ) {
        var arr = new int[ 2, 3 ] { { 1, 2, 3 }, { 4, 5, 6 } };
        // var arr = new int[ , ] { { 1, 2, 3 }, { 4, 5, 6 } };
        Console.WriteLine( arr[ 0, 1 ] ); // 2
    }
}

下标的顺序与C/C++里的一样。new的时候要么所有维度都不指定长度,要么所有维度都要指定长度。指定长度的版本应用的是这条语法规则:
new non-array-type [ expression-list ] rank-specifiersopt array-initializeropt
不指定长度的应用的是则是另一条:
new array-type array-initializer

C#也支持所谓锯齿形数组(jagged array),它只要求总的维度数量与声明吻合,不要求每个维度的元素长度相等。
using System;

static class Program {
    static void Main( string[ ] args ) {
        var arr = new int[ 2 ][ ] { new [ ] { 1, 2, 3 }, new [ ] { 4, 5, 6 } };
        // var arr = new int[ ][ ] { new [ ] { 1, 2, 3 }, new [ ] { 4, 5, 6 } };
        Console.WriteLine( arr[ 0 ][ 1 ] ); // 2
    }
}

C#规定锯齿形数组在new的时候,如果没有跟初始化器,则必须指定最外层的维度,其余的维度必须留空。如果跟有初始化器,则最外层的维度可以留空。
注意到锯齿形数组的初始化器无法用矩形数组的简写语法,内部的数组也必须写成new表达式。
顺带一记:.NET的数组可以分为SZArray和普通Array两种,前者是single-dimensional zero-based array,在CLI术语中也叫vector,只有这种数组有直接操作的MSIL/CIL指令;后者是多维数组或者下标不从0开始的数组,其相关操作都通过库方法来完成。

Java只支持锯齿形数组,多维数组实际上是数组的数组。
public class Program {
    public static void main(String[] args) {
        int[][] arr = new int[][] { { 1, 2, 3 }, { 4, 5, 6 } };
        System.out.println(arr[0][1]); // 2
    }
}

在new表达式里,如果没有跟初始化器,则最外层的维度必须指定,其余的维度可以指定也可以留空。如果跟有初始化器,则所有维度都必须留空。
除了通过ArrayCreationExpression之外,Java还允许直接用ArrayInitializer来创建数组实例:
public class Program {
    public static void main(String[] args) {
        int[][] arr = { { 1, 2, 3 }, { 4, 5, 6 } };
        System.out.println(arr[0][1]); // 2
    }
}

但ArrayInitializer必须在数组变量的声明中、或者作为ArrayCreationExpression的一部分来使用,而不能作为任意表达式使用。也就是说不能这样写:
int[] array;
array = { 0 };

Java的数组最多只能有255维。在创建多维数组时如果只指定了最外层维度的长度,会使用newarray/anewarray指令;如果指定了多于一维的长度,则使用multianewarray指令。
Java的数组变量声明时可以将表示数组的方括号跟在元素类型后作为类型的一部分,也可以跟在变量名后作为变量声明的修饰;Java中惯用的写法是前一种。

主要就是这几种看起来很像的语言的数组微妙的不同让我总是弄混 T T
到底哪里必须指定,哪里必须留空,哪里是可指定可留空……|||

其实最关键的还是“什么是可以单独存在的对象”的问题吧。这里的对象指的是广义的对象。

C和C++里的多维数组是一个整体,代表一块连续的存储空间。
声明数组的时候,C/C++关心的是“要分配多少空间”。在没有初始化器时,当然只能通过指定所有维度的长度才能计算出要分配的空间大小。有初始化器时,可以通过初始化器中元素的个数来得到最外层维度的长度,所以可以给最外层维度的长度声明留空。内层维度必须指定长度这点恐怕是为了编译方便?

C#的矩形数组也是单一的对象,指向一块连续的存储空间。

C#和Java的锯齿形数组中每个维度都是连续的存储空间,但除了最内层的一维之外,其它维度的数组保存的是指向数组的引用。这些引用确实存在,而不像C/C++中取中间维度的地址时是算出来的。
由于数组长度不影响类型匹配(数组维度和元素类型才影响),如果数组的元素是指向数组的引用,那么这些元素指向的数组的长度是多少都可以。
所以C#不允许在锯齿形多维数组的new表达式中指定除最外层维度以外的维度长度。
Java……理由是一样的但为什么语法规则就是不同呢……

说来,最近才注意到LINQv1和LINQv2都不支持矩形多维数组的初始化……NewArrayInit只能用来初始化一维数组,嵌套使用可以初始化锯齿形多维数组,但矩形多维数组就只能用NewArrayBounds来创建新实例,却不支持对应的初始化。太郁闷了。

====================================================================

F#/OCaml的多维数组也是锯齿形数组,每个维度的元素长度可以不同。
let arr = [| [| 1 |]; [| 2; 3 |] |];; // val arr : int array array
arr.[0];; // val it : int array

F#在访问数组元素的时候跟OCaml的不一样 OTL
let arr = [| [| 1 |]; [| 2; 3 |] |];; (* arr : int array array *)
arr.(0);; (* - : int array *)


Standard ML没有数组的字面量。要用数组的话可以使用标准库里的array,也可以用许多SML实现所支持的vector。不过用下标访问数组元素的时候还是得用库函数:
#[#[1], #[2,3]]; (* val it = #[#[1],#[2,3]] : int vector vector *)
Vector.sub (it, 0); (* val it = #[] : int vector *)

不过ML家族里的语言许多情况下用list和tuple就行了,毕竟不可变的数据类型在许多情况下就够用了。

Scala的数组也是锯齿形的。
def arr = Array(Array(1), Array(2, 3)) // Array[Array[Int]]
arr(0) // Array[Int]


Haskell的数组跟SML一样是标准库里的,没有字面量。要使用数组的话需要import Array。要初始化一个无规则的数组看起来挺麻烦的:
arr = array (0,1) [(0,array (0,0) [(0,1)]),(1,array (0,1) [(0,2),(1,3)])] -- Array Integer (Array Integer Integer)
arr!1!0 -- 2 :: Integer

内容有规则的数组的话,用lambda或者用list comprehension来初始化都挺方便的:
squares = array (1,100) [(i, i*i) | i <- [1..100]]
squares = mkArray (\i -> i * i) (1,100)


至于像是Ruby、JavaScript这些动态语言的数组,本来也就没有多不多维的问题,反正数组里什么都能装,自然也能容纳数组作为元素。
说来Python里[...]语法所指的序列是叫list而不是array啊,不过支持的操作都一样,没差。而且有切片用真方便。

然后还有FORTRAN的数组?老爸以前倒是经常用FORTRAN,但我就从来没学过这历史悠久的语言。只是在用Python的NumPy库时留意到一段说明,说C的二维数组是行优先存储,而FORTRAN的是列优先存储的,多维同理。另外FORTRAN的数组的下标默认是从1开始的。
分享到:
评论
1 楼 RednaxelaFX 2009-08-18  
Eric Lippert在这帖讲解了C#的数组声明,可以一读

相关推荐

Global site tag (gtag.js) - Google Analytics