C/C++多维数组内部机制

25

我有一个关于C/C++如何内部存储使用记法foo[m][n]声明的多维数组的问题。我不是在质疑纯指针等等... 我之所以问,是因为考虑速度原因...

如果我理解错了,请纠正我,但从语法上讲,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]是否(总是?)以平坦的一维数组的形式存储在内存中?如果是这样,为什么上述代码可以按照所示工作。


如果其他人有相同的问题,这里是一些进一步的信息: *foo == foo[0] == &foo[0][0]
*(foo+1) == foo[1] == &foo[1][0]
(int *)foo + 1 == &foo[0][1]
- Arrakis
6
不,foo不是指针数组;它是一个二维数组。 - Keith Thompson
4个回答

31

一个二维数组:

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 */

这里没有指针对象,无论是显式还是隐式。

数组不是指针。指针也不是数组。

常常会引起困惑的是,在大多数情况下,数组表达式会被隐式转换为指向其第一个元素的指针。(而且另一个规则说,看起来像数组参数声明的东西实际上是一个指针声明,但这不适用于本示例。)数组对象是一个数组对象;声明这样的对象并不会创建任何指针对象。引用数组对象可以创建指针(数组第一个元素的地址),但内存中没有存储指针对象。

数组对象foo被存储在内存中作为5个连续的元素,其中每个元素本身都是包含4个连续int元素的数组;因此,整个数组被存储为20个连续的int对象。

索引运算符是以指针算术为基础定义的;x[y]等价于*(x+y)。通常左操作数要么是指针表达式,要么是数组表达式;如果它是数组表达式,则该数组会被隐式转换为指针。

因此,foo[x][y]等价于*(foo[x]+y),这又等价于*(*(foo+x)+y)。(请注意,不需要任何强制转换。)幸运的是,你不必以那种方式编写它,foo[x][y]更容易理解。

请注意,您可以创建一个数据结构,可以使用相同的foo[x][y]语法访问该结构,但其中foo确实是指向指向int的指针。(在这种情况下,每个[]运算符的前缀已经是指针表达式,不需要转换。)但要做到这一点,您必须将foo声明为指向指向int的指针:

int **foo;
然后分配并初始化所有必要的内存。这比使用int foo[5][4]更灵活,因为您可以动态确定行数和每行的大小(甚至是存在与否)。 comp.lang.c FAQ的第6部分很好地解释了这一点。 编辑: 回应Arrakis的评论,重要的是要注意类型表示之间的区别。
例如,这两种类型:
struct pair { int x; int y;};
typedef int arr2[2];

很可能在内存中具有相同的表示方式(两个相邻的int对象),但是访问元素的语法非常不同。

类似地,类型int[5][4]int[20]具有相同的内存布局(20个连续的int对象),但是访问元素的语法不同。

您可以将foo[2][2]作为((int*)foo)[10]访问(将二维数组视为一维数组)。 有时这样做很有用,但严格来说行为是未定义的。 您可能会成功,因为大多数C实现不进行数组边界检查。 另一方面,优化编译器可以假定您的代码行为是已定义的,并生成任意代码以确保其如此。


先生,我不理解最后这句话的意思:“你可以靠过去,因为大多数C编译器不进行数组边界检查。”如果我强制编译器进行数组边界检查会怎么样呢? - haccks
1
@haccks:你如何“强制”编译器检查数组边界? - Keith Thompson
2
@haccks:是的,这是未定义的行为。请参见N1570。规则在6.5.6p8中说明:“……否则,行为是未定义的”,在附录J第2节中得到确认:“即使使用给定下标(如在lvalue表达式a[1][7]中,给定声明int a[4][5]),一个数组下标越界也是未定义的行为(6.5.6)。 - Keith Thompson
在我提供的代码中,我看到*p在数组边界中被计算,并且不应该调用UB(通过阅读附录J第2节所理解的内容:将指针相加或相减,使其进入或刚好超出数组对象和整数类型产生一个指向刚好超出数组对象的结果,并被用作一元*运算符的操作数进行评估(6.5.6),或者我认为我误解了某些东西。 - haccks
2
@haccks:相关的数组对象是2D数组的单行。没有25个元素的int数组,只有一个由int[5]元素组成的5个元素的数组。 - Keith Thompson
显示剩余5条评论

29

是的,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]。下面是一些二维数组的存储方式:

enter image description here


4
@Arrakis,但对编译器而言它并不是一个一维数组,它是一个二维数组。恰好的是,这个二维数组在内存中的排列方式与大小为两个维度乘积的一维数组相同。 - Michael Goldshteyn
4
这里没有指向指针的指针,无论是存储在内存中还是作为任何有效表达式的一部分。 - Keith Thompson
2
不需要强制转换。foo[1][0] 等同于 *(*(foo + 1) + 0) - Keith Thompson
@Keith Thompson,是的,那是正确的;但是,这需要两次解除引用,而转换只需要一次。当然,一个好的编译器将把你的代码优化为一次,但在 -O0 时,这个答案比你的更有效率。 - mdenton8
我撤回之前的说法--即使是在 -O0 的情况下,gcc 版本 4.9.2 编译这两个代码片段时也会生成完全相同的代码(顺带一提,该代码使用的指令比仅使用普通括号的代码多)。 - mdenton8
显示剩余4条评论

7

C 数组,甚至多维数组,都是连续的。例如 int [4][5] 类型的数组在结构上等同于类型为 int [20] 的数组。

然而,根据 C 语言语义,这些类型仍然不兼容。特别地,以下代码违反了 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 是从 foo [0] 派生出来的,它的类型是 int [5] ,有效索引必须在范围 0..5 (如果您实际访问元素,则为 0..4 )。
许多其他编程语言(Java、Perl、Python、JavaScript等)使用不规则数组来实现多维数组。通过使用指针数组,这在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

然而,锯齿数组不是连续的,指向的数组大小也不需要一致。

由于数组表达式会被隐式转换为指针表达式,所以索引锯齿和非锯齿数组看起来相同,但实际的地址计算将会有很大的不同:

&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))

3

int foo[5][4];

foo不是一个指针数组,它是一个二维数组。下面的图片可以帮助理解。

enter image description here


这就是OP所询问的内容。 - CoffeeTableEspresso

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