根据我在C:中的理解
当您声明像int array[x][y] = {0};
这样的2D数组时,程序会得到一块int
的长内存(x*y)。
当你对一个2D阵列进行malloc时,比如:
int ** array = malloc(sizeof(int*) * x);
for(int i=0;i<x;i++) {array[i] = malloc(sizeof(int) * y)};
该程序获得一块内存(x个int*
)+(x*y)个int
的长度。
我遇到的问题是:当您稍后键入array[5][0]
时,会发生什么?程序是否总是将其视为指针算术(这意味着编译器在声明数组时会为您创建一个指针数组)?还是编译器根据您创建数组的方式对该语句进行不同的处理?
编辑:已更改";int*数组";至";int**数组";
C在其前身B上添加的一大创新是,没有存储数组的基指针的地方,这意味着名称不是为指向第一个元素的指针命名,而是为数组本身命名。
数组是基本类型的数组、用户定义类型(结构、联合)的数组还是数组类型的数组都不会改变任何内容。
因此,是的,数组衰减到一个指针,该指针用于指针算术(具有讽刺意味的是,数组索引是指针算术加上解引用的糖),产生一个数组类型的数组元素,并且在指针衰减之后,该指针又用于指针算术。
计算出的所有中间值都只是这些,不需要存储在其他地方。
第二个例子实际上不是一个多维数组,而是一个指针数组,虽然使用相同的语法进行访问,但却是一个不同的野兽。
C使用操作数的类型来决定如何对其求值。
如果array
是int [x][y]
,则在array[5][0]
中:
array
是一个数组,因此它会自动转换为指向其初始元素的指针。让我们调用这个指针的值p- 则
p[5]
是指索引为5的array
的元素 p[5]
也是一个数组,因此它被转换为指向其初始元素的指针。让我们调用这个指针的值q- 则CCD_ 13指代索引为0的CCD_
- 因此,我们有
array
的元素5的元素0
如果array
是int **
,则在array[5][0]
中:
array
是一个不是数组类型的左值,因此它会自动转换为存储在其中的值。请注意,当array
是数组类型时,指向其第一个元素的指针是通过知道数组存储位置来计算的。这里,在array
不具有数组类型的情况下,从内存中获取值。再次调用加载的值p- 那么
p[5]
指的是索引为5的元素,假定存在存储在p点处的元素数组 - 因为
array
的类型是int **
,所以它指向的事物具有int *
类型。因此CCD_ 26具有类型CCD_ 27 p[5]
是一个不是数组类型的左值,因此它会自动转换为存储在其中的值。同样,这是通过从内存加载存储的值来完成的。让我们调用加载的值qq[0]
是指q指向的元素。因此,我们有一个从p[5]
指向的偏移0处的元素,而p[5]
是从array
指向的偏移5处的元素
因此array[5][0]
的计算方法不同。当它是阵列的阵列时,存储器地址是根据array
的基地址计算的。当它是int **
时,通过从存储器加载指针来计算存储器地址。
备注
左值转换是如此的自动和普遍,以至于我们经常不去想它。在x = y + z;
中,y
和z
指的是对象,这些对象的值会自动加载并在表达式中使用。这被称为左值转换。x
也指对象,但它不会转换为其值,因为赋值运算符的左操作数存在异常。(当左值是sizeof
、一元&
、++
或--
的操作数或是.
的左操作数时,也不会发生转换。)
在某些语言中,没有自动的左值转换,必须显式地加载值。例如,在BLISS中,必须写入x = .y + .z
,其中.
表示要加载值。
您的代码无效-应该是:
int **array = malloc(sizeof(int*) * x);
//or better
int **array = malloc(sizeof(*array) * x);
但通过这种方式,您不会仅为2D数组分配一个指针数组。
在这种情况下,程序必须首先取消引用指针数组。然后使用这个指针,第二个索引将引用int值。它的效率不是很高,因为至少需要从内存中读取两次。
2D阵列被分配为一个内存块。元素在内存中的位置由程序计算,无需从内存中进行额外读取。https://godbolt.org/z/5adjqxeKP
要动态分配2d数组,您需要使用指向数组的指针:
int (*array)[x] = malloc(sizeof(*array) * y);
int (*array1)[x][Y] = malloc(sizeof(*array));
和参考:
array[3][2] = 5;
(*array1)[4][5] = 6;
数组索引相当于指针算术加上解引用。具体而言,E1[E2]
与*((E1) + (E2))
完全相同
在2D数组或指针到指针的情况下,这种情况会发生两次。以array[5][0]
为例,这与*(array[5] + 0)
相同,后者与*(*(array + 5) + 0)
相同。
至于指针算术方面会发生什么,首先让我们看看2D数组的情况。在表达式array + 5
中,array
被转换为指向其第一个元素的指针,因此具有类型int(*)[y]
。因此,将5加到该指针上会将得到的指针向上移动它所指向的大小(即int[y]
)乘以5。
对于指针到指针,array + 5
将结果点向上移动它指向的大小(即int *
)乘以5。
所以它是完全相同的表达式,但指针算法不同,因为所指向的是不同的。
x
和y
是2。给定声明
int arr[2][2];
我们在记忆中得到了这个:
int
+–––+
arr: | | arr[0][0]
+–––+
| | arr[0][1]
+–––+
| | arr[1][0]
+–—-+
| | arr[1][1]
+–––+
请注意,没有为任何指针留出空间——没有与数组元素本身分离的对象arr
。
对于代码
int **arr = malloc( 2 * sizeof *arr );
for ( size_t i = 0; i < 2; i++ )
arr[i] = malloc( 2 * sizeof *arr[i] );
我们得到这个:
int ** int * int
+–––+ +–––+ +–––+
arr: | | -–> | | arr[0] ––––> | | arr[0][0]
+–––+ +–––+ +–––+
| | arr[1] ––+ | | arr[0][1]
+–––+ | +–––+
|
| +–––+
+–> | | arr[1][0]
+–––+
| | arr[1][1]
+–––+
在这种情况下,您有三个指针-arr
指向指针序列,每个指针指向int
序列。
那么,如何对arr[x][y]
进行评估呢?
请记住,表达式a[i]
被定义为*(a + i)
-给定地址a
,从该地址偏移i
元素(而不是字节!),并取消引用结果。
arr[i][j] == *(arr[i] + j) == *(*(arr + i) + j)
如果CCD_ 68是CCD_ 69的2D阵列或指向CCD_。
在第二种情况下,情况非常明显——我们处理的是一堆显式指针。arr
显式地存储arr[0]
的地址,arr[0]
显式地保存arr[0][0]
的地址,等等。所以arr[i] == *(arr + i)
和arr[i][j] == *(*(arr + i) + j)
是完全合乎逻辑的。
但是第一个病例呢?任何地方都不会显式存储指针。arr
不存储arr[0]
的地址(是没有单独的arr[0]
,这意味着没有什么可以存储arr[0][0]
的地址)。那么,如何将arr[i][j]
评价为*(*(arr + i) + j)
呢?
就像这样-除非它是sizeof
或一元&
运算符的操作数,或者是用于初始化char
数组的字符串文字,否则是类型为"的表达式;CCD_ 86〃的N元阵列;将被转换;衰变";,转换为"类型的表达式;指向CCD_ 87的指针";并且表达式的值将是数组的第一个元素的地址。
当编译器在代码中看到表达式arr
时,除非该表达式是sizeof
或一元&
的操作数,否则会用指针替换该表达式,并且该指针的值是数组第一个元素的地址。类似地,表达式arr[i]
也被替换为指针,并且该指针值是arr[i][0]
的地址。注意,在这种情况下CCD_;指向int
的2-元素数组的指针";(int (*)[2]
)而不是";指针到指向int
的指针";。
Expression Type Decays to Value
---------- ---- --------- -----
arr int [2][2] int (*)[2] Same as &arr[0]
*arr int [2] int * Same as arr[0]
arr[i] int [2] int * Same as &arr[i][0]
*arr[i] int n/a Same as arr[0][0]
arr[i][j] int n/a
&arr int (*)[2][2] n/a Address of array object
&arr[i] int (*)[2] n/a Address of the i'th subarray
所以我们可以把arr[i][j]
看作是这样评估的:
*(*(&arr[0] + i) + j)
数组的地址与其第一个元素的地址相同——表达式&arr
、arr
、&arr[0]
、arr[0]
和&arr[0][0]
都产生相同的地址值,但表达式的类型不同——int (*)[2][2]
、int (*)[2]
、int (*)[2]
、int [2] => int *
,和int *
(这可能会影响指针值的表示方式——int (*)[2]
可能与int *
有不同的表示方式,尽管在您可能遇到的任何系统上都不是这样)。
记住指针算术是如何工作的——如果p
指向T
类型的对象,那么p + 1
将产生该类型的下一个对象的地址。如果arr
指向int
的2元素数组,则arr + 1
产生int
的下一个2元素数组的地址。回到我们的第一张照片,但现在有一些额外的表达:
int int (*)[2][2] int (*)[2] int *
+–––+ ------------- ---------- -----
| | arr[0][0] <-- &arr arr *arr + 0 (arr[0] + 0)
+–––+
| | arr[0][1] <-- *arr + 1 (arr[0] + 1)
+–––+
| | arr[1][0] <-- arr + 1 *(arr + 1) + 0 (arr[1] + 0)
+–—-+
| | arr[1][1] <-- *(arr + 1) + 1 (arr[1] + 1)
+–––+
同样,每当编译器在表达式中看到arr
时,它都会将其替换为&arr[0]
的值,并使用指针算术进行下标。
好吧,
int variable[5][10];
将变量声明为5个元素的单个数组,这些元素都属于int[10]
类型(10个元素的数组),其单元格大小(假设int为4个字节)为10*4=40个字节。这意味着variable[3]
是第四个元素,位于变量加3x40=120字节的地址。
这里有一个由10个int
s组成的数组,如果你试图访问第三个元素(variable[3][2]
),你必须添加2个sizeof(int)
,这是8个字节。
所以,是的,对于2D数组,指针运算和1D数组一样有效。但是,当您使用malloc动态分配一些东西时,您需要知道编译器管理数组类型(类型是sometype[n][m]
,不同的n
和m
是不同的类型,*C中的所有类型在编译时都必须已知——这是C静态类型特征的一部分——)
如果您想使用某种方式在C中指定动态数组(使用符号[a][b]...[z]
访问的数组),其中空间是动态分配的,并且只分配所需的空间。。。您必须使用指针来解决所有中间访问,因为运行时没有关于数组大小的信息,因为您使用的是指针,而不是数组类型。不能声明数组类型并使其不完整。。。你用了一个指针。即使在参数声明中,当您声明数组类型的参数时,它也会自动转换为指针。。。因为不能按值传递数组,而且c语言不检查数组边界。
所以指针运算本质上是一维的,你可以为动态数组使用指针。。。编译器不知道单元格(或行)的大小。。。所以它不能假设每个行单元是26个整数大小或更小。。。您需要将一个指针放在数据的开头(解决上一次到最后一次的计算),并且必须创建axbxc。。。。xy y指针(双、三、四等),直到分配完整个数组。
如何做到这一点(例如,通过m
处理n
的2D矩阵)?
double **m = malloc(n * sizeof(*m)); /* size of n pointers to double */
assert(m != NULL);
for (int row = 0; row < n; row++) {
m[row] = malloc(m * sizeof m[row][0]);
assert(m[row] != NULL);
}
这将分配伪矩阵,其中q[0]
0将是有效的double
元素并且可以被访问,但是m[2]
不是m
元素的阵列,而是指向m
元素的阵列的指针。