Arrays & Slices
A fixed array is a contiguous block of N elements whose size is part of its type; a slice is a lightweight view into that storage - really just a (pointer, length) pair - that owns nothing. This topic makes the same 5-int array [10,20,30,40,50], takes a view of the middle three [20,30,40], and prints them, so you can see exactly where the array lives, how a slice borrows it without copying, and how each language handles bounds.
#include <stdio.h>
#include <stddef.h>
int main(void) {
// Fixed array: 5 ints contiguous on the stack. Its size is the type.
int a[5] = {10, 20, 30, 40, 50};
size_t n = sizeof a / sizeof a[0]; // 5, computable only here
// C has no slice type: a "view" is a raw pointer + a length you carry.
int *view = &a[1]; // points at the middle element
size_t view_len = 3; // [20, 30, 40] -- you track this
for (size_t i = 0; i < view_len; i++)
printf("%d\n", view[i]); // view[i] == a[1 + i]
// No bounds checks at all: view[3] would read a[4] (or worse) silently.
return 0; // stack array; nothing to free
}In C an array decays to a bare pointer, losing its length - so a "slice" is just &a[1] plus a size_t length you pass around by hand. There are no bounds checks: indexing past view_len is undefined behaviour, which is exactly why higher languages bundle the length into the slice itself.
#include <array>
#include <span>
#include <iostream>
int main() {
// std::array<int,5> is a fixed array that knows its size at the type level.
std::array<int, 5> a{10, 20, 30, 40, 50};
// std::span (C++20) is a non-owning view: pointer + length, no copy.
std::span<int> view = std::span{a}.subspan(1, 3); // [20, 30, 40]
for (int x : view) // range-for over the borrowed view
std::cout << x << '\n';
// C++20 std::span has no .at(): view[3] is unchecked (UB), like a raw pointer;
// a.at(4) on the std::array is the bounds-checked accessor that throws.
return 0; // a is on the stack; span owns nothing
}std::array<int,5> keeps its length in the type, and std::span is the idiomatic borrowed view - a (pointer, size) pair that copies nothing and frees nothing (it does not own the data). subspan(1, 3) carves the middle three; in C++20 std::span has no .at(), so view[i] is unchecked (out-of-bounds is undefined behaviour, exactly like the underlying array) - the bounds-checked, throwing accessor is a.at(i) on the owning std::array (a span gained its own .at() only in C++26).
// HolyC has C-style fixed arrays but no slice type or bounds checks.
I64 a[5];
a[0] = 10; a[1] = 20; a[2] = 30; a[3] = 40; a[4] = 50; // fixed, on the stack
// A "view" of the middle three is a bare pointer plus a length you carry.
I64 *view = &a[1]; // points at a[1]
I64 view_len = 3; // [20, 30, 40] -- tracked by hand
I64 i;
for (i = 0; i < view_len; i++)
Print("%d\n", view[i]); // view[i] == a[1 + i]
// No bounds checking: view[3] silently reads a[4]. Stack array: no Free needed.
HolyC arrays behave like C's: a fixed I64 a[5] lives on the stack and decays to a pointer that forgets its length, so a slice is simply &a[1] paired with a length you maintain. There are no bounds checks, and since the array is on the stack there is nothing to Free; this only runs under TempleOS.
const std = @import("std");
pub fn main() void {
// Fixed array: [5]i32, length is part of the type and known at comptime.
var a = [5]i32{ 10, 20, 30, 40, 50 };
// A slice []i32 is a fat pointer: { ptr, len }. a[1..4] borrows a, no copy.
const view: []i32 = a[1..4]; // [20, 30, 40], view.len == 3
for (view) |x| // iterate the borrowed slice
std.debug.print("{d}\n", .{x});
// Slices are bounds-checked in safe builds: a[1..6] or view[3] would panic.
// No allocator was used, so there is nothing to free.
}Zig distinguishes the fixed array [5]i32 (size in the type) from a slice []i32, which is a fat pointer carrying ptr and len. Slicing with a[1..4] borrows the array without allocating, and in safe builds both the slice bounds and every index are checked at runtime (out-of-range panics) - no allocator is involved, so nothing needs freeing.
use fmt;
export fn main() void = {
// Fixed array: [5]int, its length is part of the type.
let a: [5]int = [10, 20, 30, 40, 50];
// A slice []int is a (pointer, length, capacity) view -- borrows a, no copy.
let view: []int = a[1..4]; // [20, 30, 40], len(view) == 3
for (let i = 0z; i < len(view); i += 1)
fmt::println(view[i])!; // ! aborts on a write error
// Bounds are checked: a[1..6] or view[3] aborts. view borrows a's
// stack storage, so there is no alloc and nothing to free.
};Hare separates the fixed array [5]int from a slice []int, which is a view carrying a pointer, length, and capacity. a[1..4] borrows the array's storage without allocating, len(view) reads the length the slice carries, and indexing is bounds-checked (it aborts on overrun) - because nothing was alloc'd, no free is required.
package main
import "core:fmt"
main :: proc() {
// Fixed array: [5]int, length is part of the type, lives on the stack.
a := [5]int{10, 20, 30, 40, 50}
// A slice []int is a {data, len} view. a[1:4] borrows a -- no allocation.
view: []int = a[1:4] // [20, 30, 40], len(view) == 3
for x in view { // range over the borrowed slice
fmt.println(x)
}
// Bounds-checked by default: a[1:6] or view[3] triggers a runtime panic.
// No make/new was used, so there is nothing to delete/free.
}Odin's [5]int is a value-type fixed array, while a slice []int is a {data, len} view; a[1:4] borrows the array's stack storage with no allocation, so no delete is needed. Slice indexing and slicing are bounds-checked by default and panic on overrun (you can disable checks per-build for hot paths).
\ Forth has no slice type. A fixed array is reserved data space; a "slice"
\ is just a start address plus a length you carry on the stack ( addr len ).
CREATE A 5 CELLS ALLOT \ fixed array of 5 cells at HERE (static)
10 A 0 CELLS + ! 20 A 1 CELLS + ! 30 A 2 CELLS + !
40 A 3 CELLS + ! 50 A 4 CELLS + ! \ A = [10,20,30,40,50]
: SLICE ( addr i n -- addr' n ) \ view of n cells starting at index i
>R CELLS + R> ; \ addr' = addr + i*cell ; keep n
: .SLICE ( addr n -- ) \ print n cells from addr
0 ?DO DUP @ . CR CELL+ LOOP DROP ;
A 1 3 SLICE \ ( addr+1cell 3 ) -- view of the middle three [20,30,40]
.SLICE \ prints 20 30 40
\ No bounds checking exists: SLICE with i+n > 5 just reads past A silently.
\ ALLOT space lives until the dictionary is forgotten; it is never freed.Forth has neither a fixed-array type nor a slice type: CREATE A 5 CELLS ALLOT reserves raw data space, and a "slice" is modelled the same way C does it - an ( addr len ) pair on the stack, here produced by SLICE as base + i*cell with the length kept alongside. There are no bounds checks (an over-long view reads past A), and ALLOT data is static dictionary space that is never individually freed.