1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
|
\> [go back](/ramblings)
# Rambling: Implementing a Dynamic Array in C
Have you ever wanted C to have a fully featured, easy-to-use, generic dynamic
array, which can be implemennted in a small of time?
What if I told you that C can have a full, easy-to-use, generic dynamic array,
which can be implemented in a small amount of time? Because it can. I wrote the
whole thing without testing, and it worked *almost* first try.
Let's start with the creation of a dynamic array. You might need to scroll
horizontally if your screen is teensy weensy.
```c
#define DA_MIN_CAP 8
// Wrap the function in a macro to make it a little nicer to use.
#define da_create(T, init_cap) \
((T*)_da_create(sizeof(T), init_cap))
void* _da_create(size_t type_size, int init_cap) {
// Ensure a mimimum capacity.
init_cap = init_cap < DA_MIN_CAP
? DA_MIN_CAP
: init_cap;
// Those + 2 ints are our length and capacity. You can use
// whatever integer type you want; int is fine for this use-case.
int* arr = (int*)malloc(type_size * init_cap + sizeof(int) * 2);
// Which we assign here.
arr[0] = 0;
arr[1] = init_cap;
return (void*)(arr + 2);
}
```
So, we define a function `_da_create()`. It takes in the size of the type you
want to create, and how many elements to reserve space for. It allocates that
space, plus space for two integers.
Those two integers are your array length and array capacity. They are placed in
front of the array.
And then we return the pointer we allocated, but offset to point to the start
of your data. Finally, we wrap all this in a nice little macro called
`da_create()` so we don't need to explicitly pass the size and cast the
resulting pointer.
Tada! You now have a generic dynamic array. Okay, well not really. You have
a heap-allocated array with two useless integers in front. Now, we need to be
able to access those integers in order to make them useful:
```c
#define da_len_ptr(arr) ((int*)arr - 2)
#define da_len(arr) (*da_len_ptr(arr))
#define da_cap_ptr(arr) ((int*)arr - 1)
#define da_cap(arr) (*da_cap_ptr(arr))
```
And now you have access to them. We have a separate macro to get a pointer to
the length/capacity so we can assign it. But anyway, how do you append? Well,
like so:
```c
// This fancy lil macro just gets the pointer that
// we actually alloc'd.
#define da_base(arr) ((void*)arr - 2)
// Of course, we wrap it in a macro for convenience.
#define da_append_slot(T, arr)
((T*)_da_append_slot(sizeof(T), (void*)arr))
// This function returns the slot it just appended.
void* _da_append_slot(size_t type_size, void** arr) {
int cap = da_cap_ptr(*arr);
int len = da_len_ptr(*arr);
(*len)++;
// If we need space, grow the array.
if (*len > *cap) {
// This won't work if the initial capacity is 0, which
// is why we implemented a minimum.
*cap *= 2;
// The base array is what realloc expects.
void* base = da_base(*arr);
base = realloc(base, (type_size * *cap) + sizeof(int) * 2);
assert(base != NULL); // Just to handle the case.
*arr = (void*)((int*)base + 2);
// Update the length so we don't get a use-after-free later.
len = da_len_ptr(*arr);
}
return *arr + ((*len - 1) * type_size);
}
```
So this doesn't actually really truly append anything. It ensures the existence
of a new slot at the end of the array, increments the length, and returns a
pointer to the last element. I didn't want to name it something like
`da_ensure_one()`, because it does a bit more than just make sure there's space
for your new element.
Anyway, you can append an element onto the end of the array by doing something
like this:
```c
int* arr = da_create(int, 0);
*da_append_slot(int, &arr) = 5;
```
Which you might notice can be wrapped up in a macro:
```c
#define da_append(T, arr, elem) (*da_append_slot(T, arr) = elem)
```
And tada, you now can append elements to your very own dynamic array. And best
of all, you don't need to care too much about the internals in your usage. You
don't need to use some bespoke array data type. You don't need to worry about
storing the length and capacity; it's all already handled for you.
But before I finish, we need to clean up the array, of course:
```c
void da_free(void* arr) {
void* base = da_base(arr);
free(base);
}
```
Here's a fancy schmancy example of how to use it:
```c
int main(void) {
uint64_t* arr = da_create(uint64_t, 0);
// Append some stuff.
for (int i = 0; i < 20; i++) {
da_append(uint64_t, &arr, 900000000000000 + i);
}
// Show some data.
printf("capacity: %d, length: %d\n", da_cap(arr), da_len(arr));
for (int i = 0; i < da_len(arr); i++) {
printf("[%d] = %lu\n", i, arr[i]);
}
da_free(arr);
}
```
I'll leave implementing extra methods like `da_remove()` and `da_find()` as an
exercise to the reader. If you know the basics of C, this shouldn't be too
hard, since as you might've noticed, the code for this dynamic array is pretty
similar to how someone would usually implement one.
<hr>
For those interested, here's the full source code:
```c
#include <assert.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#define DA_MIN_CAP 8
#define da_create(T, init_capacity) \
((T*)_da_create(sizeof(T), init_capacity))
#define da_len_ptr(arr) ((int*)arr - 2)
#define da_len(arr) (*da_len_ptr(arr))
#define da_cap_ptr(arr) ((int*)arr - 1)
#define da_cap(arr) (*da_cap_ptr(arr))
#define da_base(arr) ((void*)da_len_ptr(arr))
#define da_append_slot(T, arr) \
((T*)_da_append_slot(sizeof(T), (void**)arr))
#define da_append(T, arr, elem) (*da_append_slot(T, arr) = elem)
void* _da_create(size_t type_size, int init_cap) {
init_cap = init_cap < DA_MIN_CAP
? DA_MIN_CAP
: init_cap;
int* arr = (int*)malloc(type_size * init_cap + sizeof(int) * 2);
arr[0] = 0;
arr[1] = init_cap;
return (void*)(arr + 2);
}
void* _da_append_slot(size_t type_size, void** arr) {
int* cap = da_cap_ptr(*arr);
int* len = da_len_ptr(*arr);
(*len)++;
if (*len > *cap) {
*cap *= 2;
void* base = da_base(*arr);
base = realloc(base, (type_size * *cap) + sizeof(int) * 2);
assert(base != NULL);
*arr = (void*)((int*)base + 2);
len = da_len_ptr(*arr);
}
return *arr + ((*len - 1) * type_size);
}
void da_free(void* arr) {
void* base = da_base(arr);
free(base);
}
int main(void) {
uint64_t* arr = da_create(uint64_t, 0);
int* len = da_len_ptr(arr);
for (int i = 0; i < 20; i++) {
da_append(uint64_t, &arr, 900000000000000 + i);
}
printf("capacity: %d, length: %d\n", da_cap(arr), da_len(arr));
for (int i = 0; i < da_len(arr); i++) {
printf("[%d] = %lu\n", i, arr[i]);
}
da_free(arr);
}
```
And to be clear: I'm aware of how basic this article is. I just wrote it to
fill out space on my website.
|