Dynamic / Growable Arrays
A growable array is a heap buffer that resizes as you append. The hard part is memory: who owns the buffer, how it grows (usually by reallocating into a larger block and copying), and who frees it. Here every language builds the same list [1,2,3], appends a 4, prints it, and cleans up - so you can see exactly where the allocation, the capacity-vs-length split, and the free live in each.
#include <stdio.h>
#include <stdlib.h>
int main(void) {
// Track length (used) and capacity (allocated) separately.
size_t len = 0, cap = 4;
int *xs = malloc(cap * sizeof *xs); // one heap block, owned by us
if (!xs) return 1;
for (int i = 1; i <= 3; i++)
xs[len++] = i; // [1,2,3], len=3, cap=4
// Append a 4th; grow first if the block is full.
if (len == cap) {
cap *= 2;
int *grown = realloc(xs, cap * sizeof *xs);
if (!grown) { free(xs); return 1; } // realloc fail leaves xs valid
xs = grown; // realloc may move the block
}
xs[len++] = 4; // [1,2,3,4]
for (size_t i = 0; i < len; i++)
printf("%d\n", xs[i]);
free(xs); // exactly one free for one malloc
return 0;
}C gives you no array type that grows: you keep len and cap by hand, realloc into a bigger block when full (it may move the data, so always reassign the returned pointer), and pair the single malloc with a single free. A failed realloc returns NULL without freeing the old block, so capture it in a temporary to avoid leaking.
#include <vector>
#include <iostream>
int main() {
// std::vector owns a heap buffer and tracks size()/capacity() for you.
std::vector<int> xs{1, 2, 3}; // RAII: allocation happens here
xs.push_back(4); // grows (reallocates) if size == capacity
for (int x : xs) // range-for over the owned buffer
std::cout << x << '\n';
return 0; // ~vector frees the buffer automatically
}std::vector<int> is the idiomatic growable array: it owns its heap buffer, doubles capacity on push_back, and frees everything in its destructor (RAII) - no manual delete. reserve() would pre-size to avoid reallocations; copies deep-copy the buffer while moves transfer ownership cheaply.
// HolyC: no vector type, so manage a MAlloc'd I64 array like C.
I64 len = 0, cap = 4;
I64 *xs = MAlloc(cap * sizeof(I64)); // per-task heap; reclaimed if task dies
I64 i;
for (i = 1; i <= 3; i++)
xs[len++] = i; // [1,2,3]
if (len == cap) { // grow: MAlloc new, copy, Free old
cap *= 2;
I64 *grown = MAlloc(cap * sizeof(I64));
MemCpy(grown, xs, len * sizeof(I64));
Free(xs);
xs = grown;
}
xs[len++] = 4; // [1,2,3,4]
for (i = 0; i < len; i++)
Print("%d\n", xs[i]);
Free(xs); // Free(NULL) is harmless in HolyC
HolyC has no standard growable container, so you do it C-style. MAlloc() carves from the running task's data heap and has no realloc, so growth means allocate-new, MemCpy, then Free the old block. Heaps are freed when the task ends, but pairing MAlloc/Free keeps long-lived tasks from leaking; this only runs under TempleOS.
const std = @import("std");
pub fn main() !void {
// The caller supplies the allocator; nothing allocates behind your back.
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit(); // detects leaks at shutdown
const allocator = gpa.allocator();
// ArrayList is unmanaged: it stores no allocator, so each op takes one.
var xs: std.ArrayList(i32) = .empty;
defer xs.deinit(allocator); // frees the backing buffer
try xs.appendSlice(allocator, &.{ 1, 2, 3 }); // may grow; can error (!)
try xs.append(allocator, 4); // [1,2,3,4]
for (xs.items) |x| // .items is the live slice (len)
std.debug.print("{d}\n", .{x});
}Zig has no hidden allocations: std.ArrayList(i32) is unmanaged, so it stores no allocator and you pass one explicitly to append/appendSlice/deinit. Every grow is therefore visible, and append/appendSlice return an error union (try) because reallocation can fail. defer xs.deinit(allocator) frees the buffer, and the GeneralPurposeAllocator's deinit() flags any leak you forgot.
use fmt;
export fn main() void = {
// A slice []int carries a pointer, length, and capacity.
let xs: []int = alloc([1, 2, 3])!; // heap-allocated, length 3 (! on nomem)
defer free(xs); // release the backing array
append(xs, 4)!; // grows (reallocates); ! on nomem
for (let i = 0z; i < len(xs); i += 1)
fmt::println(xs[i])!; // ! aborts on a write error
};Hare slices ([]int) are growable views over a heap array carrying length and capacity; alloc makes the backing buffer, append reallocates it when capacity is exceeded, and defer free(xs) returns it. Because any allocation can fail, alloc and append return a nomem error that you must assert with ! (or propagate with ?). Memory is manual - there is no GC - so the defer is what guarantees the single alloc is matched by a single free.
package main
import "core:fmt"
main :: proc() {
// [dynamic]int uses the implicit context.allocator.
xs: [dynamic]int
defer delete(xs) // free the backing memory
append(&xs, 1, 2, 3) // grows the buffer as needed
append(&xs, 4) // [1,2,3,4]
for x in xs { // iterate len(xs) elements
fmt.println(x)
}
}Odin's [dynamic]int is a built-in growable array backed by the implicit context.allocator; append reallocates when it runs out of capacity and delete(xs) frees it. The context system lets you swap allocators (e.g. an arena) for the whole call tree without changing this code, while defer delete keeps allocation paired with cleanup.
\ Classic Forth has no growable container; ALLOT reserves FIXED data space.
\ We carve a small cell array and track our own count, then show the heap idiom.
CREATE XS 4 CELLS ALLOT \ reserve room for 4 cells at HERE (static)
VARIABLE #XS 0 #XS ! \ our own length counter
: PUSH ( n -- ) \ store n at XS[#XS], bump the count
#XS @ CELLS XS + ! \ address = XS + count*cell ; store
1 #XS +! ;
: .XS ( -- ) \ print the #XS live cells
#XS @ 0 ?DO I CELLS XS + @ . CR LOOP ;
1 PUSH 2 PUSH 3 PUSH \ [1,2,3]
4 PUSH \ [1,2,3,4] -- fits; ALLOT space is never freed
.XS
\ For TRUE dynamic growth use the optional ALLOCATE / RESIZE / FREE word set:
\ 4 CELLS ALLOCATE THROW ( -- addr ) \ heap block, like malloc
\ addr 8 CELLS RESIZE THROW \ grow it, like realloc
\ addr FREE THROW \ release it, like freeALLOT only bumps HERE to reserve fixed, statically-sized data space - it can't grow and is never individually freed, so it doesn't map cleanly to a growable array. The closest real analog to malloc/realloc/free is the optional ALLOCATE/RESIZE/FREE word set (shown in the trailing comments), where you pair each ALLOCATE with a FREE by hand just like in C.