【问题标题】:Difference between **variable and variable[ ] [ ]?**变量和变量[] []之间的区别?
【发布时间】:2012-02-12 11:10:48
【问题描述】:

我不明白为什么我必须接收二维数组的内容 在b[][3] 而不是**b?另外,我们如何为二维数组按值调用? 此外,二维数组的地址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;
}

【问题讨论】:

  • 当您使用int ** b 时会发生什么?一些编译器错误?
  • 是错误:无法将 int[3]* 转换为 int **
  • 阅读comp.lang.c FAQ的第6部分。

标签: c++ arrays multidimensional-array double-pointer


【解决方案1】:

如果声明一个多维数组:

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 数组传递给采用指针到指针的函数。

【讨论】:

    【解决方案2】:

    ASCII 艺术规则!

    让我们用图形来看看二维数组。让我们假设数组是 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;
    

    this的类型是'指向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 build 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 的地址是一样的,但是指向的对象的大小是不同的。 &amp;A[0][0] 指向单个(短)整数; &amp;A[0] 指向一个由 3 个(短)整数组成的数组; &amp;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)
    

    这可以防止有关未使用变量的警告,并且还可以识别 3 个整数的 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 位)指针组成的数组。 &amp;a[n] 的大小为 8 个字节,因为每个字节都是一个指针。在数组位置加载数据的方法也完全不同 - 你必须查看汇编程序才能看到,因为 C 源代码看起来相同。

    在二维数组代码中,A[i][j] 的加载操作计算:

    • A的字节地址
    • (3 * i + j) * sizeof(short) 添加到该字节地址
    • 从该地址获取 2 字节整数。

    在指针代码数组中,A[i][j] 的加载操作计算:

    • a的字节地址
    • i * sizeof(short *) 添加到该字节地址
    • 从计算值中获取字节地址,称之为b
    • j * sizeof(short) 添加到b
    • 从地址b获取2字节整数

    p 的输出有些不同。请注意,地址 in p 与地址 of 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
    

    所有这些都在一个(主)函数中。在将各种指针传递给函数并访问这些指针后面的数组时,您需要自己进行并行实验。

    【讨论】:

    • 谢谢..(虽然我需要一些时间才能完全理解您的阐述)现在这激励了我.. :)
    【解决方案3】:
    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]定义了一个array1的数组,而int**定义了一个指向指针的指针。 int[3][3] 可以转换为指向 3 个int 的数组的指针(即int (*)[3]),就像int[3] 可以转换为指向int 的指针(即int*)一样。

    1.更准确地说,它定义了一个由 3 个 array-of-3-int 组成的数组。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2011-05-25
      • 2016-03-22
      • 1970-01-01
      • 1970-01-01
      • 2015-08-11
      • 2015-10-21
      相关资源
      最近更新 更多