C中的2D数组如何成为一维数组?
如果有人能向我解释以下行为,我将不胜感激:
假设我声明了一个静态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
函数,它怎么能用作一维数组呢? 我很难理解它……
当静态分配2D数组时,分配的内存是连续的,但如果动态分配buffer
可以使用这种方式吗?
具有3 x 4个元素(即矩阵)的2D数组在内存中看起来像这样:
A1 A2 A3 A4 B1 B2 B3 B4 C1 C2 C3 C4
由于底层存储是连续的,人们可以简单地将数组转换为指向第一个元素的指针并使用单个偏移量访问所有元素(这种’cast’,在这样的上下文中称为’decaying’,当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 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
是对给定类型的数组的引用(维度是完整类型的一部分)。 模板参数推导将实例化initarray
的重载, W=3
, H=4
。 对不起行话,但这就是它的工作原理。
顺便说一句,您将无法使用指针参数调用此版本的initarray
,但如果需要,您可以提供重载。 我经常写这样的东西
extern "C" void process (const char * begin, const char * end); template 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]
。 整个数组可以看作只是一系列浮点数,而不是一系列浮点数。
2Darrays的所有内存都已连续分配。
这意味着给定指向数组开头的指针,数组看起来是一个大的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数组视为一维数组的原因是编译器知道维度的大小,因此可以分配一个连续的块,然后在缓冲区中使用索引运算符时计算该内存的索引[x ] [Y]。
当您动态分配内存时,您可以选择将其设置为1D或2D,但您不能像使用静态分配的数组那样对待它,因为编译器不会知道最内层维度的大小。 所以你可以:
- 分配一个指针数组,然后为每个指针分配一个数组。 然后,您可以使用buffer [x] [y]语法。
- 分配一维数组,但是你必须自己在from缓冲区中手动计算索引[y * x_dim + x]
2D数组在内存中连续布局,因此使用正确的类型双关语,您可以将其视为已声明为1D数组:
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
替换为“指向NY
-element array of 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] = ...; ... }