【问题标题】:Initializing a 2D array like a 1D array像一维数组一样初始化二维数组
【发布时间】:2017-06-16 19:10:35
【问题描述】:

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

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

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

【问题讨论】:

  • 查找“行主要顺序”。
  • 我很惊讶我找不到这个问题的重复项。
  • 为什么 a[0][3] 和 a[1][0] 都指向 4 ?
  • 我正在起草一个包含两个问题的答案的答案。
  • @MadPhysicist -- 声明为int a[2][3] 的二维数组是一个数组数组。请注意,在大多数表达式中,a 将衰减为指向其第一个元素的指针,即 3 个 ints 的数组。

标签: c arrays


【解决方案1】:

它们的分配如下:

1 2 3
4 5 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}} 会触发什么警告?
  • “不一定如此。” - 请详细说明! “包含 int[3] 数组的数组可能在内存中不连续。” - 这是错误的。 int a[2][3] 数组数组,保证在内存中是连续的!它不可能是别的东西。如果您引用 int *a[3] 之类的东西:那是完全不同的数据类型,而不是数组数组!
  • @Olaf:没错。 int a[2][3] 在内存中是连续的,所有访问都是有效的。但是,在单独分配行的情况下,不能保证数组数组上的a[2][3] 访问模式有效。
  • @MadPhysicist:我得到warning: excess elements in array initializer 指出 5 前面的括号。它表示行太多,而不是元素太少。
  • 所以你基本上说不同的数据类型是不同的?并不意外!
【解决方案2】:

在 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 不对指针算术和数组索引进行任何边界检查”实际上是不正确的,因为即使超出数组边界的指针算术也是未定义的行为;如果违反子数组的边界是未定义的行为,这甚至是一个讨论。
  • @StephanLechner。很公平,尽管编译器确实允许您触发这种未定义的行为。正如您所指出的,我删除了违规行,因为它们与讨论无关。
  • 允许这个初始化器实际上是一个遗留问题,与内存中数组的布局无关。 Moern 编译器会对此类代码发出警告。
  • 从技术上讲,初始化器的平面列表以行优先顺序初始化数组的原因不是因为数组在内存中的存储方式,而是因为 C 的初始化规则。在 2011 标准中,第 6.7.9 节第 17 到 20 节描述了使用列表初始化声明对象(包括其子对象)的过程。假设,可以编写这些子句以指定不同的顺序,然后即使数组以行优先顺序存储,也会按该顺序进行初始化。
【解决方案3】:

从如何解释如何访问像a[1][2] 这样的二维数组的定义,“由此得出数组以行优先顺序存储”(例如,参见online C standard comitee draft / array subscripting)。 这意味着对于数组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] 正在寻址子数组 a[0] 超出其边界,例如 here

现在谈谈int a[2][3] = {1,2,3,4,5} 的解释方式。此语句是section 6.7.9 of this online C standard comitee draft 中定义的初始化,第 (20) 至 (26) 段描述了此处所需的内容:

(20) 如果聚合或联合包含的元素或成员是 聚合或联合,这些规则递归地应用于 子聚合或包含的联合。如果一个初始化器 子聚合或包含联合以左大括号开头, 由该大括号及其匹配的右大括号括起来的初始化程序 初始化子聚合的元素或成员或 包含联合。否则,列表中只有足够的初始化器是 考虑到子聚合的元素或成员或 包含联盟的第一个成员;任何剩余的初始化器 剩下来初始化聚合的下一个元素或成员 当前子聚合或包含的联合是其中的一部分。

(21) 如果大括号括起来的列表中的初始值设定项比那里少 是聚合的元素或成员,或 用于初始化已知大小的数组的字符串文字 是数组中的元素,聚合的其余部分应为 隐式初始化与具有静态存储的对象相同 持续时间。

26 个例子

(3) 声明

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

是一个带全括号初始化的定义:135初始化y的第一行(数组对象y[0]),即 y[0][0]y[0][1]y[0][2]。同样,接下来的两行初始化 y[1]y[2]。初始化程序提前结束,所以 y[3] 被初始化为 零。完全一样的效果可以通过

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

y[0] 的初始值设定项不以左大括号开头,因此使用了列表中的三个项目。接下来的三个也一样 依次为y[1]y[2]

【讨论】:

  • cppreference 不是权威资源。为什么不参考标准,分别。最终稿?
  • @Olaf:对,cppreference 不规范;但我只是在寻找示例,我发现 cppreference 在这里更简短。无论如何,你有在线可用的最终草案吗?我总是只找到我用来引用的那个...
  • 现在每个人都是批评家 - § 6.7.9 Initialization (p21) 是关键,很好的引用。
猜你喜欢
  • 2012-11-29
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2012-05-20
  • 1970-01-01
相关资源
最近更新 更多