像一维数组一样初始化二维数组

5

我已经像初始化一维数组一样初始化了一个二维数组:

int a[2][3] = {1,2,3,4,5} 

这个数组中的值是如何存储的?

1
我非常惊讶,竟然找不到这个问题的副本。 - Mad Physicist
@Fullmetal:它们没有指针,因此无法指向某些东西。 - too honest for this site
这是一些遗留的东西,你不应该在现代代码中使用。 - too honest for this site
我是指它们都打印出相同的值。 - user6219266
1
@MadPhysicist-- 声明为 int a[2][3] 的二维数组是一个数组的数组。请注意,在大多数表达式中,a 将会衰减为指向其第一个元素的指针,即一个包含 3 个 int 的数组。 - ad absurdum
显示剩余8条评论
3个回答

5

它们的分配如下:

1 2 3
4 5 0

出现 0 是因为你分配了一个大小为 6 的数组,但只指定了 5 个元素。

这被称为“行主序”。

你可能希望稍微规范一下你的代码。你的代码目前是:

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

如果您使用gcc main.c -Wall -pedantic --std=c99编译此代码,您将会收到一些警告信息:

temp.c:2:17: 警告:初始化程序周围缺少大括号 [-Wmissing-braces]

通过以下方式解决这个问题:

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

这将会产生一个新的警告:

temp.c:2:25: 警告:数组初始化程序中存在过多元素

使用以下步骤来解决此问题:

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

这个显式表示数据有两行,每行三个元素。

内存布局的一些想法

int a[2][3] 会产生一个"数组的数组"。这与"指向数组的指针的数组"类似但又不同。它们都有类似的访问语法(例如a [1] [2])。但是只有对于"数组的数组",你才能可靠地使用a+y*WIDTH+x来访问元素。

一些代码可能会更清晰:

#include <stdlib.h>
#include <stdio.h>

void PrintArray1D(int* a){
  for(int i=0;i<6;i++)
    printf("%d ",a[i]);
  printf("\n");
}

int main(){
  //Construct a two dimensional array
  int a[2][3] = {{1,2,3},{4,5,6}};

  //Construct an array of arrays
  int* b[2];
  b[0] = calloc(3,sizeof(int));
  b[1] = calloc(3,sizeof(int));

  //Initialize the array of arrays
  for(int y=0;y<2;y++)
  for(int x=0;x<3;x++)
    b[y][x] = a[y][x];

  PrintArray1D(a[0]);
  PrintArray1D(b[0]);
}

当你运行这个时,你会得到:
1 2 3 4 5 6 
1 2 3 0 0 0 

打印b会得到零(在我的机器上),因为它遇到了未初始化的内存。重点是使用连续的内存可以让您做一些方便的事情,例如设置所有值而不需要双重循环。


只是出于好奇,int a[2][3] = {{1,2},{3,4},{5}} 会触发什么警告? - Mad Physicist
1
“这不一定是这样的。” - 请详细说明!“包含两个int [3]数组的数组可能在内存中是不连续的。” - 这是错误的。int a [2] [3]是一个数组,保证在内存中是连续的!它不能是其他任何东西。如果您指的是像int *a [3]这样的内容:那是完全不同的数据类型,不是数组的数组! - too honest for this site
@Olaf:没错。int a[2][3] 在内存中是连续的,所有访问都是有效的。然而,在分别分配行的情况下,对数组的数组进行 a[2][3] 访问模式不能保证是有效的。 - Richard
@Richard:1)那个答案现在已经被删除了。2)你已经在那个答案下面评论过了,不要在你的回答中评论其他答案。3)那个答案甚至没有提到一个指针数组!也许你有自己的想法,但是就目前而言,你的回答部分是误导性的。 - too honest for this site
“如果你不明白像 int *a[3] 这样的东西,那就是一个完全不同的数据类型,而不是一个数组的数组!” - too honest for this site
显示剩余7条评论

3
在C语言中,一维数组被存储在内存中的单个线性缓冲区中,这称为“行主序”顺序。行主序意味着当您从一个元素移动到另一个元素时,最后一个索引变化得最快。列主序将意味着第一个索引变化最快,就像在MATLAB中一样。
您声明的数组只是二维的,因为编译器通过计算元素的线性地址来帮助您。在一维数组中,元素的地址是通过公式linear[x] = linear + x计算的。同样,在您的二维数组中,a[y][x] = a + 3 * y + x。通常情况下,a[y][x] = a + num_cols * y + x
您可以将数组初始化为元素的单个向量,这将首先填充第一行,然后是第二行,以此类推。由于您有两行每行三个元素,因此第一行变为1, 2, 3,而第二行变为4, 5, 0
在行末索引之外进行索引是完全有效的,至少在编译器看来是这样。在您提供的示例中,a[0][3]正在访问一个宽度为三个元素的数组中第一行的第四个元素。通过环绕,您可以看到这只是第二行的第一个元素,这更明确地表示为a[1][0]
由于松散的索引检查,只要提供一个初始化器,就可以完全省略任何数组中的第一个索引。计算线性地址的公式不依赖于第一个索引(因为它是行主序),并且总元素数由初始化器本身指定。一维示例是:int linear[] = {1, 2, 3};
请记住,数组的名称也指向其第一个元素的指针。这是两个不同的东西,可以使用相同的名称进行访问。

一个常见的说法是“C语言在指针算术和数组索引时不进行任何边界检查”,但事实上这并不准确,因为即使超出数组边界的指针算术也是未定义行为;甚至有讨论认为违反子数组边界是未定义行为。 - Stephan Lechner
@StephanLechner。说得好,尽管编译器特别允许您触发这种未定义的行为。我已经删除了有问题的代码行,因为正如您指出的那样,它们与讨论无关。 - Mad Physicist
允许这个初始化程序实际上是一个遗留问题,与内存中数组的布局无关。现代编译器会对这样的代码发出警告。 - too honest for this site
1
从技术上讲,初始化器的扁平列表按行主序列初始化数组的原因不是因为数组在内存中的存储方式,而是因为C语言初始化规则。在2011标准的第6.7.9节中,第17到20条款描述了使用列表来初始化已声明对象及其子对象的过程。假设这些条款可以编写成指定不同顺序的形式,那么即使数组以行主序列存储,初始化也将按照该顺序进行。 - Eric Postpischil

2
根据如何解释类似online C标准委员会草案/数组下标的2D数组访问的定义,“由此可知,数组按行主序存储”。这意味着对于一个数组int a[ROWS][COLUMNS],访问a[r][c]时,以int值为单位计算偏移量,如(r*COLUMNS + c)
因此,对于一个数组int a[2][3],访问a[0][1]的偏移量为0*3 + 1 = 1,访问a[1][0]的偏移量为1*3 + 0 = 3。也就是说,a[0][3]可能会导致偏移3,而a[1][0]肯定会导致偏移3。我写了“可能”,因为我认为使用a[0][3]访问数组int a[2][3]是未定义行为,因为最后一个下标的范围是0..2。因此,根据6.5.6(8)条款,表达式a[0][3]超出了其边界,如此处所述。
现在来谈谈如何解释int a[2][3] = {1,2,3,4,5}。这个语句是初始化,如这份在线C标准委员会草案的第6.7.9节所定义,第20到26段描述了这里需要的内容:

(20) If the aggregate or union contains elements or members that are aggregates or unions, these rules apply recursively to the subaggregates or contained unions. If the initializer of a subaggregate or contained union begins with a left brace, the initializers enclosed by that brace and its matching right brace initialize the elements or members of the subaggregate or the contained union. Otherwise, only enough initializers from the list are taken to account for the elements or members of the subaggregate or the first member of the contained union; any remaining initializers are left to initialize the next element or member of the aggregate of which the current subaggregate or contained union is a part.

(21) If there are fewer initializers in a brace-enclosed list than there are elements or members of an aggregate, or fewer characters in a string literal used to initialize an array of known size than there are elements in the array, the remainder of the aggregate shall be initialized implicitly the same as objects that have static storage duration.

26 EXAMPLE

(3) The declaration

      int y[4][3] = {
            { 1, 3, 5 },
            { 2, 4, 6 },
            { 3, 5, 7 },
      };

is a definition with a fully bracketed initialization: 1, 3, and 5 initialize the first row of y (the array object y[0]), namely y[0][0], y[0][1], and y[0][2]. Likewise the next two lines initialize y[1] and y[2]. The initializer ends early, so y[3] is initialized with zeros. Precisely the same effect could have been achieved by

      int y[4][3] = {
            1, 3, 5, 2, 4, 6, 3, 5, 7
      };

The initializer for y[0] does not begin with a left brace, so three items from the list are used. Likewise the next three are taken successively for y[1] and y[2].


1
cppreference不是权威资源。为什么不参考标准或最终草案呢? - too honest for this site
@Olaf:没错,cppreference不是规范性的;但我只是在寻找例子,我发现cppreference在这方面更为简洁。无论如何,你有最终草稿在线上可以获取吗?我总是只找到我用于引用的那个版本... - Stephan Lechner
现在每个人都是评论家 - § 6.7.9 初始化 (p21) 是关键,引用得当。 - David C. Rankin

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