C语言中的二维数组如何变成一维数组?

5

希望有人能够解释一下以下行为:

假设我声明了一个静态的 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;
    }
}

我的问题是,如果缓冲区是一个二维数组,一旦传递给 initarray 函数,它如何作为一个一维数组使用?我很难理解这个问题...
当二维数组静态分配时,分配的内存是连续的,但如果 buffer 是动态分配的,这种方法是否可行?
7个回答

8

一个3x4的二维数组(即矩阵)在内存中的样子如下:

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

自从底层存储是连续的,我们只需将数组转换为指向第一个元素的指针,并使用单个偏移量访问所有元素(在这种情况下称为“衰减”的这种“转换”会自动发生,当buffer传递给initarray时)。 (在此示例中,编译器将将诸如buffer [n] [m]的表达式转换为buffer + n * NY + m基本上,2D数组只是用于存储在1D数组中的2D数据的舒适符号表示法)。

4

首先,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=4initarray 重载。抱歉使用了行话,但就是这样工作的。
顺便说一下,您将无法使用指针参数调用此版本的 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);
}

现在,如果我更改字符串,我就不必在其他地方更改代码。我也不必考虑像是否正确维护了NUMBER_OF_STRINGS=2这样令人烦恼的细节。

+1 保留模板信息的做法真不错,我之前还不知道呢。 - evotopid

4

数组是一连串的对象。

数组的数组也是一连串的对象,但这些对象恰巧是数组,它们本身只是由其元素依次放置在内存中而组成。示意图:

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]。整个数组可以被视为一系列浮点数,而不是浮点数数组。


1

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

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位边界(即float的大小),编译器通常(至少MSVC)会打包到32位对齐,因此通常可以这样做。


1

一个二维数组在内存中是连续排列的,因此通过正确的类型转换,你可以将其视为已声明为一维数组:

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

所以

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

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

当您调用时

initarray(buffer, NX, NY);

表达式buffer被替换为类型为“指向float数组的NY个元素”的指针表达式,即float (*)[NY],并将该表达式传递给initarray

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

在C中,您可以将void *值分配给其他对象指针类型,反之亦然,而无需进行转换;在C++中,情况并非如此。我很想看看g ++是否会对此发出任何警告。

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

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

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

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

那么 &buffer[0][0] 相当于只有 buffer 吗? - Manolete
就数值而言,是的。就类型而言,不是。 - John Bode
为什么这很重要 - 没有保证指向float的指针和指向float数组的指针具有相同的大小或表示(非常不可能,但不是不可能)。如果将buffer传递给期望参数为float *的函数,则如果指针大小或表示不同,则可能在运行时导致问题。您可能没有在处理此类问题的系统上工作,但仍然了解它是很好的。 - John Bode

1

所有2D数组的内存都已经连续分配。

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


1

对您编辑后的问题的部分回答:

当二维数组是静态分配时,分配的内存是连续的,但如果缓冲区是动态分配的,是否可以使用这种方式?

您可以将静态分配的二维数组视为一维数组的原因是编译器知道维度的大小,因此可以分配一个连续的块,然后在使用索引运算符(如buffer[x][y])时计算该内存中的索引。

当您动态分配内存时,可以选择使其成为一维或二维,但不能像静态分配数组那样同时将其视为两者,因为编译器不会知道最内层维度的大小。因此,您可以选择以下两种方法之一:

  • 分配指针数组,然后为每个指针分配一个一维数组。然后,您可以使用buffer[x][y]语法。
  • 分配一个一维数组,但您必须手动计算索引,例如buffer[y * x_dim + x]

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