Data structures in C · about 55 min

A dynamic array

Why this matters

C arrays have a size fixed when they are created. Real programs rarely know in advance how many lines a file has, how many users are connected, or how many words a document contains. Every higher-level language has a growable list built in: Python's list, C++'s std::vector, Rust's Vec, Java's ArrayList. All of them are the structure in this lesson, a dynamic array. In C you build it yourself from malloc, realloc and a struct, and doing so teaches the pattern that every data structure in this module follows: a struct that owns heap memory, a small set of functions that keep it consistent, and one function that frees it.

The idea: length and capacity

You met growing buffers in the dynamic memory module. A dynamic array packages that technique behind an interface. It keeps three pieces of information together:

typedef struct {
    int *data;      /* heap block of cap ints, or NULL */
    size_t len;     /* elements in use: data[0] .. data[len-1] */
    size_t cap;     /* elements allocated */
} IntVec;
  IntVec v (on the stack)            heap block (cap = 8)
  +------------------+              +----+----+----+----+----+----+----+----+
  | data  o----------+------------> | 1  | 4  | 9  | 16 | 25 | ?  | ?  | ?  |
  | len   5          |              +----+----+----+----+----+----+----+----+
  | cap   8          |                 in use (len = 5)      | spare capacity
  +------------------+

The invariant, the rule that is always true between calls, is len <= cap, and data points to a block big enough for cap elements (or is NULL when cap is 0). Elements 0 to len - 1 hold meaningful values; the rest is spare room that lets the next few pushes happen without allocating.

Growing geometrically

When len == cap and another element arrives, the array must grow. The crucial decision is by how much:

Here is the complete type with initialization, push, a checked accessor and cleanup:

#include <stdio.h>
#include <stdlib.h>

typedef struct {
    int *data;
    size_t len;
    size_t cap;
} IntVec;

void vec_init(IntVec *v)
{
    v->data = NULL;
    v->len = 0;
    v->cap = 0;
}

int vec_push(IntVec *v, int value)
{
    if (v->len == v->cap) {
        size_t new_cap = v->cap ? v->cap * 2 : 4;
        int *tmp = realloc(v->data, new_cap * sizeof *tmp);
        if (tmp == NULL)
            return -1;          /* v is unchanged and still valid */
        v->data = tmp;
        v->cap = new_cap;
    }
    v->data[v->len++] = value;
    return 0;
}

int vec_get(const IntVec *v, size_t i, int *out)
{
    if (i >= v->len)
        return -1;
    *out = v->data[i];
    return 0;
}

void vec_free(IntVec *v)
{
    free(v->data);
    vec_init(v);                /* back to a valid empty state */
}

int main(void)
{
    IntVec v;
    vec_init(&v);
    for (int i = 1; i <= 10; i++) {
        if (vec_push(&v, i * i) != 0) {
            fprintf(stderr, "out of memory\n");
            vec_free(&v);
            return 1;
        }
        printf("push %3d  len=%2zu cap=%2zu\n", i * i, v.len, v.cap);
    }
    int x;
    if (vec_get(&v, 9, &x) == 0)
        printf("v[9] = %d\n", x);
    if (vec_get(&v, 10, &x) != 0)
        printf("v[10] is out of range\n");
    vec_free(&v);
    return 0;
}
push   1  len= 1 cap= 4
push   4  len= 2 cap= 4
push   9  len= 3 cap= 4
push  16  len= 4 cap= 4
push  25  len= 5 cap= 8
push  36  len= 6 cap= 8
push  49  len= 7 cap= 8
push  64  len= 8 cap= 8
push  81  len= 9 cap=16
push 100  len=10 cap=16
v[9] = 100
v[10] is out of range

Under valgrind it reports no errors and no leaks. Points worth noticing:

Inserting and removing in the middle

Appending is cheap. Inserting at index i means every element from i onwards must move one place to the right, which costs O(n):

  insert 7 at index 1:

  before   [ 3 | 5 | 9 | . ]       len 3
  shift    [ 3 | 5 | 5 | 9 ]       memmove data[1..2] to data[2..3]
  store    [ 3 | 7 | 5 | 9 ]       len 4

The source and destination ranges overlap, so use memmove, which is defined to handle overlap correctly. memcpy with overlapping ranges is undefined behavior; with some sizes it really does corrupt data. Removing is the mirror image: shift the later elements left over the removed one.

If order does not matter, there is an O(1) removal trick: copy the last element into the hole and decrease len. Many game engines and schedulers use this "swap remove".

Pointer invalidation

Because vec_push may move the whole block, any pointer into the array becomes dangling after a push:

int *first = &v.data[0];
vec_push(&v, 42);       /* may realloc and move the block */
printf("%d\n", *first); /* possibly reading freed memory */

This is the same rule as with realloc (and as std::vector iterator invalidation in C++). Hold on to indexes into a dynamic array, not pointers.

Making it generic

IntVec only stores int. C has no templates, so generic containers usually store an element size and use void * with memcpy, the same way qsort works:

typedef struct {
    void *data;
    size_t len, cap, elem_size;
} Vec;
/* push copies elem_size bytes from a pointer:  vec_push(&v, &value) */

The price is lost type checking and a memcpy per access. Another common approach is a macro that generates a typed version for each element type. For learning, a typed IntVec is clearest.

Common mistakes

Recap

Going further: measure it. Time pushing 10 million ints with doubling versus growing by 16 (time ./prog in the sandbox terminal). Then read how CPython grows lists: list_resize in Objects/listobject.c uses a factor of about 1.125 plus a constant, trading memory for copies.

Exercises

Exercise 1 · quiz

How expensive is push?

A dynamic array starts with capacity 1 and doubles its capacity whenever it is full, copying all existing elements into the new block. Roughly how many element copies does it make in total while you push 1,000,000 elements?

Answer
  • Fewer than 2,000,000 (1 + 2 + 4 + ... + 524,288 is about 1,000,000 copies)

Copies happen only at the doublings, when the array holds 1, 2, 4, ..., 524,288 elements. That sum is just under 1,048,576: about one extra copy per element. So although an individual push occasionally costs O(n), the average over many pushes is O(1), called amortized constant time. If the array grew by a fixed amount (say +10) instead, the total would be quadratic: about n^2 / 20 copies, 50 billion for a million elements. realloc can sometimes grow in place, but you cannot count on it.

Exercise 2 · write code

Implement vec_push

Complete vec_push for the IntVec type from the lesson. When the array is full (len == cap) it must grow geometrically: start at capacity 4 when empty, and double after that. Use realloc correctly (keep the old block if realloc fails, and return -1 in that case). On success append the value and return 0.

The check pushes thousands of values, verifies every element, checks that the capacity grows by doubling rather than one element at a time, and runs everything under valgrind. main prints:

len=10 cap=16 last=100

File: main.c

Starting point

#include <stdio.h>
#include <stdlib.h>

typedef struct {
    int *data;
    size_t len;
    size_t cap;
} IntVec;

void vec_init(IntVec *v)
{
    v->data = NULL;
    v->len = 0;
    v->cap = 0;
}

int vec_push(IntVec *v, int value)
{
    /* grow if full, then append */
    v->data[v->len++] = value;
    return 0;
}

void vec_free(IntVec *v)
{
    free(v->data);
    vec_init(v);
}

int main(void)
{
    IntVec v;
    vec_init(&v);
    for (int i = 1; i <= 10; i++)
        if (vec_push(&v, i * i) != 0)
            return 1;
    printf("len=%zu cap=%zu last=%d\n", v.len, v.cap, v.data[v.len - 1]);
    vec_free(&v);
    return 0;
}

Example

expected output

len=10 cap=16 last=100
Hint 1

Before writing, check if (v->len == v->cap). realloc(NULL, n) behaves like malloc(n), so the empty case needs no special allocation call.

Hint 2

new_cap = v->cap ? v->cap * 2 : 4; then realloc into a temporary pointer, check it for NULL, and only then update v->data and v->cap.

Reference solution
#include <stdio.h>
#include <stdlib.h>

typedef struct {
    int *data;
    size_t len;
    size_t cap;
} IntVec;

void vec_init(IntVec *v)
{
    v->data = NULL;
    v->len = 0;
    v->cap = 0;
}

int vec_push(IntVec *v, int value)
{
    if (v->len == v->cap) {
        size_t new_cap = v->cap ? v->cap * 2 : 4;
        int *tmp = realloc(v->data, new_cap * sizeof *tmp);
        if (tmp == NULL)
            return -1;
        v->data = tmp;
        v->cap = new_cap;
    }
    v->data[v->len++] = value;
    return 0;
}

void vec_free(IntVec *v)
{
    free(v->data);
    vec_init(v);
}

int main(void)
{
    IntVec v;
    vec_init(&v);
    for (int i = 1; i <= 10; i++)
        if (vec_push(&v, i * i) != 0)
            return 1;
    printf("len=%zu cap=%zu last=%d\n", v.len, v.cap, v.data[v.len - 1]);
    vec_free(&v);
    return 0;
}

Exercise 3 · write code

Insert in the middle

Add int vec_insert(IntVec *v, size_t i, int value), which inserts value at index i, moving the elements from i onwards one place to the right. i may equal len (insert at the end), but anything larger is an error: return -1 and change nothing. Return 0 on success. Reuse vec_push to make room (push any value, then shift), and use memmove from <string.h> for the shift (not memcpy: the source and destination overlap).

main reads numbers from stdin as pairs index value, inserts each, and finally prints the array on one line separated by spaces. If an insert fails it prints bad index N and continues.

$ echo "0 5  0 3  2 9  1 4  9 1" | ./prog
bad index 9
3 4 5 9

File: main.c

Starting point

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

typedef struct {
    int *data;
    size_t len;
    size_t cap;
} IntVec;

void vec_init(IntVec *v) { v->data = NULL; v->len = 0; v->cap = 0; }
void vec_free(IntVec *v) { free(v->data); vec_init(v); }

int vec_push(IntVec *v, int value)
{
    if (v->len == v->cap) {
        size_t new_cap = v->cap ? v->cap * 2 : 4;
        int *tmp = realloc(v->data, new_cap * sizeof *tmp);
        if (tmp == NULL)
            return -1;
        v->data = tmp;
        v->cap = new_cap;
    }
    v->data[v->len++] = value;
    return 0;
}

int vec_insert(IntVec *v, size_t i, int value)
{
    return -1;
}

int main(void)
{
    IntVec v;
    vec_init(&v);
    size_t i;
    int value;
    while (scanf("%zu %d", &i, &value) == 2)
        if (vec_insert(&v, i, value) != 0)
            printf("bad index %zu\n", i);
    for (size_t k = 0; k < v.len; k++)
        printf(k ? " %d" : "%d", v.data[k]);
    printf("\n");
    vec_free(&v);
    return 0;
}

Example

input

0 5  0 3  2 9  1 4  9 1

expected output

bad index 9
3 4 5 9

Example

input

0 1 1 2 2 3 3 4 4 5

expected output

1 2 3 4 5
Hint 1

Check i > v->len first, before changing anything.

Hint 2

After vec_push the array is one longer. The elements that must move are the old ones at i .. old_len-1, which is (v->len - 1 - i) elements.

Hint 3

memmove(&v->data[i + 1], &v->data[i], (v->len - 1 - i) * sizeof v->data[0]); then v->data[i] = value;

Reference solution
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

typedef struct {
    int *data;
    size_t len;
    size_t cap;
} IntVec;

void vec_init(IntVec *v) { v->data = NULL; v->len = 0; v->cap = 0; }
void vec_free(IntVec *v) { free(v->data); vec_init(v); }

int vec_push(IntVec *v, int value)
{
    if (v->len == v->cap) {
        size_t new_cap = v->cap ? v->cap * 2 : 4;
        int *tmp = realloc(v->data, new_cap * sizeof *tmp);
        if (tmp == NULL)
            return -1;
        v->data = tmp;
        v->cap = new_cap;
    }
    v->data[v->len++] = value;
    return 0;
}

int vec_insert(IntVec *v, size_t i, int value)
{
    if (i > v->len)
        return -1;
    if (vec_push(v, 0) != 0)        /* make room: len grows by one */
        return -1;
    memmove(&v->data[i + 1], &v->data[i], (v->len - 1 - i) * sizeof v->data[0]);
    v->data[i] = value;
    return 0;
}

int main(void)
{
    IntVec v;
    vec_init(&v);
    size_t i;
    int value;
    while (scanf("%zu %d", &i, &value) == 2)
        if (vec_insert(&v, i, value) != 0)
            printf("bad index %zu\n", i);
    for (size_t k = 0; k < v.len; k++)
        printf(k ? " %d" : "%d", v.data[k]);
    printf("\n");
    vec_free(&v);
    return 0;
}

Exercise 4 · write code

Remove without leaving a hole

Write int vec_remove(IntVec *v, size_t i, int *out): if i < len, store the element at i in *out, close the gap by moving the later elements one place left, decrease len, and return 0. Otherwise return -1 and change nothing. (The capacity stays as it is.)

main builds the array 10 20 30 40 50, then reads indexes from stdin and removes each one, printing removed X or bad index N, and finally the remaining elements:

$ echo "1 3 0 7" | ./prog
removed 20
removed 50
removed 10
bad index 7
30 40

File: main.c

Starting point

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

typedef struct {
    int *data;
    size_t len;
    size_t cap;
} IntVec;

void vec_init(IntVec *v) { v->data = NULL; v->len = 0; v->cap = 0; }
void vec_free(IntVec *v) { free(v->data); vec_init(v); }

int vec_push(IntVec *v, int value)
{
    if (v->len == v->cap) {
        size_t new_cap = v->cap ? v->cap * 2 : 4;
        int *tmp = realloc(v->data, new_cap * sizeof *tmp);
        if (tmp == NULL)
            return -1;
        v->data = tmp;
        v->cap = new_cap;
    }
    v->data[v->len++] = value;
    return 0;
}

int vec_remove(IntVec *v, size_t i, int *out)
{
    *out = v->data[i];
    v->len--;
    return 0;
}

int main(void)
{
    IntVec v;
    vec_init(&v);
    for (int k = 1; k <= 5; k++)
        if (vec_push(&v, k * 10) != 0)
            return 1;
    size_t i;
    while (scanf("%zu", &i) == 1) {
        int x;
        if (vec_remove(&v, i, &x) == 0)
            printf("removed %d\n", x);
        else
            printf("bad index %zu\n", i);
    }
    for (size_t k = 0; k < v.len; k++)
        printf(k ? " %d" : "%d", v.data[k]);
    printf("\n");
    vec_free(&v);
    return 0;
}

Example

input

1 3 0 7

expected output

removed 20
removed 50
removed 10
bad index 7
30 40

Example

input

0 0 0 0 0 0

expected output

removed 10
removed 20
removed 30
removed 40
removed 50
bad index 0
Hint 1

Valid indexes are 0 .. len-1, so reject i >= len.

Hint 2

The elements after i are data[i+1] .. data[len-1]: that is len - i - 1 of them, moving to start at data[i].

Reference solution
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

typedef struct {
    int *data;
    size_t len;
    size_t cap;
} IntVec;

void vec_init(IntVec *v) { v->data = NULL; v->len = 0; v->cap = 0; }
void vec_free(IntVec *v) { free(v->data); vec_init(v); }

int vec_push(IntVec *v, int value)
{
    if (v->len == v->cap) {
        size_t new_cap = v->cap ? v->cap * 2 : 4;
        int *tmp = realloc(v->data, new_cap * sizeof *tmp);
        if (tmp == NULL)
            return -1;
        v->data = tmp;
        v->cap = new_cap;
    }
    v->data[v->len++] = value;
    return 0;
}

int vec_remove(IntVec *v, size_t i, int *out)
{
    if (i >= v->len)
        return -1;
    *out = v->data[i];
    memmove(&v->data[i], &v->data[i + 1], (v->len - i - 1) * sizeof v->data[0]);
    v->len--;
    return 0;
}

int main(void)
{
    IntVec v;
    vec_init(&v);
    for (int k = 1; k <= 5; k++)
        if (vec_push(&v, k * 10) != 0)
            return 1;
    size_t i;
    while (scanf("%zu", &i) == 1) {
        int x;
        if (vec_remove(&v, i, &x) == 0)
            printf("removed %d\n", x);
        else
            printf("bad index %zu\n", i);
    }
    for (size_t k = 0; k < v.len; k++)
        printf(k ? " %d" : "%d", v.data[k]);
    printf("\n");
    vec_free(&v);
    return 0;
}