是的,这很有道理,但并不是在谈论“不规则”或“锯齿形”数组。实际上,当我们说到
int a[NUM_ROWS][NUM_COLS];
我们正在创建一个数组
a
,而它所包含的是其他数组。您可以将其想象为:
+---------------------------------------+
| +--------+--------+--------+--------+ |
a: [0]: | | | | | | |
| +--------+--------+--------+--------+ |
+ +
| +--------+--------+--------+--------+ |
[1]: | | | | | | |
| +--------+--------+--------+--------+ |
+ +
| +--------+--------+--------+--------+ |
[2]: | | | | | | |
| +--------+--------+--------+--------+ |
+---------------------------------------+
(这里显然 NUM_COLS 是 4,NUM_ROWS 是 3。)
二维(或更高维)数组与简单的一维数组100%类似——您只需要仔细考虑类比。如果
a
是一个数组,那么在需要其值的表达式中提到
a
会导致指向数组第一个元素的指针
&a[0]
。因此,对于我们讨论的二维数组
a
,
a
的值是
&a[0]
,而且是“指向包含
NUM_COLS
个整数的数组的指针”。
如果多维数组下标要正确工作,它就必须按照这种方式工作。如果我们写了
a[i][j]
,那么它被解释为
(a[i])[j]
。像往常一样,
a
变成了指向数组第一个元素的指针,但是
a[i]
等价于
*(a + i)
,其中指针算术运算最终被缩放到所指元素的大小——也就是说,在幕后,它更像是
*(a+i*sizeof(*a))
。因此,
sizeof(*a)
必须是
sizeof(int[NUM_COLS])
,或者说是
NUM_COLS*sizeof(int)
。这样,
a[i]
就可以得到第 i 个子数组,然后
j
可以选择一个单元格——即子数组的
int
大小的单元格。
最后需要注意的是:我口头上谈论过“多维数组”,但严格来说,正如这里的许多常客所喜欢指出的那样,C 没有多维数组;它只有一维数组,而我们认为的二维数组实际上就是一维数组,其元素恰好是其他一维数组。(如果 C 有真正的多维数组,下标可能看起来更像是
a[i,j]
而不是
a[i][j]
。)
补充说明:尽管您提到了指针算术,我也提到了指针算术,但重要的是要意识到,在
a
的定义中没有涉及指针。只有当我们尝试“获取”
a
的值或解释
a[i]
等效于
*(a+i)
时才会出现指针。对于涉及指针的数据结构,我们可以对比代码描述的情况。
int *a2[NUM_ROWS];
for(i = 0; i < NUM_ROWS; i++)
a2[i] = malloc(NUM_COLS * sizeof(int));
这给我们带来了非常不同的内存布局:
+
a2: | | +
| *
| | +
+
| | +
| *
| | +
+
| | +
| *
| | +
+
这通常被称为“不规则”或“锯齿状”数组,因为在这种情况下,并不需要所有行的长度相同。然而,几乎神奇的是,“不规则”数组中的单元格也可以使用a2 [i] [j]
表示法进行访问。如果要实现完全的动态性,我们可以使用
int **a3 = malloc(NUM_ROWS * sizeof(int *));
for(i = 0; i < NUM_ROWS; i++)
a3[i] = malloc(NUM_COLS * sizeof(int));
导致了这种内存布局:
+
a3: | |
| * |
| | |
+
|
|
V
+
| | +
| *
| | +
+
| | +
| *
| | +
+
| | +
| *
| | +
+
a3[i][j]
也能在这里使用。
(当然,在实际构建像a2
和a3
这样的"动态数组"的真正代码中,我们需要检查确保malloc
没有返回NULL
。)
int (*ptr)[7];
有透彻的理解,它创建了一个指向数组-7-int的指针。不要与int *ptr[7];
混淆,它创建了一个包含7个指向int的指针的数组。这种区别应该在本书早期就进行详细讨论。 - user3386109