MODULE 9 · LESSON 9.4 · THEORY
Pointers and Strings in Embedded C: Literals, Safe Copies, Parsing and Number Conversion
String layout and the null terminator, array versus literal pointer, the standard string functions and their traps, size-limited copies, snprintf, strchr, strstr, strtol, strtok, number-to-text conversion and common string bugs. Keep this in mind whenever you work with pointers and strings in embedded C.
What You Will Learn
✓ How a C string is stored: characters plus a terminating zero byte
✓ Array initialisation versus string literal pointers, and where each lives on the STM32F407
✓ Why modifying a literal is a bug and how const prevents it
✓ The standard string functions and their traps (strncpy, strcat)
✓ Safe copying with an explicit size and snprintf
✓ Searching and parsing with strchr, strstr, strtol and strtok
✓ Converting numbers to text without printf
✓ Common string bugs: overflow, missing terminator, comparing with ==
Key Terms
StringNull terminatorString literalchar arrayconst char *strlenstrcpystrncpysnprintfstrcmpstrtokstrtolBuffer overflowrodataTruncation
1. A String Is a char Array Ending in Zero
C has no string type. A string is a sequence of char values stored in consecutive bytes and ended by a byte with the value 0, the null terminator (written '\0'). Every library function that takes a string walks the bytes until it finds that zero. The text “STM32” therefore needs 6 bytes, not 5. Many bugs in pointers and strings in embedded C start here.
string_layout.c
#include <stdio.h>
#include <string.h>
int main(void)
{
char s[] = “STM32”;
printf(“sizeof(s) = %zu (5 letters + the terminator)\n”, sizeof s);
printf(“strlen(s) = %zu (characters before the terminator)\n”, strlen(s));
for (size_t i = 0; i < sizeof s; i++)
printf(“s[%zu] = 0x%02X %c\n”, i, (unsigned char)s[i], s[i] ? s[i] : ‘.’);
char t[8] = “ab”; /* the unused elements are filled with zeros */
printf(“t has %zu bytes, strlen(t) = %zu\n”, sizeof t, strlen(t));
return 0;
}
Output (verified by compiling and running)
sizeof(s) = 6 (5 letters + the terminator)
strlen(s) = 5 (characters before the terminator)
s[0] = 0x53 S
s[1] = 0x54 T
s[2] = 0x4D M
s[3] = 0x33 3
s[4] = 0x32 2
s[5] = 0x00 .
t has 8 bytes, strlen(t) = 2
The #1 string bug: Forgetting the terminator byte when sizing a buffer. char name[5] = "STM32"; compiles in C (the terminator is dropped), but then name is not a valid string and strlen runs past the end.
2. Array versus Literal Pointer
There are two common ways to write a string and they are not the same. This is the core rule of pointers and strings in embedded C.
| char s[] = "abc"; | const char *p = "abc"; |
|---|
| What it is | An array of 4 chars | A pointer to a string literal |
| Where the characters live | In the array (RAM if automatic or static non-const) | In read-only memory (.rodata in Flash) |
| sizeof | 4 | Size of a pointer (4 on Cortex-M4) |
| May you modify the characters? | Yes | No: undefined behavior |
| Can it be re-pointed? | No | Yes |
| RAM cost | Characters (initialised from Flash at start-up or on entry) | Only the pointer (4 bytes), or none if const |
array_vs_literal.c
#include <stdio.h>
#include <string.h>
int main(void)
{
char arr[] = “abc”; /* modifiable copy */
const char *lit = “abc”; /* read-only literal */
arr[0] = ‘X’; /* fine */
printf(“arr = %s\n”, arr);
printf(“sizeof(arr) = %zu, sizeof(lit) = %zu (PC pointers are 8 bytes; Cortex-M4: 4)\n”, sizeof arr, sizeof lit);
lit = “other”; /* re-pointing a pointer is fine */
printf(“lit = %s\n”, lit);
/* lit[0] = ‘X’; would be undefined: the literal is in read-only memory */
return 0;
}
Output (verified by compiling and running)
arr = Xbc
sizeof(arr) = 4, sizeof(lit) = 8 (PC pointers are 8 bytes; Cortex-M4: 4)
lit = other
lit.c
int main(void)
{
char *p = “abc”; /* should be const char * */
const char *q = “abc”;
q[0] = ‘X’; /* writing through pointer to const */
return p[0] + q[0];
}
GCC protects literals with -Wwrite-strings
lit.c: In function ‘main’:
lit.c:3:15: warning: initialization discards ‘const’ qualifier from pointer target type [-Wdiscarded-qualifiers]
3 | char *p = “abc”; /* should be const char * */
| ^~~~~
lit.c:5:10: error: assignment of read-only location ‘*q’
5 | q[0] = ‘X’; /* writing through pointer to const */
| ^
On the STM32F407 the literal sits in Flash. A write to it does not change the text: the store is silently ignored or raises a bus fault, depending on the region. Always declare literal pointers const char * and compile with -Wwrite-strings. Reviewers check pointers and strings in embedded C closely.
3. The Standard Functions and Their Traps
| Function | Does | Trap |
|---|
strlen(s) | Counts characters before the zero | Runs on forever without a terminator; cost grows with length |
strcpy(d, s) | Copies including the terminator | No size check: overflow if d is too small |
strncpy(d, s, n) | Copies at most n bytes | Does not terminate if s has n or more characters; pads with zeros if shorter |
strcat(d, s) | Appends s to d | No size check; slow for repeated use |
strcmp(a, b) | Compares; 0 means equal | Returns negative, zero or positive, not just -1/0/1 |
snprintf(d, n, ...) | Formats with a size limit | Always terminates (n > 0); return value is the length it wanted |
memcpy/memset | Raw byte copy/fill | Not string functions: no terminator handling |
strncpy_trap.c
#include <stdio.h>
#include <string.h>
int main(void)
{
char dst[5];
memset(dst, ‘#’, sizeof dst); /* fill, so we can see what strncpy leaves */
strncpy(dst, “firmware”, sizeof dst); /* source longer than the buffer */
printf(“bytes:”);
for (size_t i = 0; i < sizeof dst; i++) printf(” %c”, dst[i]);
printf(” <– no terminator written\n”);
dst[sizeof dst – 1] = ‘\0’; /* the usual repair */
printf(“after the repair: \”%s\”\n”, dst);
char buf[9];
int wanted = snprintf(buf, sizeof buf, “ADC=%d”, 4095);
printf(“snprintf stored \”%s\”, wanted %d characters\n”, buf, wanted);
wanted = snprintf(buf, sizeof buf, “TEMPERATURE=%d”, 25);
printf(“truncated to \”%s\”, wanted %d characters\n”, buf, wanted);
return 0;
}
Output (verified by compiling and running)
bytes: f i r m w <– no terminator written
after the repair: “firm”
snprintf stored “ADC=4095”, wanted 8 characters
truncated to “TEMPERAT”, wanted 14 characters
The return value of snprintf lets you detect truncation: if it is greater than or equal to the buffer size, the text did not fit. Prefer snprintf over strcpy/strcat/sprintf for building text in a fixed buffer. (On bare-metal newlib-nano snprintf with floating-point formats needs an extra linker option; integers are fine.) Interviewers test pointers and strings in embedded C on exactly this point.
4. Copying with an Explicit Size
A copy routine for firmware should take the destination size, always terminate and report truncation. The pattern is short enough to write once and reuse everywhere. Master pointers and strings in embedded C by practising this case.
copy_safe.c
#include <stdio.h>
#include <stddef.h>
#include <string.h>
/* returns the length of the copied text, or -1 if the text was truncated (still terminated) */
static int str_copy(char *dst, size_t size, const char *src)
{
if (dst == NULL || size == 0) return –1;
if (src == NULL) { dst[0] = ‘\0’; return –1; }
size_t i = 0;
while (i + 1 < size && src[i] != ‘\0’) { dst[i] = src[i]; i++; }
dst[i] = ‘\0’;
return (src[i] == ‘\0’) ? (int)i : –1;
}
int main(void)
{
char name[8];
printf(“\”UART2\” -> %d, \”%s\”\n”, str_copy(name, sizeof name, “UART2”), name);
printf(“\”TIMER_CHANNEL\” -> %d, \”%s\” (truncated)\n”, str_copy(name, sizeof name, “TIMER_CHANNEL”), name);
printf(“exact fit (7) -> %d, \”%s\”\n”, str_copy(name, sizeof name, “ABCDEFG”), name);
printf(“NULL source -> %d, \”%s\”\n”, str_copy(name, sizeof name, NULL), name);
return 0;
}
Output (verified by compiling and running)
“UART2” -> 5, “UART2”
“TIMER_CHANNEL” -> -1, “TIMER_C” (truncated)
exact fit (7) -> 7, “ABCDEFG”
NULL source -> -1, “”
Why a loop: The routine does one pass, never reads past the source terminator and never writes past dst[size-1]. Compare with strncpy, which pads the whole buffer with zeros (a performance trap for large buffers) and may leave it unterminated.
5. Searching and Parsing
Text protocols (AT commands, NMEA from a GPS, simple command lines) are parsed with a handful of functions. The important rule: functions that return a pointer return an address inside the original string, so the result is valid only as long as that string is. Remember this when you review pointers and strings in embedded C in code reviews.
parsing.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main(void)
{
const char *line = “+CSQ: 21,99”;
const char *colon = strchr(line, ‘:’); /* first ‘:’ */
printf(“after the colon: \”%s\”\n”, colon + 1);
const char *hit = strstr(line, “CSQ”); /* substring search */
printf(“\”CSQ\” found at offset %td\n”, hit ? hit – line : –1);
printf(“\”XYZ\” %s\n”, strstr(line, “XYZ”) ? “found” : “not found (NULL)”);
char *end;
long rssi = strtol(colon + 1, &end, 10); /* end -> first unconverted character */
printf(“rssi = %ld, next character is ‘%c’\n”, rssi, *end);
long ber = strtol(end + 1, &end, 10); /* skip the comma */
printf(“ber = %ld, at the end: %s\n”, ber, *end == ‘\0’ ? “yes” : “no”);
const char *bad = “12abc”;
long v = strtol(bad, &end, 10);
printf(“\”12abc\”: value %ld, rest \”%s\”\n”, v, end); /* detect trailing garbage */
return 0;
}
Output (verified by compiling and running)
after the colon: ” 21,99″
“CSQ” found at offset 1
“XYZ” not found (NULL)
rssi = 21, next character is ‘,’
ber = 99, at the end: yes
“12abc”: value 12, rest “abc”
strtok modifies its input
strtok_demo.c
#include <stdio.h>
#include <string.h>
int main(void)
{
char data[] = “12,7,255”; /* must be a modifiable array, not a literal */
int count = 0;
for (char *tok = strtok(data, “,”); tok != NULL; tok = strtok(NULL, “,”))
{
printf(“token %d: \”%s\”\n”, ++count, tok);
}
printf(“data after strtok: \”%s\” (the first comma became a zero byte)\n”, data);
return 0;
}
Output (verified by compiling and running)
token 1: “12”
token 2: “7”
token 3: “255”
data after strtok: “12” (the first comma became a zero byte)
strtok: It writes zero bytes into the string, keeps its position in hidden static state and is therefore neither reentrant nor usable on literals. Use strtok_r or a hand-written cursor parser in code that runs in several contexts (Lesson 9.2, Example 7).
6. Converting Numbers to Text without printf
The full printf family is large (several kilobytes of Flash even in newlib-nano) and slow. For a bootloader or a tiny UART logger a hand-written converter is common. It builds the digits backwards in a small buffer and returns a pointer to the first digit.
utoa.c
#include <stdio.h>
#include <stdint.h>
/* writes the decimal digits of v at the END of buf and returns a pointer to the first digit */
static char *utoa_dec(uint32_t v, char *buf, size_t size)
{
char *p = buf + size;
*–p = ‘\0’;
do { *–p = (char)(‘0’ + v % 10u); v /= 10u; } while (v != 0);
return p;
}
static char *utoa_hex(uint32_t v, char *buf, size_t size)
{
static const char digits[] = “0123456789ABCDEF”;
char *p = buf + size;
*–p = ‘\0’;
do { *–p = digits[v & 0xFu]; v >>= 4; } while (v != 0);
return p;
}
int main(void)
{
char buf[12]; /* 10 digits + terminator + 1 spare */
printf(“%s\n”, utoa_dec(0, buf, sizeof buf));
printf(“%s\n”, utoa_dec(4095, buf, sizeof buf));
printf(“%s\n”, utoa_dec(4294967295u, buf, sizeof buf));
printf(“0x%s\n”, utoa_hex(0x08000000u, buf, sizeof buf));
printf(“0x%s\n”, utoa_hex(0xBEEF, buf, sizeof buf));
return 0;
}
Output (verified by compiling and running)
0
4095
4294967295
0x8000000
0xBEEF
The do…while loop guarantees at least one digit, so zero prints as “0”. The largest 32-bit value needs 10 digits plus the terminator, which fits in the 12-byte buffer. Working backwards avoids reversing the string afterwards.
7. Characters and the ctype Functions
char may be signed or unsigned depending on the compiler (it is unsigned by default on ARM GCC and signed on x86). The functions in <ctype.h (isdigit, isalpha, toupper…) take an int that must be representable as unsigned char, so always cast the character first.
ctype_demo.c
#include <stdio.h>
#include <ctype.h>
int main(void)
{
const char *s = “Reg-0x2F”;
for (const char *p = s; *p; p++)
printf(“%c: digit=%d alpha=%d upper=%d hexdigit=%d -> %c\n”,
*p, !!isdigit((unsigned char)*p), !!isalpha((unsigned char)*p),
!!isupper((unsigned char)*p), !!isxdigit((unsigned char)*p), toupper((unsigned char)*p));
int digit = ‘7’ – ‘0’; /* character code arithmetic: ‘0’..’9′ are consecutive */
printf(“‘7’ – ‘0’ = %d\n”, digit);
return 0;
}
Output (verified by compiling and running)
R: digit=0 alpha=1 upper=1 hexdigit=0 -> R
e: digit=0 alpha=1 upper=0 hexdigit=1 -> E
g: digit=0 alpha=1 upper=0 hexdigit=0 -> G
-: digit=0 alpha=0 upper=0 hexdigit=0 -> –
0: digit=1 alpha=0 upper=0 hexdigit=1 -> 0
x: digit=0 alpha=1 upper=0 hexdigit=0 -> X
2: digit=1 alpha=0 upper=0 hexdigit=1 -> 2
F: digit=0 alpha=1 upper=1 hexdigit=1 -> F
‘7’ – ‘0’ = 7
The expression c - '0' converts a decimal digit character to its value because the digit characters have consecutive codes in every C implementation. The same is not guaranteed for letters in all character sets, although it holds in ASCII.
8. Common String Bugs
| Bug | Example | Fix |
|---|
Comparing with == | if (cmd == "go") compares addresses | strcmp(cmd, "go") == 0 |
| Buffer too small | char b[5]; strcpy(b, "STM32"); | Size 6 or a size-limited copy |
| Missing terminator | strncpy with a long source | Terminate explicitly |
| Modifying a literal | char *p = "abc"; p[0] = 1; | Use an array, or const char * |
sizeof on a pointer parameter | sizeof(s) in a function | Pass the size |
Printing NULL with %s | printf("%s", NULL) | Test first |
| Signed char in ctype | isdigit(c) with c > 127 | Cast to unsigned char |
cmp.c
int is_go(const char *cmd)
{
return cmd == “go”; /* compares addresses */
}
int main(void)
{
char buf[4] = “go”;
return is_go(buf);
}
GCC on comparing strings with ==
cmp.c: In function ‘is_go’:
cmp.c:3:16: warning: comparison with string literal results in unspecified behavior [-Waddress]
3 | return cmd == “go”; /* compares addresses */
| ^~
GCC reports it with -Waddress (part of -Wall); static analysers and MISRA checkers do too. The test is wrong whenever the strings are in different places, even if their text is equal: here is_go(buf) returns 0 although buf contains “go”.
cmp_demo.c
#include <stdio.h>
#include <string.h>
static int is_go_wrong(const char *cmd) { return cmd == “go”; }
static int is_go_right(const char *cmd) { return strcmp(cmd, “go”) == 0; }
int main(void)
{
char buf[4] = “go”; /* equal text, different address */
printf(“wrong : %d\n”, is_go_wrong(buf));
printf(“right : %d\n”, is_go_right(buf));
return 0;
}
Output (verified by compiling and running)
wrong : 0
right : 1
9. Best Practices for Pointers and Strings in Embedded C
- Declare literal pointers as
const char * and compile with -Wwrite-strings. - Size every buffer for the text plus the terminator.
- Pass the buffer size to every function that writes text; always terminate.
- Prefer
snprintf over sprintf, strcpy and strcat; check its return value. - Never compare strings with
==; use strcmp or memcmp. - Keep message tables in Flash:
static const char *const msgs[]. - Cast characters to
unsigned char before ctype functions. - Avoid
strtok in shared code; avoid the heavy printf family in small bootloaders. - Treat all received text as untrusted: check lengths before copying.
Key Takeaways — Lesson 9.4
- A string is a char array ending in a zero byte; always count the terminator.
char s[] = "..." is a modifiable copy; const char *p = "..." points to read-only Flash.- The classic functions do not check sizes; use size-limited code and
snprintf. strncpy does not guarantee termination.- Search functions return pointers into the original string.
- Compare strings with
strcmp, never ==.
Frequently Asked Questions
How is a string stored in C?
As consecutive char values followed by a zero byte (the null terminator). The string “STM32” occupies 6 bytes.
What is the difference between char s[] and char *s for a string?
char s[] = “abc” creates a modifiable array holding a copy of the characters. char *s = “abc” (better const char *) points to a read-only literal in Flash; modifying it is undefined behavior.
Why is strncpy considered unsafe?
If the source is as long as or longer than the size, strncpy does not write a terminating zero, so the result is not a valid string. It also pads shorter results with zeros, which wastes time on large buffers.
How do I compare two strings in C?
Use strcmp(a, b) == 0. The == operator compares the addresses of the first characters, not the text.
Should I use printf on a microcontroller?
For a small bootloader or a tight Flash budget, avoid it and use snprintf with integers or hand-written converters. newlib-nano reduces the size, but floating-point formatting still adds several kilobytes.
Ready to practise?
Next: worked examples with command matching, a UART line reader, number formatting, AT-response parsing and safe message building.
2 Comments