【问题标题】:C multidimensional arrays and dereferencing array pointersC 多维数组和取消引用数组指针
【发布时间】:2017-05-10 12:33:38
【问题描述】:

我对多维数组有些困惑。最能帮助我理解的问题是这篇文章

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,那么你在内存中应该有如下表示?

据我了解,*zippo 会转到内存位置 15 以查找该位置的值,而该位置恰好是 15。那么,不应该再次取消引用它导致再次打印 15 吗?

【问题讨论】:

  • 已经有数百个关于锯齿状数组的问题(例如int **)。为什么您期望可以使用完全不同的类型?如果你有int,你不能printf_Complex

标签: c pointers memory multidimensional-array


【解决方案1】:

你的想法太低级了。您的问题与变量名称和类型(在语言级别)有关。

当您声明int zippo[4][2] = {{2, 4}, {6, 8}, {1, 3}, {5, 7}}; 时,您最终会得到一个包含两个ints 的四个数组的数组。它们可以通过多种方式访问​​,具体取决于您需要表达的内容。以下是一些涉及的子对象:

| 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,而另一个是两个 ints 的数组。

这就是为什么你不能继续索引zippo[0][0],或者尝试在整数计算中使用zippo[0]:即使它们共享相同的存储空间,它们是具有不同含义的不同对象。

即使对数组进行索引涉及指针运算,也没有实际的指针链,也没有您最初理解所暗示的int***。都是变量名。

【讨论】:

    【解决方案2】:

    *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
    

    正如您在此处所注意到的,对于前 3 个 printf,对应的程序集完全相同(变化的是 LCx,它对应于格式)。 最后 2 个 printf 也是一样。

    我的理解是,由于编译器知道zippo 是一个二维数组,因此知道*zippo 是一维数组,其数据从第一个元素的地址开始。

    【讨论】:

    • 不容忽视!
    • 代码调用未定义的行为。你读取了一个int 数组作为指向int 的指针。 %p 期望 void * 并且您必须强制转换的事实是 UB 的另一个原因(编译器实际上应该抱怨)。 “编译器知道*zippo 甚至不存在于内存中”是无稽之谈。 *zippo 当然存在。它是一个数组。
    • @olaf 代码不会调用未定义的行为。如果用printf(" * ((int **) zippo) = %d\n", * ((int **) zippo) ); 更改最后一行,则输出始终为 2(当然编译器会抱怨期望 int * 而不是 int)。另一件事:我将上面的 C 代码转换为程序集以了解究竟发生了什么,似乎*zippo 被视为zippo。我会更新我的答案,但我想先听听你的意见。谢谢
    • rq : 2 是数组的第一个值
    • 不明白你的意思。如果您并不真正了解所有含义,请不要使用演员表。只是为了让编译器静音而疯狂地强制转换是灾难的保证。
    【解决方案3】:

    虽然看起来if the memory address of zippo is 30 and the value of zippo and *zippo is 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,其余的只到一个地址。 这和声明不一样

    int *****zappo;
    

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

    【讨论】:

    • 啊,这更有意义。我来自 Java,并且习惯于仅动态分配的数组。谢谢
    • 欢迎。这是C的一个怪癖,称为数组衰减为指针。例如,可以在《Deep C Secrets》第 4 章中找到更多详细信息。但对于操作知识不是必需的。
    • 不要在这个级别使用实现细节。在 C 语言中,自动变量不需要堆栈。 *zappo 不是一个指针,而是一个数组! **zappo 不是一个指针,而是一个数组! ...而且它们都没有0xfe 的值。那只是他们的地址。数组的值是多少?数组不是指针(实际上是在后半部分声明的)!
    猜你喜欢
    • 2021-03-27
    • 1970-01-01
    • 1970-01-01
    • 2012-07-15
    • 2013-04-29
    • 2021-12-02
    • 1970-01-01
    相关资源
    最近更新 更多