C中的2D阵列如何变成1D阵列

How do 2D arrays in C become 1D arrays?

本文关键字:阵列 何变成 1D 2D 中的      更新时间:2023-10-16

如果有人能向我解释以下行为,我将不胜感激:

假设我声明一个静态2D阵列

float buffer[NX][NY];

现在,如果我想填充这个数组,我注意到可以这样做:

initarray(buffer, NX, NY);
#define INITDATAVAL 0.5
void initarray(void *ptr, int nx, int ny)
{
  int i, j;
  float *data = (float *) ptr;
  for (i=0; i < nx*ny; i++)
    {
      data[i] = INITDATAVAL;
    }
}

我的问题是,如果缓冲区是一个2D数组,那么一旦它被传递给initarray函数,它怎么能被用作1D数组呢?我很难理解…

当静态分配2D阵列时,分配的内存是连续的,但如果动态分配buffer,是否可以使用这种方式?

内存中包含3 x 4个元素(即矩阵)的2D数组如下所示:

A1 A2 A3 A4 B1 B2 B3 B4 C1 C2 C3 C4

由于底层存储是连续的,因此可以简单地将数组转换为指向第一个元素的指针,并使用单个偏移量访问所有元素(这种"强制转换"在这种上下文中被称为"衰减",当buffer传递给initarray时会自动发生)。

(在本示例中,编译器将把buffer[n][m]之类的表达式转换为buffer + n*NY+m。基本上,2D数组只是存储在1D数组中的2D数据的一种方便的表示法)。

首先,initarray应该采用float*参数,而不是void*

将数组转换为指针时,将丢失有关维度的类型信息。您实际上是在将它转换为指向第一个元素的指针,并确认存储是连续的。

char foo [2][2] = { {'a','b'}, {'c','d'} }; // Stored as 'a', 'b', 'c', 'd'

可以使用模板保留标注信息。

template <int W, int H>
void initarray (float (&input)[W][H]) {
    for (int x = 0; x < W; ++x) {
        for (int y = 0; y < H; ++y) {
            input [x][y] = INITDATAVAL;
        }
    }
}
int main () {
    float array [3][4];
    initarray (array);
}

这里,input是对给定类型的数组的引用(维度是完整类型的一部分)。模板参数推导将用W=3H=4实例化initarray的重载。很抱歉使用了行话,但这就是它的工作原理。

顺便说一句,您将无法使用指针参数调用此版本的initarray,但如果需要,您可以提供重载。我经常写这样的

extern "C" void process (const char * begin, const char * end);
template <typename N>
void process (const char * (&string_list) [N]) {
    process (string_list, string_list + N);
}

这个想法是提供最通用的接口,在单独的翻译单元或库中实现一次,然后提供更友好、更安全的接口。

const char * strings [] = {"foo", "bar"};
int main () {
    process (strings);
}

现在,如果我更改strings,就不必在其他地方更改代码。我也不必考虑令人恼火的细节,比如我是否正确维护了NUMBER_OF_STRINGS=2

数组是一系列连续的对象。

数组也是一系列连续的对象,但这些对象恰好是数组,它们本身只是由端到端放置在内存中的元素组成的。图片:

float a[2][3];
a[0]                      a[1]
+-------+-------+-------++-------+-------+-------+
|float  |float  |float  ||float  |float  |float  |
|a[0][0]|a[0][1]|a[0][2]||a[1][0]|a[1][1]|a[1][2]|
|       |       |       ||       |       |       |
+-------+-------+-------++-------+-------+-------+

由于这是一行中包含浮点数的一系列单元格,因此也可以将其视为6个浮点数的单个数组(如果通过适当的指针查看)。新图片:

float* b(&a[0][0]);//The &a[0][0] here is not actually necessary
                   //(it could just be *a), but I think
                   //it makes it clearer.
+-------+-------+-------++-------+-------+-------+
|float  |float  |float  ||float  |float  |float  |
|*(b+0) |*(b+1) |*(b+2) ||*(b+3) |*(b+4) |*(b+5) |
|       |       |       ||       |       |       |
+-------+-------+-------++-------+-------+-------+
^       ^       ^        ^       ^       ^       
|       |       |        |       |       |       
b      b+1     b+2      b+3     b+4     b+5

如您所见,a[0][0]变成b[0]a[1][0]变成b[3]。整个数组可以看作只是一系列的浮点,而不是一系列的浮动数组。

2D阵列的所有内存都已连续分配。

这意味着,给定一个指向数组开头的指针,数组看起来是一个大的1D数组,因为2D数组中的每一行都在最后一行之后。

数据只是按顺序存储在磁盘上。像这样:

0:              buffer[0][0],
1:              buffer[0][1],
.                ...
NY-2:           buffer[0][NY-2],
NY-1:           buffer[0][NY-1],
NY:             buffer[1][0],
NY+1:           buffer[1][1],
.                ...
NY*2-2:         buffer[1][NY-2],
NY*2-1:         buffer[1][NY-1],
.                ...
NY*(NX-1):      buffer[NX-1][0],
NY*(NX-1)+1:    buffer[NX-1][1],
.                ...
NY*(NX-1)+NY-2: buffer[NX-1][NY-2],
NY*(NX-1)+NY-1: buffer[NX-1][NY-1],

数组本质上是指向第一个元素的指针。因此,在for循环中所做的是顺序填充数据,而数据也可以被解释为包含整个数据块的单个数组(float[])或指针(float*)。

值得注意的是,在一些(旧的/特殊的)系统上,数据可能会被填充。但所有x86系统都填充到32位边界(这是浮点大小),编译器通常(至少是MSVC)打包到32位对齐,所以这样做通常是可以的。

编辑后的问题的部分答案:

当静态分配2D阵列时,分配的内存是连续的,但如果动态分配缓冲区,可以使用这种方式吗?

可以将静态分配的2D数组视为1D数组的原因是,编译器知道维度的大小,因此可以分配连续块,然后当您在缓冲区[x][y]中使用索引运算符时,它会计算该内存的索引。

当您动态分配内存时,您可以选择将其设为1D或2D,但不能像对待静态分配的数组那样将其同时处理,因为编译器不会知道最内部维度的大小。所以你可以:

  • 分配一个指针数组,然后为每个指针分配一个1D数组。然后可以使用buffer[x][y]语法
  • 分配一个1D数组,但您必须在from缓冲区[y*x_dim+x]中手动计算索引

一个2D数组在内存中连续排列,因此使用正确的类型punning,您可以将其视为已声明为1D数组的

T a[N][M];
T *p = (&a[0][0]);

所以

a[i][j] == p[i*N + j]

除非是sizeof或一元&运算符的操作数,或者是用于初始化声明中的数组的字符串文字,否则类型为"T的N元素数组"的表达式将转换为类型为"指向T的指针"的表达式,其值为数组的第一个元素的地址。

当你呼叫时

initarray(buffer, NX, NY);

将表达式buffer替换为类型为"指向NY的指针-float的元素数组"或float (*)[NY]的表达式,并将此表达式传递给initarray

现在,表达式buffer&buffer[0][0]是相同的(数组的地址与数组中第一个元素的地址相同),但类型不同(float (*)[NY]float *相反)。这在某些情况下很重要。

在C中,可以将void *值分配给其他对象指针类型,反之亦然,而无需强制转换;这在C++中不是真的。我很好奇g++是否对此提出了任何警告。

如果是我,我会明确地传递缓冲区的第一个元素的地址:

initarray(&buffer[0][0], NX, NY);

并将第一个参数的类型从void *更改为float *,以使一切尽可能直接:

void initarray(float *data, int nx, int ny)
{
  ...
  data[i] = ...;
  ...
}