2012-02-02 133 views
5

我将不胜感激,如果有人可以给我解释以下行为:C中的二维数组如何变成一维数组?

说我声明静态二维数组

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是动态分配的,则可以使用这种方法吗?

回答

6

2D阵列用3×4个元素(即,基质)看起来像这样在存储器中:

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

由于底层存储是连续的,人们可以简单地将数组转换成指针的第一个元素并使用单个偏移量访问所有元素(这种'cast',在这种情况下称为'decaying',当buffer传递给initarray时会自动发生)。 (在本示例中,编译器会将诸如buffer[n][m]之类的表达式翻译为buffer + n*NY+m基本上,二维数组只是存储在一维数组中的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=3,H=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

+0

+1用于保留模板信息。我甚至不知道这一点。 – evotopid 2012-02-02 16:22:01

1

二维数组的所有内存已被连续分配。

这意味着给定一个指向数组开头的指针,数组看起来像是一个很大的一维数组,因为二维数组中的每一行都跟随着最后一个数组。

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位边界(这是浮点大小),编译器通常(至少MSVC)打包为32位对齐,所以通常这样做可以。

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

部分回答您的问题编辑:

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

可以治疗静态分配的2D阵列作为一维数组是编译器知道的尺寸的尺寸,以便可以分配一个连续的块,然后,它计算到该存储器中的索引时,使用索引的原因如缓冲区[x] [y]中的运算符。

当您动态分配内存时,您可以选择使其成为1D或2D,但无法像使用静态分配数组那样对待它,因为编译器不会知道最内层维度的大小。所以你可以:

  • 分配一个指针数组,然后为每个分配一个1D数组。然后可以使用buffer [x] [y]语法。
  • 分配一维数组,但随后必须手动计算在从缓冲器自己的索引[Y * x_dim + X]
+0

明白了。谢谢! – Manolete 2012-02-02 17:01:02

1

二维阵列被连续地在存储器与右型双关摆出来,所以你可以把它好像它已被声明为一维数组:

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

所以

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

防爆当它是sizeof或一元&运算符的操作数时,或者是在声明中用于初始化数组的字符串文字时,将类型为“T的N元素数组”的表达式转换为类型为“pointer到T“,它的值是数组的第一个元素的地址。

当调用

initarray(buffer, NX, NY); 

表达buffer被替换类型的“指针NYfloat - 元素阵列”,或float (*)[NY]的表达式,该表达式被传递给initarray

现在,值表达式的buffer&buffer[0][0]是相同的(在阵列的地址是相同的阵列中的第一个元素的地址),但类型不是(float (*)[NY]相对于float *)。这在某些情况下很重要。在C中,您可以将void *值分配给其他对象指针类型,反之亦然,不需要强制转换;这是而不是在C++中为true。我很想知道g ++是否会抛出任何关于这个的警告。

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

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

,并从void *改变第一个参数的类型float *,只是把一切都尽可能直接可能的:

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

so'&buffer [0] [0]'相当于'buffer'吗? – Manolete 2012-02-03 12:11:50

+0

就价值而言,是的。在类型方面,没有。 – 2012-02-03 12:26:59

+0

为什么它很重要 - 不能保证指向'float'的指针和指向'float'数组的指针具有相同的大小或表示(很不可能,但不是不可能)。如果指针大小或表示不相同,将'buffer'传递给需要'float *'作为参数的函数可能会导致运行时出现问题。你可能不是在一个系统上工作,那会是一个问题,但是也应该意识到它是一样的。 – 2012-02-03 20:42:23