IMHO its some kind of lets say "hacky". At least when seeing this for the 1st time its not so obvious. But its not that hard as soon you once got it.
I will give an example and try to visualize it. Hopefully this way its simpler to spot how it works beneath.
struct Numbers {
int size;
int nums[1]; // <- variable length array
};
int main() {
int size = 4;
struct Numbers *fourNums = NULL;
/* Allocate enough space for our struct PLUS extra space which will
* represent our array later. */
fourNums = malloc( sizeof(*fourNums) + (size-1)*sizeof(fourNums->nums[0]) );
fourNums->size = size;
/* Use it as it would be an array. */
for( int i=0 ; i<fourNums->size ; ++i ){
fourNums->nums[i] = 101 + i;
}
}
After our allocation this will look like:
.-- sizeof(int) for our "int size" member
|
| .-- sizeof(int[1]) for our "int num[1]" member
| |
| | .-- sizeof(int[3]) which we allocate additionally.
| | | HINT: One less than "size", because
| | | num[0] is already present in struct itself.
v v v
+------+------+--------------------+
| | | |
+------+------+--------------------+
And then when we fill it this will end up like:
.-- our value for "size"
|
| .-- num[0] will end up here.
| |
| | .-- num[1] through num[3] get
| | | stored here.
v v v
+------+------+--------------------+
| 4 | 101 | 102 , 103 , 104 |
+------+------+--------------------+
Effectively the 1st array entry gets placed at the space which is the strange looking num[1] in our struct. But all other indexes get placed beyond that structure. This works, because we did allocate more memory than the structure actually is in size and then we simply use that space behind to store our array there ;)
Just to give some more insight. When seeing arrays, we often see them declared as pointers. So I'll also visualize this scenario to point out some differences:
struct Numbers {
int size;
int *nums; // <- This time we're using a pointer.
};
int main() {
int size = 4;
struct Numbers *fourNums = NULL;
/* This time we need TWO allocations. Or we may choose to place the object
* on the stack instead. But in this example I'll use anoter alloc. */
fourNums = malloc(sizeof *fourNums); // <- No additional space needed.
fourNums->size = size;
fourNums->nums = malloc( size * sizeof(*fourNums->nums) ); // <- But need another allocation for the array.
/* Accessing feels the same. */
for( int i=0 ; i<fourNums->size ; ++i ){
fourNums->nums[i] = 101 + i;
}
}
This time the allocation looks different:
.-- sizeof(int) for our "int size" member
|
| .-- sizeof(int*) for our pointer-to-array
| |
| | .-- sizeof(int[4]) our array but
| | | somewhere else in memory.
v v v
+------+------+ +---------------------------+
| | | | |
+------+------+ +---------------------------+
Then also when filled looks different:
.-- our "size". This is the same as before.
|
| .-- This time, we need an additional pointer
| | to our array (there is no need for this
| | in our 1st approach).
| |
| | .-- Our array. This time we need to
| | | store ALL elemens here.
v v v
+------+------+ +------+------+------+------+
| 4 |0xABBA|---->| 101 | 102 | 103 | 104 |
+------+------+ +------+------+------+------+
Edit: Using sizeof *ptr
instead sizeof type
as suggested in comments. Thx.