C Part I · Module 8 · Strings
Standard String Functions
The previous chapter implemented length, copy and compare so that the pointer work behind them would stop being mysterious. The standard library supplies those three and about a dozen more, and in real code you should call them rather than writing your own — they are correct, widely tested, and frequently faster than anything you would produce. What you cannot get from a reference page is the part that actually matters, which is what each one assumes and what happens when the assumption does not hold. Every function in this chapter has a precondition it is unable to verify, several of them behave differently from what their names suggest, and one pair with nearly identical names has opposite contracts about the single most important question in the module.
Intermediate18 min readCPart IFundamentalsStrings
Part I · Module 8 · Chapter 8.4 · Standard String Functions
1. The Header
#include <string.h>Everything in this chapter comes from <string.h>. Without that line the compiler has no prototypes, which Chapter 5.5 established is an error since C99 — so the omission is caught rather than silently guessed.
Nothing in the header is magic. Chapter 8.3's three functions have the same shapes and the same contracts as the library's; what the library adds is correctness you did not have to write and, usually, an implementation tuned for the machine.
2. The Three You Already Wrote
#include <stdio.h>
#include <string.h>
int main(void)
{
printf("strlen %zu\n", strlen("calibration"));
char buffer[32];
strcpy(buffer, "sensor");
printf("after strcpy [%s]\n", buffer);
printf("strcmp equal %d\n", strcmp("abc", "abc"));
printf("strcmp sign %d\n", strcmp("ab", "abc") < 0);
return 0;
}Output:
strlen 11
after strcpy [sensor]
strcmp equal 0
strcmp sign 1strlen returns the number of characters before the terminator, as a size_t. It walks the string, so it is not free — calling it inside a loop condition means re-walking the string on every pass.
strcpy copies the source including its terminator. Chapter 8.3's callout applies unchanged: it has no way to know how large the destination is, and it will write as many bytes as the source needs.
strcmp returns a sign. Chapter 8.3 proved with a measured −99 that the magnitude is not part of the contract, and the library's version makes the same promise and no more:
if (strcmp(a, b) == 0) { /* equal */ }
if (strcmp(a, b) < 0) { /* a sorts first */ }Never == 1 and never == -1.
3. Joining and Searching
#include <stdio.h>
#include <string.h>
int main(void)
{
char buffer[32];
strcpy(buffer, "sensor");
strcat(buffer, "-01");
printf("after strcat [%s] len %zu\n", buffer, strlen(buffer));
printf("strncmp first 3 %d\n", strncmp("abcZZZ", "abcYYY", 3));
const char *found = strchr("temperature", 'p');
printf("strchr 'p' index %td\n", found - "temperature");
printf("strchr missing %d\n", strchr("abc", 'z') == NULL);
const char *sub = strstr("calibration data", "ration");
printf("strstr index %td\n", sub - "calibration data");
printf("strstr missing %d\n", strstr("abc", "zz") == NULL);
return 0;
}Output:
after strcat [sensor-01] len 9
strncmp first 3 0
strchr 'p' index 3
strchr missing 1
strstr index 5
strstr missing 1strcat appends the source to the end of the destination's existing string. It first walks the destination to find its terminator, then copies — so the destination must already be a valid string, and must have room for its current contents plus the source plus one.
strncmp compares at most n characters. strncmp("abcZZZ", "abcYYY", 3) is 0 because the first three match, which makes it the right tool for prefix tests.
strchr finds the first occurrence of a character and returns a pointer to it, or NULL if it is absent. Subtracting the string's start from the result gives the index — Chapter 7.8 §5's pointer difference. A result of NULL must be checked before it is used, which is Chapter 7.4's rule arriving in the place you will meet it most.
strstr does the same for a substring.
Both searches return a pointer into the string you passed, not a copy. If you pass a const char *, you get a pointer into read-only storage — Chapter 8.2's rule still applies to the result.
4. The strn Family Is Not Uniformly Safer
The functions with an n in the name look like a family with one idea: do the same thing, but bounded. They are not. The two most used members have opposite contracts about termination, and both behaviours are surprising.
strncpy writes exactly n bytes — which may not include a terminator
#include <stdio.h>
#include <string.h>
int main(void)
{
char dest[6];
for (size_t i = 0; i < sizeof dest; i++) { dest[i] = '#'; }
strncpy(dest, "abcdefgh", 6); /* source is LONGER than n */
printf("bytes: ");
for (size_t i = 0; i < sizeof dest; i++) { printf("%d ", dest[i]); }
printf("\n terminator present? %d\n", dest[5] == '\0');
char dest2[8];
for (size_t i = 0; i < sizeof dest2; i++) { dest2[i] = '#'; }
strncpy(dest2, "abc", 8); /* source is SHORTER than n */
printf("bytes: ");
for (size_t i = 0; i < sizeof dest2; i++) { printf("%d ", dest2[i]); }
printf("\n");
return 0;
}Output:
bytes: 97 98 99 100 101 102
terminator present? 0
bytes: 97 98 99 0 0 0 0 0Two surprises, in opposite directions.
Source longer than n: there is no terminator. Six bytes were written — a b c d e f — and the '#' that was in dest[5] is gone, replaced by 'f'. The result is a char array that is not a string, exactly Chapter 8.1 §5's distinction. Passing it to %s or strlen is Chapter 6.4's undefined behaviour.
Source shorter than n: the entire remainder is filled with zeros. Not one terminator — all five remaining bytes. For a large destination and a short string that is a great deal of writing for no benefit.
strncat appends at most n characters — and always terminates
char tail[16];
strcpy(tail, "abc");
strncat(tail, "defghij", 3);
printf("[%s] len %zu\n", tail, strlen(tail));[abcdef] len 6Three characters were appended and a terminator was written, giving a valid string.
So the n means something different in each function:
| What n means | Writes | Always terminates? | |
|---|---|---|---|
strncpy | exactly n bytes written | n bytes | no |
strncat | at most n characters appended | up to n + 1 bytes | yes |
Read the second row's middle column carefully. strncat may write n + 1 bytes, so passing the destination's full size is wrong — the space you must leave is for the existing string plus n plus the terminator.
The
ninstrncpyis a byte count for the destination. Theninstrncatis a character budget for the source. They are not the same kind of number, and treating them as one is a direct route to Chapter 8.5's first failure mode.
5. The Preconditions, Collected
Every function here has at least one assumption it cannot verify. This table is the chapter.
| Function | Assumes | Cannot check |
|---|---|---|
strlen(s) | s is terminated within its storage | that a terminator exists |
strcpy(d, s) | s terminated; d has strlen(s) + 1 bytes | the size of d |
strcat(d, s) | both terminated; d has room for both plus one | the size of d |
strcmp(a, b) | both terminated | that terminators exist |
strncmp(a, b, n) | both readable for up to n bytes | — |
strncpy(d, s, n) | d has n bytes | that the result is terminated |
strncat(d, s, n) | d terminated and has room for n + 1 more | the size of d |
strchr(s, c) | s terminated | — |
strstr(h, n) | both terminated | — |
Two patterns stand out, and both are consequences of things this module has already proved.
Nothing can check a destination's size, because Chapter 6.1 established that an array carries no length and Chapter 7.7 established that it loses even its compile-time size at a function boundary. A function receiving char *d has one address and no extent.
Nothing can check that a terminator exists, because the terminator is an ordinary byte value with no separate marker — Chapter 8.1 §2.
Every one of these functions is a contract where the caller makes a promise the callee cannot verify. Chapter 5.9 called that an unverifiable precondition; here there are nine of them, and they are the reason Chapter 8.5 exists.
6. A Realistic Use
#include <stdio.h>
#include <string.h>
int main(void)
{
char label[32] = {0};
const char *prefix = "sensor";
const char *suffix = "-01";
if (strlen(prefix) + strlen(suffix) + 1 <= sizeof label) {
strcpy(label, prefix);
strcat(label, suffix);
printf("label [%s] len %zu of %zu\n", label, strlen(label), sizeof label);
} else {
printf("label would not fit\n");
}
if (strncmp(label, "sensor", 6) == 0) {
printf("prefix matches\n");
}
const char *dash = strchr(label, '-');
if (dash != NULL) {
printf("suffix starts at index %td\n", dash - label);
}
return 0;
}Output:
label [sensor-01] len 9 of 32
prefix matches
suffix starts at index 9Four habits in that program are worth copying, and each one closes a gap from §5's table.
= {0} at the declaration. Chapter 8.1 §7's insurance — the buffer is a valid empty string before anything is written to it, so an interrupted assembly still leaves something printable.
The size is checked before copying, by the caller, because the functions cannot. strlen(prefix) + strlen(suffix) + 1 <= sizeof label is the precondition from §5's table written out as code. Note that sizeof label works here because label is an array in scope — Chapter 7.7's rule means this check cannot be written inside a function that received it as a parameter.
strncmp is used for the prefix test, rather than copying part of the string somewhere to compare it.
strchr's result is checked against NULL before being used, which is Chapter 7.4's rule in the place it comes up most.
7. What You Should Be Able to Say Now
- All of these come from
<string.h>; without it the calls are an error, not a guess. strlenwalks the string — it is not free, and not something to call in a loop condition.strcpyandstrcatcannot know the destination's size. That is Chapter 6.1 and Chapter 7.7, not an oversight.strcmpreturns a sign — compare against zero, exactly as Chapter 8.3 proved.strncmpis the prefix test, and it is the onestrnfunction with no surprises.strncpywrites exactly n bytes and may leave no terminator — verified:dest[5]was'f'.strncpywith a short source zero-fills the entire remainder, not just one byte.strncatappends at most n characters and always terminates, writing up to n + 1 bytes.- The
nmeans different things in the two functions — a destination byte count, and a source character budget. - Every function has a precondition it cannot verify; the caller must check the size, before the call.
8. Exercises
Exercise 1 — predict the output. Do not compile yet.
char b[20] = {0};
strcpy(b, "abc");
strcat(b, "de");
printf("%zu %s\n", strlen(b), b);
printf("%d %d\n", strcmp(b, "abcde"), strncmp(b, "abcXX", 3));Exercise 2 — predict the bytes. For char d[5] filled with '#' beforehand, give all five bytes and say whether the result is a string.
strncpy(d, "ab", 5);
strncpy(d, "abcdefg", 5);Exercise 3 — size the call. For char dest[10] already holding "abc", say the largest n you may safely pass to strncat(dest, source, n), and show the arithmetic.
Exercise 4 — fix the copy. This is intended to be safe and is not. Say what is wrong, and give the corrected three lines.
char name[8];
strncpy(name, user_label, sizeof name);
printf("%s\n", name);Exercise 5 — state the preconditions. Write out, in Chapter 5.9's contract format, what strcat(d, s) needs, promises, assumes and cannot check.
Exercise 6 — choose the function. For each task, name the function you would use and the check you must perform first.
- test whether a label begins with
"adc" - find where the first
'='appears in a setting - join two pieces of text into a 64-byte buffer
- decide whether two labels are identical
- find whether
"error"appears anywhere in a message
Exercise 7 — explain the asymmetry. strncpy and strncat take an n that means different things. State both meanings, give the number of bytes each may write, and say which one may leave the destination unterminated.
Exercise 8 — audit the gaps. From §5's table, list every function that cannot check the destination's size, and explain in two sentences — using Chapter 6.1 and Chapter 7.7 — why no implementation could do better.
9. Summary
<string.h>supplies these; a missing include is an error, not a silent guess.strlenwalks;strcpy/strcatcannot know the destination's size;strcmpreturns a sign.strncmpis the clean prefix test.strchr/strstrreturn a pointer into your string, orNULL— check it.strncpywrites exactly n bytes, may leave no terminator, and zero-fills a short copy.strncatappends at most n characters, always terminates, and may write n + 1 bytes.- The two
nparameters are different kinds of number. - Every function has an unverifiable precondition; the size check belongs to the caller, before the call.
= {0}, a size check, and aNULLcheck close most of the gaps.
The table in §5 lists nine preconditions and names, for each, the thing the function cannot check. Those are not nine separate hazards. They collapse into three failure modes, which between them account for essentially every string bug in C: writing past the end of a buffer, quietly keeping only part of the text, and losing the terminator so that everything downstream reads into storage that was never part of the string. The final chapter of this module names all three, shows each one happening, and gives the small set of habits that rules out all of them at once.
Standards & specifications
- Governing standard
- ISO/IEC 9899 (the C language standard)(opens ISO/IEC (JTC 1/SC 22/WG 14) in a new tab)
Defines the C language itself — the type system, expression semantics, object model, storage duration and the behaviour categories a conforming implementation must honour. It is a FAMILY of editions (C89/C90, C99, C11, C17, C23) and this curriculum names the edition wherever a claim depends on one. It does not define any foreign-function interface to a hardware description language, and it does not define platform behaviour: what a memory-mapped access does on real silicon is the implementation's and the device's contract, not this standard's. Working drafts published by WG14 are freely available and are what most engineers read day to day; a draft is not the published standard.
This page also covers RTL structure, verification approach and debugging technique. Those are engineering practice built on the standard, not requirements the standard itself imposes.
Where this fits
Part of the C Programming curriculum.
