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.
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
- cppreference: Array Declaration
- CERT C: String and Memory Rules
- Barr Group: Embedded C Coding Standard