Theory-Pointers and Strings in Embedded C: Literals, Safe Copies, Parsing and Number Conversion-Free Embedded C Training in Hyderabad

Need Help or More Information?

Questions about this free C programming course, embedded C training or career guidance? Get in touch.

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.

55 min
Read Time
Intermediate
Level
M9 · L4
Module / Lesson

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.

“STM32” in memory
‘S’
0x53
‘T’
0x54
‘M’
0x4D
‘3’
0x33
‘2’
0x32
‘\0’
0x00 terminator
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 isAn array of 4 charsA pointer to a string literal
Where the characters liveIn the array (RAM if automatic or static non-const)In read-only memory (.rodata in Flash)
sizeof4Size of a pointer (4 on Cortex-M4)
May you modify the characters?YesNo: undefined behavior
Can it be re-pointed?NoYes
RAM costCharacters (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

FunctionDoesTrap
strlen(s)Counts characters before the zeroRuns on forever without a terminator; cost grows with length
strcpy(d, s)Copies including the terminatorNo size check: overflow if d is too small
strncpy(d, s, n)Copies at most n bytesDoes not terminate if s has n or more characters; pads with zeros if shorter
strcat(d, s)Appends s to dNo size check; slow for repeated use
strcmp(a, b)Compares; 0 means equalReturns negative, zero or positive, not just -1/0/1
snprintf(d, n, ...)Formats with a size limitAlways terminates (n > 0); return value is the length it wanted
memcpy/memsetRaw byte copy/fillNot 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

BugExampleFix
Comparing with ==if (cmd == "go") compares addressesstrcmp(cmd, "go") == 0
Buffer too smallchar b[5]; strcpy(b, "STM32");Size 6 or a size-limited copy
Missing terminatorstrncpy with a long sourceTerminate explicitly
Modifying a literalchar *p = "abc"; p[0] = 1;Use an array, or const char *
sizeof on a pointer parametersizeof(s) in a functionPass the size
Printing NULL with %sprintf("%s", NULL)Test first
Signed char in ctypeisdigit(c) with c > 127Cast 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.

Need Help or More Information?

Questions about this free C programming course, embedded C training or career guidance? Get in touch.

2 Comments

Leave a Reply

Your email address will not be published. Required fields are marked *