C多维数组与解除引用数组指针

4
我对多维数组有些困惑。其中一个最接近帮助我理解的问题是这篇文章:Pointer address in a C multidimensional array
我有一个多维数组,初始化如下:int zippo[4][2] = {{2, 4}, {6, 8}, {1, 3}, {5, 7}}; 当我打印变量zippo*zippo时,它们显示相同的内存地址,但是当我打印**zippo时,它会打印2(第一个子数组中的第一个值)。我的问题是编译器如何知道当对zippo进行两次解引用以打印第一个数组的第一个值时该怎么做?例如,为了简单起见,如果zippo的内存地址是30,zippo*zippo的值都是15,则在内存中应该有以下表示形式:

memory addresses

据我理解,*zippo会去内存位置15查找该位置的值,恰好为15。因此,再次对其进行解引用难道不会再次打印出15吗?

关于嵌套数组(类似 int **)已经有上百个问题了。你为什么期望可以使用完全不同的类型呢?如果你有一个 int,你不能用 printf 来打印 _Complex - too honest for this site
3个回答

1
你的思路太低级了。你的问题涉及变量名和类型(在语言层面上)。
当你声明 int zippo[4][2] = {{2, 4}, {6, 8}, {1, 3}, {5, 7}}; 时,你最终得到一个由四个包含两个int的数组组成的数组。它们可以通过多种方式访问,具体取决于你需要表达什么。以下是一些涉及的子对象:
| 2 | 4 | 6 | 8 | 1 | 3 | 5 | 7 |    The storage for zippo: 8 contiguous ints

|<--+---+---+---+---+---+---+-->|    zippo (the whole array), is an int[4][2]
|<--+-->|                            zippo[0] (also known as *zippo) is an int[2]
                |<--+-->|            zippo[2] is also an int[2]
|<->|                                zippo[0][0] (also known as **zippo) is an int
            |<->|                    zippo[1][1] is also an int

你可以看到这些子对象可以重叠,并且在某些情况下共享地址。它们仍然是不同的对象(对于您、语言和编译器),原因在于它们的类型。
例如,zippo [0]zippo [0] [0](它的第一半)具有相同的地址,但其中一个是 int,而另一个是包含两个 int 的数组。
这就是为什么您不能继续索引 zippo [0] [0],或尝试在整数计算中使用 zippo [0]:即使它们共享相同的存储空间,它们也是具有不同含义的不同对象。
即使索引数组涉及指针算术,也没有实际的指针链,没有您最初理解的 int***。这都是变量名。

0

不要让*zippo去到15号位置然后找到该位置的值。 如果是这样的话,那么printf(" * ((int **) zippo) = %p\n", * ((int **) zippo) );将会输出与printf(" *zippo = %p\n", *zippo);相同的内容,但事实并非如此。

当我运行这段代码时,我得到了以下结果:

#include <stdio.h>

int zippo[4][2] = {{2, 4}, {6, 8}, {1, 3}, {5, 7}};

int main(){
    printf("zippo[0] = %p\n", (void *) (zippo[0]) );
    printf("  zippo = %p\n", (void *) zippo);
    printf("  *zippo = %p\n", (void *) (*zippo) );
    printf("  **zippo = %d\n", (int) ( **zippo) );
    printf("  * ((int **) zippo)  = %p\n", (void *) (* ((int **) zippo) ));
}

这是我得到的:

zippo = 0x804a040
*zippo = 0x804a040
**zippo = 2
* ((int **) zippo)  = 0x2

我使用 gcc -Wall -Wextra -Wpedantic -pedantic 编译了这段代码,以确保没有警告被隐藏,并使用选项 -m32 来获得 32 位地址(与 int 相同大小)。

实际上,我很难理解其中发生了什么,所以我决定查看相应的汇编代码。使用 gcc -S file.c -o file.s 我得到了以下结果。

第一个变量声明:

    .globl  zippo
    .data
    .align 32
    .type   zippo, @object
    .size   zippo, 32
zippo:
    .long   2
    .long   4
    .long   6
    .long   8
    .long   1
    .long   3
    .long   5
    .long   7
    .section    .rodata
.LC0:
    .string "zippo[0] = %p\n"
.LC1:
    .string "  zippo = %p\n"
.LC2:
    .string "  *zippo = %p\n"
.LC3:
    .string "  **zippo = %d\n"
.LC4:
    .string "  * ((int **) zippo)  = %p\n"

printf("zippo[0] = %p\n", (void *) (zippo[0]) ); 的相应汇编代码为:

movl    $zippo, %esi
movl    $.LC0, %edi
movl    $0, %eax
call    printf

printf(" zippo = %p\n", (void *) zippo); 的相应汇编代码为:

movl    $zippo, %esi
movl    $.LC1, %edi
movl    $0, %eax
call    printf

printf(" *zippo = %p\n", (void *) (*zippo) );

的相应汇编代码为:

movl    $zippo, %esi
movl    $.LC2, %edi
movl    $0, %eax
call    printf

printf(" **zippo = %d\n", (int) ( **zippo) ); 的相应汇编代码为:

movl    $zippo, %eax
movl    (%rax), %eax
movl    %eax, %esi
movl    $.LC3, %edi
movl    $0, %eax
call    printf

关于 printf(" * ((int **) zippo) = %p\n", (void *) (* ((int **) zippo) )); 的相应汇编代码如下:

movl    $zippo, %eax
movq    (%rax), %rax
movq    %rax, %rsi
movl    $.LC4, %edi
movl    $0, %eax
call    printf

正如您在这里所注意到的,对于前三个printf,相应的汇编代码完全相同(变化的是LCx,它对应于格式)。 最后2个printf也是一样的。
我的理解是,由于编译器知道 zippo 是一个二维数组,因此知道 * zippo 是一个一维数组,其数据从第一个元素的地址开始。

它没有被忽略! - too honest for this site
*(int **)zippo 返回一个指针! 但是zippo [0] [0]是一个整数,不是一个指针。 您的代码确实会调用 UB,并且任何现代编译器都将在启用推荐警告时发出警告。 我先前评论的其余部分也适用。 - too honest for this site
@Stargateur 谢谢你的备注。我添加了必要的转型并使用 -pedantic 编译,以确保没有警告。 - Hedi
“-pedantic” 不包括所有推荐警告,但如果你有意使用扩展程序,则会错误地发出警告。而深入挖掘汇编代码并不对语言问题有意义。这甚至不是特定于实现的,而是取决于完整的语境。gcc 以非常激进的优化而闻名。而且,该代码确实调用了 UB。不要在错误的代码中使用强制转换! - too honest for this site
重新添加了zippo的声明。感谢您的评论。 - Hedi
显示剩余7条评论

-1

虽然看起来好像 如果zippo的内存地址是30,而zippo和*zippo的值为15 正在发生,但实际上并没有发生。 要考虑数据类型。想象一个超级维数组。

int zappo[2][3][4][5][6] = {{{{{45,55,66,77,88,99},{12,22,32....

当您像这样定义变量(在堆栈上,而不是使用链接的malloc)时,编译器不会为zappo分配一个值,另一个值为*zappo,另一个值为**zappo等。它将45,55,66,77,88,99,12,22,32写入内存中的连续块,例如在0xfe处。现在在编译时,它知道
zappo is a pointer, and has a value 0xfe
*zappo is a pointer, and has a value 0xfe
**zappo is a pointer, and has a value 0xfe
***zappo is a pointer, and has a value 0xfe
****zappo is a pointer, and has a value 0xfe
*****zappo is a pointer, and has a value 0xfe, but this one points to an int!

编译器是按照数据类型来思考的。因此,只有最后一次解引用会得到一个整数,而其余的则只是一个地址。 这与声明不同。
int *****zappo;

并且费力地手动创建数组结构(在堆中,使用一些分配)。这就是你可以使用盒子类比的地方。


啊,这样就说得通了。我是从Java过来的,习惯于只有动态分配数组。谢谢。 - BlaqICE
欢迎。这是 C 语言的一个怪癖,被称为数组衰变成指针。更多细节可以在《深入 C 语言秘籍》第四章中找到。但对于操作知识来说并非必要。 - Elan
3
不要在此级别使用实现细节。C语言中自动变量不需要堆栈。 *zappo 不是指针,而是数组! **zappo 也不是指针,而是数组!... 而且它们都没有一个值为 0xfe。那只是它们的地址。数组的值是什么?数组不是指针(这实际上是您所说的后半部分)! - too honest for this site

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