【问题标题】:How are multi-dimensional arrays formatted in memory?多维数组如何在内存中格式化?
【发布时间】:2021-08-24 21:18:02
【问题描述】:

在 C 中,我知道我可以使用以下代码在堆上动态分配一个二维数组:

int** someNumbers = malloc(arrayRows*sizeof(int*));

for (i = 0; i < arrayRows; i++) {
    someNumbers[i] = malloc(arrayColumns*sizeof(int));
}

显然,这实际上创建了一个指向一组单独的一维整数数组的指针的一维数组,当我要求时,“系统”可以理解我的意思:

someNumbers[4][2];

但是当我静态声明一个二维数组时,如下行...:

int someNumbers[ARRAY_ROWS][ARRAY_COLUMNS];

...是否在堆栈上创建了类似的结构,或者它完全是另一种形式? (即它是一维指针数组吗?如果不是,它是什么,以及如何找出对它的引用?)

另外,当我说“系统”时,究竟是什么负责解决这个问题?内核?还是C编译器在编译的时候整理出来的?

【问题讨论】:

  • 如果可以的话,我会给予超过 +1。
  • @toohonestforthissite 确实如此。对此进行扩展:循环和调用 malloc() 不会产生 N 维数组。。它导致指针数组[指向指针数组[...]] 以完全分离一维数组。请参阅Correctly allocating multi-dimensional arrays 了解如何分配 TRUE N 维数组。

标签: c arrays memory data-structures stack-memory


【解决方案1】:

静态二维数组看起来像一个数组数组 - 它只是在内存中连续布局。数组与指针不同,但因为您经常可以互换使用它们,所以有时会让人感到困惑。但是,编译器会正确跟踪,这使得一切都很好。您确实必须小心您提到的静态二维数组,因为如果您尝试将一个传递给采用int ** 参数的函数,就会发生不好的事情。这是一个简单的例子:

int array1[3][2] = {{0, 1}, {2, 3}, {4, 5}};

在内存中是这样的:

0 1 2 3 4 5

一模一样

int array2[6] = { 0, 1, 2, 3, 4, 5 };

但是如果你尝试将array1 传递给这个函数:

void function1(int **a);

你会得到一个警告(应用程序将无法正确访问数组):

warning: passing argument 1 of ‘function1’ from incompatible pointer type

因为二维数组与int ** 不同。可以说,数组自动衰减为指针只会“深一层”。您需要将函数声明为:

void function2(int a[][2]);

void function2(int a[3][2]);

让一切都快乐。

同样的概念扩展到 n 维数组。但是,在您的应用程序中利用这种有趣的业务通常只会使其更难理解。所以在外面要小心。

【讨论】:

  • 感谢您的解释。所以“void function2(int a[][2]);”将接受静态和动态声明的 2D 吗?而且我想如果第一个维度保留为[],那么传递数组的长度仍然是一种很好的做法/必要的?
  • @Chris 我不这么认为——你将很难让 C 将堆栈或全局分配的数组混合成一堆指针。
  • @JasonK。 - 不。数组不是指针。数组在某些情况下“衰减”为指针,但它们绝对相同。
  • 要明确:是的,克里斯“传递数组的长度仍然是一个好习惯”作为单独的参数,否则使用 std::array 或 std::vector (这是 C++ 不旧C)。我认为我们同意@CarlNorum 对新用户的概念和实践,引用 Quora 上的 Anders Kaseorg 的话:“学习 C 的第一步是理解指针和数组是一回事。第二步是理解指针和数组是不同的。”
  • @JasonK。 “学习 C 的第一步是理解指针和数组是一回事。” - 这句话非常错误和误导!理解它们相同确实是最重要的一步,但大多数情况下,数组转换为指向第一个元素的指针运营商! sizeof(int[100]) != sizeof(int *)(除非你找到了一个带有100 * sizeof(int)字节/int的平台,但那是另一回事。
【解决方案2】:

答案是基于 C 并不真正 2D 数组的想法 - 它有数组的数组。当你声明这个时:

int someNumbers[4][2];

您要求 someNumbers 是一个由 4 个元素组成的数组,其中该数组的每个元素都是 int [2] 类型(它本身就是一个由 2 个 ints 组成的数组)。

难题的另一部分是数组总是在内存中连续布局。如果您要求:

sometype_t array[4];

那么这将永远是这样的:

| sometype_t | sometype_t | sometype_t | sometype_t |

(4 个 sometype_t 对象并排排列,中间没有空格)。所以在你的someNumbersarray-of-arrays 中,它看起来像这样:

| int [2]    | int [2]    | int [2]    | int [2]    |

每个int [2] 元素本身就是一个数组,如下所示:

| int        | int        |

总的来说,你得到了这个:

| int | int  | int | int  | int | int  | int | int  |

【讨论】:

  • 看着最终的布局让我觉得 int a[][] 可以作为 int *...访问吗?
  • @user3238855: 类型不兼容,但是如果你得到指向数组数组中第一个int 的指针(例如,通过评估a[0]&amp;a[0][0]),那么是的,您可以抵消它以顺序访问每个int)。
【解决方案3】:
unsigned char MultiArray[5][2]={{0,1},{2,3},{4,5},{6,7},{8,9}};

在内存中等于:

unsigned char SingleArray[10]={0,1,2,3,4,5,6,7,8,9};

【讨论】:

    【解决方案4】:

    也回答你的问题:两者,尽管编译器正在做大部分繁重的工作。

    对于静态分配的数组,“系统”将是编译器。它将像为任何堆栈变量一样保留内存。

    在 malloc 数组的情况下,“系统”将是 malloc 的实现者(通常是内核)。编译器将分配的只是基指针。

    编译器总是会按照声明的类型来处理类型,除非在 Carl 给出的示例中它可以找出可互换的用法。这就是为什么如果你将 [][] 传递给一个函数,它必须假定它是一个静态分配的平面,其中 ** 被假定为指向指针的指针。

    【讨论】:

    • @Jon L. 我不会说 malloc 是由内核实现的,而是由内核原语(例如 brk)之上的 libc 实现的
    • @ManuelSelva:标准没有规定malloc 的实现位置和方式,并留给实现,分别。环境。对于独立环境,它是可选的,就像标准库中需要链接函数的所有部分一样(这是需求实际产生的结果,而不是标准规定的字面意思)。对于一些现代托管环境,它确实依赖于内核函数,要么是完整的东西,要么是(例如 Linux),正如您使用 stdlib 和内核原语编写的那样。对于非虚拟内存单进程系统,只能是stdlib。
    【解决方案5】:

    假设,我们定义并初始化了 a1a2,如下所示 (c99):

    int a1[2][2] = {{142,143}, {144,145}};
    int **a2 = (int* []){ (int []){242,243}, (int []){244,245} };
    

    a1 是一个齐次二维数组,在内存中具有简单的连续布局,表达式 (int*)a1 被评估为指向其第一个元素的指针:

    a1 --> 142 143 144 145
    

    a2从一个异构的二维数组初始化,是一个指向int*类型值的指针,即解引用表达式*a2计算为int*类型的值,内存布局不必是连续的:

    a2 --> p1 p2
           ...
    p1 --> 242 243
           ...
    p2 --> 244 245
    

    尽管内存布局和访问语义完全不同,但数组访问表达式的 C 语言语法对于同构和异构 2D 数组看起来完全一样:

    • 表达式a1[1][0]将从a1数组中取出值144
    • 表达式a2[1][0]将从a2数组中取出值244

    编译器知道a1 的访问表达式对int[2][2] 类型进行操作,而a2 的访问表达式对int** 类型进行操作。生成的汇编代码将遵循同构或异构访问语义。

    int[N][M] 类型的数组被类型转换然后作为int** 类型访问时,代码通常会在运行时崩溃,例如:

    ((int**)a1)[1][0]   //crash on dereference of a value of type 'int'
    

    【讨论】:

      【解决方案6】:

      要访问特定的二维数组,请考虑数组声明的内存映射,如下代码所示:

          0  1
      a[0]0  1
      a[1]2  3
      

      要访问每个元素,只需将您感兴趣的数组作为参数传递给函数就足够了。然后使用列的偏移量来单独访问每个元素。

      int a[2][2] ={{0,1},{2,3}};
      
      void f1(int *ptr);
      
      void f1(int *ptr)
      {
          int a=0;
          int b=0;
          a=ptr[0];
          b=ptr[1];
          printf("%d\n",a);
          printf("%d\n",b);
      }
      
      int main()
      {
         f1(a[0]);
         f1(a[1]);
          return 0;
      }
      

      【讨论】:

        猜你喜欢
        • 2021-05-13
        • 1970-01-01
        • 1970-01-01
        • 2021-10-28
        • 2017-04-17
        • 1970-01-01
        • 2021-12-02
        • 2014-09-05
        相关资源
        最近更新 更多