C(和 C++)中的二维数组实际上是一维数组,其元素是一维数组。索引运算符 [] 具有从左到右的语义,因此对于类型 arr[N][M],首先评估第一个索引(具有 N 个元素)。结果表达式,例如arr[0],arr 中的第一个元素,是一个有 M 个元素的一维数组。当然,该数组可以再次索引,例如arr[0][1],导致第一个子数组中的第二个 int。
C 语言的一个怪癖是,如果您使用数组作为函数参数,则函数看到 是指向第一个元素的指针。用作参数的数组“衰减”,或者如标准所说,以这种方式“调整”。这与二维数组没有什么不同,只是二维数组的元素本身就是数组。因此,接收函数得到的是一个指向int arr[M]的指针。
考虑:如果你想传递一个简单的整数数组,比如intArr[3],给一个函数,函数看到的是一个指向第一个元素的指针。这样的函数声明可能看起来像void f(int *intPtr),对于这个例子来说,只是用f(intArr) 调用。另一种编写方式是void f(int intPtr[])。它的含义完全相同:参数是指向 int 的指针,而不是数组。它指向一连串整数中的第一个(甚至可能是唯一一个)元素。
二维数组的逻辑是完全相同的——除了所讨论的元素具有“M ints 数组”类型,例如int subArr[M]。这种类型的指针参数可以用两种方式编写,比如简单的 int 数组:作为指针,如 void f(int (*subArrPtr)[M]) 或以数组表示法,顶层元素的数量未知,如 void f(int arr[][M])。与简单的 int 数组一样,这两个参数符号完全等效且可互换。两者实际上都声明了一个指针,所以(*subArrPtr)[M] 可以这么说更切中要害(呃),但可能更晦涩。
(*subArrPtr) 中有趣的括号的原因是我们必须首先取消引用指针才能获得实际的数组,然后才能索引它。如果没有括号,索引运算符[] 将具有优先权。您可以在this table 中查找优先级。 [] 在第 1 组中具有最高优先级,而取消引用运算符 *(不是乘法!)在第 2 组中。如果没有括号,我们将首先索引,然后才取消引用数组元素(因此它必须是一个指针),也就是说,我们将声明一个指针数组,而不是一个指向数组的指针。
因此,您的函数的两个可能的、可互换的签名是
void function( int (*arrArg)[M] ); // pointer notation
void function( int arrArg[][M] ); // "array" notation (but actually a pointer)
整个程序,也纠正了 Ted 提到的问题,并且没有打印原始值(毕竟我们知道它们),如下所示。我还调整了二维数组的初始化,以便子数组变得可见。 C 对初始化结构和数组非常宽松;它只是让您编写连续的值并随时间填充嵌套子对象的元素。但我认为显示结构有助于理解代码并揭示错误,例如子数组中的元素数量错误。我以一种方式声明了该函数,并以另一种方式定义了它,以表明函数签名是等效的。我还更改了定义和函数的名称以赋予它们更多含义。
#include<stdio.h>
#define NUM_ELEMS_SUBARRAY 4
#define NUM_ELEMS_ARRAY 2
/// @arrArg Is a pointer to the first in a row of one-dimensional
/// arrays with NUM_ELEMS_SUBARRAY ints each.
void add50ToElems(int arrArg[][NUM_ELEMS_SUBARRAY]);
int main()
{
// Show the nested structure of the 2-dimensional array.
int arr[NUM_ELEMS_ARRAY][NUM_ELEMS_SUBARRAY] =
{
{10, 11, 12, 13},
{14, 15, 16, 17}
};
// Modify the array
add50ToElems(arr);
// print results
for (int i = 0; i < NUM_ELEMS_ARRAY; i++) {
for (int j = 0; j < NUM_ELEMS_SUBARRAY; j++)
{
printf("value of arr[%d][%d]: %d\n", i, j, arr[i][j]);
}
}
return 0;
}
// Equivalent to declaration above
void add50ToElems(int (*arrArg)[NUM_ELEMS_SUBARRAY])
{
for (int i = 0; i < NUM_ELEMS_ARRAY; i++)
{
for (size_t j = 0; j < NUM_ELEMS_SUBARRAY; j++)
{
//arrArg[i][j] = arrArg[i][j] + 50;
arrArg[i][j] += 50; // more idiomatic
}
}
}
为什么将二维数组传递给期望指针指向的函数是错误的?让我们考虑一下void f(int *p) 的含义。它接收一个指向 int 的指针,该指针通常是数组的开头,即在内存中一个接一个地放置的一系列 int 的开头。例如
void f(int *p) { for(int i=0; i<3; ++i) { printf("%d ", p[i]); }
可以用指向数组第一个元素的指针调用:
static int arr[3];
void g() { f(arr); }
当然,这个最小的例子是不安全的(f 怎么知道有三个整数?)但它可以达到目的。
那么void f(int **p); 是什么意思?类似地,它是一个指针,指向一系列指针中的第一个,这些指针一个接一个地位于内存中。我们已经知道为什么如果我们传递二维数组的地址会造成灾难:那里的对象不是指针,而是所有整数!考虑一下:
int arr1[2] = { 1,2 };
int arr2[2] = { 2,3 };
int arr3[2] = { 3,4 };
// This array contains addresses which point
// to the first element in each of the above arrays.
int *arrOfPtrToStartOfArrays[3] // The array of pointers
= { arr1, arr2, arr3 }; // arrays decay to pointers
int **ptrToArrOfPtrs = arrOfPtrToStartOfArrays;
void f(int **pp)
{
for(int pi=0; pi<3; pi++) // iterate the pointers in the array
{
int *p = pp[pi]; // pp element is a pointer
// iterate through the ints starting at each address
// pointed to by pp[pi]
for(int i=0; i<2; i++) // two ints in each arr
{
printf("%d ", pp[pi][i]); // show double indexing of array of pointers
// Since pp[pi] is now p, we can also say:
printf("%d\n", p[i]); // index int pointer
}
}
}
int main()
{
f(ptrToArrOfPtrs);
}
f 遍历指针数组。 它认为那个地址上的值,以及随后的地址,都是指针!这就是声明int **pp 的含义。
现在,如果我们改为传递一个充满整数的数组的地址,f 仍然会认为那里的内存充满了指针。像上面的 int *p = pp[i]; 这样的表达式将读取一个整数(例如 1)并认为它是一个地址。然后 printf 调用中的p[i] 将尝试访问地址 1 处的内存。
让我们最后讨论一下为什么应该将二维数组作为指向指针的指针传递的想法如此普遍。一个原因是,虽然将二维数组参数声明为 <strike>void f(int **arr);</strike> 是完全错误的,但您可以访问它的第一个(但只能是第一个)元素,例如int i = **arr。这样做的原因是第一个解引用为您提供了第一个子数组,您可以依次应用解引用运算符,产生它的第一个元素。但是,如果将数组作为参数传递给函数,它不会衰减为指向指针的指针,而是如所讨论的那样衰减为指向其第一个元素的指针。
第二个混淆来源是访问指针数组中的元素使用与访问真正二维数组中的元素相同的双索引:pp[pi][i] 与 arr[i][j]。 但是,这些表达式生成的代码完全不同,如果传递了错误的类型,就会引发灾难。顺便说一下,你的编译器会警告这一点。