I would appreciate if someone could explain to me the following behavior:
Say I declare a static 2D array
float buffer[NX][NY];
Now, if I want to populate this array, I have notice that it could be done this way:
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;
}
}
My question is, if buffer is a 2D array, how can it be used as a 1D array once it is passed to initarray
function? I am struggling to understand it...
When 2D arrays are statically allocated, the memory allocated is contiguous, but could this way be used if buffer
is dynamically allocated instead?
A 2D array with 3 x 4 elements (ie a matrix) looks like this in memory:
A1 A2 A3 A4 B1 B2 B3 B4 C1 C2 C3 C4
Since the underlying storage is continuous, one can simply convert the array to a pointer to the first element and access all elements using a single offset (this 'cast', which is called 'decaying' in such a context, happens automatically when buffer
is passed to initarray
).
(In this sample, the compiler would translate an expression such as buffer[n][m]
to buffer + n*NY+m
Basically, 2D arrays are just a comfortable notation for 2D data stored in 1D arrays).
For a start, initarray
should take a float*
argument, not void*
.
When you convert an array to a pointer, you lose type information about the dimension. You're really converting it to a pointer to the first element, and acknowledging that storage is contiguous.
char foo [2][2] = { {'a','b'}, {'c','d'} }; // Stored as 'a', 'b', 'c', 'd'
You can retain dimension information with templates.
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);
}
Here, input
is a reference to an array of the given type (and dimensionality is part of the full type). Template argument deduction will instantiate an overload of initarray
with W=3
, H=4
. Sorry for the jargon, but that's how it works.
Incidentally, you will not be able to call this version of initarray
with a pointer argument, but you can provide overloads if you want. I often write things like this
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);
}
The idea is to provide the most-general possible interface, implement it once in a separate translation unit or library, or whatever, and then provide friendlier, safer interfaces.
const char * strings [] = {"foo", "bar"};
int main () {
process (strings);
}
Now if I change strings
, I don't have to change the code elsewhere. I also don't have to think about irritating details like whether I have maintained NUMBER_OF_STRINGS=2
correctly.
An array is a contiguous series of objects.
An array of arrays is also a contiguous series of objects, but these objects happen to be arrays, which are themselves just made up of their elements placed end-to-end in memory. Picture:
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]|
| | | || | | |
+-------+-------+-------++-------+-------+-------+
As this is a series of cells in a row containing floats, it can also be viewed as a single array of 6 floats (if viewed through an appropriate pointer). New picture:
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
As you can see, a[0][0]
becomes b[0]
, and a[1][0]
becomes b[3]
. The whole array can be seen as just a series of floats, and not a series of arrays of floats.
All the memory for the 2D array has been allocated contiguously.
This means that given a pointer to the start of the array, the array appears to be a large 1D array as each row in the 2D array follows the last.
The data is simply stored sequentially on disk. Like so:
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],
The array is essentially a pointer to the first element. So what you do in the for loop is sequentially fill data, while the data just as well could be interpreted as a single array containing the whole block of data ( float[]
) or as a pointer ( float*
).
Worth noting is that on some (old/peculiar) systems the data may be padded. But all x86 systems pad to 32-bit boundary (which is the size of a float) and compilers usually (at least MSVC) pack to 32-bit alignment, so it's usually ok to do this.
Partial answer to your edited question:
When 2D arrays are statically allocated, the memory allocated is contiguous, but could this way be used if buffer is dynamically allocated instead?
The reason you can treat a statically allocated 2D array as a 1D array is that the compiler knows the sizes of the dimensions so can allocate a contiguous block and then it calculates the index into that memory when you use the index operators as in buffer[x][y].
When you allocate memory dynamically you can choose to make it 1D or 2D, but you cannot treat it as both like you can with a statically allocated array, because the compiler will not know the size of your innermost dimension. So you can either:
A 2D array is laid out contiguously in memory, so with the right type punning you can treat it as though it had been declared as a 1D array:
T a[N][M];
T *p = (&a[0][0]);
so
a[i][j] == p[i*N + j]
Except when it is the operand of the sizeof
or unary &
operators, or is a string literal being used to initialize an array in a declaration, an expression of type "N-element array of T
" is converted to an expression of type "pointer to T
", and its value is the address of the first element of the array.
When you call
initarray(buffer, NX, NY);
the expression buffer
is replaced with an expression of type "pointer to NY
-element array of float
", or float (*)[NY]
, and this expression is passed to initarray
.
Now, the values of the expressions buffer
and &buffer[0][0]
are the same (the address of an array is the same as the address of the first element in the array), but the types are not ( float (*)[NY]
as opposed to float *
). This matters in some contexts.
In C, you can assign void *
values to other object pointer types and vice-versa without a cast; this is not true in C++. I'd be curious to see if g++ throws up any warnings about this.
If it were me, I'd pass the address of the first element of buffer explicitly:
initarray(&buffer[0][0], NX, NY);
and change the type of the first parameter from void *
to float *
, just to keep everything as direct as possible:
void initarray(float *data, int nx, int ny)
{
...
data[i] = ...;
...
}
The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.