Loading header...

Arrays, Strings, and Safe Buffers

Most embedded failures are not caused by sophisticated algorithms. They are caused by ordinary data movement done unsafely: one byte written past the end of a buffer, one missing string terminator, one unchecked packet length, one queue that silently overflows.

Learning Objectives

By the end of this lesson, you should be able to:

  • distinguish arrays from pointers clearly enough to manage memory safely;
  • explain how C strings are represented;
  • use array lengths and bounds checks deliberately;
  • design fixed-size buffers that fail predictably instead of corrupting memory.

Arrays Are Contiguous Storage

uint8_t buffer[8];

This creates eight adjacent bytes. Valid indexes are 0 through 7.

flowchart LR B0["buffer[0]"] --> B1["buffer[1]"] --> B2["buffer[2]"] --> B3["buffer[3]"] --> B4["buffer[4]"] --> B5["buffer[5]"] --> B6["buffer[6]"] --> B7["buffer[7]"]

Writing buffer[8] is out of bounds and therefore a bug, even if it appears to "work" during early testing.

Array Length

Within the same scope as a real array object, length is often computed with:

#define ARRAY_LEN(x) (sizeof(x) / sizeof((x)[0]))

Example:

uint16_t samples[16];
size_t n = ARRAY_LEN(samples);

This works only when x is actually an array, not when it has decayed to a pointer.

C Strings

A C string is an array of characters terminated by a null byte '\0'.

char msg[] = "OK";

Memory layout:

  • msg[0] = 'O'
  • msg[1] = 'K'
  • msg[2] = '\0'

That terminator is the reason many string bugs happen. If it is missing, string functions keep reading past intended data.

Buffer Interfaces Need Lengths

Bad:

void uart_send(char *s);

Better when data may not be a null-terminated string:

void uart_send_bytes(const uint8_t *data, size_t len);

Explicit length is safer and more general for binary protocols.

When a function receives a pointer, it usually cannot recover the original array capacity. Pass capacity and used length explicitly:

typedef struct {
    uint8_t *data;
    size_t capacity;
    size_t used;
} byte_span_t;

This makes ownership visible: the function can write only up to capacity, and the caller can inspect how many bytes are valid.

Safe Copy Patterns

When receiving bytes into a buffer:

if (rx_count < RX_BUFFER_LEN) {
    rx_buffer[rx_count++] = byte;
} else {
    overflow_flag = true;
}

This does not eliminate all design choices, but it makes overflow behavior explicit.

For packet parsers, validate the length before reading fields:

bool parse_header(const uint8_t *frame, size_t len, uint8_t *type)
{
    if ((frame == NULL) || (type == NULL) || (len < 3u)) {
        return false;
    }

    *type = frame[1];
    return true;
}

The check happens before frame[1] is read, so a short or corrupt frame fails predictably.

Strings in Embedded Systems

Strings cost RAM and Flash, and many firmware messages are really protocol fields, not human text. Use strings when human-readable interaction matters, but do not treat every byte stream as a string.

Examples where strings make sense:

  • CLI commands over UART;
  • human-readable logs;
  • simple AT command interaction.

Examples where length-delimited buffers are better:

  • binary sensor packets;
  • CRC-protected frames;
  • DMA receive rings.

Off-By-One Errors

Suppose you want room for 15 visible characters plus a terminator. The array must be length 16:

char name[16];

If you fill all 16 positions with visible characters, there is no room left for '\0'.

Example: Safe Line Accumulator

bool line_push_char(char *buf, size_t capacity, size_t *len, char ch)
{
    if ((*len + 1u) >= capacity) {
        return false;
    }

    buf[*len] = ch;
    (*len)++;
    buf[*len] = '\0';
    return true;
}

Why the + 1 check? Because the function reserves space for the new character and the terminator.

Practical Design Rules

  • Give buffers names that reflect purpose and capacity.
  • Track used length separately from capacity.
  • Keep protocol parsers length-aware.
  • Avoid unbounded string functions in firmware.
  • Decide what overflow means: reject, truncate, wrap, or flag fault.
  • Reserve one byte for '\0' when storing C strings.

Common Mistakes

  • Forgetting the null terminator.
  • Confusing buffer capacity with current content length.
  • Calling string functions on binary data.
  • Using sizeof(ptr) when the code needed array length.
  • Writing one extra byte past the end during append operations.

Summary

Arrays give you contiguous storage, strings are arrays terminated by '\0', and safe firmware depends on always knowing capacity, used length, and ownership. Most buffer bugs are simple arithmetic mistakes. Deliberate bounds handling is one of the highest-value habits in embedded C.

Further Reading

Mind Map

mindmap root((Arrays and Buffers)) Core idea Contiguous storage Index starts at zero Capacity is fixed Used length changes Strings Char array Ends with null byte Need terminator space Not for binary frames Calculations Last index is N minus 1 ARRAY_LEN equals sizeof array over element String capacity needs text plus one Check len before field read Design rules Pass pointer plus length Track capacity and used Define overflow policy Avoid unbounded copy Keep binary length based Practical checks Test full buffer Test zero length Test missing terminator Watch sizeof pointer Common mistakes Off by one Buffer overrun Binary as string No overflow flag