我不明白为什么我必须收到2D数组的内容
在b[][3]
而非**b
?另外,对于2D数组,我们如何按值调用?
此外,2D数组arr
的地址等于arr
的内容等于*arr
等于&arr[0][0]
;所有地址都一样。我无法清楚地看清楚它;有人可以向我解释如何实际存储多维数组。 “图片有用的链接将受到欢迎”。
#include "hfile.h" // contains all needed H files
void caller(int b[][3]) // why can't we write **b?
{
int k=100;
printf("\n****Caller:****\n");
for(int i=0;i<3;i++)
{
for(int j=0;j<3;j++)
{
b[i][j]=k++;
printf("\t %d",b[i][j]);
}
printf("\n");
}
}
int main()
{
int arr[3][3]={1,2,3,4,5,6,7,8,9}; // original containts
caller(arr); // Called caller function passing containts of "arr"
printf("\n****Orignal****\n");
for(int i=0;i<3;i++)
{
for(int j=0;j<3;j++)
printf("\t %d",arr[i][j]);
printf("\n");
}
return 0;
}
答案 0 :(得分:5)
如果声明多维数组:
int b[M][N];
存储是连续的。因此,当您访问元素时,例如(x = b[i][j];
),编译器生成与此等效的代码:
int *c = (int *)b; // Treat as a 1D array
int k = (i*N + j); // Offset into 1D array
x = c[k];
当您通过指向指针访问元素时,编译器不知道维度,并生成如下代码:
int *t = b[i]; // Follow first pointer (produces another pointer)
x = t[j]; // Follow second pointer
即。它只是遵循指针。
这些是完全不兼容的,因此编译器会阻止您将真正的2D数组传递给采用指针指针的函数。
答案 1 :(得分:1)
ASCII艺术规则!
让我们以图形方式看一个2D阵列。假设数组是2字节short
整数,并且地址也方便地为2字节。如果您愿意,这可能是Zilog Z80芯片,但这只是为了方便保持数字小。
short A[3][3];
+---------+---------+---------+
| A[0][0] | A[0][1] | A[0][2] |
+---------+---------+---------+
| A[1][0] | A[1][1] | A[1][2] |
+---------+---------+---------+
| A[2][0] | A[2][1] | A[2][2] |
+---------+---------+---------+
我们假设地址为:A = 0x4000
。然后,数组元素的short *
地址为:
&A[0][0] = 0x4000;
&A[0][1] = 0x4002;
&A[0][2] = 0x4004;
&A[1][0] = 0x4006;
&A[1][1] = 0x4008;
&A[1][2] = 0x400A;
&A[2][0] = 0x400C;
&A[2][1] = 0x400E;
&A[2][2] = 0x4010;
现在,还应该注意到你可以写:
&A[0] = 0x4000;
&A[1] = 0x4006;
&A[2] = 0x400C;
这些指针的类型是'指向short
'的数组[3]或short (*A)[3]
的指针。
你也可以写:
&A = 0x4000;
这是'指向short
'的数组[3] [3]或short (*A)[3][3]
的指针。
其中一个主要区别在于对象的大小,正如此代码所示:
#include <stdio.h>
#include <inttypes.h>
static void print_address(const char *tag, uintptr_t address, size_t size);
int main(void)
{
char buffer[32];
short A[3][3] = { { 0, 1, 2 }, { 3, 4, 5 }, { 6, 7, 8 } };
int i, j;
print_address("A", (uintptr_t)A, sizeof(A));
print_address("&A", (uintptr_t)&A, sizeof(*(&A)));
for (i = 0; i < 3; i++)
{
for (j = 0; j < 3; j++)
{
sprintf(buffer, "&A[%d][%d]", i, j);
print_address(buffer, (uintptr_t)&A[i][j], sizeof(*(&A[i][j])));
}
}
for (i = 0; i < 3; i++)
{
sprintf(buffer, "&A[%d]", i);
print_address(buffer, (uintptr_t)&A[i], sizeof(*(&A[i])));
}
putchar('\n');
for (i = 0; i < 3; i++)
{
for (j = 0; j < 3; j++)
{
printf(" A[%d][%d] = %d", i, j, A[i][j]);
}
putchar('\n');
}
return 0;
}
static void print_address(const char *tag, uintptr_t address, size_t size)
{
printf("%-8s = 0x%.4" PRIXPTR " (size %zu)\n", tag, address & 0xFFFF, size);
}
该程序使用print_address()
函数中的屏蔽操作伪造16位地址。
在MacOS X 10.7.2上以64位模式编译时的输出(GCC'i686-apple-darwin11-llvm-gcc-4.2(GCC)4.2.1(基于Apple Inc. build 5658)(LLVM构建) 2335.15.00)'),是:
A = 0xD5C0 (size 18)
&A = 0xD5C0 (size 18)
&A[0][0] = 0xD5C0 (size 2)
&A[0][1] = 0xD5C2 (size 2)
&A[0][2] = 0xD5C4 (size 2)
&A[1][0] = 0xD5C6 (size 2)
&A[1][1] = 0xD5C8 (size 2)
&A[1][2] = 0xD5CA (size 2)
&A[2][0] = 0xD5CC (size 2)
&A[2][1] = 0xD5CE (size 2)
&A[2][2] = 0xD5D0 (size 2)
&A[0] = 0xD5C0 (size 6)
&A[1] = 0xD5C6 (size 6)
&A[2] = 0xD5CC (size 6)
A[0][0] = 0 A[0][1] = 1 A[0][2] = 2
A[1][0] = 3 A[1][1] = 4 A[1][2] = 5
A[2][0] = 6 A[2][1] = 7 A[2][2] = 8
我在32位模式下编译了没有屏蔽操作的变体,得到了输出:
A = 0xC00E06D0 (size 18)
&A = 0xC00E06D0 (size 18)
&A[0][0] = 0xC00E06D0 (size 2)
&A[0][1] = 0xC00E06D2 (size 2)
&A[0][2] = 0xC00E06D4 (size 2)
&A[1][0] = 0xC00E06D6 (size 2)
&A[1][1] = 0xC00E06D8 (size 2)
&A[1][2] = 0xC00E06DA (size 2)
&A[2][0] = 0xC00E06DC (size 2)
&A[2][1] = 0xC00E06DE (size 2)
&A[2][2] = 0xC00E06E0 (size 2)
&A[0] = 0xC00E06D0 (size 6)
&A[1] = 0xC00E06D6 (size 6)
&A[2] = 0xC00E06DC (size 6)
A[0][0] = 0 A[0][1] = 1 A[0][2] = 2
A[1][0] = 3 A[1][1] = 4 A[1][2] = 5
A[2][0] = 6 A[2][1] = 7 A[2][2] = 8
在64位模式下,变体的输出为:
A = 0x7FFF65BB15C0 (size 18)
&A = 0x7FFF65BB15C0 (size 18)
&A[0][0] = 0x7FFF65BB15C0 (size 2)
&A[0][1] = 0x7FFF65BB15C2 (size 2)
&A[0][2] = 0x7FFF65BB15C4 (size 2)
&A[1][0] = 0x7FFF65BB15C6 (size 2)
&A[1][1] = 0x7FFF65BB15C8 (size 2)
&A[1][2] = 0x7FFF65BB15CA (size 2)
&A[2][0] = 0x7FFF65BB15CC (size 2)
&A[2][1] = 0x7FFF65BB15CE (size 2)
&A[2][2] = 0x7FFF65BB15D0 (size 2)
&A[0] = 0x7FFF65BB15C0 (size 6)
&A[1] = 0x7FFF65BB15C6 (size 6)
&A[2] = 0x7FFF65BB15CC (size 6)
A[0][0] = 0 A[0][1] = 1 A[0][2] = 2
A[1][0] = 3 A[1][1] = 4 A[1][2] = 5
A[2][0] = 6 A[2][1] = 7 A[2][2] = 8
32位和64位地址版本中存在很多噪声,因此我们可以保留“伪16位”地址版本。
请注意A[0][0]
的地址如何与A[0]
和A
的地址相同,但指向的对象的大小是不同的。 &A[0][0]
指向单个(短)整数; &A[0]
指向3(短)整数的数组; &A
指向3x3(短)整数数组。
现在我们需要看一下short **
的工作原理;它的工作方式完全不同这是一些测试代码,与前一个示例相关但不同。
#include <stdio.h>
#include <inttypes.h>
static void print_address(const char *tag, uintptr_t address, size_t size);
int main(void)
{
char buffer[32];
short t[3] = { 99, 98, 97 };
short u[3] = { 88, 87, 86 };
short v[3] = { 77, 76, 75 };
short w[3] = { 66, 65, 64 };
short x[3] = { 55, 54, 53 };
short y[3] = { 44, 43, 42 };
short z[3] = { 33, 32, 31 };
short *a[3] = { t, v, y };
short **p = a;
int i, j;
print_address("t", (uintptr_t)t, sizeof(t));
print_address("u", (uintptr_t)u, sizeof(u));
print_address("v", (uintptr_t)v, sizeof(v));
print_address("w", (uintptr_t)w, sizeof(w));
print_address("x", (uintptr_t)x, sizeof(x));
print_address("y", (uintptr_t)y, sizeof(y));
print_address("z", (uintptr_t)z, sizeof(z));
print_address("a", (uintptr_t)a, sizeof(a));
print_address("&a", (uintptr_t)&a, sizeof(*(&a)));
for (i = 0; i < 3; i++)
{
for (j = 0; j < 3; j++)
{
sprintf(buffer, "&a[%d][%d]", i, j);
print_address(buffer, (uintptr_t)&a[i][j], sizeof(*(&a[i][j])));
}
}
for (i = 0; i < 3; i++)
{
sprintf(buffer, "&a[%d]", i);
print_address(buffer, (uintptr_t)&a[i], sizeof(*(&a[i])));
}
putchar('\n');
for (i = 0; i < 3; i++)
{
for (j = 0; j < 3; j++)
{
printf(" a[%d][%d] = %d", i, j, a[i][j]);
}
putchar('\n');
}
putchar('\n');
print_address("p", (uintptr_t)p, sizeof(*(p)));
print_address("&p", (uintptr_t)&p, sizeof(*(&p)));
for (i = 0; i < 3; i++)
{
for (j = 0; j < 3; j++)
{
sprintf(buffer, "&p[%d][%d]", i, j);
print_address(buffer, (uintptr_t)&p[i][j], sizeof(*(&p[i][j])));
}
}
for (i = 0; i < 3; i++)
{
sprintf(buffer, "&p[%d]", i);
print_address(buffer, (uintptr_t)&p[i], sizeof(*(&p[i])));
}
putchar('\n');
for (i = 0; i < 3; i++)
{
for (j = 0; j < 3; j++)
{
printf(" p[%d][%d] = %d", i, j, p[i][j]);
}
putchar('\n');
}
return 0;
}
static void print_address(const char *tag, uintptr_t address, size_t size)
{
printf("%-8s = 0x%.4" PRIXPTR " (size %zu)\n", tag, address & 0xFFFF, size);
}
这是一个分为两半的程序。一半剖析了数组a
;另一个解剖双指针p
。以下是一些有助于理解这一点的ASCII艺术:
+------+------+------+ +------+------+------+
| 99 | 98 | 97 | t = 0x1000 | 88 | 87 | 86 | u = 0x1100
+------+------+------+ +------+------+------+
+------+------+------+ +------+------+------+
| 77 | 76 | 75 | v = 0x1200 | 66 | 65 | 64 | w = 0x1300
+------+------+------+ +------+------+------+
+------+------+------+ +------+------+------+
| 55 | 54 | 53 | x = 0x1400 | 44 | 43 | 42 | y = 0x1500
+------+------+------+ +------+------+------+
+------+------+------+
| 33 | 32 | 31 | z = 0x1600
+------+------+------+
+--------+--------+--------+
| 0x1000 | 0x1200 | 0x1500 | a = 0x2000
+--------+--------+--------+
+--------+
| 0x2000 | p = 0x3000
+--------+
请注意,数组t
.. z
位于“任意”位置 - 在图中不连续。某些数组可能是全局变量,例如,来自另一个文件,而其他数组可能是同一文件中但在函数外部的静态变量,而其他数组是静态的但是函数的本地变量,以及这些本地自动变量。您可以看到p
是一个包含地址的变量;地址是数组a
的地址。反过来,数组a
包含3个地址,其他3个数组的地址。
这是程序的64位编译的输出,人为拆分。它通过屏蔽掉除十六进制地址的最后4位之外的所有地址来模拟16位地址。
t = 0x75DA (size 6)
u = 0x75D4 (size 6)
v = 0x75CE (size 6)
w = 0x75C8 (size 6)
x = 0x75C2 (size 6)
y = 0x75BC (size 6)
z = 0x75B6 (size 6)
这可以防止有关未使用变量的警告,还可以识别7个整数数组的地址。
a = 0x7598 (size 24)
&a = 0x7598 (size 24)
&a[0][0] = 0x75DA (size 2)
&a[0][1] = 0x75DC (size 2)
&a[0][2] = 0x75DE (size 2)
&a[1][0] = 0x75CE (size 2)
&a[1][1] = 0x75D0 (size 2)
&a[1][2] = 0x75D2 (size 2)
&a[2][0] = 0x75BC (size 2)
&a[2][1] = 0x75BE (size 2)
&a[2][2] = 0x75C0 (size 2)
&a[0] = 0x7598 (size 8)
&a[1] = 0x75A0 (size 8)
&a[2] = 0x75A8 (size 8)
a[0][0] = 99 a[0][1] = 98 a[0][2] = 97
a[1][0] = 77 a[1][1] = 76 a[1][2] = 75
a[2][0] = 44 a[2][1] = 43 a[2][2] = 42
注意重要的区别。 a
的大小现在是24个字节,而不是18个,因为它是一个3(64位)指针的数组。 &a[n]
的大小为8个字节,因为每个都是指针。在数组位置加载数据的方法也大不相同 - 你必须查看汇编程序才能看到它,因为C源代码看起来是一样的。
在2D数组代码中,A[i][j]
的加载操作计算:
A
(3 * i + j) * sizeof(short)
添加到该字节地址在指针代码数组中,A[i][j]
的加载操作计算:
a
i * sizeof(short *)
添加到该字节地址b
j * sizeof(short)
添加到b
b
p
的输出略有不同。请注意,特别是 p
中的地址与 p
的地址不同。但是,一旦你过去了,行为基本相同。
p = 0x7598 (size 8)
&p = 0x7590 (size 8)
&p[0][0] = 0x75DA (size 2)
&p[0][1] = 0x75DC (size 2)
&p[0][2] = 0x75DE (size 2)
&p[1][0] = 0x75CE (size 2)
&p[1][1] = 0x75D0 (size 2)
&p[1][2] = 0x75D2 (size 2)
&p[2][0] = 0x75BC (size 2)
&p[2][1] = 0x75BE (size 2)
&p[2][2] = 0x75C0 (size 2)
&p[0] = 0x7598 (size 8)
&p[1] = 0x75A0 (size 8)
&p[2] = 0x75A8 (size 8)
p[0][0] = 99 p[0][1] = 98 p[0][2] = 97
p[1][0] = 77 p[1][1] = 76 p[1][2] = 75
p[2][0] = 44 p[2][1] = 43 p[2][2] = 42
所有这些都在一个(主要)功能中。在向函数传递各种指针并访问这些指针后面的数组时,您需要对自己进行并行实验。
答案 2 :(得分:0)
void caller(int b[][3]) // why can't we write **b ?
您可以撰写int **b
,但之后您无法将arr
传递给此函数,因为arr
定义为int arr[3][3]
,与int **
类型不兼容。
arr
可以转换为int (*)[3]
但不能转换为int **
。所以你可以这样写:
void caller(int (*b)[3]) //ok
实际上int[3][3]
定义了一个数组 1 的数组,而int**
定义了一个指向指针的指针。 int[3][3]
可以转换为指向3 int
(int (*)[3]
)数组的指针,就像int[3]
可以转换为指向int
的指针一样{ {1}})。
1。更确切地说,它定义了一个3 array-of-3-int。
的数组