C / C ++multidimensional array内部
我有一个关于C / C ++如何在内部存储使用符号foo[m][n]
声明的multidimensional array的问题。 我不质疑纯指针指针等…我是问,因为速度的原因…
纠正我,如果我错了,但语法上foo
是一个指针数组,它们自己指向一个数组
int foo[5][4] *(foo + i) // returns a memory address *( *(foo + i) + j) // returns an int
我从很多地方听说过,C / C ++编译器在后台将foo[m][n]
转换为一维数组(用i * width + j
计算所需的一维索引)。 但是,如果这是真的,那么以下将成立
*(foo + 1) // should return element foo[0][1]
因此,我的问题是: foo[m][n]
是否(总是)作为一个一维数组存储在内存中? 如果是这样,为什么上面的代码工作如图所示。
是的,C / C ++将多维(矩形)数组存储为连续的内存区域。 但是,你的语法是不正确的。 要修改元素foo[0][1]
,以下代码将起作用:
*((int *)foo+1)=5;
显式转换是必要的,因为foo+1
&foo[1]
是完全一样的,与foo[0][1]
并不完全相同。 *(foo+1)
是指向平坦内存区域中第五个元素的指针。 换句话说, *(foo+1)
基本上是foo[1]
而**(foo+1)
是foo[1][0]
。 这里是如何为你的二维数组布局内存:
一个二维数组:
int foo[5][4];
只不过是一个数组的数组:
typedef int row[4]; /* type "row" is an array of 4 ints */ row foo[5]; /* the object "foo" is an array of 5 rows */
这里没有指针对象,无论是显式的还是隐式的。
数组不是指针。 指针不是数组。
常常引起混淆的是,在大多数情况下,数组expression式被隐式转换为指向其第一个元素的指针。 (另外一个规则是,看起来像一个数组参数声明实际上是一个指针声明,但是在这个例子中不适用。)一个数组对象是一个数组对象; 声明这样一个对象不会创build任何指针对象。 引用一个数组对象可以创build一个指针值 (数组的第一个元素的地址),但是没有指针对象存储在内存中。
数组对象foo
作为5个连续的元素存储在内存中,其中每个元素本身是一个由4个连续的int
元素组成的数组; 整个事情因此被存储为20个连续的int
对象。
索引操作符是根据指针运算来定义的; x[y]
相当于*(x + y)
。 通常,左操作数将是指针expression式或数组expression式; 如果它是一个数组expression式,则该数组将隐式转换为一个指针。
所以foo[x][y]
相当于*(foo[x] + y)
,这相当于*(*(foo + x) + y)
。 (请注意,不需要强制转换。)幸运的是,您不必这样写, foo[x][y]
更容易理解。
请注意,您可以创build一个可以使用相同的foo[x][y]
语法访问的数据结构,但是其中foo
实际上是指向int指针的指针。 (在这种情况下,每个[]
运算符的前缀已经是一个指针expression式,并且不需要被转换)。但是要做到这一点,你必须声明foo
作为指针指针 – INT:
int **foo;
然后分配和初始化所有必要的内存。 这比int foo[5][4]
更灵活,因为您可以dynamic地确定每行的行数和大小(甚至存在)。
comp.lang.c FAQ的第6部分很好地解释了这一点。
编辑:
为了回应Arrakis的评论,重要的是要记住types和表示之间的区别。
例如,这两种types:
struct pair { int x; int y;}; typedef int arr2[2];
很可能在内存中有相同的表示forms(连续的两个int
对象),但访问这些元素的语法是完全不同的。
同样,typesint[5][4]
和int[20]
具有相同的内存布局(20个连续的int
对象),但访问元素的语法不同。
你可以像((int*)foo)[10]
foo[2][2]
那样访问foo[2][2]
(把二维数组看作是一维数组)。 有时这样做是有用的,但严格来说,行为是不确定的。 你可能会放弃它,因为大多数C实现不做数组边界检查。 另一方面,优化编译器可以假设你的代码的行为是被定义的,如果没有的话就会产生任意的代码。
C数组(甚至是multidimensional array)是连续的,也就是说int [4][5]
的数组在结构上等同于int [20]
types的数组。
但是,根据C语言的语义,这些types仍然是不相容的。 特别是下面的代码违反了C标准:
int foo[4][5] = { { 0 } }; int *p = &foo[0][0]; int x = p[12]; // undefined behaviour - can't treat foo as int [20]
原因是C标准是(可能有意)措辞使得边界检查实现成为可能:由于p
是从types为int [5]
foo[0]
派生的,所以有效索引必须在范围0..5
(如果实际访问元素,则为0..4
)。
许多其他编程语言(Java,Perl,Python,JavaScript等)使用锯齿形数组来实现multidimensional array。 在C中通过使用指针数组也是可能的:
int *bar[4] = { NULL }; bar[0] = (int [3]){ 0 }; bar[1] = (int [5]){ 1, 2, 3, 4 }; int y = bar[1][2]; // y == 3
但是,锯齿状的数组不是连续的,指向的数组不需要大小一致。
由于将数组expression式隐式转换为指针expression式,因此索引锯齿形和非锯齿形数组看起来是相同的,但是实际的地址计算将会非常不同:
&foo[1] == (int (*)[5])((char *)&foo + 1 * sizeof (int [5])) &bar[1] == (int **)((char *)&bar + 1 * sizeof (int *)) &foo[1][2] == (int *)((char *)&foo[1] + 2 * sizeof (int)) == (int *)((char *)&foo + 1 * sizeof (int [5]) + 2 * sizeof (int)) &bar[1][2] == (int *)((char *)bar[1] + 2 * sizeof (int)) // no & before bar! == (int *)((char *)*(int **)((char *)&bar + 1 * sizeof (int *)) + 2 * sizeof (int))
int foo[5][4];
foo
不是一个指针数组; 它是一个数组的数组。 下图将有所帮助。