Embedded HandbookApps
All sections › IX. The C language: from basics to advanced
STM32F103, STM32F407, STM32G431 · level basic · 13 min read
Open in the interactive handbook

68. Arrays and strings

An array is several values of the same type stored one after another. A string in C is the same kind of array of characters, with a zero at the end. The language has no other "string", and almost all string bugs follow from the fact that it is just an array.

On a microcontroller arrays raise a question nobody asks on a computer: where they are stored. A single word, const, decides whether a table takes space only in Flash or also in RAM, of which there is tens of times less. This was measured on real object files.

Terms used in the documentation

Term Meaning
array, element, index a run of values, one value, its number
initializer list the list of initial values
designated initializer an initial value given with its index
out-of-bounds access access past the end of an array
variable-length array (VLA) an array sized at run time
row-major order rows stored one after another
null-terminated string a string ending in a zero byte
string literal a string constant in the source
buffer overflow writing past the end of a buffer
truncation cutting off what does not fit
scan set a set of characters in scanf
checksum, CRC a check value, a cyclic redundancy code

68.1 An array: identical elements in a row

int a[5];                   /* пять int подряд: a[0] … a[4] */
float t[24];                /* температура по часам */

Indices start at zero, and the last one is one less than the size. All elements sit back to back, and a[i] is *(a + i) (section 67.4).

The number of elements is computed like this:

#define COUNT(arr) (sizeof(arr) / sizeof((arr)[0]))

This works as long as arr is a real array. An array passed to a function becomes a pointer, and there the formula gives a wrong answer (section 67.5).

The size of an array must be known at compile time: a number or a #define. A variable will not do — except in the case described in 68.5.


68.2 Initial values

int x[5] = {7, 5};          /* 7 5 0 0 0 — недостающие становятся нулями */
int z[100] = {0};           /* все сто — нули */
static const int lut[5] = {[3] = 9};   /* 0 0 0 9 0 — задан только четвёртый */

The rule is simple: if at least one element is given, all the others are zeros. The [3] = 9 form is handy for tables where the index is a command or state number: you can see which number maps to what.

Without initial values it works as for any variable (section 64.1): a local array contains garbage, a global or static one contains zeros.

Zeroing an array later is a separate task: = {0} works only in the declaration. Afterwards you use memset(a, 0, sizeof a) or a loop — and they are the same thing: the compiler turns both the zeroing loop and memset into one and the same call to the library function __aeabi_memclr4.


68.3 Where an array lives on the chip

This is what happens to the same 256-byte array depending on its declaration — according to the section sizes of the object file:

Declaration Flash RAM
const unsigned char table[256] = {1}; 256 0
unsigned char table[256] = {1}; 256 256
unsigned char zeros[256]; 0 256
local array in a function 0 256 on the stack while the function runs

The second row is the most expensive. An array without const but with initial values takes space twice: the image of the initial values sits in Flash and is copied to RAM at startup (section 11.2). A font, sine or command table declared without const is kilobytes of RAM spent on nothing. Hence the rule from section 3.6: everything that does not change is const.

The third row is cheaper: an array of zeros is not stored in Flash at all, the startup code simply zeroes the required RAM area.

The fourth is about the stack. A large local array eats stack on every call (section 66.7), so kilobyte buffers are made static or global.


68.4 Going out of bounds

Nobody checks the index. a[5] on a five-element array reads or writes whatever follows in memory — usually a neighbouring variable:

int a[5];
int n = 1;
a[n - 2] = 3;       /* a[-1]: запись перед началом массива */
a[5] = 100;         /* запись после конца */

The compiler notices an out-of-bounds access only when the index is a constant: for a[5] it warns without any flags — "array index 5 is past the end of the array". An index taken from a variable is not checked at all.

On a computer such a bug sometimes crashes the program; on the chip almost never — there is simply other data after the array, and it is silently corrupted. It shows up as "a variable changes by itself" — in a place that has nothing to do with the bug.

The string "hello" with a zero at the end and a write past an array of five ints: the debugger memory window

The protection is discipline: keep the size next to the array, check the index at the function entry, and always limit the length when receiving data.


68.5 Variable-length arrays

The C99 standard allows a local array whose size is known only at run time:

void process(int n)
{
    int buf[n];         /* размер — значение n */
    ...
}

On a computer this is convenient. On a microcontroller it is dangerous: the array goes on the stack, and how much stack is needed depends on the data. Neither you nor the linker can compute that in advance (section 66.7).

The compiler accepts such an array silently — even with -Wall -Wextra. Only the separate flag -Wvla warns about it, and in microcontroller projects it is worth enabling. The right replacement is an array of the maximum size plus a separate variable with the number of elements in use.


68.6 Common operations

Take int a[6] = {5, 3, 8, 1, 9, 2}.

Position of the minimum and maximum — one pass that remembers the index; here the minimum is at position 3, the maximum at 4.

Rotate left — remember the first element, shift the rest, put the first at the end: 3 8 1 9 2 5.

Reverse — swap elements from both ends up to the middle: 2 9 1 8 3 5.

void reverse(int *a, int n)
{
    for (int i = 0; i < n / 2; i++) {
        int t = a[i];
        a[i] = a[n - 1 - i];
        a[n - 1 - i] = t;
    }
}

Bubble sort — neighbouring elements that are out of order are swapped; on each pass the largest one "bubbles up" to the end. If a pass made no swaps at all, the array is already sorted and you can stop:

for (int i = 0; i < n - 1; i++) {
    int swapped = 0;
    for (int j = 0; j < n - 1 - i; j++)
        if (a[j] > a[j + 1]) { swap(&a[j], &a[j + 1]); swapped = 1; }
    if (!swapped) break;
}

On ten elements this is 9 comparisons if the array is already sorted, and 45 if it is sorted in reverse. Selection sort — find the minimum of the remaining part and put it in place — gives the same result. For small arrays on the chip both are fine; for large ones there is qsort in the standard library (section 74), and how to compare algorithms is covered in section 76.


68.7 Two-dimensional arrays

int m[3][5];            /* 3 строки по 5 элементов */

In memory this is one row after another: first the five elements of m[0], then the five of m[1], then m[2]. Adjacent rows are 5 elements apart, a row takes 20 bytes, the whole array 60. The compiler computes the address of m[i][j] as start + (i · 5 + j) · 4.

This has two practical consequences.

Traverse the array by rows: the inner loop runs over the last index. That way the accesses go sequentially through memory.

A two-dimensional array is passed to a function with the row size — otherwise the compiler cannot compute the address:

void print(int rows, int a[][5]);              /* число столбцов — константа */
void print(int rows, int cols, const int *a);  /* a[i * cols + j]; вызов: print(3, 5, &m[0][0]) */

The form a[rows][cols] with variable sizes is also a variable-length array, and -Wvla (section 68.5) will warn about it.

An example from the chip: the frame of a 128 × 64 pixel monochrome display. Each byte holds eight pixels of a column, so the frame is uint8_t fb[8][128], 1024 bytes. On a chip with 20 KB of RAM that is 5 % of all memory — which is why the image is often drawn straight into the display, without a frame in memory.

Left: an int 3 × 5 array laid out in memory, the element of row 1 and column 2 sits at offset 28; right: a 128 × 64 frame of eight pages — the pixel x = 10, y = 21 is bit 5 of byte 10 in page 2


68.8 A string is an array with a zero at the end

char st[] = "hello";    /* 6 байт: h e l l o \0 */
char buf[10] = "hello"; /* 10 байт: hello, \0 и ещё четыре нуля */

The length of a string is not stored anywhere. The end is marked by a character with code zero, and all functions walk the string until they meet it. Hence a difference you must remember:

buf from the example
sizeof(buf) — space for the array 10
strlen(buf) — characters before the zero 5

A string in memory: sizeof versus strlen and the 0x55 byte after "hello" following strncpy

strlen is simple inside:

size_t my_strlen(const char *s)
{
    size_t n = 0;
    while (*s++) n++;
    return n;
}

Two consequences. First: strlen walks the whole string every time — calling it in the condition of a loop over the same string is wasteful. Second, important for data exchange: binary data is not a string. strlen will take a 0x00 byte in the middle of a packet for the end (section 37).

And do not confuse the three zeros: '\0' is the character with code 0, the end of a string; '0' is a digit, code 48; "0" is a string of a digit and a terminating zero, two bytes.


68.9 Strings in Flash and in RAM

Placement of strings follows the same story as arrays, but with one subtlety:

Declaration Flash RAM
char buf[] = "hello"; a copy 6 bytes — the array can be changed
const char *msg = "hello"; 10 bytes — the string and the initial value of the pointer 4 bytes of pointer
const char *const msg = "hello"; 10 bytes — the string and the pointer 0

In the second row the string itself is in Flash, but the pointer to it is an ordinary variable that can be redirected, so it is in RAM, and its initial value is copied from Flash at startup (as in table 68.3). To move the pointer into Flash as well, you need a second const. In Flash there is no difference — 10 bytes in both cases — but it saves RAM: 4 bytes per string, 400 for a table of a hundred messages.

You must not write to a string literal: it is undefined behaviour, and on STM32 the literal sits in Flash, which an ordinary write cannot change anyway (section 11.5). If the string needs to change, declare an array.


68.10 String functions

The standard functions from string.h — strlen, strcpy, strcat, strcmp and others — are covered in detail in section 74. Here are three traps.

Strings cannot be compared with ==. That compares addresses, not contents: two arrays with the same text "hello" give false for ==. Compare with strcmp: it returns zero when they are equal, a negative number if the first string is smaller, and a positive one if it is larger.

strcpy does not know the size of the destination and writes until it meets a zero in the source. A string longer than the buffer overwrites everything behind it.

strncpy does not save you the way it seems. If the string does not fit, it copies exactly as many characters as allowed — and does not add the terminating zero:

char d[8];
strncpy(d, "hello world", 5);   /* d = h e l l o и дальше — что было в памяти */

The safe way is snprintf: it always adds the zero and never writes more than allowed:

char e[6];
int n = snprintf(e, sizeof e, "%s", "hello world");   /* e = "hello", n = 11 */
if (n >= (int)sizeof e) { /* не поместилось */ }

It returns the length the result would have had without the limit — from it you can see that the string was truncated. On the chip this is the main tool for building strings: messages to a port, lines for a display.


68.11 Parsing strings

A line from a serial port is the most common input on the chip. sscanf parses it by a format, and a scan set %[…] defines what counts as part of a field:

char key[20], val[20];
int r = sscanf(line, "%19[0-9]=%19[a-z]", key, val);

The width 19 is the buffer size minus room for the terminating zero. Without it a long line from the port overwrites memory past the buffer (section 74.5).

sscanf returns how many fields it managed to fill — that is the parse check:

String Result
123=hello 2 — both fields
123=123 1 — no letters after =
hello=123 0 — the first field is not digits
empty −1 — input ended

Checking the result is mandatory: with zero or one, val keeps whatever it held before.

For numbers on the chip strtol is used more often — it reports where the number ended and does not drag in the whole format parser (section 74).

A byte array and a checksum. A typical array operation on the chip is computing the checksum of a packet. Here is CRC-8 with the polynomial 0x31 and the initial value 0xFF:

uint8_t crc8(const uint8_t *p, unsigned len)
{
    uint8_t crc = 0xFF;
    while (len--) {
        crc ^= *p++;
        for (int b = 0; b < 8; b++)
            crc = (crc & 0x80) ? (uint8_t)((crc << 1) ^ 0x31) : (uint8_t)(crc << 1);
    }
    return crc;
}

For the string "123456789" it gives 0xF7 — this is the check value of the algorithm, used to verify any implementation. The hardware CRC block is faster, but not for every CRC: on F1 and F4 it can do only CRC-32 with the polynomial 0x04C11DB7, and this CRC-8 is beyond it. The polynomial 0x31, a width of 8 bits and the initial value can be set only on the configurable block — on G4, L4 and the other families from section 12.5.


68.12 Pitfalls

A table "ate" the RAM. It is declared without const — it takes space both in Flash and in RAM.

A variable changes by itself. A write past the end of a neighbouring array.

The array size inside a function is wrong. sizeof of a pointer; pass the size as a parameter.

The stack overflowed on some data and not on other data. A variable-length array. The -Wvla flag finds such places.

A two-dimensional array is traversed slowly. The inner loop runs over the first index — across memory.

strlen returned less than the number of bytes received. The binary data contained a zero.

sizeof of a string is one more than its length. The terminating zero.

if (s == "on") never fires. Addresses are compared; you need strcmp.

After strncpy the string "continues" with garbage. The terminating zero was not added.

A display line overwrote neighbouring data. strcpy or sprintf without a length limit; you need snprintf.

Command parsing sometimes takes an old value. The result of sscanf is not checked.

Writing to a string does not work. It is a constant in Flash; you need an array.


68.13 Summary

Next: section 69 "Floating-point numbers".

Open in the interactive handbook