像一维数组一样初始化二维数组
任何人都可以解释这些值将如何存储在这个数组中:
int a[2][3] = {1,2,3,4,5}
使用2Darrays,但将其分配为1Darrays。
它们分配如下:
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:警告:初始化器周围缺少大括号[-Wateing-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 #include 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
给出零(在我的机器上),因为它会进入未初始化的内存。 结果是使用连续的内存允许你做方便的事情,让我们设置所有的值,而不需要双循环。
在C中,1Darrays以所谓的“行主要”顺序存储在存储器中的单个线性缓冲器中。 行主要意味着当您从元素到元素时,最后一个索引变化最快。 专业列意味着第一个索引变化最快,例如在MATLAB中。
您声明的数组仅为2D,编译器通过为您计算元素的线性地址来帮助您。 1Darrays中元素的地址计算为linear[x] = linear + x
。 同样,对于2D数组, 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};
。
请记住,数组的名称也指向指向其第一个元素的指针。 这是两个可以通过相同名称访问的不同内容。
根据如何解释对a[1][2]
这样的2D数组的访问的定义,“由此得出数组按行主顺序存储”(例如,参见此在线C标准委员会草案/ 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]
a[0][3]
是未定义的行为,因为最后一个下标的范围是0..2
。 因此,根据6.5.6(8),表达式a[0][3]
正在解决子数组a[0]
超出其界限,例如, 这里所述 。
现在解释如何解释int a[2][3] = {1,2,3,4,5}
。 该声明是本在线C标准委员会草案第6.7.9节中定义的初始化,第(20)至(26)段描述了此处所需的内容:
(20)如果聚合或联合包含聚合或联合的元素或成员,则这些规则以递归方式应用于子聚合或包含的联合。 如果子集合或包含的并集的初始值设定项以左括号开头,则由该括号括起的初始值设定项及其匹配的右括号初始化子集合或所包含的并集的元素或成员。 否则,只有列表中的足够的初始值设定项用于说明子集合的元素或成员或所包含的并集的第一个成员; 任何剩余的初始值设定项都用于初始化当前子聚合或包含的union所属的聚合的下一个元素或成员。
(21)如果括号括起的列表中的初始值设定项少于聚合的元素或成员,或者用于初始化已知大小的数组的字符串文字中的字符数少于数组中的元素,则剩余的聚合应与具有静态存储持续时间的对象隐式初始化。
26例子
(3)声明
int y[4][3] = { { 1, 3, 5 }, { 2, 4, 6 }, { 3, 5, 7 }, };
是一个完全括号初始化的定义:
3
和5
初始化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[0]
不以左括号开头,因此使用列表中的三个项目。 同样地,接下来的三个连续采用y[1]
和y[2]
。