多维数组在内存中的格式是怎样的?

216

在C语言中,我知道可以使用以下代码在堆上动态分配二维数组:

int** someNumbers = malloc(arrayRows*sizeof(int*));

for (i = 0; i < arrayRows; i++) {
    someNumbers[i] = malloc(arrayColumns*sizeof(int));
}

显然,这实际上创建了一个指向许多单独的一维整数数组的指针的一维数组,并且当我请求时,“系统”可以理解我的意思:

someNumbers[4][2];

但是当我像下面这行代码一样静态地声明一个二维数组时...

int someNumbers[ARRAY_ROWS][ARRAY_COLUMNS];

类似的结构是否在堆栈上创建,还是完全不同的形式? (即,它是指针的一维数组吗?如果不是,它是什么,如何找到对它的引用?)

另外,当我说“系统”时,实际上是谁负责解决这个问题?内核?还是C编译器在编译时解决?


2
@toohonestforthissite 的确如此。更进一步地说:循环调用 malloc() 并不会得到一个 N 维数组,而是得到指向完全独立的一维数组的指针数组 [指向指针数组[...]]。请参见 正确分配多维数组 以了解如何分配真正的 N 维数组。 - Andrew Henle
6个回答

174

一个静态的二维数组看起来像一个数组的数组 - 它只是在内存中连续地放置。 数组并不等同于指针,但是因为你可以经常将它们基本上互换使用,所以有时会感到混淆。 不过编译器会正确跟踪这些,这使得一切都能很好地对齐。 但是要小心像你提到的静态2D数组,因为如果尝试将其传递给一个带有 int ** 参数的函数,那么将会发生糟糕的事情。 下面是一个快速的例子:

int array1[3][2] = {{0, 1}, {2, 3}, {4, 5}};

在内存中看起来像这样:

0 1 2 3 4 5

完全相同于:

int array2[6] = { 0, 1, 2, 3, 4, 5 };

但是如果你尝试将array1传递给这个函数:

void function1(int **a);

你会收到一个警告(应用程序将无法正确访问数组):

warning: passing argument 1 of ‘function1’ from incompatible pointer type

因为二维数组不同于int **。数组自动降解为指针只会发生在一层级上。你需要这样声明函数:

void function2(int a[][2]);
或者
void function2(int a[3][2]);
使一切顺畅。 这个概念同样适用于n维数组。在你的应用程序中利用这种有趣的特性通常只会使它更难理解。所以要小心。

感谢您的解释。 那么"void function2(int a[][2]);"将接受静态和动态声明的二维数组吗?如果第一维保留为[],传递数组长度仍然是良好实践/必要的吗? - Chris Cooper
1
@Chris 我不这么认为 - 你会很难将C语言中的堆栈或全局分配的数组转换成一堆指针。 - Carl Norum
当然,a[][]应该可以工作!在C中,每个数组实际上只是一个带有偏移量的指针,内存分配是唯一的真正区别。我不会期望上面提到的a**的警告会阻止您编译,尽管这取决于编译器和设置。 - Jason K.
9
@JasonK. - 不对。数组不是指针。在某些情况下,数组会"衰变"为指针,但它们绝对相同。 - Carl Norum
2
要明确一点:是的,克里斯,“将数组长度作为单独参数传递仍然是一种好的做法”,否则请使用std::array或std::vector(这是C ++,而不是旧的C)。我认为我们都同意@CarlNorum,在概念上和实际上对新用户来说,引用Quora上的Anders Kaseorg的话:“学习C的第一步是理解指针和数组是相同的东西。第二步是理解指针和数组是不同的。” - Jason K.
2
@JasonK. “学习C语言的第一步是理解指针和数组是相同的东西。”这句话非常错误和误导人!实际上,理解它们不是相同的东西是最重要的一步,而是对于大多数运算符,数组被转换为指向第一个元素的指针!sizeof(int[100]) != sizeof(int *)(除非你找到一个平台,它有100 * sizeof(int)字节/int,但那是另一回事。) - too honest for this site

91

这个答案的基本思路是 C 语言实际上没有真正的二维数组 - 它有数组的数组。当你声明这样一个数组时:

int someNumbers[4][2];
你要求someNumbers是一个由4个元素组成的数组,每个元素是类型为int [2](这本身就是一个由2个int组成的数组)的数组。
另一个关键点是数组在内存中总是按顺序排列。如果你这样要求:
sometype_t array[4];

那么它看起来始终会是这个样子:

| sometype_t | sometype_t | sometype_t | sometype_t |

有4个sometype_t对象彼此相邻地排列,中间没有空格。因此,在您的someNumbers数组中,它将如下所示:

| int [2]    | int [2]    | int [2]    | int [2]    |

而每个int [2]元素本身都是一个数组,看起来像这样:

| int        | int        |

总的来说,你得到了这个:

| int | int  | int | int  | int | int  | int | int  |

2
看最终的布局让我想到 int a[][] 可以被访问为 int* … 对吧? - Narcisse Doudieu Siewe
4
这些类型不兼容,但如果你获取数组中第一个int的指针(例如通过计算a[0]&a[0][0]),那么是的,你可以对其进行偏移以便顺序访问每个int - caf

32
unsigned char MultiArray[5][2]={{0,1},{2,3},{4,5},{6,7},{8,9}};

内存中等于:

unsigned char SingleArray[10]={0,1,2,3,4,5,6,7,8,9};

5
作为回答,编译器起到了很大的作用,但两者都有所涉及。
对于静态分配的数组,“系统”将是编译器。它会像处理任何堆栈变量一样保留内存。
对于malloc的数组,“系统”将是malloc的实现者(通常是内核)。编译器所分配的只是基本指针。
编译器始终会按照声明时的类型来处理,除了Carl所举的例子,其中可以确定可互换使用情况。这就是为什么如果将[][]传递给函数,它必须假定它是静态分配的平面,其中**被假定为指向指针。

@Jon L. 我不会说malloc是由内核实现的,而是由libc在内核原语(如brk)之上实现的。 - Manuel Selva
@ManuelSelva:malloc 的实现位置和方式没有被标准规定,而是由实现者或环境决定。对于自由环境来说,像所有需要链接函数的标准库部分一样,它是可选的(这实际上是要求的结果,而不是字面上的标准规定)。对于一些现代托管环境,它确实依赖于内核函数,可以是完整的内容,也可以是(例如 Linux)同时使用 stdlib 和内核原语。对于非虚拟内存单进程系统,它只能是 stdlib。 - too honest for this site

3
假设我们有以下初始化的变量a1a2(C99):
int a1[2][2] = {{142,143}, {144,145}};
int **a2 = (int* []){ (int []){242,243}, (int []){244,245} };

a1 是一个具有连续内存平面布局的同质化 2D 数组,表达式 (int*)a1 的计算结果是指向其第一个元素的指针:

a1 --> 142 143 144 145

a2 是从异构的 2D 数组初始化而来,它是指向类型为 int* 的值的指针,即解引用表达式 *a2 的值评估为类型为 int* 的值,内存布局不一定连续:

a2 --> p1 p2
       ...
p1 --> 242 243
       ...
p2 --> 244 245

尽管内存布局和访问语义完全不同,但是C语言中用于二维数组访问表达式的语法在同构和异构2D数组中完全相同:
- 表达式a1[1][0]将从a1数组中获取值144 - 表达式a2[1][0]将从a2数组中获取值244 编译器知道a1的访问表达式操作的是类型int[2][2],而a2的访问表达式操作的是类型int**。生成的汇编代码将遵循同构或异构访问语义。
当将类型为int[N][M]的数组进行类型转换并作为int**类型访问时,代码通常会在运行时崩溃,例如:
((int**)a1)[1][0]   //crash on dereference of a value of type 'int'

1
要访问特定的二维数组,请考虑以下代码中显示的数组声明的内存映射:
    0  1
a[0]0  1
a[1]2  3

要访问每个元素,只需将您感兴趣的数组作为参数传递给函数即可。然后使用列的偏移量单独访问每个元素。

int a[2][2] ={{0,1},{2,3}};

void f1(int *ptr);

void f1(int *ptr)
{
    int a=0;
    int b=0;
    a=ptr[0];
    b=ptr[1];
    printf("%d\n",a);
    printf("%d\n",b);
}

int main()
{
   f1(a[0]);
   f1(a[1]);
    return 0;
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接