Inside string.h
Working with text12 min readLesson 1 of 3
1.1 Everything starts at the terminator
A C string is a char array whose last used byte is '\0'. Every <string.h> function finds the end by scanning for that byte, so a missing terminator turns a bounded operation into a walk through unrelated memory.
The functions fall into groups: measure (strlen), copy (strcpy, strncpy, memcpy), compare (strcmp, strncmp, memcmp), search (strchr, strrchr, strstr), and build (strcat, strncat, snprintf).
Each function assumes its arguments are readable strings unless the name ends in n or takes a size. Those variants limit how much they touch, and they still depend on valid input.
#include <stdio.h>
#include <string.h>
int main(void)
{
const char *text = "pointer arithmetic";
const char *space = strchr(text, ' ');
const char *sub = strstr(text, "arith");
printf("first space at %td\n", space - text);
printf("found: %s\n", sub);
printf("length: %zu\n", strlen(text));
return 0;
}Output
first space at 7 found: arithmetic length: 18
1.2 strcpy has no size
strcpy(dest, src) copies characters until the terminator, then copies the terminator. The destination must be large enough, and the function cannot check.
strncpy(dest, src, n) copies at most n bytes. If the source is shorter, the rest of dest is padded with zeros. If the source is n bytes or longer, no terminator is written.
That behaviour is the source of many bugs. A safer habit is snprintf(dest, sizeof dest, "%s", src), which always terminates and reports truncation through its return value.
#include <stdio.h>
#include <string.h>
int main(void)
{
char exact[8];
char small[5];
strncpy(exact, "1234567", sizeof exact - 1);
exact[sizeof exact - 1] = '\0';
strncpy(small, "abcdef", sizeof small - 1);
small[sizeof small - 1] = '\0';
printf("%s (%zu)\n", exact, strlen(exact));
printf("%s (%zu)\n", small, strlen(small));
return 0;
}Output
1234567 (7) abcd (4)
1.3 snprintf reports what it wanted
snprintf(buf, size, fmt, ...) writes at most size - 1 characters and always terminates when size is greater than zero. It returns the number of characters the full output would need, excluding the terminator.
That return value is the truncation test. A negative result means an encoding error. A result greater than or equal to size means the output was cut.
When you build a string in pieces, the same value tells you how far the buffer advanced. Clamp it to the remaining space before using it as an offset.
#include <stdio.h>
int main(void)
{
char buffer[8];
int needed = snprintf(buffer, sizeof buffer, "%s", "truncated text");
printf("buffer: %s\n", buffer);
printf("needed: %d\n", needed);
printf("fits: %d\n", needed < (int)sizeof buffer);
return 0;
}Output
buffer: truncat needed: 14 fits: 0
1.4 Concatenation needs room for two
strcat(dest, src) finds the terminator in dest and copies src after it. The destination needs space for the current text, the new text, and one more byte for the terminator.
strncat(dest, src, n) copies at most n characters and then always adds a terminator. It protects the new characters but not the old ones, so dest still has to be large enough for what it already holds.
With a buffer and a used count, snprintf(dest + used, cap - used, "%s", src) is easier to reason about. It never writes past cap and gives you the new length.
#include <stdio.h>
#include <string.h>
int main(void)
{
char path[32] = "docs/";
size_t used = strlen(path);
strncat(path + used, "notes", sizeof path - used - 1);
printf("%s\n", path);
printf("length: %zu\n", strlen(path));
return 0;
}Output
docs/notes length: 10
1.5 Trimming the newline with strcspn
Input from fgets usually ends with a newline, and a comparison against a word fails until that newline is removed.
strcspn(text, "\r\n") returns the number of characters before the first carriage return or newline. Setting that position to '\0' removes the line ending, whatever the platform produced.
The same function cuts a string at the first character from a set. strcspn(text, ":") gives the position of the first colon without a loop.
#include <stdio.h>
#include <string.h>
static void strip_line(char *text)
{
text[strcspn(text, "\r\n")] = '\0';
}
int main(void)
{
char first[20] = "hello\n";
char second[20] = "world\r\n";
strip_line(first);
strip_line(second);
printf("[%s][%s]\n", first, second);
printf("%zu %zu\n", strlen(first), strlen(second));
return 0;
}Output
[hello][world] 5 5
1.6 Comparing prefixes and memory blocks
strcmp compares whole strings. strncmp(a, b, n) compares at most n characters, which is how a parser tests a prefix: strncmp(line, "GET ", 4) == 0.
memcmp(a, b, n) compares bytes and does not stop at a terminator. Use it for binary data, and use strcmp for text.
Both return a negative value, zero, or a positive value. Testing == 0 covers equality, and comparisons with < and > work for sorting, though the exact difference is not specified.
#include <stdio.h>
#include <string.h>
int main(void)
{
const char *request = "GET /index.html";
unsigned char a[3] = {1, 2, 3};
unsigned char b[3] = {1, 2, 4};
printf("prefix: %d\n", strncmp(request, "GET ", 4) == 0);
printf("post: %d\n", strncmp(request, "POST", 4) == 0);
printf("bytes equal: %d\n", memcmp(a, b, 3) == 0);
return 0;
}Output
prefix: 1 post: 0 bytes equal: 0
1.7 Splitting with strtok
strtok(text, delimiters) replaces the next delimiter with '\0' and returns a pointer to the token. The next call passes NULL to continue from the saved position.
It modifies the input in place, so pass a writable copy and not a string literal. It also keeps hidden state between calls, which makes it unsuitable for nested loops over two strings.
For those cases, scan with strchr and keep the position in a variable you own. A manual tokenizer records the start, finds the next delimiter, and repeats from the character after it.
#include <stdio.h>
#include <string.h>
int main(void)
{
char record[] = "Ada,Lovelace,1815";
int field = 0;
for (char *token = strtok(record, ","); token != NULL; token = strtok(NULL, ",")) {
printf("%d: %s\n", field, token);
field++;
}
return 0;
}Output
0: Ada 1: Lovelace 2: 1815
